deel-local-cli 1.1.0 → 1.2.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.en.md +347 -9
- package/README.md +317 -6
- package/package.json +2 -2
- package/src/agent/budget.js +0 -14
- package/src/agent/compact.js +87 -0
- package/src/agent/evolve.js +213 -0
- package/src/agent/loop.js +38 -1
- package/src/agent/modes.js +2 -0
- package/src/agent/route.js +55 -3
- package/src/agent/session.js +145 -1
- package/src/agent/threads.js +154 -0
- package/src/backend/http.js +41 -4
- package/src/commands.js +268 -4
- package/src/config.js +10 -1
- package/src/oneshot.js +4 -0
- package/src/preview/serve.js +326 -0
- package/src/repl.js +210 -34
- package/src/safety/guard.js +37 -2
- package/src/safety/undo.js +1 -1
- package/src/setup.js +2 -2
- package/src/skills/builtin//352/262/200/354/202/254-/353/250/274/354/240/200/SKILL.md +64 -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 +78 -0
- package/src/skills/builtin//353/201/235/352/271/214/354/247/200-/355/225/230/352/270/260/SKILL.md +65 -0
- package/src/skills/builtin//354/212/244/354/212/244/353/241/234-/352/262/200/355/206/240/SKILL.md +74 -0
- package/src/skills/builtin//354/260/224/353/237/254/353/263/264/352/270/260/SKILL.md +59 -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 +73 -0
- package/src/skills/builtin//354/275/224/353/223/234-/354/244/204/354/235/264/352/270/260/SKILL.md +66 -0
- package/src/skills/discover.js +18 -3
- package/src/tools/edit-match.js +16 -5
- package/src/tools/excel-com.js +1 -1
- package/src/tools/excel.js +1 -1
- package/src/tools/fsutil.js +0 -8
- package/src/tools/index.js +1 -1
- package/src/tools/todo.js +19 -3
- package/src/ui/ansi.js +6 -2
- package/src/ui/inputbox.js +37 -8
- package/src/ui/md.js +227 -0
- package/src/ui/motion.js +212 -0
- package/src/ui/prompt.js +1 -1
- package/src/ui/screen.js +1 -1
- package/src/ui/status.js +14 -1
- package/src/ui/working.js +0 -3
- package/src/ui/wrap.js +1 -8
package/src/agent/session.js
CHANGED
|
@@ -112,6 +112,12 @@ export class Session {
|
|
|
112
112
|
this.stepsSet = maxSteps != null;
|
|
113
113
|
this.maxSteps = maxSteps ?? 24;
|
|
114
114
|
this.messages = [];
|
|
115
|
+
// 추정 × 보정 = 실제. 서버가 알려 주는 진짜 토큰 수로 매 턴 고쳐 나간다.
|
|
116
|
+
// 1 은 '아직 안 배웠다' 이고, 그때는 추정을 그대로 쓴다. 배운다() 를 볼 것.
|
|
117
|
+
this.보정 = 1;
|
|
118
|
+
this.보정잰것 = 0;
|
|
119
|
+
// 겪어 본 것 요약 (agent/evolve.js). 켤 때 repl 이 채운다.
|
|
120
|
+
this.배움요약 = null;
|
|
115
121
|
this.filesRead = new Map(); // 경로 → 추정 토큰
|
|
116
122
|
this.changes = new Map(); // 경로 → {added, removed, times}. /diff 가 본다
|
|
117
123
|
this.skills = []; // 켜질 때 이 PC 에서 찾은 것들
|
|
@@ -219,6 +225,15 @@ export class Session {
|
|
|
219
225
|
* 바뀌는 셈이 된다 — 무엇 때문에 답이 달라졌는지 알 길이 없어진다.
|
|
220
226
|
*/
|
|
221
227
|
if (this.memory) parts.push(this.memory);
|
|
228
|
+
/*
|
|
229
|
+
* 이 PC·이 폴더에서 겪어 본 것 (agent/evolve.js).
|
|
230
|
+
*
|
|
231
|
+
* 기억(memory)은 사람이 적어 주는 것이고, 이건 **겪어서 저절로 쌓인 것**이다.
|
|
232
|
+
* 여기 한 줄이 헛도는 걸음 서너 개를 없앤다 — 안 되는 명령을 또 부르고,
|
|
233
|
+
* 잘릴 걸 알면서 큰 Write 를 또 보내는 걸음들이다. 그래서 자리를 내줄 값이 있다.
|
|
234
|
+
* 상한은 evolve.js 가 못 박는다(220토큰).
|
|
235
|
+
*/
|
|
236
|
+
if (this.배움요약) parts.push(this.배움요약);
|
|
222
237
|
const listed = this.listedSkills();
|
|
223
238
|
if (listed.length) {
|
|
224
239
|
parts.push(
|
|
@@ -282,7 +297,41 @@ export class Session {
|
|
|
282
297
|
* 매 요청에 통째로 들어가는데 어느 칸에도 안 잡혔다.
|
|
283
298
|
* · 지금 모드 문구 — modes.js 의 say. 모드마다 수백 토큰이다.
|
|
284
299
|
*/
|
|
285
|
-
|
|
300
|
+
/**
|
|
301
|
+
* 추정을 실제에 맞춰 간다.
|
|
302
|
+
*
|
|
303
|
+
* estimateTokens 는 추정이다 — 토크나이저를 안 싣기 때문이다(의존성 0개).
|
|
304
|
+
* 그런데 서버는 매 응답에 **진짜 값**을 실어 준다(usage.prompt_tokens ·
|
|
305
|
+
* prompt_eval_count). 여태 그 값은 /cost 에만 쓰고 버렸다.
|
|
306
|
+
*
|
|
307
|
+
* 안 맞으면 두 가지가 조용히 나빠진다.
|
|
308
|
+
* · 적게 잡으면 — 남은 자리를 넉넉히 보고 답 상한을 크게 잡는다. 답이 잘린다.
|
|
309
|
+
* · 많이 잡으면 — 아직 자리가 있는데 접기 시작한다. 창을 놀린다.
|
|
310
|
+
* 둘 다 화면에는 아무 말도 안 뜬다. 그래서 스스로 재게 한다.
|
|
311
|
+
*
|
|
312
|
+
* 조심한 것:
|
|
313
|
+
* · 실제값을 안 주는 서버가 있다. 0 이면 아무것도 안 배운다.
|
|
314
|
+
* · 표본이 작으면 비율이 튄다. 첫 몇백 토큰짜리 대화는 건너뛴다.
|
|
315
|
+
* · 0.5~2배 밖은 안 믿는다. 서버가 딴 것을 세고 있을 수 있다.
|
|
316
|
+
* · 되먹임이 겹치지 않게, 비교는 **보정 안 먹인 추정**으로 한다.
|
|
317
|
+
*
|
|
318
|
+
* @returns {number|null} 새 보정 배수. 못 배웠으면 null
|
|
319
|
+
*/
|
|
320
|
+
배운다(실제) {
|
|
321
|
+
const n = Number(실제);
|
|
322
|
+
if (!Number.isFinite(n) || n <= 0) return null;
|
|
323
|
+
const 추정 = this.#원추정().used;
|
|
324
|
+
if (추정 < 200) return null;
|
|
325
|
+
const 비율 = n / 추정;
|
|
326
|
+
if (비율 < 0.5 || 비율 > 2) return null;
|
|
327
|
+
// 첫 번은 그대로 받고, 그 뒤로는 천천히 따라간다. 한 번 튄 값에 안 휘둘린다.
|
|
328
|
+
this.보정 = this.보정잰것 ? this.보정 + (비율 - this.보정) * 0.3 : 비율;
|
|
329
|
+
this.보정잰것++;
|
|
330
|
+
return this.보정;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** 보정을 안 먹인 날 추정. 배운다() 가 견주는 값이다. */
|
|
334
|
+
#원추정() {
|
|
286
335
|
// 폴더 지문도 매 요청에 통째로 나간다. 시스템 프롬프트 쪽에 같이 센다 —
|
|
287
336
|
// 안 세면 '남은 자리' 가 그만큼 뻥튀기되고, effort.js 가 그 값으로 출력
|
|
288
337
|
// 상한을 잡으므로 답이 조용히 잘리기 시작한다.
|
|
@@ -315,6 +364,7 @@ export class Session {
|
|
|
315
364
|
{ label: '시스템 프롬프트', n: sys },
|
|
316
365
|
{ label: this.rules ? `규칙 (${this.rules.name})` : '규칙 (없음)', n: rules },
|
|
317
366
|
{ label: `기억 (${기억줄}줄)`, n: 기억 },
|
|
367
|
+
{ label: '겪어 본 것', n: this.배움요약 ? estimateTokens(this.배움요약) : 0 },
|
|
318
368
|
{ label: `스킬 목록 (${listed.length}/${this.skills.length}개)`, n: skills },
|
|
319
369
|
{ label: '도구 정의', n: 도구 },
|
|
320
370
|
{ label: '대화 이력', n: history },
|
|
@@ -325,6 +375,27 @@ export class Session {
|
|
|
325
375
|
return { rows, used, total, left: Math.max(0, total - used) };
|
|
326
376
|
}
|
|
327
377
|
|
|
378
|
+
/**
|
|
379
|
+
* 화면과 예산이 보는 값. 배운 보정을 먹여서 내놓는다.
|
|
380
|
+
*
|
|
381
|
+
* 줄마다 보정을 먹인다 — 합계만 고치면 표의 줄을 더한 값과 합계가 안 맞아서,
|
|
382
|
+
* 보는 사람이 어느 쪽을 믿어야 할지 모르게 된다.
|
|
383
|
+
*/
|
|
384
|
+
breakdown() {
|
|
385
|
+
const 날것 = this.#원추정();
|
|
386
|
+
if (!(this.보정잰것 > 0) || this.보정 === 1) return 날것;
|
|
387
|
+
const rows = 날것.rows.map((r) => ({ ...r, n: Math.round(r.n * this.보정) }));
|
|
388
|
+
const used = rows.reduce((a, r) => a + r.n, 0);
|
|
389
|
+
return {
|
|
390
|
+
rows,
|
|
391
|
+
used,
|
|
392
|
+
total: 날것.total,
|
|
393
|
+
left: Math.max(0, 날것.total - used),
|
|
394
|
+
보정: this.보정,
|
|
395
|
+
보정잰것: this.보정잰것,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
328
399
|
/**
|
|
329
400
|
* 도구 정의가 몇 토큰인가.
|
|
330
401
|
*
|
|
@@ -408,3 +479,76 @@ export function safeHead(messages, k) {
|
|
|
408
479
|
while (h > 0 && messages[h - 1]?.tool_calls?.length) h--;
|
|
409
480
|
return h;
|
|
410
481
|
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* 도중에 죽은 대화를 다시 쓸 수 있게 손본다.
|
|
485
|
+
*
|
|
486
|
+
* 왜 필요한가:
|
|
487
|
+
* store 는 메시지가 오갈 때마다 즉시 적는다(jsonl). 그래서 도구를 **돌리는
|
|
488
|
+
* 도중에** 죽으면 `assistant(tool_calls)` 만 적히고 그 결과가 없다. 긴 Bash 를
|
|
489
|
+
* 돌리는 중, 전원이 나가는 중, 창을 닫는 중 — 흔한 자리다.
|
|
490
|
+
*
|
|
491
|
+
* 그 이력을 그대로 이어받아 보내면 OpenAI 규격 서버는 400 을 낸다. 호출 뒤에는
|
|
492
|
+
* 결과가 와야 한다는 규격이다. compact.js 가 safeCut 으로 막고 있는 바로 그
|
|
493
|
+
* 사고인데, **이어받기 길에는 그 안전망이 없었다.** 이어받자마자 첫 마디에서
|
|
494
|
+
* 죽으니, 사람 눈에는 '이어하기가 고장 났다' 로 보인다.
|
|
495
|
+
*
|
|
496
|
+
* 무엇을 하나:
|
|
497
|
+
* 결과가 없는 호출을 **지운다.** 가짜 결과를 채우지 않는다 — 안 돌아간 도구를
|
|
498
|
+
* 돌았다고 적으면 모델이 그 거짓말 위에서 계속한다. 파일을 안 고쳤는데 고친
|
|
499
|
+
* 줄 알고 다음 단계로 넘어가는 것이 제일 나쁘다.
|
|
500
|
+
* 호출이 전부 빠졌는데 할 말도 없으면 그 메시지째 지운다.
|
|
501
|
+
* 짝 없는 결과(앞에 호출이 없는 tool)도 지운다 — 머리가 잘려 나간 이력이다.
|
|
502
|
+
*
|
|
503
|
+
* 규격이 둘이라 짝짓는 법도 둘이다 (adapter.js 의 toolMessage):
|
|
504
|
+
* OpenAI { role:'tool', tool_call_id } → id 로 짝짓는다
|
|
505
|
+
* Ollama { role:'tool', tool_name } → id 가 없다. 순서로 짝짓는다
|
|
506
|
+
*
|
|
507
|
+
* @returns {{messages: object[], 고친것: number}} 고친것 = 걷어낸 호출·결과 수
|
|
508
|
+
*/
|
|
509
|
+
export function repairToolPairs(messages) {
|
|
510
|
+
const out = [];
|
|
511
|
+
let 고친것 = 0;
|
|
512
|
+
|
|
513
|
+
for (let i = 0; i < (messages?.length ?? 0); i++) {
|
|
514
|
+
const m = messages[i];
|
|
515
|
+
|
|
516
|
+
// 적다 만 줄. 도중에 죽으면 JSONL 한 줄이 반만 적히고, 그 자리가 빈 값이나
|
|
517
|
+
// role 없는 조각으로 읽힌다. 그대로 보내면 서버가 거절한다 — 걷어내는 것이
|
|
518
|
+
// 이 함수의 일이므로 여기서 같이 턴다.
|
|
519
|
+
if (!m || typeof m !== 'object' || typeof m.role !== 'string') { 고친것++; continue; }
|
|
520
|
+
|
|
521
|
+
// 호출 없이 굴러다니는 결과. 앞이 잘려 나간 이력이다.
|
|
522
|
+
if (m.role === 'tool') { 고친것++; continue; }
|
|
523
|
+
|
|
524
|
+
if (m?.role !== 'assistant' || !m.tool_calls?.length) { out.push(m); continue; }
|
|
525
|
+
|
|
526
|
+
// 이 호출에 딸린 결과 묶음 — 바로 뒤에 붙어 있는 tool 들이 전부다.
|
|
527
|
+
const 결과 = [];
|
|
528
|
+
let j = i + 1;
|
|
529
|
+
while (j < messages.length && messages[j]?.role === 'tool') 결과.push(messages[j++]);
|
|
530
|
+
i = j - 1; // 결과는 여기서 같이 처리한다
|
|
531
|
+
|
|
532
|
+
const 있는id = new Set(결과.map((r) => r?.tool_call_id).filter(Boolean));
|
|
533
|
+
const 남길호출 = 있는id.size
|
|
534
|
+
? m.tool_calls.filter((c) => 있는id.has(c?.id))
|
|
535
|
+
: m.tool_calls.slice(0, 결과.length); // id 가 없는 규격 — 순서로 본다
|
|
536
|
+
const 남길id = new Set(남길호출.map((c) => c?.id).filter(Boolean));
|
|
537
|
+
const 남길결과 = 있는id.size
|
|
538
|
+
? 결과.filter((r) => 남길id.has(r.tool_call_id))
|
|
539
|
+
: 결과.slice(0, 남길호출.length);
|
|
540
|
+
|
|
541
|
+
고친것 += (m.tool_calls.length - 남길호출.length) + (결과.length - 남길결과.length);
|
|
542
|
+
|
|
543
|
+
if (남길호출.length) {
|
|
544
|
+
out.push(남길호출.length === m.tool_calls.length ? m : { ...m, tool_calls: 남길호출 });
|
|
545
|
+
out.push(...남길결과);
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
// 남은 호출이 없다. 할 말이라도 있으면 그건 살린다.
|
|
549
|
+
const 글 = typeof m.content === 'string' ? m.content.trim() : '';
|
|
550
|
+
if (글) { const { tool_calls: _버림, ...나머지 } = m; out.push(나머지); }
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return { messages: out, 고친것 };
|
|
554
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// 한 창에서 대화를 여러 갈래로 굴린다.
|
|
2
|
+
//
|
|
3
|
+
// ── 왜 만드나 ───────────────────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// 일하다 보면 곁가지가 생긴다. "이 함수 왜 이래?" "이 오류 뭐야?" 같은 것들이다.
|
|
6
|
+
// 지금은 그걸 하던 대화에 그냥 던진다. 그러면 두 가지가 같이 나빠진다.
|
|
7
|
+
//
|
|
8
|
+
// 1) 곁가지가 읽어 온 파일·오류·시행착오가 **본 줄기의 자리를 먹는다.**
|
|
9
|
+
// 한 번 물어본 것 때문에 하던 일이 접히는 자리까지 밀려간다.
|
|
10
|
+
// 2) 모델이 방금 본 곁가지에 끌려간다. 하던 일로 돌아오라고 다시 말해야 한다.
|
|
11
|
+
//
|
|
12
|
+
// 창을 하나 더 띄우면 되지 않느냐 — 안 된다. 새 창은 연결도, 스킬도, 폴더 지문도,
|
|
13
|
+
// 되돌리기 이력도 처음부터 다시 잡는다. 켜는 데만 몇 초씩 들고, `/undo` 는 창마다
|
|
14
|
+
// 따로 놀아서 어느 창이 뭘 되돌리는지 알 수 없게 된다.
|
|
15
|
+
//
|
|
16
|
+
// 그래서 **대화만** 여러 벌 갖는다.
|
|
17
|
+
//
|
|
18
|
+
// 같이 쓰는 것 연결·모델·도구·스킬·작업 범위·되돌리기·감사 기록
|
|
19
|
+
// 갈래마다 따로 오간 말·토큰 셈·할 일 목록·저장 파일
|
|
20
|
+
//
|
|
21
|
+
// Task 도구와는 다른 것이다. Task 는 모델이 스스로 떼어 내는 것이고 결과만 돌아온다.
|
|
22
|
+
// 갈래는 **사람이** 오가는 것이고, 오간 말이 그대로 남아서 나중에 되돌아올 수 있다.
|
|
23
|
+
//
|
|
24
|
+
// ── 조심한 것 ───────────────────────────────────────────────────────────
|
|
25
|
+
//
|
|
26
|
+
// 갈래마다 저장 파일을 따로 준다. 한 파일에 섞어 적으면 이어하기가 두 갈래를
|
|
27
|
+
// 한 줄기로 읽어서, 서로 상관없는 대화가 한 덩어리가 된다.
|
|
28
|
+
|
|
29
|
+
/** 이름이 없을 때 붙여 줄 이름. 사람이 목록에서 알아볼 수 있으면 된다. */
|
|
30
|
+
function 기본이름(n) { return `갈래 ${n}`; }
|
|
31
|
+
|
|
32
|
+
export class Threads {
|
|
33
|
+
/**
|
|
34
|
+
* @param {object} session 지금 도는 대화. 첫 갈래가 된다.
|
|
35
|
+
* @param {object} ctx 할 일 목록이 여기 붙어 있다
|
|
36
|
+
* @param {function} 새store () => Store — 갈래마다 저장 파일을 새로 연다
|
|
37
|
+
* @param {object} 첫store 지금 대화가 쓰고 있던 저장 파일
|
|
38
|
+
*/
|
|
39
|
+
constructor(session, ctx, 새store, 첫store) {
|
|
40
|
+
this.session = session;
|
|
41
|
+
this.ctx = ctx;
|
|
42
|
+
this.새store = 새store;
|
|
43
|
+
this.갈래들 = [{ 이름: '본줄기', messages: session.messages, usage: session.usage, todos: ctx?.todos ?? null, store: 첫store }];
|
|
44
|
+
this.자리 = 0;
|
|
45
|
+
this.센것 = 1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
현재() { return this.갈래들[this.자리]; }
|
|
49
|
+
현재store() { return this.현재().store; }
|
|
50
|
+
개수() { return this.갈래들.length; }
|
|
51
|
+
|
|
52
|
+
/** 지금 화면에서 오가던 것을 갈래에 넣어 둔다. 옮기기 전에 반드시 부른다. */
|
|
53
|
+
#담아두기() {
|
|
54
|
+
const g = this.현재();
|
|
55
|
+
g.messages = this.session.messages;
|
|
56
|
+
g.usage = this.session.usage;
|
|
57
|
+
g.todos = this.ctx?.todos ?? null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 갈래 하나를 화면으로 꺼낸다. */
|
|
61
|
+
#꺼내기(i) {
|
|
62
|
+
this.자리 = i;
|
|
63
|
+
const g = this.현재();
|
|
64
|
+
this.session.messages = g.messages;
|
|
65
|
+
this.session.usage = g.usage;
|
|
66
|
+
if (this.ctx) this.ctx.todos = g.todos;
|
|
67
|
+
// 상태줄이 지금 어느 갈래인지 보여 줄 수 있게 남긴다.
|
|
68
|
+
// 갈래가 하나뿐이면 안 보인다 — 안 쓰는 사람 화면은 그대로여야 한다.
|
|
69
|
+
this.session.갈래표 = this.갈래들.length > 1 ? g.이름 : null;
|
|
70
|
+
return g;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 새 갈래. 빈 대화로 시작한다.
|
|
75
|
+
* @param {string} 이름
|
|
76
|
+
* @param {object[]} 물려줄것 갈라내기면 지금 대화의 사본
|
|
77
|
+
*/
|
|
78
|
+
새로(이름 = '', 물려줄것 = null) {
|
|
79
|
+
this.#담아두기();
|
|
80
|
+
this.센것++;
|
|
81
|
+
const store = this.새store();
|
|
82
|
+
const g = {
|
|
83
|
+
이름: String(이름 ?? '').trim() || 기본이름(this.센것),
|
|
84
|
+
messages: 물려줄것 ? [...물려줄것] : [],
|
|
85
|
+
usage: { in: 0, out: 0, calls: 0, ms: 0 },
|
|
86
|
+
todos: null,
|
|
87
|
+
store,
|
|
88
|
+
};
|
|
89
|
+
this.갈래들.push(g);
|
|
90
|
+
// 갈라낸 것은 지금까지 오간 말을 새 파일에도 적어 둔다. 안 적으면 그 갈래를
|
|
91
|
+
// 나중에 이어할 때 앞부분이 없어서 무슨 얘기였는지 알 수 없다.
|
|
92
|
+
if (물려줄것?.length) { for (const m of 물려줄것) store.append(m); }
|
|
93
|
+
return this.#꺼내기(this.갈래들.length - 1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 지금 대화를 그대로 복사해 새 갈래로 나간다. */
|
|
97
|
+
갈라내기(이름 = '') {
|
|
98
|
+
return this.새로(이름, this.session.messages);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 갈래를 찾는다. 번호(1부터)도 되고 이름 일부도 된다.
|
|
103
|
+
* @returns {number} 못 찾으면 -1
|
|
104
|
+
*/
|
|
105
|
+
찾기(말) {
|
|
106
|
+
const s = String(말 ?? '').trim();
|
|
107
|
+
if (!s) return -1;
|
|
108
|
+
if (/^\d+$/.test(s)) {
|
|
109
|
+
const i = Number(s) - 1;
|
|
110
|
+
return i >= 0 && i < this.갈래들.length ? i : -1;
|
|
111
|
+
}
|
|
112
|
+
const 낮 = s.toLowerCase();
|
|
113
|
+
const 딱 = this.갈래들.findIndex((g) => g.이름.toLowerCase() === 낮);
|
|
114
|
+
if (딱 >= 0) return 딱;
|
|
115
|
+
return this.갈래들.findIndex((g) => g.이름.toLowerCase().includes(낮));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
옮기기(말) {
|
|
119
|
+
const i = this.찾기(말);
|
|
120
|
+
if (i < 0) return null;
|
|
121
|
+
if (i === this.자리) return this.현재();
|
|
122
|
+
this.#담아두기();
|
|
123
|
+
return this.#꺼내기(i);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 갈래를 닫는다. 마지막 하나는 못 닫는다 — 닫으면 대화가 없어진다.
|
|
128
|
+
* 저장 파일은 안 지운다. `/sessions` 로 다시 찾아갈 수 있어야 한다.
|
|
129
|
+
*/
|
|
130
|
+
닫기(말 = '') {
|
|
131
|
+
if (this.갈래들.length <= 1) return { ok: false, why: '갈래가 하나뿐입니다.' };
|
|
132
|
+
const i = 말 ? this.찾기(말) : this.자리;
|
|
133
|
+
if (i < 0) return { ok: false, why: '그런 갈래가 없습니다.' };
|
|
134
|
+
if (i === this.자리) this.#담아두기();
|
|
135
|
+
const [닫은것] = this.갈래들.splice(i, 1);
|
|
136
|
+
// 닫힌 자리보다 뒤에 있었으면 번호가 하나 당겨진다.
|
|
137
|
+
const 다음 = i === this.자리 ? Math.max(0, i - 1) : (this.자리 > i ? this.자리 - 1 : this.자리);
|
|
138
|
+
this.자리 = 다음;
|
|
139
|
+
this.#꺼내기(다음);
|
|
140
|
+
return { ok: true, 닫은것, 지금: this.현재() };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** 화면에 뿌릴 목록. */
|
|
144
|
+
목록() {
|
|
145
|
+
return this.갈래들.map((g, i) => ({
|
|
146
|
+
번호: i + 1,
|
|
147
|
+
이름: g.이름,
|
|
148
|
+
지금: i === this.자리,
|
|
149
|
+
말수: (i === this.자리 ? this.session.messages : g.messages).length,
|
|
150
|
+
토큰: (i === this.자리 ? this.session.usage : g.usage)?.in ?? 0,
|
|
151
|
+
id: g.store?.id ?? null,
|
|
152
|
+
}));
|
|
153
|
+
}
|
|
154
|
+
}
|
package/src/backend/http.js
CHANGED
|
@@ -89,13 +89,50 @@ function normalizeError(err) {
|
|
|
89
89
|
return m;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
// 게이트웨이 앞단 프록시가 자격증명을 받아오다 실패한 것을 알아본다.
|
|
93
|
+
//
|
|
94
|
+
// 사내 게이트웨이는 앞에 프록시를 두고, 프록시가 매번 바깥 명령을 돌려 토큰을 받아 온다.
|
|
95
|
+
// 그 명령이 실패하면 이런 것이 본문에 실려 온다 —
|
|
96
|
+
//
|
|
97
|
+
// [PROXY ERROR] Command '['databricks', 'auth', 'token', '--host', 'dbc-...',
|
|
98
|
+
// '--profile', 'a@b.com', '-o', 'json']' returned non-zero exit status 1.
|
|
99
|
+
//
|
|
100
|
+
// 이걸 그대로 뱉으면 쓸모가 없다. 모델도 deel 도 아니고 **그 PC 의 로그인이 만료된 것**인데,
|
|
101
|
+
// 화면만 보고는 무엇을 해야 할지 알 수가 없다(실제로 겪었다).
|
|
102
|
+
// 그래서 명령·호스트·프로필을 뽑아 칠 것까지 만들어 준다. 못 알아보면 null 이다.
|
|
103
|
+
export function 프록시힌트(말) {
|
|
104
|
+
const s = String(말 ?? '');
|
|
105
|
+
if (!/returned non-zero exit status/i.test(s)) return null;
|
|
106
|
+
// 바깥 따옴표가 대괄호까지 감싸고 있어 그냥 따옴표로 끊으면 짝이 어긋난다. 대괄호 안만 본다.
|
|
107
|
+
// 단 앞머리의 `[PROXY ERROR]` 도 대괄호라 먼저 걸린다 — 따옴표로 시작하는 것만 골라야 한다.
|
|
108
|
+
const 인자 = [...(s.match(/\[(\s*'[^\]]*)\]/)?.[1] ?? '').matchAll(/'([^']*)'/g)].map((m) => m[1]);
|
|
109
|
+
const 도구 = 인자[0] || '외부 명령';
|
|
110
|
+
const 뒤 = (이름) => { const i = 인자.indexOf(이름); return i >= 0 ? (인자[i + 1] ?? null) : null; };
|
|
111
|
+
|
|
112
|
+
if (도구 === 'databricks') {
|
|
113
|
+
const host = 뒤('--host');
|
|
114
|
+
const profile = 뒤('--profile');
|
|
115
|
+
const 다시 = ['databricks auth login',
|
|
116
|
+
host ? `--host ${/^https?:\/\//.test(host) ? host : `https://${host}`}` : null,
|
|
117
|
+
profile ? `--profile ${profile}` : null].filter(Boolean).join(' ');
|
|
118
|
+
return `게이트웨이가 Databricks 토큰을 못 받았습니다 — 모델이 아니라 로그인이 만료된 것입니다.\n`
|
|
119
|
+
+ `그 PC 에서 다시 로그인하세요: ${다시}`;
|
|
120
|
+
}
|
|
121
|
+
return `게이트웨이 앞단 프록시가 '${도구}' 를 돌리다 실패했습니다 — 모델이 아니라 프록시 쪽 문제입니다.`;
|
|
122
|
+
}
|
|
123
|
+
|
|
92
124
|
// 서버가 준 오류 본문에서 사람이 읽을 문장만 뽑는다.
|
|
93
125
|
export function serverMessage(r) {
|
|
94
126
|
if (r.error) return r.error;
|
|
95
127
|
const j = r.json;
|
|
96
128
|
const cand = j?.error?.message ?? j?.error ?? j?.message ?? j?.detail;
|
|
97
|
-
|
|
98
|
-
if (cand)
|
|
99
|
-
if (
|
|
100
|
-
|
|
129
|
+
let 말;
|
|
130
|
+
if (typeof cand === 'string') 말 = cand;
|
|
131
|
+
else if (cand) 말 = JSON.stringify(cand).slice(0, 200);
|
|
132
|
+
else if (r.text) 말 = String(r.text).replace(/\s+/g, ' ').slice(0, 200);
|
|
133
|
+
else return `HTTP ${r.status}`;
|
|
134
|
+
// 알아본 것이 있으면 원문 대신 그것을 앞에 세운다. 원문은 뒤에 한 줄로 남긴다 —
|
|
135
|
+
// 사내 담당자에게 그대로 보여 줘야 할 때가 있다.
|
|
136
|
+
const 힌트 = 프록시힌트(말);
|
|
137
|
+
return 힌트 ? `${힌트}\n원문: ${말.slice(0, 160)}` : 말;
|
|
101
138
|
}
|