connectbase-client 3.5.3 → 3.7.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/CHANGELOG.md +52 -0
- package/dist/cli.js +258 -118
- package/dist/connect-base.umd.js +3 -3
- package/dist/index.d.mts +74 -3
- package/dist/index.d.ts +74 -3
- package/dist/index.js +85 -0
- package/dist/index.mjs +85 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,58 @@
|
|
|
3
3
|
본 SDK 의 모든 주요 변경사항을 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/) 형식으로 기록합니다.
|
|
4
4
|
버전은 [Semantic Versioning](https://semver.org/lang/ko/) 을 따릅니다.
|
|
5
5
|
|
|
6
|
+
## [3.6.0] - 2026-05-01
|
|
7
|
+
|
|
8
|
+
### Added — `cb.realtime.stream()` 멀티모달 메시지 (Vision) 지원
|
|
9
|
+
|
|
10
|
+
`cb.realtime.stream()` 의 `content` 필드가 `string` 외에도 OpenAI Vision spec
|
|
11
|
+
호환 array 형식 (`{ type: 'text', text }` 또는 `{ type: 'image_url', image_url: { url } }`
|
|
12
|
+
파트 배열) 을 받을 수 있게 됐습니다. 이미지 첨부 채팅을 SDK 한 줄로 작성 가능.
|
|
13
|
+
|
|
14
|
+
**예시:**
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
await cb.realtime.stream(
|
|
18
|
+
[{
|
|
19
|
+
role: 'user',
|
|
20
|
+
content: [
|
|
21
|
+
{ type: 'text', text: '이 이미지에 보이는 동물은?' },
|
|
22
|
+
{ type: 'image_url', image_url: { url: 'https://.../cat.jpg' } },
|
|
23
|
+
],
|
|
24
|
+
}],
|
|
25
|
+
{ onToken, onDone, onError },
|
|
26
|
+
{ provider: 'openai_compatible' },
|
|
27
|
+
)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Provider 별 처리:**
|
|
31
|
+
|
|
32
|
+
- **`openai` / `openai_compatible` (vLLM, LM Studio, Ollama 등)**: content array 를
|
|
33
|
+
그대로 passthrough — vLLM 의 Qwen-VL / LLaVA / InternVL 등 모든 OpenAI Vision
|
|
34
|
+
호환 비전 모델 즉시 동작.
|
|
35
|
+
- **`claude`**: `data:image/...;base64,...` URI 는 Claude 의 `source.type=base64`
|
|
36
|
+
콘텐츠 블록으로 변환, `https://...` URL 은 `source.type=url` 로 위임 (Anthropic
|
|
37
|
+
API 가 fetch 담당). 텍스트 파트는 `type=text` 블록으로 변환.
|
|
38
|
+
- **`gemini`**: Gemini API 가 외부 URL fetch 를 지원하지 않으므로 SDK 가 사전
|
|
39
|
+
fetch (timeout 30s, 20MB 사이즈 캡, image/* MIME 검증) → base64 `inline_data`
|
|
40
|
+
로 변환. data URI 는 즉시 분해.
|
|
41
|
+
|
|
42
|
+
**SDK 타입 변경:**
|
|
43
|
+
|
|
44
|
+
- `StreamMessage.content`: `string` → `string | StreamContentPart[]` (union, 하위 호환).
|
|
45
|
+
- 신규 export: `StreamContentPart`, `StreamTextPart`, `StreamImageURLPart`.
|
|
46
|
+
|
|
47
|
+
**서버 변경:**
|
|
48
|
+
|
|
49
|
+
- socket-server `protocol.StreamMessage.Content` 를 `json.RawMessage` 로 변경 후
|
|
50
|
+
새 helper `protocol.ParseStreamContent()` 가 string/array 를 통일 디코드.
|
|
51
|
+
- pkg/ai 의 `Message` 에 `Parts []ContentPart` 추가 — provider 별 wire format 으로
|
|
52
|
+
분기 직렬화. `Message.Content` (string) 는 기존과 동일하게 유지 → 모든 string-only
|
|
53
|
+
호출자 (core-server, mcp-server 등) 회귀 무영향.
|
|
54
|
+
|
|
55
|
+
**회귀 가드:** 11 multimodal provider 직렬화 테스트 + 11 protocol/handler 검증
|
|
56
|
+
테스트 + 3 convertMessages plumbing 테스트 추가.
|
|
57
|
+
|
|
6
58
|
## [3.5.3] - 2026-05-01
|
|
7
59
|
|
|
8
60
|
### Fixed — `connectbase tunnel --label` 의 `tunnel_id` 추출 실패
|
package/dist/cli.js
CHANGED
|
@@ -1431,10 +1431,36 @@ function createWsPongFrame() {
|
|
|
1431
1431
|
header[1] = 128 | 0;
|
|
1432
1432
|
return Buffer.concat([header, maskKey]);
|
|
1433
1433
|
}
|
|
1434
|
+
function createWsBinaryFrame(payload) {
|
|
1435
|
+
const len = payload.length;
|
|
1436
|
+
const maskKey = crypto.randomBytes(4);
|
|
1437
|
+
let header;
|
|
1438
|
+
if (len < 126) {
|
|
1439
|
+
header = Buffer.alloc(2);
|
|
1440
|
+
header[0] = 130;
|
|
1441
|
+
header[1] = 128 | len;
|
|
1442
|
+
} else if (len < 65536) {
|
|
1443
|
+
header = Buffer.alloc(4);
|
|
1444
|
+
header[0] = 130;
|
|
1445
|
+
header[1] = 128 | 126;
|
|
1446
|
+
header.writeUInt16BE(len, 2);
|
|
1447
|
+
} else {
|
|
1448
|
+
header = Buffer.alloc(10);
|
|
1449
|
+
header[0] = 130;
|
|
1450
|
+
header[1] = 128 | 127;
|
|
1451
|
+
header.writeBigUInt64BE(BigInt(len), 2);
|
|
1452
|
+
}
|
|
1453
|
+
const masked = Buffer.alloc(len);
|
|
1454
|
+
for (let i = 0; i < len; i++) {
|
|
1455
|
+
masked[i] = payload[i] ^ maskKey[i % 4];
|
|
1456
|
+
}
|
|
1457
|
+
return Buffer.concat([header, maskKey, masked]);
|
|
1458
|
+
}
|
|
1434
1459
|
var WsFrameParser = class {
|
|
1435
1460
|
constructor(handlers) {
|
|
1436
1461
|
this.buffer = Buffer.alloc(0);
|
|
1437
1462
|
this.onMessage = handlers.onMessage;
|
|
1463
|
+
this.onBinary = handlers.onBinary;
|
|
1438
1464
|
this.onClose = handlers.onClose;
|
|
1439
1465
|
this.onPing = handlers.onPing;
|
|
1440
1466
|
}
|
|
@@ -1479,6 +1505,13 @@ var WsFrameParser = class {
|
|
|
1479
1505
|
case 1:
|
|
1480
1506
|
this.onMessage(payload.toString("utf-8"));
|
|
1481
1507
|
break;
|
|
1508
|
+
case 2:
|
|
1509
|
+
if (this.onBinary) {
|
|
1510
|
+
const copy = Buffer.alloc(payload.length);
|
|
1511
|
+
payload.copy(copy);
|
|
1512
|
+
this.onBinary(copy);
|
|
1513
|
+
}
|
|
1514
|
+
break;
|
|
1482
1515
|
case 8:
|
|
1483
1516
|
this.onClose();
|
|
1484
1517
|
break;
|
|
@@ -1664,7 +1697,7 @@ async function startTunnel(port, config, tunnelOpts) {
|
|
|
1664
1697
|
const tunnelServerUrl = getTunnelServerUrl(config.baseUrl);
|
|
1665
1698
|
const parsedUrl = new URL(tunnelServerUrl);
|
|
1666
1699
|
const isHttps = parsedUrl.protocol === "https:";
|
|
1667
|
-
let wsPath = `/
|
|
1700
|
+
let wsPath = `/v2/tunnel/connect?app_id=${encodeURIComponent(appId)}&local_port=${port}`;
|
|
1668
1701
|
if (tunnelOpts?.timeout) {
|
|
1669
1702
|
wsPath += `&timeout=${tunnelOpts.timeout}`;
|
|
1670
1703
|
}
|
|
@@ -1741,6 +1774,15 @@ ${colors.cyan}ConnectBase Tunnel${colors.reset}`);
|
|
|
1741
1774
|
warn(`\uBA54\uC2DC\uC9C0 \uD30C\uC2F1 \uC2E4\uD328: ${e}`);
|
|
1742
1775
|
}
|
|
1743
1776
|
},
|
|
1777
|
+
onBinary: (data) => {
|
|
1778
|
+
if (data.length < 8) {
|
|
1779
|
+
warn(`v2 binary frame too short (${data.length} bytes)`);
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
const streamId = data.readBigUInt64BE(0).toString();
|
|
1783
|
+
const payload = data.subarray(8);
|
|
1784
|
+
routeStreamData(streamId, payload);
|
|
1785
|
+
},
|
|
1744
1786
|
onClose: () => {
|
|
1745
1787
|
info("\uC11C\uBC84\uAC00 \uC5F0\uACB0\uC744 \uC885\uB8CC\uD588\uC2B5\uB2C8\uB2E4");
|
|
1746
1788
|
sock.destroy();
|
|
@@ -1804,6 +1846,190 @@ ${colors.cyan}ConnectBase Tunnel${colors.reset}`);
|
|
|
1804
1846
|
info(`${(delay / 1e3).toFixed(0)}\uCD08 \uD6C4 \uC7AC\uC5F0\uACB0 \uC2DC\uB3C4... (${reconnectAttempts}/${maxReconnectAttempts})`);
|
|
1805
1847
|
setTimeout(connect, delay);
|
|
1806
1848
|
}
|
|
1849
|
+
const streams = /* @__PURE__ */ new Map();
|
|
1850
|
+
function routeStreamData(streamId, payload) {
|
|
1851
|
+
const f = streams.get(streamId);
|
|
1852
|
+
if (!f) return;
|
|
1853
|
+
f.feedBody(payload);
|
|
1854
|
+
}
|
|
1855
|
+
function sendControl(sock, msg) {
|
|
1856
|
+
try {
|
|
1857
|
+
sock.write(createWsTextFrame(JSON.stringify(msg)));
|
|
1858
|
+
} catch {
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
function sendBinary(sock, streamIdStr, payload) {
|
|
1862
|
+
try {
|
|
1863
|
+
const header = Buffer.alloc(8);
|
|
1864
|
+
header.writeBigUInt64BE(BigInt(streamIdStr), 0);
|
|
1865
|
+
sock.write(createWsBinaryFrame(Buffer.concat([header, payload])));
|
|
1866
|
+
} catch {
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
function startHTTPStream(sock, streamId, open, localPort) {
|
|
1870
|
+
const method = open.method || "GET";
|
|
1871
|
+
const reqPath = open.path || "/";
|
|
1872
|
+
const query = open.query || "";
|
|
1873
|
+
const headers = open.headers || {};
|
|
1874
|
+
const fullPath = query ? `${reqPath}?${query}` : reqPath;
|
|
1875
|
+
const localHeaders = {};
|
|
1876
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
1877
|
+
if (k.toLowerCase() !== "host") localHeaders[k] = v;
|
|
1878
|
+
}
|
|
1879
|
+
localHeaders["host"] = `localhost:${localPort}`;
|
|
1880
|
+
let started = false;
|
|
1881
|
+
let cancelled = false;
|
|
1882
|
+
const localReq = http.request(
|
|
1883
|
+
{ hostname: "127.0.0.1", port: localPort, path: fullPath, method, headers: localHeaders },
|
|
1884
|
+
(res) => {
|
|
1885
|
+
const respHeaders = {};
|
|
1886
|
+
for (const [k, v] of Object.entries(res.headers)) {
|
|
1887
|
+
if (v == null) continue;
|
|
1888
|
+
respHeaders[k] = Array.isArray(v) ? v.join(", ") : v;
|
|
1889
|
+
}
|
|
1890
|
+
started = true;
|
|
1891
|
+
sendControl(sock, {
|
|
1892
|
+
type: "stream_response",
|
|
1893
|
+
stream_id: streamId,
|
|
1894
|
+
status: res.statusCode || 502,
|
|
1895
|
+
headers: respHeaders
|
|
1896
|
+
});
|
|
1897
|
+
const methodColor = method === "GET" ? colors.green : method === "POST" ? colors.blue : colors.yellow;
|
|
1898
|
+
log(`${colors.dim}${(/* @__PURE__ */ new Date()).toLocaleTimeString()}${colors.reset} ${methodColor}${method}${colors.reset} ${reqPath} \u2192 ${res.statusCode}`);
|
|
1899
|
+
res.on("data", (chunk) => {
|
|
1900
|
+
if (cancelled) return;
|
|
1901
|
+
sendBinary(sock, streamId, chunk);
|
|
1902
|
+
});
|
|
1903
|
+
res.on("end", () => {
|
|
1904
|
+
if (cancelled) return;
|
|
1905
|
+
sendControl(sock, { type: "stream_eof", stream_id: streamId, side: "upstream" });
|
|
1906
|
+
sendControl(sock, { type: "stream_close", stream_id: streamId });
|
|
1907
|
+
streams.delete(streamId);
|
|
1908
|
+
});
|
|
1909
|
+
res.on("error", (err) => {
|
|
1910
|
+
sendControl(sock, { type: "stream_close", stream_id: streamId, error: `upstream_error: ${err.message}` });
|
|
1911
|
+
streams.delete(streamId);
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
);
|
|
1915
|
+
localReq.on("error", (err) => {
|
|
1916
|
+
warn(`\uB85C\uCEEC \uC11C\uBC84 \uC5F0\uACB0 \uC2E4\uD328 (${method} ${reqPath}): ${err.message}`);
|
|
1917
|
+
if (!started) {
|
|
1918
|
+
sendControl(sock, {
|
|
1919
|
+
type: "stream_response",
|
|
1920
|
+
stream_id: streamId,
|
|
1921
|
+
status: 502,
|
|
1922
|
+
headers: { "content-type": "application/json" }
|
|
1923
|
+
});
|
|
1924
|
+
sendBinary(sock, streamId, Buffer.from(JSON.stringify({ error: `Local server error: ${err.message}` })));
|
|
1925
|
+
}
|
|
1926
|
+
sendControl(sock, { type: "stream_close", stream_id: streamId, error: `upstream_error: ${err.message}` });
|
|
1927
|
+
streams.delete(streamId);
|
|
1928
|
+
});
|
|
1929
|
+
const forwarder = {
|
|
1930
|
+
kind: "http",
|
|
1931
|
+
feedBody: (chunk) => {
|
|
1932
|
+
if (!cancelled) localReq.write(chunk);
|
|
1933
|
+
},
|
|
1934
|
+
endBody: () => {
|
|
1935
|
+
if (!cancelled) localReq.end();
|
|
1936
|
+
},
|
|
1937
|
+
cancel: () => {
|
|
1938
|
+
cancelled = true;
|
|
1939
|
+
try {
|
|
1940
|
+
localReq.destroy();
|
|
1941
|
+
} catch {
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
};
|
|
1945
|
+
streams.set(streamId, forwarder);
|
|
1946
|
+
}
|
|
1947
|
+
function startWSStream(sock, streamId, open, localPort) {
|
|
1948
|
+
const reqPath = open.path || "/";
|
|
1949
|
+
const query = open.query || "";
|
|
1950
|
+
const headers = open.headers || {};
|
|
1951
|
+
const fullPath = query ? `${reqPath}?${query}` : reqPath;
|
|
1952
|
+
const localHeaders = {};
|
|
1953
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
1954
|
+
if (k.toLowerCase() !== "host") localHeaders[k] = v;
|
|
1955
|
+
}
|
|
1956
|
+
localHeaders["host"] = `localhost:${localPort}`;
|
|
1957
|
+
let cancelled = false;
|
|
1958
|
+
let upstream = null;
|
|
1959
|
+
const req = http.request({
|
|
1960
|
+
hostname: "127.0.0.1",
|
|
1961
|
+
port: localPort,
|
|
1962
|
+
path: fullPath,
|
|
1963
|
+
method: "GET",
|
|
1964
|
+
headers: localHeaders
|
|
1965
|
+
});
|
|
1966
|
+
req.on("upgrade", (res, sk) => {
|
|
1967
|
+
upstream = sk;
|
|
1968
|
+
const respHeaders = {};
|
|
1969
|
+
for (const [k, v] of Object.entries(res.headers)) {
|
|
1970
|
+
if (v == null) continue;
|
|
1971
|
+
respHeaders[k] = Array.isArray(v) ? v.join(", ") : v;
|
|
1972
|
+
}
|
|
1973
|
+
sendControl(sock, {
|
|
1974
|
+
type: "stream_response",
|
|
1975
|
+
stream_id: streamId,
|
|
1976
|
+
status: res.statusCode || 101,
|
|
1977
|
+
headers: respHeaders,
|
|
1978
|
+
websocket: true
|
|
1979
|
+
});
|
|
1980
|
+
log(`${colors.dim}${(/* @__PURE__ */ new Date()).toLocaleTimeString()}${colors.reset} ${colors.cyan}WS${colors.reset} ${reqPath} \u2192 101`);
|
|
1981
|
+
sk.on("data", (chunk) => {
|
|
1982
|
+
if (cancelled) return;
|
|
1983
|
+
sendBinary(sock, streamId, chunk);
|
|
1984
|
+
});
|
|
1985
|
+
sk.on("close", () => {
|
|
1986
|
+
if (cancelled) return;
|
|
1987
|
+
sendControl(sock, { type: "stream_close", stream_id: streamId });
|
|
1988
|
+
streams.delete(streamId);
|
|
1989
|
+
});
|
|
1990
|
+
sk.on("error", (err) => {
|
|
1991
|
+
sendControl(sock, { type: "stream_close", stream_id: streamId, error: `upstream_error: ${err.message}` });
|
|
1992
|
+
streams.delete(streamId);
|
|
1993
|
+
});
|
|
1994
|
+
});
|
|
1995
|
+
req.on("response", (res) => {
|
|
1996
|
+
const respHeaders = {};
|
|
1997
|
+
for (const [k, v] of Object.entries(res.headers)) {
|
|
1998
|
+
if (v == null) continue;
|
|
1999
|
+
respHeaders[k] = Array.isArray(v) ? v.join(", ") : v;
|
|
2000
|
+
}
|
|
2001
|
+
sendControl(sock, {
|
|
2002
|
+
type: "stream_response",
|
|
2003
|
+
stream_id: streamId,
|
|
2004
|
+
status: res.statusCode || 502,
|
|
2005
|
+
headers: respHeaders,
|
|
2006
|
+
websocket: false
|
|
2007
|
+
});
|
|
2008
|
+
sendControl(sock, { type: "stream_close", stream_id: streamId });
|
|
2009
|
+
streams.delete(streamId);
|
|
2010
|
+
});
|
|
2011
|
+
req.on("error", (err) => {
|
|
2012
|
+
sendControl(sock, { type: "stream_close", stream_id: streamId, error: `upstream_error: ${err.message}` });
|
|
2013
|
+
streams.delete(streamId);
|
|
2014
|
+
});
|
|
2015
|
+
req.end();
|
|
2016
|
+
const forwarder = {
|
|
2017
|
+
kind: "ws",
|
|
2018
|
+
feedBody: (chunk) => {
|
|
2019
|
+
if (!cancelled && upstream) upstream.write(chunk);
|
|
2020
|
+
},
|
|
2021
|
+
endBody: () => {
|
|
2022
|
+
},
|
|
2023
|
+
cancel: () => {
|
|
2024
|
+
cancelled = true;
|
|
2025
|
+
try {
|
|
2026
|
+
upstream?.destroy();
|
|
2027
|
+
} catch {
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
};
|
|
2031
|
+
streams.set(streamId, forwarder);
|
|
2032
|
+
}
|
|
1807
2033
|
async function handleMessage(msg, sock, localPort) {
|
|
1808
2034
|
switch (msg.type) {
|
|
1809
2035
|
case "tunnel_ready": {
|
|
@@ -1851,9 +2077,38 @@ ${colors.dim}Ctrl+C\uB85C \uC885\uB8CC${colors.reset}
|
|
|
1851
2077
|
`);
|
|
1852
2078
|
break;
|
|
1853
2079
|
}
|
|
1854
|
-
case "
|
|
1855
|
-
|
|
2080
|
+
case "stream_open": {
|
|
2081
|
+
const sid = String(msg.stream_id ?? "");
|
|
2082
|
+
if (!sid) {
|
|
2083
|
+
warn("stream_open with empty stream_id");
|
|
2084
|
+
break;
|
|
2085
|
+
}
|
|
2086
|
+
const kind = msg.kind || "http";
|
|
2087
|
+
if (kind === "ws") {
|
|
2088
|
+
startWSStream(sock, sid, msg, localPort);
|
|
2089
|
+
} else {
|
|
2090
|
+
startHTTPStream(sock, sid, msg, localPort);
|
|
2091
|
+
}
|
|
1856
2092
|
break;
|
|
2093
|
+
}
|
|
2094
|
+
case "stream_eof": {
|
|
2095
|
+
const sid = String(msg.stream_id ?? "");
|
|
2096
|
+
const side = msg.side;
|
|
2097
|
+
if (side === "client") {
|
|
2098
|
+
const f = streams.get(sid);
|
|
2099
|
+
f?.endBody();
|
|
2100
|
+
}
|
|
2101
|
+
break;
|
|
2102
|
+
}
|
|
2103
|
+
case "stream_close": {
|
|
2104
|
+
const sid = String(msg.stream_id ?? "");
|
|
2105
|
+
const f = streams.get(sid);
|
|
2106
|
+
if (f) {
|
|
2107
|
+
f.cancel();
|
|
2108
|
+
streams.delete(sid);
|
|
2109
|
+
}
|
|
2110
|
+
break;
|
|
2111
|
+
}
|
|
1857
2112
|
case "tunnel_error": {
|
|
1858
2113
|
const result = handleTunnelError(msg, appId, localPort);
|
|
1859
2114
|
error(result.message);
|
|
@@ -1868,121 +2123,6 @@ ${colors.dim}Ctrl+C\uB85C \uC885\uB8CC${colors.reset}
|
|
|
1868
2123
|
break;
|
|
1869
2124
|
}
|
|
1870
2125
|
}
|
|
1871
|
-
function forwardRequest(msg, sock, localPort) {
|
|
1872
|
-
const requestId = msg.request_id;
|
|
1873
|
-
const method = msg.method;
|
|
1874
|
-
const reqPath = msg.path;
|
|
1875
|
-
const query = msg.query || "";
|
|
1876
|
-
const headers = msg.headers || {};
|
|
1877
|
-
const bodyBase64 = msg.body;
|
|
1878
|
-
const fullPath = query ? `${reqPath}?${query}` : reqPath;
|
|
1879
|
-
const localHeaders = {};
|
|
1880
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
1881
|
-
if (key.toLowerCase() !== "host") {
|
|
1882
|
-
localHeaders[key] = value;
|
|
1883
|
-
}
|
|
1884
|
-
}
|
|
1885
|
-
localHeaders["host"] = `localhost:${localPort}`;
|
|
1886
|
-
const reqOptions = {
|
|
1887
|
-
hostname: "127.0.0.1",
|
|
1888
|
-
port: localPort,
|
|
1889
|
-
path: fullPath,
|
|
1890
|
-
method,
|
|
1891
|
-
headers: localHeaders
|
|
1892
|
-
};
|
|
1893
|
-
const localReq = http.request(reqOptions, (res) => {
|
|
1894
|
-
const responseHeaders = {};
|
|
1895
|
-
for (const [key, value] of Object.entries(res.headers)) {
|
|
1896
|
-
if (value) responseHeaders[key] = Array.isArray(value) ? value.join(", ") : value;
|
|
1897
|
-
}
|
|
1898
|
-
const contentType = (responseHeaders["content-type"] || "").toLowerCase();
|
|
1899
|
-
const transferEncoding = (responseHeaders["transfer-encoding"] || "").toLowerCase();
|
|
1900
|
-
const isStreaming = contentType.includes("text/event-stream") || transferEncoding.includes("chunked");
|
|
1901
|
-
if (isStreaming) {
|
|
1902
|
-
try {
|
|
1903
|
-
sock.write(createWsTextFrame(JSON.stringify({
|
|
1904
|
-
type: "http_response_start",
|
|
1905
|
-
request_id: requestId,
|
|
1906
|
-
status: res.statusCode || 200,
|
|
1907
|
-
headers: responseHeaders
|
|
1908
|
-
})));
|
|
1909
|
-
} catch {
|
|
1910
|
-
warn(`\uC2A4\uD2B8\uB9AC\uBC0D \uC2DC\uC791 \uC804\uC1A1 \uC2E4\uD328: ${requestId}`);
|
|
1911
|
-
return;
|
|
1912
|
-
}
|
|
1913
|
-
const methodColor = method === "GET" ? colors.green : method === "POST" ? colors.blue : colors.yellow;
|
|
1914
|
-
log(`${colors.dim}${(/* @__PURE__ */ new Date()).toLocaleTimeString()}${colors.reset} ${methodColor}${method}${colors.reset} ${reqPath} \u2192 ${res.statusCode} ${colors.cyan}[stream]${colors.reset}`);
|
|
1915
|
-
res.on("data", (chunk) => {
|
|
1916
|
-
try {
|
|
1917
|
-
sock.write(createWsTextFrame(JSON.stringify({
|
|
1918
|
-
type: "http_response_chunk",
|
|
1919
|
-
request_id: requestId,
|
|
1920
|
-
data: chunk.toString("base64")
|
|
1921
|
-
})));
|
|
1922
|
-
} catch {
|
|
1923
|
-
warn(`\uC2A4\uD2B8\uB9AC\uBC0D \uCCAD\uD06C \uC804\uC1A1 \uC2E4\uD328: ${requestId}`);
|
|
1924
|
-
}
|
|
1925
|
-
});
|
|
1926
|
-
res.on("end", () => {
|
|
1927
|
-
try {
|
|
1928
|
-
sock.write(createWsTextFrame(JSON.stringify({
|
|
1929
|
-
type: "http_response_end",
|
|
1930
|
-
request_id: requestId
|
|
1931
|
-
})));
|
|
1932
|
-
} catch {
|
|
1933
|
-
}
|
|
1934
|
-
});
|
|
1935
|
-
res.on("error", (err) => {
|
|
1936
|
-
try {
|
|
1937
|
-
sock.write(createWsTextFrame(JSON.stringify({
|
|
1938
|
-
type: "http_response_error",
|
|
1939
|
-
request_id: requestId,
|
|
1940
|
-
error: err.message
|
|
1941
|
-
})));
|
|
1942
|
-
} catch {
|
|
1943
|
-
}
|
|
1944
|
-
});
|
|
1945
|
-
} else {
|
|
1946
|
-
const chunks = [];
|
|
1947
|
-
res.on("data", (chunk) => chunks.push(chunk));
|
|
1948
|
-
res.on("end", () => {
|
|
1949
|
-
const body = Buffer.concat(chunks);
|
|
1950
|
-
const response = {
|
|
1951
|
-
type: "http_response",
|
|
1952
|
-
request_id: requestId,
|
|
1953
|
-
status: res.statusCode || 200,
|
|
1954
|
-
headers: responseHeaders,
|
|
1955
|
-
body: body.length > 0 ? body.toString("base64") : ""
|
|
1956
|
-
};
|
|
1957
|
-
try {
|
|
1958
|
-
sock.write(createWsTextFrame(JSON.stringify(response)));
|
|
1959
|
-
const methodColor = method === "GET" ? colors.green : method === "POST" ? colors.blue : colors.yellow;
|
|
1960
|
-
log(`${colors.dim}${(/* @__PURE__ */ new Date()).toLocaleTimeString()}${colors.reset} ${methodColor}${method}${colors.reset} ${reqPath} \u2192 ${res.statusCode}`);
|
|
1961
|
-
} catch {
|
|
1962
|
-
warn(`\uC751\uB2F5 \uC804\uC1A1 \uC2E4\uD328: ${requestId}`);
|
|
1963
|
-
}
|
|
1964
|
-
});
|
|
1965
|
-
}
|
|
1966
|
-
});
|
|
1967
|
-
localReq.on("error", (err) => {
|
|
1968
|
-
const response = {
|
|
1969
|
-
type: "http_response",
|
|
1970
|
-
request_id: requestId,
|
|
1971
|
-
status: 502,
|
|
1972
|
-
headers: { "content-type": "application/json" },
|
|
1973
|
-
body: Buffer.from(JSON.stringify({ error: `Local server error: ${err.message}` })).toString("base64")
|
|
1974
|
-
};
|
|
1975
|
-
try {
|
|
1976
|
-
sock.write(createWsTextFrame(JSON.stringify(response)));
|
|
1977
|
-
} catch {
|
|
1978
|
-
}
|
|
1979
|
-
warn(`\uB85C\uCEEC \uC11C\uBC84 \uC5F0\uACB0 \uC2E4\uD328 (${method} ${reqPath}): ${err.message}`);
|
|
1980
|
-
});
|
|
1981
|
-
if (bodyBase64) {
|
|
1982
|
-
localReq.write(Buffer.from(bodyBase64, "base64"));
|
|
1983
|
-
}
|
|
1984
|
-
localReq.end();
|
|
1985
|
-
}
|
|
1986
2126
|
connect();
|
|
1987
2127
|
await new Promise(() => {
|
|
1988
2128
|
});
|