deel-local-cli 1.10.0 → 1.13.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 +1167 -1166
- package/README.md +1224 -1223
- package/bin/deel.js +1 -1
- package/package.json +2 -2
- package/src/acp/map.js +367 -362
- package/src/acp/serve.js +810 -769
- package/src/agent/budget.js +156 -153
- package/src/agent/commit.js +534 -511
- package/src/agent/compact.js +158 -31
- package/src/agent/effort.js +308 -197
- package/src/agent/evolve.js +263 -213
- package/src/agent/filemem.js +16 -2
- package/src/agent/loop.js +1728 -1516
- package/src/agent/memory.js +156 -152
- package/src/agent/mention.js +210 -210
- package/src/agent/recall.js +222 -209
- package/src/agent/session.js +1056 -948
- package/src/backend/adapter.js +1355 -798
- package/src/backend/cachemark.js +223 -0
- package/src/backend/detect.js +327 -288
- package/src/backend/http.js +460 -458
- package/src/backend/mcp.js +407 -395
- package/src/backend/price.js +260 -197
- package/src/backend/probe.js +487 -473
- package/src/backend/quota.js +274 -157
- package/src/backend/retry.js +30 -6
- package/src/backend/tokens.js +59 -0
- package/src/backend/toolfit.js +352 -325
- package/src/backend/wire.js +715 -0
- package/src/commands.js +3184 -2998
- package/src/config.js +283 -244
- package/src/i18n/en.js +545 -520
- package/src/i18n/ja.js +498 -473
- package/src/i18n/ko.js +592 -567
- package/src/i18n/zh.js +498 -473
- package/src/oneshot.js +625 -549
- package/src/pack/tar.js +154 -132
- package/src/pack/zip.js +235 -217
- package/src/plugins/manage.js +416 -336
- package/src/providers/bedrock.js +17 -0
- package/src/repl.js +2616 -2515
- package/src/safety/audit.js +148 -129
- package/src/safety/guard.js +641 -526
- package/src/safety/network.js +200 -157
- package/src/tools/fastgrep.js +229 -214
- package/src/tools/fsutil.js +291 -250
- package/src/tools/index.js +2560 -2523
- package/src/tools/jobs.js +876 -795
- package/src/tools/label.js +52 -0
- package/src/tools/spawn.js +213 -99
- package/src/tools/verify.js +358 -357
- package/src/tools/webfetch.js +418 -393
- package/src/ui/export.js +233 -217
- package/src/ui/pick.js +115 -115
- package/src/ui/status.js +617 -610
package/src/safety/network.js
CHANGED
|
@@ -1,157 +1,200 @@
|
|
|
1
|
-
// 어디로 말을 걸 수 있는지 한 곳에서 정한다.
|
|
2
|
-
//
|
|
3
|
-
// 왜 필요한가:
|
|
4
|
-
// 코딩 에이전트는 소스 코드를 통째로 모델에 보낸다. 그 주소가 어디인지가 전부다.
|
|
5
|
-
// "설정한 곳으로만 갑니다" 를 말로 하면 언젠가 거짓이 된다. 코드가 막아야 한다.
|
|
6
|
-
//
|
|
7
|
-
// 규칙:
|
|
8
|
-
// 1) 기본은 전부 거절. 허용 목록에 오른 자리만 통과.
|
|
9
|
-
// 2) 모델 호출은 setup 에서 정한 그 주소 하나만.
|
|
10
|
-
// 3) 플러그인 받기(github)는 사용자가 그 명령을 칠 때만 잠깐 열린다.
|
|
11
|
-
// 4) offline 이면 이 컴퓨터 밖은 전부 거절 — 3번도 막힌다.
|
|
12
|
-
//
|
|
13
|
-
// 여기를 지나지 않는 요청은 없다. http.js 의 req() 가 매번 물어본다.
|
|
14
|
-
|
|
15
|
-
export class NetBlocked extends Error {
|
|
16
|
-
constructor(url, why) {
|
|
17
|
-
super(`허용되지 않은 주소입니다: ${url}\n ${why}`);
|
|
18
|
-
this.name = 'NetBlocked';
|
|
19
|
-
this.url = url;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const LOCAL = new Set(['localhost', '127.0.0.1', '::1', '0.0.0.0', '::']);
|
|
24
|
-
|
|
25
|
-
/*
|
|
26
|
-
* 이 컴퓨터·사내망 주소인가. WebFetch 가 안 읽을 곳, 오프라인 잠금이 그래도 열어 줄 곳.
|
|
27
|
-
*
|
|
28
|
-
* 글자만 보면 빠져나가는 철자가 있었다 — `localhost.`(끝에 점)와 `[::ffff:127.0.0.1]`
|
|
29
|
-
* (IPv4 를 IPv6 에 싼 것)은 둘 다 127.0.0.1 에 붙는데 '바깥' 으로 읽혔다. 되돌림이
|
|
30
|
-
* 그리로 가면 사내 서비스를 읽게 된다. 그래서 먼저 철자를 편다: 끝 점을 떼고, 대괄호를
|
|
31
|
-
* 벗기고, ::ffff: 로 싼 IPv4 는 점 네 개 꼴로 되돌린 뒤 본다.
|
|
32
|
-
* 169.254.* (링크 로컬 — 클라우드 메타데이터가 여기 산다)와 IPv6 의 fe80:: · fc00::/7 도
|
|
33
|
-
* 이 컴퓨터·사내망으로 친다.
|
|
34
|
-
*/
|
|
35
|
-
export const isLocalHost = (h) => {
|
|
36
|
-
let s = String(h ?? '').trim().toLowerCase().replace(/\.$/, '').replace(/^\[|\]$/g, '');
|
|
37
|
-
if (s.startsWith('::ffff:')) {
|
|
38
|
-
const 뒤 = s.slice(7);
|
|
39
|
-
const hex = /^([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(뒤);
|
|
40
|
-
s = hex
|
|
41
|
-
? [hex[1], hex[2]].flatMap((x) => { const n = parseInt(x, 16); return [n >> 8, n & 255]; }).join('.')
|
|
42
|
-
: 뒤;
|
|
43
|
-
}
|
|
44
|
-
if (LOCAL.has(s)) return true;
|
|
45
|
-
if (/^(127|10|0)\./.test(s) || /^192\.168\./.test(s) || /^172\.(1[6-9]|2\d|3[01])\./.test(s) || /^169\.254\./.test(s)) return true;
|
|
46
|
-
if (/^fe[89ab][0-9a-f]:/.test(s) || /^f[cd][0-9a-f]{2}:/.test(s)) return true;
|
|
47
|
-
return false;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* 적어
|
|
75
|
-
*/
|
|
76
|
-
export function
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
*
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
*
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
1
|
+
// 어디로 말을 걸 수 있는지 한 곳에서 정한다.
|
|
2
|
+
//
|
|
3
|
+
// 왜 필요한가:
|
|
4
|
+
// 코딩 에이전트는 소스 코드를 통째로 모델에 보낸다. 그 주소가 어디인지가 전부다.
|
|
5
|
+
// "설정한 곳으로만 갑니다" 를 말로 하면 언젠가 거짓이 된다. 코드가 막아야 한다.
|
|
6
|
+
//
|
|
7
|
+
// 규칙:
|
|
8
|
+
// 1) 기본은 전부 거절. 허용 목록에 오른 자리만 통과.
|
|
9
|
+
// 2) 모델 호출은 setup 에서 정한 그 주소 하나만.
|
|
10
|
+
// 3) 플러그인 받기(github)는 사용자가 그 명령을 칠 때만 잠깐 열린다.
|
|
11
|
+
// 4) offline 이면 이 컴퓨터 밖은 전부 거절 — 3번도 막힌다.
|
|
12
|
+
//
|
|
13
|
+
// 여기를 지나지 않는 요청은 없다. http.js 의 req() 가 매번 물어본다.
|
|
14
|
+
|
|
15
|
+
export class NetBlocked extends Error {
|
|
16
|
+
constructor(url, why) {
|
|
17
|
+
super(`허용되지 않은 주소입니다: ${url}\n ${why}`);
|
|
18
|
+
this.name = 'NetBlocked';
|
|
19
|
+
this.url = url;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const LOCAL = new Set(['localhost', '127.0.0.1', '::1', '0.0.0.0', '::']);
|
|
24
|
+
|
|
25
|
+
/*
|
|
26
|
+
* 이 컴퓨터·사내망 주소인가. WebFetch 가 안 읽을 곳, 오프라인 잠금이 그래도 열어 줄 곳.
|
|
27
|
+
*
|
|
28
|
+
* 글자만 보면 빠져나가는 철자가 있었다 — `localhost.`(끝에 점)와 `[::ffff:127.0.0.1]`
|
|
29
|
+
* (IPv4 를 IPv6 에 싼 것)은 둘 다 127.0.0.1 에 붙는데 '바깥' 으로 읽혔다. 되돌림이
|
|
30
|
+
* 그리로 가면 사내 서비스를 읽게 된다. 그래서 먼저 철자를 편다: 끝 점을 떼고, 대괄호를
|
|
31
|
+
* 벗기고, ::ffff: 로 싼 IPv4 는 점 네 개 꼴로 되돌린 뒤 본다.
|
|
32
|
+
* 169.254.* (링크 로컬 — 클라우드 메타데이터가 여기 산다)와 IPv6 의 fe80:: · fc00::/7 도
|
|
33
|
+
* 이 컴퓨터·사내망으로 친다.
|
|
34
|
+
*/
|
|
35
|
+
export const isLocalHost = (h) => {
|
|
36
|
+
let s = String(h ?? '').trim().toLowerCase().replace(/\.$/, '').replace(/^\[|\]$/g, '');
|
|
37
|
+
if (s.startsWith('::ffff:')) {
|
|
38
|
+
const 뒤 = s.slice(7);
|
|
39
|
+
const hex = /^([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(뒤);
|
|
40
|
+
s = hex
|
|
41
|
+
? [hex[1], hex[2]].flatMap((x) => { const n = parseInt(x, 16); return [n >> 8, n & 255]; }).join('.')
|
|
42
|
+
: 뒤;
|
|
43
|
+
}
|
|
44
|
+
if (LOCAL.has(s)) return true;
|
|
45
|
+
if (/^(127|10|0)\./.test(s) || /^192\.168\./.test(s) || /^172\.(1[6-9]|2\d|3[01])\./.test(s) || /^169\.254\./.test(s)) return true;
|
|
46
|
+
if (/^fe[89ab][0-9a-f]:/.test(s) || /^f[cd][0-9a-f]{2}:/.test(s)) return true;
|
|
47
|
+
return false;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 이 이름이 **실제로 어디로 풀리나.** 하나라도 사내·로컬이면 막는다.
|
|
52
|
+
*
|
|
53
|
+
* ── 왜 이름만 보면 모자란가 ───────────────────────────────────────────
|
|
54
|
+
*
|
|
55
|
+
* 위 isLocalHost 는 **글자**를 본다. `localhost` · `127.0.0.1` ·
|
|
56
|
+
* `[::ffff:127.0.0.1]` 같은 철자를 촘촘히 편다. 그런데 이런 주소는 그 어느
|
|
57
|
+
* 철자에도 안 걸린다:
|
|
58
|
+
*
|
|
59
|
+
* https://내부로푸는이름.example.com/ ← A 레코드가 169.254.169.254
|
|
60
|
+
*
|
|
61
|
+
* 이름은 평범한 공개 도메인이고, DNS 만 공격자가 쥐고 있으면 된다. 글자
|
|
62
|
+
* 검사를 지나서 **클라우드 메타데이터**(임시 자격증명이 나온다)나 사내
|
|
63
|
+
* 서비스에 그대로 닿는다. 모델에게 주소 한 줄을 읽히는 것으로 끝난다.
|
|
64
|
+
*
|
|
65
|
+
* 그래서 이름을 실제로 풀어 보고, 돌아온 주소를 **전부** 본다. 하나라도
|
|
66
|
+
* 사내·로컬이면 막는다 — 여러 개 중 하나만 안전한 척하는 응답도 있다.
|
|
67
|
+
*
|
|
68
|
+
* ── 여기까지가 이 자가 막는 것 ────────────────────────────────────────
|
|
69
|
+
*
|
|
70
|
+
* 푼 뒤에 실제 연결이 다시 풀린다(TOCTOU). 그 사이에 DNS 를 바꾸면 이 검사를
|
|
71
|
+
* 지나고도 다른 데로 갈 수 있다. 그것까지 막으려면 푼 주소를 소켓에 못
|
|
72
|
+
* 박아야 하는데, `fetch` 는 그 자리를 안 내준다. 여기서 막는 것은 **한 번
|
|
73
|
+
* 푸는 것으로 끝나는 흔한 길**이고, 못 막는 것은 위에 적은 그 틈이다.
|
|
74
|
+
* 적어 두지 않으면 다음 사람이 이것을 완전한 자물쇠로 여긴다.
|
|
75
|
+
*/
|
|
76
|
+
export async function 사설로풀리나(hostname) {
|
|
77
|
+
const h = String(hostname ?? '').trim();
|
|
78
|
+
if (!h) return null;
|
|
79
|
+
// 숫자로 적힌 주소는 풀 것이 없다 — 위 isLocalHost 가 이미 본다.
|
|
80
|
+
if (/^[0-9.]+$/.test(h) || h.includes(':') || h.startsWith('[')) return null;
|
|
81
|
+
let 주소들;
|
|
82
|
+
try {
|
|
83
|
+
const { lookup } = await import('node:dns/promises');
|
|
84
|
+
주소들 = await lookup(h, { all: true, verbatim: true });
|
|
85
|
+
} catch {
|
|
86
|
+
// 못 풀면 어차피 못 붙는다. 여기서 막을 것도 없다.
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const 걸린것 = 주소들.map((a) => a.address).filter((a) => isLocalHost(a));
|
|
90
|
+
return 걸린것.length ? { 주소들: 주소들.map((a) => a.address), 걸린것 } : null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function originOf(url) {
|
|
94
|
+
const u = new URL(url);
|
|
95
|
+
return `${u.protocol}//${u.host}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 하나뿐인 문지기. 모듈 하나에 상태를 두는 것은 일부러다 —
|
|
99
|
+
// 여기저기서 각자 예외를 두면 자물쇠가 아니게 된다.
|
|
100
|
+
const gate = {
|
|
101
|
+
allow: new Set(), // 통과시킬 origin 들
|
|
102
|
+
offline: false, // true 면 이 컴퓨터 밖은 전부 거절
|
|
103
|
+
log: [], // 실제로 나간 곳 (사람이 확인용)
|
|
104
|
+
enforced: true,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/*
|
|
108
|
+
* 모델 연결 주소를 허용 목록에 올린다. **이전에 올린 것은 지운다.**
|
|
109
|
+
*
|
|
110
|
+
* 여럿을 한 번에 줄 수 있다. 다만 그건 "하나씩 더 쌓는" 길이 아니다 — 부르는
|
|
111
|
+
* 쪽이 지금 열려 있어야 할 것 **전부**를 한 번에 말하는 것이다. 쌓는 길을
|
|
112
|
+
* 내주면 여기저기서 한 줄씩 더하다가 자물쇠가 자물쇠가 아니게 된다.
|
|
113
|
+
*
|
|
114
|
+
* 여럿이 필요한 자리는 하나다. 사람이 스킴을 안 적은 주소를 넣었을 때,
|
|
115
|
+
* 자리마다 기본으로 붙이는 스킴이 달라서(로컬은 http, Azure 는 https) 둘 다
|
|
116
|
+
* 열어 둬야 우리가 만든 주소를 우리 자물쇠가 막지 않는다. 호스트는 사람이
|
|
117
|
+
* 적어 넣은 그 하나뿐이라 넓어지는 것이 아니다.
|
|
118
|
+
*/
|
|
119
|
+
export function allowEndpoint(baseUrl) {
|
|
120
|
+
gate.allow.clear();
|
|
121
|
+
for (const u of Array.isArray(baseUrl) ? baseUrl : [baseUrl]) {
|
|
122
|
+
if (u) gate.allow.add(originOf(u));
|
|
123
|
+
}
|
|
124
|
+
return [...gate.allow];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** 잠깐 한 곳을 더 연다. 되돌리는 함수를 준다 — 반드시 finally 에서 부른다. */
|
|
128
|
+
/*
|
|
129
|
+
* 잠깐 연다. 돌려주는 함수로 닫는다.
|
|
130
|
+
*
|
|
131
|
+
* 같은 집을 여럿이 겹쳐 열 수 있다 — WebFetch 다섯 개가 한 집에 줄을 서면
|
|
132
|
+
* 첫 것이 닫을 때 나머지 넷이 막히면 안 된다. 그래서 센다. 마지막이 닫을 때만
|
|
133
|
+
* 정말 닫고, 원래(allowEndpoint 로) 열려 있던 집은 끝까지 안 닫는다.
|
|
134
|
+
*/
|
|
135
|
+
const 잠깐연것 = new Map(); // origin → { n, 원래 }
|
|
136
|
+
export function allowTemporarily(url) {
|
|
137
|
+
const o = originOf(url);
|
|
138
|
+
const 칸 = 잠깐연것.get(o) ?? { n: 0, 원래: gate.allow.has(o) };
|
|
139
|
+
칸.n += 1;
|
|
140
|
+
잠깐연것.set(o, 칸);
|
|
141
|
+
gate.allow.add(o);
|
|
142
|
+
let 닫았나 = false;
|
|
143
|
+
return () => {
|
|
144
|
+
if (닫았나) return; // 두 번 닫아도 남의 몫을 닫지 않는다
|
|
145
|
+
닫았나 = true;
|
|
146
|
+
칸.n -= 1;
|
|
147
|
+
if (칸.n > 0) return;
|
|
148
|
+
잠깐연것.delete(o);
|
|
149
|
+
if (!칸.원래) gate.allow.delete(o);
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function setOffline(on) { gate.offline = !!on; return gate.offline; }
|
|
154
|
+
export function isOffline() { return gate.offline; }
|
|
155
|
+
export function allowed() { return [...gate.allow]; }
|
|
156
|
+
export function contacted() { return gate.log.slice(); }
|
|
157
|
+
export function resetNet() { gate.allow.clear(); 잠깐연것.clear(); gate.log.length = 0; gate.offline = false; 마지막경유 = null; }
|
|
158
|
+
|
|
159
|
+
/*
|
|
160
|
+
* 거쳐 간 프록시.
|
|
161
|
+
*
|
|
162
|
+
* 프록시는 허용 목록에 오르는 자리가 아니다 — 목적지는 여전히 그 한 자리이고,
|
|
163
|
+
* 프록시는 거기까지 가는 **길**이다. 그래도 패킷이 실제로 닿는 첫 기계는 프록시라서,
|
|
164
|
+
* 심사서와 화면에는 "이 프록시를 거친다" 가 적혀야 한다. 어디를 거쳤는지 여기 남긴다.
|
|
165
|
+
*/
|
|
166
|
+
let 마지막경유 = null;
|
|
167
|
+
export function 프록시경유() { return 마지막경유; }
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 이 주소로 나가도 되는가. 안 되면 던진다.
|
|
171
|
+
* 통과한 것은 기록에 남는다 — "무엇이 어디로 갔나" 를 나중에 보여 주기 위해서다.
|
|
172
|
+
* @param {string|null} 거쳐 이 요청이 거칠 프록시 주소. 없으면 직접 간다.
|
|
173
|
+
*/
|
|
174
|
+
export function checkUrl(url, 거쳐 = null) {
|
|
175
|
+
let u;
|
|
176
|
+
try { u = new URL(url); } catch { throw new NetBlocked(url, '주소 형식이 아닙니다.'); }
|
|
177
|
+
|
|
178
|
+
if (u.protocol !== 'http:' && u.protocol !== 'https:') {
|
|
179
|
+
throw new NetBlocked(url, `${u.protocol} 는 쓰지 않습니다.`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const local = isLocalHost(u.hostname);
|
|
183
|
+
if (gate.offline && !local) {
|
|
184
|
+
throw new NetBlocked(url, '오프라인 모드입니다 — 이 컴퓨터 밖으로는 나가지 않습니다.');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const origin = `${u.protocol}//${u.host}`;
|
|
188
|
+
if (!gate.allow.has(origin)) {
|
|
189
|
+
throw new NetBlocked(url,
|
|
190
|
+
gate.allow.size
|
|
191
|
+
? `지금 허용된 곳: ${[...gate.allow].join(', ')}`
|
|
192
|
+
: '연결이 정해지지 않았습니다. deel setup 을 먼저 하세요.');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const seen = gate.log.find((x) => x.origin === origin);
|
|
196
|
+
if (seen) { seen.n++; if (거쳐) seen.via = 거쳐; }
|
|
197
|
+
else gate.log.push({ origin, n: 1, local, via: 거쳐 ?? null });
|
|
198
|
+
if (거쳐) 마지막경유 = { url: 거쳐 };
|
|
199
|
+
return true;
|
|
200
|
+
}
|