deel-local-cli 1.20.13 → 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.
Files changed (155) hide show
  1. package/README.ko.md +96 -78
  2. package/README.md +119 -81
  3. package/bin/deel.js +398 -32
  4. package/package.json +2 -1
  5. package/src/acp/jsonrpc.js +87 -7
  6. package/src/acp/map.js +23 -3
  7. package/src/acp/serve.js +333 -25
  8. package/src/agent/agents.js +110 -12
  9. package/src/agent/askcheck.js +50 -6
  10. package/src/agent/asks.js +59 -5
  11. package/src/agent/budget.js +9 -2
  12. package/src/agent/card.js +8 -2
  13. package/src/agent/commit.js +168 -16
  14. package/src/agent/compact.js +93 -21
  15. package/src/agent/{/354/213/240/353/242/260/353/217/204.js → confidence.js} +1 -1
  16. package/src/agent/effort.js +85 -15
  17. package/src/agent/evidence.js +63 -7
  18. package/src/agent/evolve.js +132 -24
  19. package/src/agent/grade.js +12 -3
  20. package/src/agent/loop.js +408 -81
  21. package/src/agent/memory.js +176 -29
  22. package/src/agent/mention.js +22 -5
  23. package/src/agent/models.js +31 -7
  24. package/src/agent/modes.js +25 -10
  25. package/src/agent/outschema.js +381 -56
  26. package/src/agent/pins.js +33 -2
  27. package/src/agent/project.js +73 -8
  28. package/src/agent/recall.js +40 -9
  29. package/src/agent/{/354/204/261/355/225/234/352/270/260/353/241/235.js → recordshape.js} +19 -8
  30. package/src/agent/review.js +143 -13
  31. package/src/agent/route.js +253 -26
  32. package/src/agent/salvage.js +2 -1
  33. package/src/agent/session.js +457 -31
  34. package/src/agent/store.js +232 -22
  35. package/src/agent/threads.js +52 -3
  36. package/src/backend/adapter.js +508 -85
  37. package/src/backend/azure.js +12 -1
  38. package/src/backend/cachemark.js +31 -5
  39. package/src/backend/clientcert.js +23 -1
  40. package/src/backend/ctxsize.js +56 -9
  41. package/src/backend/detect.js +252 -19
  42. package/src/backend/http.js +383 -42
  43. package/src/backend/learn.js +101 -11
  44. package/src/backend/mcp.js +333 -24
  45. package/src/backend/price.js +61 -13
  46. package/src/backend/probe.js +188 -36
  47. package/src/backend/proxy.js +108 -14
  48. package/src/backend/quota.js +226 -30
  49. package/src/backend/retry.js +46 -3
  50. package/src/backend/scan.js +47 -10
  51. package/src/backend/scanui.js +57 -7
  52. package/src/backend/toolfit.js +204 -22
  53. package/src/backend/vision.js +14 -1
  54. package/src/backend/wire.js +58 -10
  55. package/src/cmdnames.js +16 -0
  56. package/src/commands/common.js +70 -0
  57. package/src/commands/extend.js +300 -0
  58. package/src/commands/model.js +852 -0
  59. package/src/commands/view.js +328 -0
  60. package/src/commands/work.js +823 -0
  61. package/src/commands.js +193 -2082
  62. package/src/completion.js +45 -3
  63. package/src/config.js +452 -39
  64. package/src/configexplain.js +125 -14
  65. package/src/doctor.js +164 -26
  66. package/src/i18n/en.js +22 -0
  67. package/src/i18n/index.js +31 -1
  68. package/src/i18n/ja.js +25 -1
  69. package/src/i18n/ko.js +25 -2
  70. package/src/i18n/zh.js +25 -1
  71. package/src/lsp/client.js +111 -9
  72. package/src/lsp/servers.js +86 -22
  73. package/src/oneshot.js +325 -65
  74. package/src/pack/sbom.js +28 -5
  75. package/src/pack/selfpack.js +54 -5
  76. package/src/pack/sheet.en.js +24 -3
  77. package/src/pack/tar.js +45 -8
  78. package/src/pack/zip.js +103 -8
  79. package/src/plugins/manage.js +165 -14
  80. package/src/preview/serve.js +239 -23
  81. package/src/providers/bedrock.js +30 -5
  82. package/src/providers/gemini.js +8 -0
  83. package/src/providers/index.js +17 -1
  84. package/src/repl.js +254 -61
  85. package/src/report.js +7 -2
  86. package/src/reset.js +151 -25
  87. package/src/safety/audit.js +108 -8
  88. package/src/safety/authcmd.js +75 -5
  89. package/src/safety/guard.js +507 -15
  90. package/src/safety/hooks.js +172 -20
  91. package/src/safety/keystore.js +178 -18
  92. package/src/safety/network.js +209 -6
  93. package/src/safety/policy.js +377 -36
  94. package/src/safety/runmode.js +9 -2
  95. package/src/safety/secrets.js +243 -17
  96. package/src/safety/shellenv.js +64 -3
  97. package/src/safety/trust.js +298 -32
  98. package/src/safety/undo.js +409 -17
  99. package/src/setup.js +175 -17
  100. package/src/skills/discover.js +122 -19
  101. package/src/stats.js +57 -16
  102. package/src/tools/{/355/231/225/354/235/270/353/262/225.js → checkmethods.js} +26 -1
  103. package/src/tools/clipboard.js +53 -20
  104. package/src/tools/convert.js +118 -37
  105. package/src/tools/desc.en.js +48 -5
  106. package/src/tools/doc2md.js +11 -1
  107. package/src/tools/docs.js +94 -18
  108. package/src/tools/edit-match.js +212 -13
  109. package/src/tools/encoding.js +525 -28
  110. package/src/tools/excel-com.js +63 -15
  111. package/src/tools/excel.js +12 -7
  112. package/src/tools/fastgrep.js +444 -36
  113. package/src/tools/fig.js +21 -4
  114. package/src/tools/fsutil.js +235 -21
  115. package/src/tools/hwpxwrite.js +80 -19
  116. package/src/tools/ignore.js +55 -7
  117. package/src/tools/index.js +1029 -145
  118. package/src/tools/jobs.js +325 -50
  119. package/src/tools/kiwi.js +50 -5
  120. package/src/tools/lsp.js +164 -32
  121. package/src/tools/outline.js +52 -12
  122. package/src/tools/pdf.js +366 -56
  123. package/src/tools/shell.js +12 -4
  124. package/src/tools/spawn.js +203 -18
  125. package/src/tools/todo.js +73 -4
  126. package/src/tools/verify.js +312 -43
  127. package/src/tools/webfetch.js +184 -25
  128. package/src/tools/xlsx.js +232 -31
  129. package/src/ui/ansi.js +209 -15
  130. package/src/ui/banner.js +2 -1
  131. package/src/ui/complete.js +46 -4
  132. package/src/ui/diff.js +4 -2
  133. package/src/ui/export.js +91 -17
  134. package/src/ui/inputbox.js +58 -18
  135. package/src/ui/intro.js +9 -2
  136. package/src/ui/level.js +32 -7
  137. package/src/ui/md.js +101 -15
  138. package/src/ui/motion.js +5 -2
  139. package/src/ui/notify.js +7 -1
  140. package/src/ui/office.js +37 -5
  141. package/src/ui/pastechip.js +3 -2
  142. package/src/ui/pick.js +85 -12
  143. package/src/ui/prompt.js +176 -13
  144. package/src/ui/screen.js +2 -1
  145. package/src/ui/spinner.js +6 -1
  146. package/src/ui/status.js +80 -12
  147. package/src/ui/wrap.js +23 -37
  148. /package/src/agent/{/353/213/250/352/263/204.js" → phase.js} +0 -0
  149. /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
  150. /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
  151. /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
  152. /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
  153. /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
  154. /package/src/skills/builtin/{/354/260/224/353/237/254/353/263/264/352/270/260/SKILL.md" → spike/SKILL.md} +0 -0
  155. /package/src/skills/builtin/{/352/262/200/354/202/254-/353/250/274/354/240/200/SKILL.md" → test-first/SKILL.md} +0 -0
@@ -5,6 +5,7 @@
5
5
  // warn 되긴 하는데 조건이 붙음
6
6
  // skip 앞 검사가 실패해 확인 불가
7
7
  import { req, headersFor, serverMessage } from './http.js';
8
+ import { 언제풀리나 } from './quota.js';
8
9
  import { probeCtx } from './ctxsize.js';
9
10
  import { 눈검사메시지 } from './vision.js';
10
11
  import {
@@ -12,7 +13,7 @@ import {
12
13
  assistantMessage, toolMessage,
13
14
  } from './adapter.js';
14
15
  import { 벤더 } from './toolfit.js';
15
- import { 세션이름짓기 } from './wire.js';
16
+ import { 세션이름짓기, 기본카드 } from './wire.js';
16
17
  import { 말 } from '../i18n/index.js';
17
18
 
18
19
  const READ_TOOL = {
@@ -53,7 +54,21 @@ const READ_TOOL = {
53
54
 
54
55
  /** 이 회사가 무엇을 받는지까지 봐야 몸통이 맞다 (toolfit.js 의 벤더). */
55
56
  function 몸통(conn, opts) {
56
- return buildBody(conn.kind, { model: conn.model, 회사: 벤더(conn), maxTokens: 128, ...opts });
57
+ /*
58
+ * 출력 상한의 **이름**만은 카드에서 받는다. 안 주면 buildBody 가 옛 이름·새 이름을 둘 다
59
+ * 싣는데, 추론 모델(gpt-5·o 계열)은 옛 이름을 튕긴다. 그러면 첫 칸이 400 으로 죽고 나머지가
60
+ * 「확인 불가」 로 건너뛰어져, setup 이 streaming·tools·json 을 전부 false 로 **저장**했다.
61
+ *
62
+ * 그래서 **새 이름만 받는 모델**(카드의 출력칸 '새것')일 때만 카드를 준다. 그 카드는
63
+ * 추론 눈금 형식(effort)도 같이 들고 있어 짝이 맞는다. 나머지는 여태처럼 카드 없이 —
64
+ * 카드를 주면 캐시 표식·생각 형식이 얹혀, 진단이 재려는 「맨 몸통에 어떻게 답하나」 가
65
+ * 우리 짐작이 섞인 몸통의 답이 된다. 조각 카드도 안 된다: 카드가 있으면 눈금을 짐작하지
66
+ * 않아서(adapter.js 의 나가는눈금) 추론 강도 칸이 통째로 빠진다.
67
+ */
68
+ const 카드 = conn.전선 ?? 기본카드(conn);
69
+ return buildBody(conn.kind, {
70
+ model: conn.model, 회사: 벤더(conn), maxTokens: 128, 카드: 카드?.출력칸 === '새것' ? 카드 : null, ...opts,
71
+ });
57
72
  }
58
73
 
59
74
  // 추론 모델일 때 기본 대화 칸에 덧붙일 설명.
@@ -148,13 +163,34 @@ export async function probe(conn, onStep = () => {}) {
148
163
  const facts = { shape, base, auth, model };
149
164
 
150
165
  const add = (r) => { results.push(r); onStep(r); return r; };
151
- const call = (opts) => req(url(endpoint(shape)), {
166
+ /*
167
+ * ── 잠깐 막힌 것을 「못 한다」 로 저장하지 않는다 ────────────────────────
168
+ *
169
+ * 사람이 본 것: 설치하는 순간 게이트웨이가 429 를 한 번 냈다. 화면은 「기본 대화 ✗ Rate limit
170
+ * reached」 와 여덟 칸의 「확인 불가」 였고, 프로필에는 streaming·tools·json·think·vision 이 전부
171
+ * false 로 저장됐다(setup.js 가 `facts.x ?? false` 로 적는다). 도구를 아예 안 쓰는 연결이 됐다.
172
+ *
173
+ * 실제로 있었던 일: 각 칸이 요청 **한 번**의 성패로 능력을 정했다. 429·502·503·504 는 모델이 무엇을
174
+ * 할 수 있는지에 대해 아무 말도 안 한다 — 지금 바쁘다는 말이다 (6회차 Gemini 더듬6 P5·B3·B4·B5).
175
+ *
176
+ * 이제 지키는 규칙: 그런 답이면 한 번 쉬었다(Retry-After, 없으면 2초 · 10초 넘게는 안 기다림)
177
+ * 다시 묻는다. 시간 초과는 다시 안 묻는다 — 이미 60~120초를 기다린 뒤라 설치 화면이 곱절로 멈춘다.
178
+ */
179
+ const 잠깐막힘 = (r) => [429, 502, 503, 504].includes(r?.status);
180
+ const 한번부르기 = (opts) => req(url(endpoint(shape)), {
152
181
  method: 'POST',
153
182
  headers: H(),
154
183
  body: 몸통(conn, opts),
155
184
  timeout: opts.timeout ?? 60000,
156
185
  stream: opts.stream,
157
186
  });
187
+ const call = async (opts) => {
188
+ const r = await 한번부르기(opts);
189
+ if (!잠깐막힘(r)) return r;
190
+ const 초 = 언제풀리나(r.headers?.get?.('retry-after') ?? null);
191
+ await new Promise((ok) => setTimeout(ok, Math.min(Math.max((초 ?? 2) * 1000, 500), 10000)));
192
+ return 한번부르기(opts);
193
+ };
158
194
  // 답 읽기도 루프와 같은 함수를 쓴다. 도구 부름은 { id, name, args } 로
159
195
  // 고르게 나온다 — 규격마다 다른 자리를 여기서 또 헤아리지 않는다.
160
196
  const 읽기 = (r) => extractMessage(shape, r?.json);
@@ -167,6 +203,9 @@ export async function probe(conn, onStep = () => {}) {
167
203
  let got = basic.ok ? 읽기(basic) : { content: '', thinking: '' };
168
204
  let thinkingModel = false;
169
205
  let retried = false;
206
+ // 사고를 **끌 수 있다고 확인한** 모델인가. 짐작이 아니라 실제로 끄고 본문을 받아 봤을 때만 선다.
207
+ let 사고끌수있나 = false;
208
+ let 넉넉 = 256; // 기본 대화가 본문을 낸 상한 — 추론 모델이면 뒤 칸도 이 아래로 안 준다 (아래 상한)
170
209
 
171
210
  /*
172
211
  * ── 사고를 **글로 안 내주는** 모델도 여기로 온다 ──────────────────────
@@ -187,27 +226,72 @@ export async function probe(conn, onStep = () => {}) {
187
226
  */
188
227
  const 상한에걸렸나 = (e) => e?.stopped === 'length' || e?.stopped === 'max_tokens';
189
228
  const 생각만했나 = (e) => !!e?.thinking || 상한에걸렸나(e) || (e?.usage?.reasoning ?? 0) > 0;
229
+ /*
230
+ * ── 본문이 **왔어도** 추론 모델일 수 있다 ──────────────────────────────
231
+ *
232
+ * 아래 갈래가 `!got.content` 안에만 있었다. 그런데 1+1 은 쉬운 물음이라,
233
+ * 256 안에서 생각도 하고 답도 내는 추론 모델이 많다. 그 모델은 여기를 안
234
+ * 지나가고 **보통 모델로 적혔다.** 그러면 아래 `상한()` 이 아무 일도 안 해서
235
+ * 뒤 칸을 128·32 로 묻고, 그 상한은 생각에 다 나가 본문이 빈다 —
236
+ * 시스템·스트리밍·그림이 「못 한다」 로 적히고 프로필에 false 로 남는다.
237
+ * 이 파일이 아래 248–261줄에서 없애려던 바로 그 고장이다 (8회차 뒷단-배우기).
238
+ *
239
+ * 다만 **상한에 걸렸다**는 것만으로는 안 친다. 본문이 온 판에서 그것은 보통
240
+ * 모델이 길게 답하다 잘린 것과 구별이 안 된다. 본문이 있을 때는 사고 글이나
241
+ * 생각 토큰 — 즉 **생각했다는 증거**가 있어야 추론 모델로 본다.
242
+ */
243
+ const 생각한흔적 = (e) => !!e?.thinking || (e?.usage?.reasoning ?? 0) > 0;
244
+ if (basic.ok && (생각한흔적(got) || (!got.content && 생각만했나(got)))) thinkingModel = true;
190
245
 
191
246
  if (basic.ok && !got.content && 생각만했나(got)) {
192
- thinkingModel = true;
193
247
  retried = true;
194
248
  const second = await call({ ...ASK, maxTokens: 1024, think: false, timeout: 90000 });
195
- if (second.ok) {
196
- const e2 = 읽기(second);
197
- if (e2.content) { basic = second; got = e2; }
198
- else {
199
- // 사고를 못 끄는 서버 — 상한만 크게 올려 한 번 더.
200
- const third = await call({ ...ASK, maxTokens: 2048, timeout: 120000 });
201
- if (third.ok && 읽기(third).content) { basic = third; got = 읽기(third); }
202
- }
249
+ const e2 = second.ok ? 읽기(second) : null;
250
+ if (e2?.content) { basic = second; got = e2; 넉넉 = 1024; 사고끌수있나 = true; }
251
+ else {
252
+ /*
253
+ * 사고를 못 끄는 서버 — 상한만 크게 올려 한 번 더.
254
+ *
255
+ * 이 걸음이 `if (second.ok)` **안에** 있었다. 그래서 `think:false` 를
256
+ * 400 으로 **거절하는** 서버는 여기에 영영 못 닿았다 — 상한만 올리면
257
+ * 답하는 모델인데 기본 대화가 ✗ 로 끝나고 나머지 여덟 칸이 전부
258
+ * 「확인 불가」 가 됐다. 그 판정이 프로필에 전부 false 로 남는다.
259
+ * 칸 하나를 거절한 것과 모델이 못 하는 것은 다른 말이다.
260
+ */
261
+ const third = await call({ ...ASK, maxTokens: 2048, timeout: 120000 });
262
+ const e3 = third.ok ? 읽기(third) : null;
263
+ if (e3?.content) { basic = third; got = e3; 넉넉 = 2048; }
203
264
  }
204
265
  }
205
266
 
206
267
  const basicText = got.content;
207
268
  const basicOk = basic.ok && !!basicText;
208
269
  facts.thinkingModel = thinkingModel;
209
- // 사고를 끌 수 있는 모델이면 이후 검사에서 꺼서 토큰과 시간을 아낀다.
210
- const quiet = shape === 'ollama' && thinkingModel ? { think: false, maxTokens: 512 } : {};
270
+ /*
271
+ * 사고를 끌 수 있는 모델이면 이후 검사에서 꺼서 토큰과 시간을 아낀다.
272
+ *
273
+ * 여기가 `thinkingModel` 만 봤다. 그런데 추론 모델이라고 다 끌 수 있는 것은
274
+ * 아니다 — 위에서 `think:false` 를 **400 으로 거절한** 서버도 추론 모델이다.
275
+ * 그 창구에 이 칸을 다시 실으면 뒤 여덟 번이 전부 400 이고, 프로필에는
276
+ * 도구·JSON·스트리밍이 다 false 로 남는다. 서버가 안 받는다고 말한 칸은
277
+ * 그 뒤로 안 보낸다 — 끌 수 있다고 **확인한** 모델에만 붙인다.
278
+ */
279
+ const quiet = shape === 'ollama' && 사고끌수있나 ? { think: false } : {};
280
+ /*
281
+ * ── 기본 대화를 넉넉히 받은 모델은 뒤 칸도 그만큼 준다 ───────────────────
282
+ *
283
+ * 사람이 본 것: 추론 모델을 붙였더니 기본 대화는 ✓(추론 모델)인데 도구 호출 ✗ 「도구를 안 부르고 글로만
284
+ * 답합니다」 · 구조적 출력 「스키마를 안 지킴 — 받은 값 ""」 · 스트리밍 「한 번에 옵니다」 였고, 프로필에
285
+ * tools·json·streaming 이 false 로 저장됐다. 도구를 못 쓰는 에이전트가 됐다.
286
+ *
287
+ * 실제로 있었던 일: 기본 대화는 256 → 1024 → 2048 로 올려서야 본문을 받았는데, 뒤 칸들은 다시 128
288
+ * (시스템·스트리밍) · 256(도구 결과) · 512(도구·JSON) 로 물었다. 사고를 끌 수 있는 Ollama 만 quiet 로
289
+ * 넉넉히 줬고, 사고를 못 끄는 창구(OpenAI 계열 추론 모델)는 그 상한을 생각에 다 쓰고 빈 본문을 냈다
290
+ * (6회차 Gemini 더듬6 P2). 모델이 못 한 것이 아니라 우리가 말할 자리를 안 준 것이다.
291
+ *
292
+ * 이제 지키는 규칙: 추론 모델이면 뒤 칸의 상한을 **기본 대화가 본문을 낸 상한** 아래로 내리지 않는다.
293
+ */
294
+ const 상한 = (n) => (thinkingModel ? Math.max(n, 넉넉) : n);
211
295
 
212
296
  add({
213
297
  id: 'chat',
@@ -236,6 +320,7 @@ export async function probe(conn, onStep = () => {}) {
236
320
  // 2. 시스템 메시지 — 규칙과 스킬이 먹느냐가 여기 달렸다.
237
321
  const sys = await call({
238
322
  ...quiet,
323
+ maxTokens: 상한(128),
239
324
  messages: [
240
325
  { role: 'system', content: '너는 무슨 질문을 받든 정확히 DEEL 한 단어만 답한다.' },
241
326
  { role: 'user', content: '안녕하세요' },
@@ -256,6 +341,7 @@ export async function probe(conn, onStep = () => {}) {
256
341
  const st = await call({
257
342
  ...quiet,
258
343
  messages: [{ role: 'user', content: '1부터 20까지 세어보세요.' }],
344
+ maxTokens: 상한(128),
259
345
  stream: true,
260
346
  timeout: 45000,
261
347
  });
@@ -272,8 +358,17 @@ export async function probe(conn, onStep = () => {}) {
272
358
  if (done) break;
273
359
  const text = dec.decode(value, { stream: true });
274
360
  const hits = (text.match(/(^|\n)data:|"done"\s*:/g) ?? []).length;
275
- if (hits && !firstMs) firstMs = Date.now() - t0;
276
- chunks += hits || (text.trim() ? 1 : 0);
361
+ /*
362
+ * 조각으로 셀 것을 먼저 정하고, **그것으로** 첫 응답 시각을 잰다.
363
+ *
364
+ * 여기가 `hits` 만 보고 시각을 쟀다. 그런데 바로 아랫줄은 `data:` 도
365
+ * `"done":` 도 안 쓰는 평문 청크도 조각으로 센다 — 그런 창구에서는
366
+ * 조각이 다섯인데 첫 응답만 `0ms` 로 나갔다. 잰 값이 아니라 지어낸
367
+ * 값이고, 사람은 그걸 「번개같이 빠른 창구」 로 읽는다.
368
+ */
369
+ const 센것 = hits || (text.trim() ? 1 : 0);
370
+ if (센것 && !firstMs) firstMs = Date.now() - t0;
371
+ chunks += 센것;
277
372
  }
278
373
  reader.cancel().catch(() => {});
279
374
  } catch (e) {
@@ -293,21 +388,37 @@ export async function probe(conn, onStep = () => {}) {
293
388
  add({
294
389
  id: 'stream',
295
390
  label: '스트리밍',
296
- status: chunks > 2 ? 'ok' : (st.ok && !읽다실패) ? 'warn' : 'no',
391
+ // 못 읽은 것은 ✗ 가 아니다 — 「안 된다」 가 아니라 「못 쟀다」 다 (아래 머리말, 그림 칸과 같은 자세).
392
+ status: chunks > 2 ? 'ok' : st.ok ? 'warn' : 'no',
297
393
  detail: chunks > 2
298
394
  ? `조각 ${chunks}개, 첫 응답 ${firstMs}ms`
299
- : 읽다실패 ? `흘러오는 것을 읽지 못했습니다 — ${읽다실패}`
395
+ : 읽다실패 ? `흘러오는 것을 읽지 못했습니다 — ${읽다실패} (못 쟀습니다 — 켠 채로 둡니다)`
300
396
  : st.ok ? '한 번에 옵니다 — 화면은 스피너로 대체합니다' : serverMessage(st),
301
397
  ms: st.ms,
302
398
  });
303
- facts.streaming = chunks > 2;
399
+ /*
400
+ * 못 쟀으면 **켠 채로** 둔다 — 되돌아올 수 있는 쪽으로 틀린다 (아래 그림 칸 머리말).
401
+ *
402
+ * 여기가 `chunks > 2` 하나였다. 바로 위 catch 는 「우리 잘못이 서버 진단으로 둔갑하는
403
+ * 것이다 — 사람은 멀쩡한 창구를 스트리밍 안 되는 창구로 알고 쓴다」 고 적어 놓고 화면
404
+ * 글자만 고쳤고, **프로필에 남는 값**은 옛것 그대로 false 였다 (사냥6 막판-뒷단).
405
+ * setup.js 가 `facts.streaming ?? false` 로 적으므로 그 false 는 그 연결의 스트리밍을
406
+ * 영영 끈다 — 화면은 답을 한 번에 통째로 띄우고, 왜 그런지는 어디에도 안 남는다.
407
+ * 그림 칸·추론 칸은 같은 판에서 이 규칙을 적용했는데 이 칸만 빠져 있었다.
408
+ */
409
+ facts.streaming = chunks > 2 || !!읽다실패;
304
410
 
305
411
  // 4. 도구 호출 — 에이전트의 생사가 걸린 검사.
412
+ //
413
+ // 실제로 준 상한을 **한 번만 셈해서** 아래 화면 글에도 그대로 쓴다. 두 자리에
414
+ // 따로 적어 두면 추론 모델에서 어긋난다 — 1024 로 물어 놓고 화면에는 512 라고
415
+ // 적었고, 그 줄을 읽은 사람은 엉뚱한 자리를 올린다 (8회차 뒷단-배우기).
416
+ const 도구상한 = 상한(512);
306
417
  const tl = await call({
307
418
  ...quiet,
308
419
  messages: [{ role: 'user', content: 'config.json 파일을 읽어 주세요.' }],
309
420
  tools: [READ_TOOL],
310
- maxTokens: 512,
421
+ maxTokens: 도구상한,
311
422
  timeout: 90000,
312
423
  });
313
424
  const tcalls = tl.ok ? 읽기(tl).toolCalls : [];
@@ -328,14 +439,22 @@ export async function probe(conn, onStep = () => {}) {
328
439
  * 잘림을 남의 흠으로 적지 않는다 (34차 리뷰).
329
440
  */
330
441
  const 잘린인자 = gotCall && tcalls[0]?.argsBroken === true;
331
- const 원문에있나 = 잘린인자 && String(tcalls[0]?.rawArgs ?? '').includes('config');
442
+ /*
443
+ * argsBroken 은 이제 「잘림」 과 「끝까지 온 틀린 JSON」 둘 다다 — 어댑터가 argsCut 으로 가른다
444
+ * (사냥5 L5-5 · adapter.js 잘린모양인가). argsCut 이 false 면 우리 상한 탓이 아니다. 여기서 원문에
445
+ * config 가 있다고 「잘렸을 뿐」 · ok 로 적으면 홑따옴표로 적는 모델을 우리 탓으로 덮고 좋은 모델로
446
+ * 판정한다 — 위 머리말과 거꾸로 된 틀림이다.
447
+ */
448
+ const 모양틀림 = 잘린인자 && tcalls[0]?.argsCut === false;
449
+ const 원문에있나 = 잘린인자 && !모양틀림 && String(tcalls[0]?.rawArgs ?? '').includes('config');
332
450
  add({
333
451
  id: 'tools',
334
452
  label: '도구 호출',
335
453
  status: gotCall ? (argOk || 원문에있나 ? 'ok' : 'warn') : 'no',
336
454
  detail: gotCall
337
455
  ? `${tcalls[0]?.name} 호출됨${argOk ? ''
338
- : 원문에있나 ? ` — 인자가 우리 상한(${512}토큰)에 잘렸을 뿐, 값은 제대로 왔습니다`
456
+ : 원문에있나 ? ` — 인자가 우리 상한(${도구상한}토큰)에 잘렸을 뿐, 값은 제대로 왔습니다`
457
+ : 모양틀림 ? ' — 인자 JSON 모양이 틀렸습니다 (홑따옴표·끝 쉼표 같은 것 — 편집 도구가 자주 거절당합니다)'
339
458
  : 잘린인자 ? ' — 인자 JSON 이 잘려 왔습니다 (상한을 올려 다시 보세요)'
340
459
  : ' — 인자가 부정확, 편집 신뢰성 작업이 더 필요합니다'}`
341
460
  : tl.ok ? '도구를 안 부르고 글로만 답합니다' : serverMessage(tl),
@@ -366,7 +485,7 @@ export async function probe(conn, onStep = () => {}) {
366
485
  { role: 'user', content: 'port 값이 몇인가요? 숫자만 답하세요.' },
367
486
  ],
368
487
  tools: [READ_TOOL],
369
- maxTokens: 256,
488
+ maxTokens: 상한(256),
370
489
  });
371
490
  const said = rt.ok ? 읽기(rt).content : '';
372
491
  add({
@@ -389,16 +508,25 @@ export async function probe(conn, onStep = () => {}) {
389
508
  required: ['answer'],
390
509
  additionalProperties: false,
391
510
  };
392
- // 한 번은 흔들릴 수 있으므로 실패하면 한 번만 더 본다.
511
+ /*
512
+ * 한 번은 흔들릴 수 있으므로 실패하면 한 번만 더 본다.
513
+ *
514
+ * 그 「실패」 를 `!parsed` 로 적어 뒀다. 그러면 **스키마를 어겨도 JSON 으로만
515
+ * 읽히면** 다시 안 물었다 — `{"result":21}` 한 번에 `warn` 이 되고, 그 판정이
516
+ * 프로필에 `json:false` 로 남는다. 한 번 흔들린 것과 못 하는 것을 가르자고 둔
517
+ * 되풀이인데, 정작 제일 흔한 흔들림(칸 이름을 한 번 잘못 적는 것)에서 안 돌았다.
518
+ * 여기서 보려는 것은 「읽히나」 가 아니라 **「스키마를 지키나」** 다.
519
+ */
520
+ const 스키마맞나 = (x) => !!(x && 'answer' in x);
393
521
  let js = null;
394
522
  let parsed = null;
395
523
  let raw = '';
396
- for (let attempt = 0; attempt < 2 && !parsed; attempt++) {
524
+ for (let attempt = 0; attempt < 2 && !스키마맞나(parsed); attempt++) {
397
525
  js = await call({
398
526
  ...quiet,
399
527
  messages: [{ role: 'user', content: '3 곱하기 7은?' }],
400
528
  json: schema,
401
- maxTokens: 512,
529
+ maxTokens: 상한(512),
402
530
  timeout: 90000,
403
531
  });
404
532
  if (!js.ok) break;
@@ -417,7 +545,7 @@ export async function probe(conn, onStep = () => {}) {
417
545
  if (읽힌것 && typeof 읽힌것 === 'object' && !Array.isArray(읽힌것)) parsed = 읽힌것;
418
546
  } catch { /* 글로만 답하는 서버 — 아래에서 '스키마를 안 지킴' 으로 적는다 */ }
419
547
  }
420
- const jsonOk = !!(parsed && 'answer' in parsed);
548
+ const jsonOk = 스키마맞나(parsed);
421
549
  add({
422
550
  id: 'json',
423
551
  label: '구조적 출력',
@@ -448,7 +576,7 @@ export async function probe(conn, onStep = () => {}) {
448
576
  let 눈 = await call({
449
577
  ...quiet,
450
578
  messages: [눈검사메시지(shape)],
451
- maxTokens: 32,
579
+ maxTokens: 상한(32),
452
580
  timeout: 60000,
453
581
  });
454
582
  /*
@@ -469,7 +597,7 @@ export async function probe(conn, onStep = () => {}) {
469
597
  let 눈읽은것 = 읽기(눈);
470
598
  if (눈.ok && !눈읽은것.content && 상한에걸렸나(눈읽은것)) {
471
599
  const 다시 = await call({
472
- ...quiet, messages: [눈검사메시지(shape)], maxTokens: 512, think: false, timeout: 90000,
600
+ ...quiet, messages: [눈검사메시지(shape)], maxTokens: 상한(512), think: false, timeout: 90000,
473
601
  });
474
602
  if (다시.ok) { 눈 = 다시; 눈읽은것 = 읽기(다시); }
475
603
  }
@@ -538,10 +666,24 @@ export async function probe(conn, onStep = () => {}) {
538
666
  capped: 상한에걸렸나(e),
539
667
  });
540
668
  } else {
541
- seen.push({ lv, ms: r.ms, err: serverMessage(r) });
669
+ seen.push({ lv, ms: r.ms, err: serverMessage(r), status: r.status });
542
670
  }
543
671
  }
544
672
  const bothOk = seen.every((s) => !s.err);
673
+ /*
674
+ * ── 거절과 못 잰 것을 가른다 ────────────────────────────────────────────
675
+ *
676
+ * 사람이 본 것: 이 칸이 ✗ 「파라미터 거부됨 — Unrecognized request argument supplied: reasoning_effort」
677
+ * 인데 프로필에는 think: true 가 저장됐다. 그 뒤로 매 요청에 서버가 거절한 그 칸을 실어 보냈다.
678
+ *
679
+ * 실제로 있었던 일: 아래 `facts.think = differs || !잴것이있었나` 에서 잴것이있었나 가 bothOk 를 품고
680
+ * 있어, 요청이 **실패하기만 하면** 「못 쟀다 → 켠 채로」 가 됐다. 서버가 분명히 거절한 것까지 (6회차
681
+ * Gemini 더듬6 B1).
682
+ *
683
+ * 이제 지키는 규칙: 4xx(시간 초과 408·속도 429 빼고)는 서버가 **이 칸을 안 받는다고 말한 것** — false.
684
+ * 429·5xx·끊김은 아무 말도 안 한 것이라 위 그림 칸 머리말대로 켠 채로 둔다.
685
+ */
686
+ const 거절됨 = seen.some((s) => s.err && s.status >= 400 && s.status < 500 && s.status !== 408 && s.status !== 429);
545
687
  const capped = bothOk && seen.every((s) => s.capped);
546
688
  // 사고 길이가 눈에 띄게 다르거나, 출력량이 20% 넘게 차이나야 "먹는다"고 본다.
547
689
  const thoughtGap = bothOk ? Math.abs(seen[0].thought - seen[1].thought) : 0;
@@ -565,21 +707,31 @@ export async function probe(conn, onStep = () => {}) {
565
707
  add({
566
708
  id: 'think',
567
709
  label: '추론 강도 조절',
568
- status: !bothOk ? 'no' : capped ? 'warn' : differs ? 'ok' : 'warn',
710
+ status: !bothOk ? (거절됨 ? 'no' : 'warn') : capped ? 'warn' : differs ? 'ok' : 'warn',
569
711
  detail: !bothOk
570
- ? `파라미터 거부됨 — ${seen.find((s) => s.err)?.err}`
712
+ ? 거절됨
713
+ ? `파라미터 거부됨 — ${seen.find((s) => s.err)?.err}`
714
+ : `못 쟀습니다 — 서버가 지금 답을 못 했습니다 (${seen.find((s) => s.err)?.err}). 조절은 켠 채로 둡니다.`
571
715
  : capped
572
- ? `둘 다 토큰 상한(${THINK_CAP})에 걸려 비교 불가 — 루프 층에서 조절합니다`
716
+ ? `둘 다 토큰 상한(${THINK_CAP})에 걸려 비교 불가 — 못 쟀습니다. 조절은 켠 채로 둡니다.`
573
717
  : differs
574
718
  ? `${fmt(seen[0])} · ${fmt(seen[1])}`
575
719
  : 잴것이있었나
576
- ? `차이 없음 (${fmt(seen[0])} · ${fmt(seen[1])}) — 루프 층에서 조절합니다`
720
+ ? `차이 없음 (${fmt(seen[0])} · ${fmt(seen[1])}) — 강도를 바꿔도 답이 안 바뀝니다`
577
721
  : `못 쟀습니다 — 사고도 usage 도 안 주는 창구입니다 (${fmt(seen[0])} · ${fmt(seen[1])}).`
578
722
  + ' 조절은 켠 채로 둡니다 — 안 받는 창구면 서버가 거절하면서 말해 주고, 그때 배웁니다.',
579
723
  ms: seen.reduce((a, s) => a + (s.ms ?? 0), 0),
580
724
  });
581
- // 못 쟀으면 켠 채로 둔다 — 되돌아올 수 있는 쪽으로 틀린다 (위 그림 칸 머리말).
582
- facts.think = differs || !잴것이있었나;
725
+ /*
726
+ * 못 쟀으면 켠 채로 둔다 — 되돌아올 수 있는 쪽으로 틀린다 (위 그림 칸 머리말).
727
+ *
728
+ * `capped` 가 이 셈에서 빠져 있었다. 그래서 화면에는 「비교 불가」 라고 적고
729
+ * 같은 줄에서 조절을 껐다 — **못 쟀는데 안 된다고 적는** 바로 그 자리다
730
+ * (아래 「잴 칸이 없었던 판」 머리말이 말하는 것과 같은 틀림). 둘 다 상한에
731
+ * 걸린 것은 이 모델이 강도를 안 받는다는 말이 아니라 우리가 자리를 좁게 준
732
+ * 것이다 (8회차 뒷단-배우기).
733
+ */
734
+ facts.think = differs || (bothOk ? (capped || !잴것이있었나) : !거절됨);
583
735
 
584
736
  // 8. 컨텍스트 길이 — 파일을 몇 개까지 한 번에 읽힐 수 있느냐.
585
737
  //
@@ -22,23 +22,55 @@
22
22
  // 지원하지 않는 것은 그렇다고 말한다: https:// 프록시, socks5://, PAC 파일, 윈도우
23
23
  // 레지스트리 프록시, NTLM/Negotiate 인증. 조용히 직접 가는 것보다 낫다.
24
24
 
25
- const 루프백 = (h) => {
26
- const s = String(h ?? '').toLowerCase().replace(/^\[|\]$/g, '');
27
- return s === 'localhost' || s === '::1' || /^127\./.test(s) || s === '0.0.0.0';
28
- };
25
+ import { 루프백인가, 숫자주소펴기, 여섯펴기 } from '../safety/network.js';
26
+
27
+ /*
28
+ * 루프백은 safety/network.js 가 편다 (루프백인가).
29
+ *
30
+ * 여기는 `/^127\./` 로 **글자 앞머리**를 봤다(사냥4 W15). 그래서 `127.example.com` 은
31
+ * 이름인데 루프백으로 읽혀 프록시를 건너뛰었고 — 바깥으로 직접 못 나가는 사내에서는
32
+ * 그대로 「연결 실패」 다 — 진짜 루프백인 `[::ffff:127.0.0.1]` 은 프록시로 돌아 나갔다.
33
+ * network.js 가 같은 탈을 먼저 고쳤는데 여기만 옛 잣대로 남아 있었다. 잣대를 두 벌 두면
34
+ * 늘 한쪽만 고쳐진다 — 그래서 옮겨 적지 않고 불러다 쓴다.
35
+ */
36
+ const 루프백 = (h) => 루프백인가(h);
37
+
38
+ /*
39
+ * 탈 글에 실을 주소 — 사람 이름·비밀번호를 가린다.
40
+ *
41
+ * 탈 글은 화면과 진단 보고서에 그대로 찍힌다. `http://user:secret@…` 를 못 읽었다고
42
+ * 원문째 적으면, 비밀번호를 파일에 안 남기려고 해 둔 것들이 그 한 줄로 무너진다.
43
+ */
44
+ const 가린주소 = (s) => String(s).replace(/^((?:[a-z][a-z0-9+.-]*:)?\/\/)?[^/@\s]*@/i, '$1…@');
29
45
 
30
46
  /** 'http://user:pass@host:8080' → { url, host, port, auth }. 못 읽으면 { 탈 } */
31
47
  export function 프록시읽기(값) {
32
48
  const s = String(값 ?? '').trim();
33
49
  if (!s) return null;
34
50
  let u;
35
- try { u = new URL(/^[a-z][a-z0-9+.-]*:\/\//i.test(s) ? s : `http://${s}`); } catch { return { 탈: `프록시 주소를 읽을 수 없습니다: ${s}` }; }
51
+ try { u = new URL(/^[a-z][a-z0-9+.-]*:\/\//i.test(s) ? s : `http://${s}`); } catch { return { 탈: `프록시 주소를 읽을 수 없습니다: ${가린주소(s)}` }; }
36
52
  if (u.protocol !== 'http:') {
37
- return { 탈: `${u.protocol}// 프록시는 지원하지 않습니다 (http:// 프록시만 됩니다): ${s}` };
53
+ return { 탈: `${u.protocol}// 프록시는 지원하지 않습니다 (http:// 프록시만 됩니다): ${가린주소(s)}` };
54
+ }
55
+ /*
56
+ * ── 사람 이름·비밀번호를 못 푸는 판 (사냥4 W6) ────────────────────────
57
+ *
58
+ * 비밀번호에 맨 `%` 가 들어 있으면(`p%zz` · `100%`) URL 은 읽히는데 decodeURIComponent
59
+ * 가 URIError 를 던졌다. 여기는 켤 때(config.load → 프록시정하기) 부르는 자리라, 그
60
+ * 한 번의 던짐이 **모든 실행을** 「URI malformed」 한 줄로 끝냈다 — 무엇이 틀렸는지
61
+ * 말해 줄 deel doctor 조차 못 띄운다. 비밀번호에 % 가 드는 일은 드물지 않다.
62
+ *
63
+ * 머리말이 약속한 대로 { 탈 } 로 남기고 직접 간다. 무엇을 고치면 되는지(%25)까지
64
+ * 말하되, 비밀번호는 싣지 않는다.
65
+ */
66
+ let auth = null;
67
+ if (u.username) {
68
+ try {
69
+ auth = `Basic ${Buffer.from(`${decodeURIComponent(u.username)}:${decodeURIComponent(u.password)}`).toString('base64')}`;
70
+ } catch {
71
+ return { 탈: `프록시 주소의 사용자·비밀번호를 읽을 수 없습니다 — 글자 % 는 %25 로 적어야 합니다: http://…@${u.host}` };
72
+ }
38
73
  }
39
- const auth = u.username
40
- ? `Basic ${Buffer.from(`${decodeURIComponent(u.username)}:${decodeURIComponent(u.password)}`).toString('base64')}`
41
- : null;
42
74
  return {
43
75
  url: `http://${u.host}`,
44
76
  host: u.hostname,
@@ -60,16 +92,64 @@ export function 우회할까(hostname, port, 목록) {
60
92
  if (항목 === '*') return true;
61
93
  let 이름 = 항목;
62
94
  let 포트 = null;
63
- const m = /^(.*):(\d+)$/.exec(항목);
64
- if (m && !/^\[?[0-9a-f:]+\]?$/i.test(항목)) { 이름 = m[1]; 포트 = Number(m[2]); }
95
+ /*
96
+ * ── 세 꼴이 안 맞았다 (사냥4 W14) ──────────────────────────────────
97
+ *
98
+ * [fd00::5] 대괄호를 안 벗겨서 글자가 끝내 달랐다
99
+ * [fd00::5]:8080 통째로 IPv6 로 보고 포트를 안 갈랐다
100
+ * 10.0.0.0/8 대역을 글자로 견줬다
101
+ *
102
+ * curl · Go · Python 이 다 받는 꼴이라 사내 NO_PROXY 에 흔히 적혀 있다. 안 맞으면
103
+ * 사내 대역이 **조용히** 프록시로 돌아 나간다 — 되면 느리고, 안 되면 「연결 실패」 다.
104
+ * 포트를 가르는 것은 이제 「콜론 앞에 콜론이 없을 때」 뿐이다. `fd00::5` 는 가르지 않는다.
105
+ */
106
+ const 괄호 = /^\[([^\]]+)\](?::(\d+))?$/.exec(항목);
107
+ if (괄호) { 이름 = 괄호[1]; 포트 = 괄호[2] ? Number(괄호[2]) : null; }
108
+ else {
109
+ const m = /^(.*):(\d+)$/.exec(항목);
110
+ if (m && !m[1].includes(':')) { 이름 = m[1]; 포트 = Number(m[2]); }
111
+ }
65
112
  if (포트 !== null && 포트 !== p) continue;
113
+ if (이름.includes('/')) { if (대역에드나(h, 이름)) return true; continue; }
66
114
  이름 = 이름.replace(/^\*/, '').replace(/^\./, '');
67
115
  if (!이름) continue;
68
116
  if (h === 이름 || h.endsWith(`.${이름}`)) return true;
117
+ // IPv6 는 같은 주소를 여러 철자로 적는다(fd00::5 · fd00:0::5). 펴서 견준다.
118
+ if (h.includes(':') && 이름.includes(':')) {
119
+ const 가 = 여섯펴기(h);
120
+ const 나 = 여섯펴기(이름);
121
+ if (가 && 나 && 가.every((x, i) => x === 나[i])) return true;
122
+ }
69
123
  }
70
124
  return false;
71
125
  }
72
126
 
127
+ /**
128
+ * 이 주소가 `10.0.0.0/8` · `fd00::/8` 같은 대역 안인가. 이름에는 안 걸린다.
129
+ * 주소를 펴는 것은 safety/network.js 의 것을 쓴다 — `10.1` 같은 줄인 꼴도 거기서 편다.
130
+ */
131
+ function 대역에드나(h, 대역) {
132
+ const [밑, 길이글] = String(대역).split('/');
133
+ if (!/^\d{1,3}$/.test(길이글 ?? '')) return false;
134
+ const 길이 = Number(길이글);
135
+ const 넷밑 = 숫자주소펴기(밑);
136
+ const 넷h = 숫자주소펴기(h);
137
+ if (넷밑 && 넷h) {
138
+ if (길이 > 32) return false;
139
+ const 수 = (x) => x.split('.').reduce((n, v) => n * 256 + Number(v), 0);
140
+ const 칸 = 2 ** (32 - 길이);
141
+ return Math.floor(수(넷h) / 칸) === Math.floor(수(넷밑) / 칸);
142
+ }
143
+ const 여섯밑 = 여섯펴기(밑);
144
+ const 여섯h = 여섯펴기(h);
145
+ if (!여섯밑 || !여섯h || 길이 > 128) return false;
146
+ for (let i = 0, 남은 = 길이; i < 8 && 남은 > 0; i++, 남은 -= 16) {
147
+ const 가림 = (0xffff << (16 - Math.min(16, 남은))) & 0xffff;
148
+ if ((여섯밑[i] & 가림) !== (여섯h[i] & 가림)) return false;
149
+ }
150
+ return true;
151
+ }
152
+
73
153
  let 지금 = { 프록시: null, 출처: null, 우회: [], 로컬우회: true, 탈: null };
74
154
 
75
155
  /**
@@ -134,9 +214,7 @@ export function 프록시고르기(url) {
134
214
  if (!것) return null;
135
215
  let u;
136
216
  try { u = new URL(url); } catch { return null; }
137
- if (지금.로컬우회 && 루프백(u.hostname)) return null;
138
- const 포트 = Number(u.port || (u.protocol === 'https:' ? 443 : 80));
139
- if (우회할까(u.hostname, 포트, 지금.우회)) return null;
217
+ if (프록시비켜가나(u)) return null;
140
218
 
141
219
  if (지금.출처 === 'config') return { ...것, 출처: 'config' };
142
220
  const k = u.protocol === 'https:' ? 'https' : 'http';
@@ -145,6 +223,22 @@ export function 프록시고르기(url) {
145
223
  return { ...p, 출처: 것[k] ? 것.출처[k] : 것.출처.all };
146
224
  }
147
225
 
226
+ /**
227
+ * 이 주소는 프록시가 있어도 안 거치나 — 루프백(로컬우회) · NO_PROXY.
228
+ *
229
+ * 프록시고르기 가 가르는 자리이고, 진단 보고서도 같은 것으로 가른다: 못 쓰는 프록시(socks5 등)를
230
+ * 알리는 줄은 **어차피 안 거칠 주소**에는 안 낸다. 로컬 모델이 안 붙는 까닭을 찾는 사람을
231
+ * 상관없는 프록시 쪽으로 보내게 된다 (2.0.0 6회차 RP1).
232
+ * @param {string|URL} url 못 읽는 주소면 false
233
+ */
234
+ export function 프록시비켜가나(url) {
235
+ let u;
236
+ try { u = url instanceof URL ? url : new URL(url); } catch { return false; }
237
+ if (지금.로컬우회 && 루프백(u.hostname)) return true;
238
+ const 포트 = Number(u.port || (u.protocol === 'https:' ? 443 : 80));
239
+ return 우회할까(u.hostname, 포트, 지금.우회);
240
+ }
241
+
148
242
  /** 검사용 — 아무것도 안 정한 상태로. */
149
243
  export function 프록시지우기() {
150
244
  지금 = { 프록시: null, 출처: null, 우회: [], 로컬우회: true, 탈: null };