deel-local-cli 1.12.0 → 1.13.1
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 +6 -5
- package/README.md +6 -5
- package/package.json +60 -60
- package/src/acp/map.js +30 -25
- package/src/acp/serve.js +810 -810
- package/src/agent/budget.js +156 -156
- package/src/agent/commit.js +32 -9
- package/src/agent/compact.js +441 -420
- package/src/agent/effort.js +308 -308
- package/src/agent/evolve.js +263 -263
- package/src/agent/loop.js +1728 -1707
- package/src/agent/memory.js +156 -156
- package/src/agent/mention.js +210 -210
- package/src/agent/recall.js +22 -9
- package/src/agent/session.js +1056 -1049
- package/src/backend/adapter.js +1355 -1159
- package/src/backend/cachemark.js +223 -150
- package/src/backend/detect.js +327 -327
- package/src/backend/http.js +3 -1
- package/src/backend/mcp.js +15 -3
- package/src/backend/price.js +69 -6
- package/src/backend/probe.js +17 -3
- package/src/backend/quota.js +274 -250
- package/src/backend/tokens.js +25 -3
- package/src/backend/toolfit.js +352 -352
- package/src/backend/wire.js +715 -600
- package/src/commands.js +3184 -3135
- package/src/config.js +39 -0
- package/src/i18n/en.js +545 -544
- package/src/i18n/ja.js +498 -497
- package/src/i18n/ko.js +592 -591
- package/src/i18n/zh.js +498 -497
- package/src/oneshot.js +625 -620
- package/src/pack/tar.js +23 -1
- package/src/pack/zip.js +19 -1
- package/src/plugins/manage.js +85 -5
- package/src/repl.js +2616 -2602
- package/src/safety/audit.js +22 -3
- package/src/safety/guard.js +124 -9
- package/src/safety/network.js +43 -0
- package/src/tools/fastgrep.js +15 -0
- package/src/tools/fsutil.js +291 -265
- package/src/tools/index.js +40 -3
- package/src/tools/jobs.js +87 -6
- package/src/tools/label.js +52 -0
- package/src/tools/spawn.js +114 -0
- package/src/tools/verify.js +358 -358
- package/src/tools/webfetch.js +28 -3
- package/src/ui/export.js +26 -10
- package/src/ui/pick.js +115 -115
- package/src/ui/status.js +617 -613
package/src/tools/webfetch.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// · 사설·로컬 주소는 거절. 사내 서버를 모델이 긁어 오게 두지 않는다.
|
|
12
12
|
// · 오프라인이면 아예 거절.
|
|
13
13
|
// · 받은 것은 글자만 뽑고 길이를 자른다.
|
|
14
|
-
import { allowTemporarily, isOffline, isLocalHost } from '../safety/network.js';
|
|
14
|
+
import { allowTemporarily, isOffline, isLocalHost, 사설로풀리나 } from '../safety/network.js';
|
|
15
15
|
import { 원시요청, 몸읽기 } from '../backend/http.js';
|
|
16
16
|
import { decode as decodeBytes } from './encoding.js';
|
|
17
17
|
import { 웹글자수 } from '../agent/budget.js';
|
|
@@ -182,6 +182,21 @@ export function 웹되돌림(다음, { allowPrivate = false } = {}) {
|
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
/**
|
|
186
|
+
* 되돌림 한 홉을 통째로 본다 — 글자와 **닿는 곳** 둘 다.
|
|
187
|
+
*
|
|
188
|
+
* 첫 주소만 이름을 풀어 보면 소용이 없다. 공개 주소로 시작해 302 한 번으로
|
|
189
|
+
* 사내로 들어가는 것이 가장 흔한 길이라, 홉마다 같은 것을 물어야 한다.
|
|
190
|
+
*/
|
|
191
|
+
export async function 웹되돌림검사(다음, { allowPrivate = false } = {}) {
|
|
192
|
+
웹되돌림(다음, { allowPrivate });
|
|
193
|
+
if (allowPrivate) return;
|
|
194
|
+
const 푼것 = await 사설로풀리나(다음.hostname);
|
|
195
|
+
if (푼것) {
|
|
196
|
+
throw new Error(`되돌린 곳이 사내·로컬로 풀립니다 (${다음.hostname} → ${푼것.걸린것.join(', ')}) — 따라가지 않습니다`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
185
200
|
export async function webFetch(args, { allowPrivate = false, 모델컨텍스트 = null, signal = null } = {}) {
|
|
186
201
|
const raw = String(args?.url ?? '').trim();
|
|
187
202
|
/*
|
|
@@ -213,6 +228,16 @@ export async function webFetch(args, { allowPrivate = false, 모델컨텍스트
|
|
|
213
228
|
if (isLocalHost(u.hostname) && !allowPrivate) {
|
|
214
229
|
return { error: `이 컴퓨터·사내망 주소는 이 도구로 읽지 않습니다: ${u.hostname}\n 파일은 Read, 사내 서버는 사람이 직접 확인하세요.` };
|
|
215
230
|
}
|
|
231
|
+
/*
|
|
232
|
+
* 이름이 멀쩡해도 **닿는 곳**은 사내일 수 있다 (network.js 의 사설로풀리나).
|
|
233
|
+
* 이름만 보는 검사는 A 레코드 한 줄로 지나간다.
|
|
234
|
+
*/
|
|
235
|
+
if (!allowPrivate) {
|
|
236
|
+
const 푼것 = await 사설로풀리나(u.hostname);
|
|
237
|
+
if (푼것) {
|
|
238
|
+
return { error: `이 주소는 사내·로컬로 풀립니다: ${u.hostname} → ${푼것.걸린것.join(', ')}\n 이름은 바깥이지만 닿는 곳이 안입니다. 사내 서버는 사람이 직접 확인하세요.` };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
216
241
|
|
|
217
242
|
const close = allowTemporarily(u.origin);
|
|
218
243
|
// 되돌림(redirect)으로 옮겨 간 집도 그 한 번만 연다. 한 홉마다 문지기를 지나므로
|
|
@@ -237,8 +262,8 @@ export async function webFetch(args, { allowPrivate = false, 모델컨텍스트
|
|
|
237
262
|
*/
|
|
238
263
|
signal,
|
|
239
264
|
stream: true, // 상한까지만 받는다 — 다 받아 놓고 버리지 않는다
|
|
240
|
-
되돌림: (다음) => {
|
|
241
|
-
|
|
265
|
+
되돌림: async (다음) => {
|
|
266
|
+
await 웹되돌림검사(다음, { allowPrivate });
|
|
242
267
|
열어둔.push(allowTemporarily(다음.origin));
|
|
243
268
|
},
|
|
244
269
|
}), signal);
|
package/src/ui/export.js
CHANGED
|
@@ -23,7 +23,10 @@
|
|
|
23
23
|
import { writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
24
24
|
import { join } from 'node:path';
|
|
25
25
|
import { 증거모으기 } from '../agent/evidence.js';
|
|
26
|
+
// 도구 부름과 글이 어디 실리는지는 규격마다 다르다 — 읽는 규칙은 한 곳뿐이다.
|
|
27
|
+
import { 부른것들, 본문글 } from '../backend/adapter.js';
|
|
26
28
|
import { 말, 언어 } from '../i18n/index.js';
|
|
29
|
+
import { 첫이름 } from '../tools/label.js';
|
|
27
30
|
|
|
28
31
|
/** HTML 로 안전하게. 문서 전체가 이 함수 하나를 지나야 한다. */
|
|
29
32
|
function 글자(s) {
|
|
@@ -39,11 +42,14 @@ const 민글 = (s) => String(s ?? '').replace(/\x1b\[[0-9;]*m/g, '');
|
|
|
39
42
|
|
|
40
43
|
/** 도구 호출 한 줄 요약. 보고서는 기록이지 재현이 아니다 — 인자 전체를 안 싣는다. */
|
|
41
44
|
function 도구줄(tc) {
|
|
42
|
-
|
|
45
|
+
// 부른것들() 이 규격을 이미 펴서 { id, name, args } 로 준다.
|
|
46
|
+
const 이름 = tc?.name ?? '?';
|
|
43
47
|
let 핵심 = '';
|
|
44
48
|
try {
|
|
45
|
-
const a =
|
|
46
|
-
|
|
49
|
+
const a = tc?.args ?? {};
|
|
50
|
+
// 이름 차례는 tools/label.js 한 곳에서 온다. 여기 목록에는 url 이 빠져
|
|
51
|
+
// 있어서, 웹을 읽은 줄이 보고서에서만 빈 괄호로 남았다.
|
|
52
|
+
핵심 = 첫이름(a) ?? a.command ?? '';
|
|
47
53
|
} catch { /* 인자가 잘렸어도 이름은 보여 준다 */ }
|
|
48
54
|
핵심 = String(핵심);
|
|
49
55
|
if (핵심.length > 80) 핵심 = 핵심.slice(0, 80) + '…';
|
|
@@ -62,19 +68,29 @@ export function 보고서짓기(session, { scope = null, audit = null } = {}) {
|
|
|
62
68
|
const 보임 = (p) => { try { return scope?.show ? scope.show(p) : String(p); } catch { return String(p); } };
|
|
63
69
|
|
|
64
70
|
// ── 재료 ──────────────────────────────────────────────────────────────
|
|
65
|
-
const 첫말 = (session.messages ?? []).find((m) => m.role === 'user')
|
|
66
|
-
const 제목 = String(첫말
|
|
71
|
+
const 첫말 = 본문글((session.messages ?? []).find((m) => m.role === 'user'));
|
|
72
|
+
const 제목 = String(첫말 || 말('export.untitled')).split('\n')[0].slice(0, 80);
|
|
67
73
|
|
|
68
74
|
const 흐름 = [];
|
|
69
75
|
for (const m of session.messages ?? []) {
|
|
70
|
-
|
|
71
|
-
|
|
76
|
+
/*
|
|
77
|
+
* 규격을 안 가린다.
|
|
78
|
+
*
|
|
79
|
+
* 여기가 `typeof m.content === 'string'` 과 `m.tool_calls` 로 읽고 있었다.
|
|
80
|
+
* Anthropic 꼴은 둘 다 안 맞는다 — 모델이 한 말은 블록 배열 안에 있고
|
|
81
|
+
* 부름은 `tool_use` 블록이다. 그래서 그 창구로 나눈 대화를 `/export`
|
|
82
|
+
* 하면 **모델 말과 도구가 통째로 빈** 보고서가 나왔다. 오류는 없다.
|
|
83
|
+
*/
|
|
84
|
+
const 글 = 본문글(m);
|
|
85
|
+
if (m.role === 'user') {
|
|
86
|
+
// Anthropic 꼴에서 사람 차례에 도구 결과가 섞여 온다. 그건 글이 아니다.
|
|
87
|
+
if (글.trim()) 흐름.push({ 갈래: 'user', 글 });
|
|
72
88
|
} else if (m.role === 'assistant') {
|
|
73
|
-
const 부른것 = (m
|
|
89
|
+
const 부른것 = 부른것들(m).map(도구줄);
|
|
74
90
|
if (부른것.length) 흐름.push({ 갈래: 'tools', 글: 부른것.join(' · ') });
|
|
75
|
-
if (
|
|
91
|
+
if (글.trim()) 흐름.push({ 갈래: 'assistant', 글 });
|
|
76
92
|
}
|
|
77
|
-
//
|
|
93
|
+
// 도구 결과 알맹이는 안 싣는다 — 길고, 요점은 답과 증거 절에 이미 있다.
|
|
78
94
|
}
|
|
79
95
|
|
|
80
96
|
const 바뀐것 = [...(session.changes ?? new Map()).entries()]
|
package/src/ui/pick.js
CHANGED
|
@@ -1,115 +1,115 @@
|
|
|
1
|
-
// 고른 번호 + 하고 싶은 말, 한 줄에서 갈라 읽기.
|
|
2
|
-
//
|
|
3
|
-
// ── 사람 말 ─────────────────────────────────────────────────────────────
|
|
4
|
-
//
|
|
5
|
-
// 「사용자에게 계획 물어볼 때 번호만 입력하는 게 아니라, 번호를 고르고
|
|
6
|
-
// 의견이 있으면 의견도 입력할 수 있게」
|
|
7
|
-
//
|
|
8
|
-
// 여태는 둘 중 하나였다. 숫자만 치면 그 줄을 고른 것이고, 그 밖의 글은
|
|
9
|
-
// **고른 것 없이** 통째로 자유 답이었다. 그래서 「2번인데 파일은 나눠서」
|
|
10
|
-
// 라고 치면 2번을 고른 사실이 사라진다. 사람은 골랐다고 생각하는데
|
|
11
|
-
// 모델에게는 고른 것이 없는 문장 하나만 간다.
|
|
12
|
-
//
|
|
13
|
-
// 실제로는 이렇게 답하고 싶어진다 —
|
|
14
|
-
//
|
|
15
|
-
// 2 파일은 나눠서 해줘 (고르고 + 덧붙이고)
|
|
16
|
-
// 2: 파일은 나눠서 해줘
|
|
17
|
-
// 2, 파일은 나눠서 해줘
|
|
18
|
-
// 2 - 파일은 나눠서 해줘
|
|
19
|
-
// 1,3 (둘 다)
|
|
20
|
-
// 1 3 둘 다 하되 순서는 반대로
|
|
21
|
-
//
|
|
22
|
-
// ── 어디서 멈추나 ───────────────────────────────────────────────────────
|
|
23
|
-
//
|
|
24
|
-
// 숫자로 읽는 것은 **머리에 붙은, 목록 안에 있는 수**뿐이다. 그 뒤로는 다
|
|
25
|
-
// 사람 말이다. 이 선이 없으면 「3개만 해줘」 가 3번을 고른 것이 된다 —
|
|
26
|
-
// 사람이 고른 적 없는 것을 골랐다고 하는 셈이라, 안 받느니만 못하다.
|
|
27
|
-
// 그래서 수 바로 뒤에 글자가 붙어 있으면(3개) 수로 안 읽는다.
|
|
28
|
-
//
|
|
29
|
-
// 목록에 없는 수도 안 읽는다. 선택지가 둘인데 「5분만 기다려」 라고 치면
|
|
30
|
-
// 그건 고른 것이 아니라 하는 말이다.
|
|
31
|
-
|
|
32
|
-
/** 수 뒤에 올 수 있는 것 — 끝, 빈칸, 그리고 흔한 구분표. `1.5` 는 수가 아니다. */
|
|
33
|
-
const 수뒤 = /^(\d{1,3})(?:\s*[.)::\]](?!\d)|\s*[,,、·]|\s|$)/;
|
|
34
|
-
|
|
35
|
-
/** 수와 말 사이에 사람이 흔히 넣는 것들. 말 앞머리에서 떼어 낸다. */
|
|
36
|
-
const 사이표 = /^[\s,,、·::\-–—>]+/;
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* 답 한 줄을 「고른 번호들」과 「덧붙인 말」로 가른다.
|
|
40
|
-
*
|
|
41
|
-
* @param {string} 답 사람이 친 그대로
|
|
42
|
-
* @param {string[]} 고를것 보여 준 선택지
|
|
43
|
-
* @returns {{번호들: number[], 덧말: string, 글: string}}
|
|
44
|
-
* 글 — 모델에게 보낼 한 줄. 고른 것이 없으면 친 글 그대로다.
|
|
45
|
-
*/
|
|
46
|
-
export function 고른것풀기(답, 고를것 = []) {
|
|
47
|
-
const 원 = String(답 ?? '').trim();
|
|
48
|
-
const 목록 = Array.isArray(고를것) ? 고를것 : [];
|
|
49
|
-
if (!원) return { 번호들: [], 덧말: '', 글: '' };
|
|
50
|
-
|
|
51
|
-
const 번호들 = [];
|
|
52
|
-
let 남은 = 원;
|
|
53
|
-
for (;;) {
|
|
54
|
-
const m = 수뒤.exec(남은);
|
|
55
|
-
if (!m) break;
|
|
56
|
-
const n = Number(m[1]);
|
|
57
|
-
// 목록 밖의 수는 고른 것이 아니라 하는 말이다.
|
|
58
|
-
if (!Number.isInteger(n) || n < 1 || n > 목록.length) break;
|
|
59
|
-
// 같은 것을 두 번 쳐도 한 번만 센다 — 「1 1」 을 두 갈래로 읽을 까닭이 없다.
|
|
60
|
-
if (!번호들.includes(n)) 번호들.push(n);
|
|
61
|
-
남은 = 남은.slice(m[0].length).replace(사이표, '');
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const 덧말 = 남은.trim();
|
|
65
|
-
// 아무 수도 못 읽었으면 여태와 똑같다 — 친 글이 그대로 답이다.
|
|
66
|
-
if (!번호들.length) return { 번호들: [], 덧말: '', 글: 원 };
|
|
67
|
-
|
|
68
|
-
const 고른 = 번호들.map((n) => String(목록[n - 1] ?? '').trim()).filter(Boolean);
|
|
69
|
-
const 글 = 고른.join(' · ') + (덧말 ? ` — ${덧말}` : '');
|
|
70
|
-
return { 번호들, 덧말, 글 };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* 계획 상자의 답을 읽는다.
|
|
75
|
-
*
|
|
76
|
-
* 갈래는 셋(진행·그만·다시)이고, 들어오는 모양은 다섯이다.
|
|
77
|
-
* · 그냥 진행 — ⏎ · y · yes · ㅇ · 진행
|
|
78
|
-
* · 진행하되 한마디 — `y 검사도 같이 넣어줘`
|
|
79
|
-
* · 그만 — n · no · ㄴ
|
|
80
|
-
* · 그 밖의 글 — 그 말대로 다시 짜라 (여태 그대로)
|
|
81
|
-
* · `3 이건 빼자` — 3단계에 대한 말로 붙여서 다시 짜라
|
|
82
|
-
*
|
|
83
|
-
* ⏎ 하나로 진행하는 것은 안 건드린다. 계획을 읽고 「그래」 하는 데
|
|
84
|
-
* 두 글자를 더 치게 하면, 그 두 글자 때문에 안 읽고 넘기게 된다.
|
|
85
|
-
*
|
|
86
|
-
* @param {string} 답
|
|
87
|
-
* @param {number} 단계수 계획이 몇 단계인가 (번호를 가려내는 데 쓴다)
|
|
88
|
-
* @returns {{갈래: '진행'|'그만'|'다시', 덧말: string, 단계: number|null}}
|
|
89
|
-
*/
|
|
90
|
-
export function 계획답풀기(답, 단계수 = 0) {
|
|
91
|
-
const 원 = String(답 ?? '').trim();
|
|
92
|
-
if (!원) return { 갈래: '진행', 덧말: '', 단계: null };
|
|
93
|
-
|
|
94
|
-
const 첫낱말 = 원.split(/\s+/)[0].toLowerCase();
|
|
95
|
-
const 나머지 = 원.slice(원.split(/\s+/)[0].length).replace(사이표, '').trim();
|
|
96
|
-
|
|
97
|
-
if (['n', 'no', 'ㄴ', '아니', '아니요', '취소'].includes(첫낱말)) {
|
|
98
|
-
return { 갈래: '그만', 덧말: '', 단계: null };
|
|
99
|
-
}
|
|
100
|
-
if (['y', 'yes', 'ㅇ', '네', '예', '진행', 'ok', 'go'].includes(첫낱말)) {
|
|
101
|
-
return { 갈래: '진행', 덧말: 나머지, 단계: null };
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// 「3 이건 빼자」 — 그 단계에 대고 하는 말이다.
|
|
105
|
-
const m = 수뒤.exec(원);
|
|
106
|
-
if (m) {
|
|
107
|
-
const n = Number(m[1]);
|
|
108
|
-
if (Number.isInteger(n) && n >= 1 && n <= 단계수) {
|
|
109
|
-
const 말 = 원.slice(m[0].length).replace(사이표, '').trim();
|
|
110
|
-
if (말) return { 갈래: '다시', 덧말: 말, 단계: n };
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return { 갈래: '다시', 덧말: 원, 단계: null };
|
|
115
|
-
}
|
|
1
|
+
// 고른 번호 + 하고 싶은 말, 한 줄에서 갈라 읽기.
|
|
2
|
+
//
|
|
3
|
+
// ── 사람 말 ─────────────────────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// 「사용자에게 계획 물어볼 때 번호만 입력하는 게 아니라, 번호를 고르고
|
|
6
|
+
// 의견이 있으면 의견도 입력할 수 있게」
|
|
7
|
+
//
|
|
8
|
+
// 여태는 둘 중 하나였다. 숫자만 치면 그 줄을 고른 것이고, 그 밖의 글은
|
|
9
|
+
// **고른 것 없이** 통째로 자유 답이었다. 그래서 「2번인데 파일은 나눠서」
|
|
10
|
+
// 라고 치면 2번을 고른 사실이 사라진다. 사람은 골랐다고 생각하는데
|
|
11
|
+
// 모델에게는 고른 것이 없는 문장 하나만 간다.
|
|
12
|
+
//
|
|
13
|
+
// 실제로는 이렇게 답하고 싶어진다 —
|
|
14
|
+
//
|
|
15
|
+
// 2 파일은 나눠서 해줘 (고르고 + 덧붙이고)
|
|
16
|
+
// 2: 파일은 나눠서 해줘
|
|
17
|
+
// 2, 파일은 나눠서 해줘
|
|
18
|
+
// 2 - 파일은 나눠서 해줘
|
|
19
|
+
// 1,3 (둘 다)
|
|
20
|
+
// 1 3 둘 다 하되 순서는 반대로
|
|
21
|
+
//
|
|
22
|
+
// ── 어디서 멈추나 ───────────────────────────────────────────────────────
|
|
23
|
+
//
|
|
24
|
+
// 숫자로 읽는 것은 **머리에 붙은, 목록 안에 있는 수**뿐이다. 그 뒤로는 다
|
|
25
|
+
// 사람 말이다. 이 선이 없으면 「3개만 해줘」 가 3번을 고른 것이 된다 —
|
|
26
|
+
// 사람이 고른 적 없는 것을 골랐다고 하는 셈이라, 안 받느니만 못하다.
|
|
27
|
+
// 그래서 수 바로 뒤에 글자가 붙어 있으면(3개) 수로 안 읽는다.
|
|
28
|
+
//
|
|
29
|
+
// 목록에 없는 수도 안 읽는다. 선택지가 둘인데 「5분만 기다려」 라고 치면
|
|
30
|
+
// 그건 고른 것이 아니라 하는 말이다.
|
|
31
|
+
|
|
32
|
+
/** 수 뒤에 올 수 있는 것 — 끝, 빈칸, 그리고 흔한 구분표. `1.5` 는 수가 아니다. */
|
|
33
|
+
const 수뒤 = /^(\d{1,3})(?:\s*[.)::\]](?!\d)|\s*[,,、·]|\s|$)/;
|
|
34
|
+
|
|
35
|
+
/** 수와 말 사이에 사람이 흔히 넣는 것들. 말 앞머리에서 떼어 낸다. */
|
|
36
|
+
const 사이표 = /^[\s,,、·::\-–—>]+/;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 답 한 줄을 「고른 번호들」과 「덧붙인 말」로 가른다.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} 답 사람이 친 그대로
|
|
42
|
+
* @param {string[]} 고를것 보여 준 선택지
|
|
43
|
+
* @returns {{번호들: number[], 덧말: string, 글: string}}
|
|
44
|
+
* 글 — 모델에게 보낼 한 줄. 고른 것이 없으면 친 글 그대로다.
|
|
45
|
+
*/
|
|
46
|
+
export function 고른것풀기(답, 고를것 = []) {
|
|
47
|
+
const 원 = String(답 ?? '').trim();
|
|
48
|
+
const 목록 = Array.isArray(고를것) ? 고를것 : [];
|
|
49
|
+
if (!원) return { 번호들: [], 덧말: '', 글: '' };
|
|
50
|
+
|
|
51
|
+
const 번호들 = [];
|
|
52
|
+
let 남은 = 원;
|
|
53
|
+
for (;;) {
|
|
54
|
+
const m = 수뒤.exec(남은);
|
|
55
|
+
if (!m) break;
|
|
56
|
+
const n = Number(m[1]);
|
|
57
|
+
// 목록 밖의 수는 고른 것이 아니라 하는 말이다.
|
|
58
|
+
if (!Number.isInteger(n) || n < 1 || n > 목록.length) break;
|
|
59
|
+
// 같은 것을 두 번 쳐도 한 번만 센다 — 「1 1」 을 두 갈래로 읽을 까닭이 없다.
|
|
60
|
+
if (!번호들.includes(n)) 번호들.push(n);
|
|
61
|
+
남은 = 남은.slice(m[0].length).replace(사이표, '');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const 덧말 = 남은.trim();
|
|
65
|
+
// 아무 수도 못 읽었으면 여태와 똑같다 — 친 글이 그대로 답이다.
|
|
66
|
+
if (!번호들.length) return { 번호들: [], 덧말: '', 글: 원 };
|
|
67
|
+
|
|
68
|
+
const 고른 = 번호들.map((n) => String(목록[n - 1] ?? '').trim()).filter(Boolean);
|
|
69
|
+
const 글 = 고른.join(' · ') + (덧말 ? ` — ${덧말}` : '');
|
|
70
|
+
return { 번호들, 덧말, 글 };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 계획 상자의 답을 읽는다.
|
|
75
|
+
*
|
|
76
|
+
* 갈래는 셋(진행·그만·다시)이고, 들어오는 모양은 다섯이다.
|
|
77
|
+
* · 그냥 진행 — ⏎ · y · yes · ㅇ · 진행
|
|
78
|
+
* · 진행하되 한마디 — `y 검사도 같이 넣어줘`
|
|
79
|
+
* · 그만 — n · no · ㄴ
|
|
80
|
+
* · 그 밖의 글 — 그 말대로 다시 짜라 (여태 그대로)
|
|
81
|
+
* · `3 이건 빼자` — 3단계에 대한 말로 붙여서 다시 짜라
|
|
82
|
+
*
|
|
83
|
+
* ⏎ 하나로 진행하는 것은 안 건드린다. 계획을 읽고 「그래」 하는 데
|
|
84
|
+
* 두 글자를 더 치게 하면, 그 두 글자 때문에 안 읽고 넘기게 된다.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} 답
|
|
87
|
+
* @param {number} 단계수 계획이 몇 단계인가 (번호를 가려내는 데 쓴다)
|
|
88
|
+
* @returns {{갈래: '진행'|'그만'|'다시', 덧말: string, 단계: number|null}}
|
|
89
|
+
*/
|
|
90
|
+
export function 계획답풀기(답, 단계수 = 0) {
|
|
91
|
+
const 원 = String(답 ?? '').trim();
|
|
92
|
+
if (!원) return { 갈래: '진행', 덧말: '', 단계: null };
|
|
93
|
+
|
|
94
|
+
const 첫낱말 = 원.split(/\s+/)[0].toLowerCase();
|
|
95
|
+
const 나머지 = 원.slice(원.split(/\s+/)[0].length).replace(사이표, '').trim();
|
|
96
|
+
|
|
97
|
+
if (['n', 'no', 'ㄴ', '아니', '아니요', '취소'].includes(첫낱말)) {
|
|
98
|
+
return { 갈래: '그만', 덧말: '', 단계: null };
|
|
99
|
+
}
|
|
100
|
+
if (['y', 'yes', 'ㅇ', '네', '예', '진행', 'ok', 'go'].includes(첫낱말)) {
|
|
101
|
+
return { 갈래: '진행', 덧말: 나머지, 단계: null };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 「3 이건 빼자」 — 그 단계에 대고 하는 말이다.
|
|
105
|
+
const m = 수뒤.exec(원);
|
|
106
|
+
if (m) {
|
|
107
|
+
const n = Number(m[1]);
|
|
108
|
+
if (Number.isInteger(n) && n >= 1 && n <= 단계수) {
|
|
109
|
+
const 말 = 원.slice(m[0].length).replace(사이표, '').trim();
|
|
110
|
+
if (말) return { 갈래: '다시', 덧말: 말, 단계: n };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { 갈래: '다시', 덧말: 원, 단계: null };
|
|
115
|
+
}
|