connectbase-client 3.39.2 → 3.42.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 CHANGED
@@ -3,6 +3,47 @@
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.42.0] - 2026-07-02
7
+
8
+ ### Added — 웹 배포 대용량 바이너리 Object Storage 직접 업로드 (플랫폼 이슈 019f221b)
9
+
10
+ 3.41.0 의 fail-fast 를 이어, 한도를 넘는 **바이너리**를 배포할 수 있게 했다. 파일당 6 MiB(TiDB
11
+ 단일 엔트리 한도)를 넘는 바이너리(3D 모델·큰 동영상/폰트 등)를 배포하지 못하던 문제 해결.
12
+
13
+ - **대용량 바이너리 직접 업로드**: 한도를 넘는 바이너리는 presigned URL 로 **Object Storage 의 서빙
14
+ 경로에 직접** 업로드하고, 배포 요청에는 `pre_uploaded` 메타데이터만 전송한다. web-proxy 가 서빙하는
15
+ 바로 그 경로에 안착하므로 동일 origin·참조 재작성/CORS 불필요. (대응 core-server 필요. 미지원 서버면
16
+ 3.41.0 과 동일하게 fail-fast.)
17
+ - **fail-fast 유지**: 오프로드 불가한 파일(대용량 **텍스트**, 또는 서버 미지원)은 업로드 전에 즉시 중단.
18
+ - **3D 에셋 확장자 추가**: `.glb`/`.bin`(바이너리), `.gltf`(JSON)을 허용 확장자에 추가 +
19
+ 정확한 MIME(`model/gltf-binary` 등). 이제 3D 모델을 확장자 위장 없이 번들에 그대로 둘 수 있다.
20
+
21
+ ## [3.41.0] - 2026-07-02
22
+
23
+ ### Added — 웹 배포 파일당 크기 한도 사전 검증 (fail-fast)
24
+
25
+ `connectbase deploy` 가 파일을 수집한 직후, 서버로 업로드하기 **전에** 각 파일이 웹 스토리지
26
+ 파일당 크기 한도를 넘는지 검사한다(플랫폼 이슈 019f221b). 기존에는 전량 수집·업로드를 마친
27
+ 뒤에야 서버에서 원시 DB 오류(`Error 8025 (HY000): entry too large`)로 실패해 원인 파악이 어려웠다.
28
+
29
+ - 초과 파일이 있으면 어떤 파일이 얼마나 큰지(바이너리는 원본→base64 크기까지) 나열하고 즉시 종료한다.
30
+ - 검사 기준: 파일 내용은 DB(TiDB Text 컬럼)에 저장되며 단일 엔트리 한도 6 MiB 를 넘을 수 없다.
31
+ 바이너리는 base64(약 1.33x)로 저장되므로 원본 기준 실질 상한 약 4.5 MB. 텍스트는 UTF-8 바이트로 정확히 계산.
32
+
33
+ ## [3.40.0] - 2026-06-28
34
+
35
+ ### Added — KB 하이브리드(시맨틱) 검색 지원
36
+
37
+ 지식베이스 검색에 BM25 키워드 + 벡터 의미 검색을 RRF(Reciprocal Rank Fusion)로 융합하는
38
+ 하이브리드 검색을 추가했다. 콘솔 > **AI** 에서 임베딩을 켠 앱(gemini / openai BYOK)에서
39
+ 동작하며, 오타·동의어·의역에 강하다.
40
+
41
+ - **\`KnowledgeSearchRequest.hybrid?: boolean\`** — \`cb.knowledge.search()\` 에 hybrid 옵션 추가.
42
+ 미설정(기본)이면 임베딩을 켠 앱에서 자동 적용, \`false\` 면 키워드 전용 검색을 강제.
43
+ - **\`AIChatRequest.hybrid?: boolean\`** — \`cb.ai.chat()\` / \`cb.ai.chatStream()\` 의 RAG 검색에도
44
+ 동일 옵션 적용. 미설정이면 임베딩을 켠 앱에서 자동 적용된다.
45
+ - 임베딩 미설정 앱에서는 무시되며 기존 BM25 검색으로 동작한다(하위호환, 회귀 없음).
46
+
6
47
  ## [3.39.2] - 2026-06-26
7
48
 
8
49
  ### Performance — 터널 프록시 응답 지연 제거 (TCP_NODELAY / keep-alive)
package/dist/cli.js CHANGED
@@ -32,6 +32,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
32
32
  var cli_exports = {};
33
33
  __export(cli_exports, {
34
34
  computeDeployDiff: () => computeDeployDiff,
35
+ guessMimeType: () => guessMimeType,
36
+ isAllowedDeployExt: () => isAllowedDeployExt,
37
+ isBinaryDeployExt: () => isBinaryDeployExt,
35
38
  makeRequest: () => makeRequest,
36
39
  normalizeRelativePath: () => normalizeRelativePath,
37
40
  parseArgs: () => parseArgs,
@@ -191,7 +194,11 @@ var ALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([
191
194
  ".xml",
192
195
  ".webmanifest",
193
196
  ".map",
194
- ".wasm"
197
+ ".wasm",
198
+ // 3D 에셋: .glb/.bin 은 바이너리, .gltf 는 JSON(텍스트)
199
+ ".glb",
200
+ ".gltf",
201
+ ".bin"
195
202
  ]);
196
203
  var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
197
204
  ".png",
@@ -213,8 +220,46 @@ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
213
220
  ".webm",
214
221
  ".pdf",
215
222
  ".zip",
216
- ".wasm"
223
+ ".wasm",
224
+ ".glb",
225
+ ".bin"
217
226
  ]);
227
+ var MIME_BY_EXT = {
228
+ ".png": "image/png",
229
+ ".jpg": "image/jpeg",
230
+ ".jpeg": "image/jpeg",
231
+ ".gif": "image/gif",
232
+ ".webp": "image/webp",
233
+ ".avif": "image/avif",
234
+ ".ico": "image/x-icon",
235
+ ".svg": "image/svg+xml",
236
+ ".woff": "font/woff",
237
+ ".woff2": "font/woff2",
238
+ ".ttf": "font/ttf",
239
+ ".otf": "font/otf",
240
+ ".eot": "application/vnd.ms-fontobject",
241
+ ".mp3": "audio/mpeg",
242
+ ".wav": "audio/wav",
243
+ ".ogg": "audio/ogg",
244
+ ".mp4": "video/mp4",
245
+ ".webm": "video/webm",
246
+ ".pdf": "application/pdf",
247
+ ".zip": "application/zip",
248
+ ".wasm": "application/wasm",
249
+ ".glb": "model/gltf-binary",
250
+ ".gltf": "model/gltf+json",
251
+ ".bin": "application/octet-stream"
252
+ };
253
+ function guessMimeType(filePath) {
254
+ return MIME_BY_EXT[path2.extname(filePath).toLowerCase()] || "application/octet-stream";
255
+ }
256
+ function isAllowedDeployExt(filePath) {
257
+ return ALLOWED_EXTENSIONS.has(path2.extname(filePath).toLowerCase());
258
+ }
259
+ function isBinaryDeployExt(filePath) {
260
+ return BINARY_EXTENSIONS.has(path2.extname(filePath).toLowerCase());
261
+ }
262
+ var WEB_FILE_MAX_STORED_BYTES = 6 * 1024 * 1024 - 64 * 1024;
218
263
  function sha256Hex(value) {
219
264
  return crypto.createHash("sha256").update(value, "utf8").digest("hex");
220
265
  }
@@ -281,6 +326,146 @@ function collectFiles(dir, baseDir = dir) {
281
326
  }
282
327
  return files;
283
328
  }
329
+ function formatSize(bytes) {
330
+ if (bytes >= 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
331
+ return `${(bytes / 1024).toFixed(1)} KB`;
332
+ }
333
+ function validateFileSizes(files) {
334
+ const oversized = files.map((f) => {
335
+ const storedBytes = Buffer.byteLength(f.content, "utf8");
336
+ const originalBytes = f.isBinary ? Math.round(f.content.length * 3 / 4) : storedBytes;
337
+ return { path: f.path, isBinary: f.isBinary, storedBytes, originalBytes };
338
+ }).filter((f) => f.storedBytes > WEB_FILE_MAX_STORED_BYTES);
339
+ if (oversized.length === 0) return;
340
+ error(`\uC6F9 \uC2A4\uD1A0\uB9AC\uC9C0 \uD30C\uC77C\uB2F9 \uD06C\uAE30 \uD55C\uB3C4(\uC57D ${formatSize(WEB_FILE_MAX_STORED_BYTES)})\uB97C \uCD08\uACFC\uD55C \uD30C\uC77C\uC774 \uC788\uC5B4 \uBC30\uD3EC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4:`);
341
+ for (const f of oversized) {
342
+ const detail = f.isBinary ? `${formatSize(f.originalBytes)} \u2192 base64 ${formatSize(f.storedBytes)}` : formatSize(f.storedBytes);
343
+ error(` \u2022 ${f.path} (${detail})`);
344
+ }
345
+ log("");
346
+ warn("\uC815\uC801 \uD638\uC2A4\uD305 \uD30C\uC77C\uC740 DB \uB2E8\uC77C \uC5D4\uD2B8\uB9AC(6 MiB)\uC5D0 \uC800\uC7A5\uB418\uBA70, \uBC14\uC774\uB108\uB9AC\uB294 base64(\uC57D 1.33x)\uB85C");
347
+ warn("\uBD80\uD480\uC5B4 \uC6D0\uBCF8 \uAE30\uC900 \uC57D 4.5 MB \uAC00 \uC2E4\uC9C8 \uC0C1\uD55C\uC785\uB2C8\uB2E4.");
348
+ warn(`\uB300\uC6A9\uB7C9 \uC5D0\uC14B(3D \uBAA8\uB378\xB7\uB3D9\uC601\uC0C1\xB7\uD070 \uD3F0\uD2B8 \uB4F1)\uC740 \uD30C\uC77C \uC2A4\uD1A0\uB9AC\uC9C0(${colors.cyan}cb.storage${colors.reset}, presigned URL)\uC5D0`);
349
+ warn("\uC62C\uB9AC\uACE0 URL \uB85C \uCC38\uC870\uD558\uC138\uC694. \uD30C\uC77C \uC2A4\uD1A0\uB9AC\uC9C0\uB294 \uD30C\uC77C\uB2F9 \uACE0\uC815 \uC0C1\uD55C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
350
+ process.exit(1);
351
+ }
352
+ async function requestDeployPresign(baseStorageUrl, headers, files, timeoutMs) {
353
+ const body = JSON.stringify({ files });
354
+ const res = await makeRequest(`${baseStorageUrl}/deploy/presign`, "POST", headers, body, { timeoutMs });
355
+ if (res.status === 404) return null;
356
+ if (res.status < 200 || res.status >= 300) {
357
+ const data = res.data;
358
+ const msg = typeof data === "object" && data !== null ? data.message || data.error || JSON.stringify(data) : `HTTP ${res.status}`;
359
+ throw new Error(`presign \uC694\uCCAD \uC2E4\uD328: ${msg}`);
360
+ }
361
+ const parsed = res.data;
362
+ return parsed?.urls ?? [];
363
+ }
364
+ function putToPresignedUrl(uploadUrl, body, contentType, timeoutMs) {
365
+ return new Promise((resolve2, reject) => {
366
+ let parsed;
367
+ try {
368
+ parsed = new URL(uploadUrl);
369
+ } catch {
370
+ reject(new Error("\uC798\uBABB\uB41C \uC5C5\uB85C\uB4DC URL"));
371
+ return;
372
+ }
373
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
374
+ reject(new Error(`\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uC5C5\uB85C\uB4DC URL \uC2A4\uD0B4: ${parsed.protocol}`));
375
+ return;
376
+ }
377
+ const lib = parsed.protocol === "https:" ? https : http;
378
+ const options = {
379
+ hostname: parsed.hostname,
380
+ port: parsed.port || (parsed.protocol === "https:" ? 443 : 80),
381
+ path: parsed.pathname + parsed.search,
382
+ method: "PUT",
383
+ family: 4,
384
+ autoSelectFamily: false,
385
+ headers: {
386
+ "Content-Type": contentType || "application/octet-stream",
387
+ "Content-Length": body.length
388
+ }
389
+ };
390
+ const req = lib.request(options, (res) => {
391
+ const chunks = [];
392
+ res.on("data", (chunk) => chunks.push(chunk));
393
+ res.on("end", () => {
394
+ const status = res.statusCode || 0;
395
+ if (status >= 200 && status < 300) {
396
+ resolve2();
397
+ return;
398
+ }
399
+ reject(
400
+ new Error(
401
+ `Object Storage \uC5C5\uB85C\uB4DC \uC2E4\uD328 (HTTP ${status}): ${Buffer.concat(chunks).toString("utf8").slice(0, 200)}`
402
+ )
403
+ );
404
+ });
405
+ });
406
+ req.on("socket", (socket) => {
407
+ socket.setKeepAlive(true, 3e4);
408
+ });
409
+ req.setTimeout(timeoutMs, () => {
410
+ req.destroy(new Error(`\uC5C5\uB85C\uB4DC \uC2DC\uAC04 \uCD08\uACFC (${Math.round(timeoutMs / 1e3)}\uCD08)`));
411
+ });
412
+ req.on("error", reject);
413
+ req.end(body);
414
+ });
415
+ }
416
+ async function offloadLargeBinaries(baseStorageUrl, headers, diff, _envLabel, timeoutMs) {
417
+ const oversized = diff.upsert.filter((f) => Buffer.byteLength(f.content, "utf8") > WEB_FILE_MAX_STORED_BYTES);
418
+ if (oversized.length === 0) return;
419
+ const largeText = oversized.filter((f) => !f.is_binary);
420
+ const largeBinaries = oversized.filter((f) => f.is_binary);
421
+ if (largeText.length > 0) {
422
+ error(`\uC6F9 \uC2A4\uD1A0\uB9AC\uC9C0 \uD30C\uC77C\uB2F9 \uD06C\uAE30 \uD55C\uB3C4(\uC57D ${formatSize(WEB_FILE_MAX_STORED_BYTES)})\uB97C \uCD08\uACFC\uD55C \uD14D\uC2A4\uD2B8 \uD30C\uC77C\uC774 \uC788\uC5B4 \uBC30\uD3EC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4:`);
423
+ for (const f of largeText) error(` \u2022 ${f.path} (${formatSize(Buffer.byteLength(f.content, "utf8"))})`);
424
+ log("");
425
+ warn("\uD070 JS/CSS \uB294 \uCF54\uB4DC \uC2A4\uD50C\uB9AC\uD305\uC73C\uB85C \uBD84\uD560\uD558\uC138\uC694. (\uBC14\uC774\uB108\uB9AC\uC640 \uB2EC\uB9AC \uD14D\uC2A4\uD2B8\uB294 \uC624\uBE0C\uC81D\uD2B8 \uC2A4\uD1A0\uB9AC\uC9C0\uB85C \uC624\uD504\uB85C\uB4DC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.)");
426
+ process.exit(1);
427
+ }
428
+ if (largeBinaries.length === 0) return;
429
+ const presignFiles = largeBinaries.map((f) => ({
430
+ path: f.path,
431
+ size: Buffer.from(f.content, "base64").length,
432
+ mime_type: guessMimeType(f.path)
433
+ }));
434
+ let presigned;
435
+ try {
436
+ presigned = await requestDeployPresign(baseStorageUrl, headers, presignFiles, timeoutMs);
437
+ } catch (err) {
438
+ error(`\uB300\uC6A9\uB7C9 \uD30C\uC77C \uC5C5\uB85C\uB4DC \uC900\uBE44 \uC2E4\uD328: ${err instanceof Error ? err.message : err}`);
439
+ process.exit(1);
440
+ }
441
+ if (presigned === null) {
442
+ error(`\uC11C\uBC84\uAC00 \uB300\uC6A9\uB7C9 \uD30C\uC77C \uC9C1\uC811 \uC5C5\uB85C\uB4DC\uB97C \uC9C0\uC6D0\uD558\uC9C0 \uC54A\uC544 \uB2E4\uC74C \uD30C\uC77C\uC744 \uBC30\uD3EC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4(\uC57D ${formatSize(WEB_FILE_MAX_STORED_BYTES)} \uCD08\uACFC):`);
443
+ for (const f of largeBinaries) error(` \u2022 ${f.path} (${formatSize(Buffer.from(f.content, "base64").length)})`);
444
+ log("");
445
+ warn(`\uB300\uC6A9\uB7C9 \uC5D0\uC14B\uC740 \uD30C\uC77C \uC2A4\uD1A0\uB9AC\uC9C0(${colors.cyan}cb.storage${colors.reset})\uC5D0 \uC62C\uB9AC\uACE0 URL \uB85C \uCC38\uC870\uD558\uAC70\uB098, \uC11C\uBC84 \uC5C5\uB370\uC774\uD2B8 \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694.`);
446
+ process.exit(1);
447
+ }
448
+ const urlByPath = new Map(presigned.map((p) => [p.path, p.upload_url]));
449
+ info(`\uB300\uC6A9\uB7C9 \uBC14\uC774\uB108\uB9AC ${largeBinaries.length}\uAC1C\uB97C Object Storage \uB85C \uC9C1\uC811 \uC5C5\uB85C\uB4DC\uD569\uB2C8\uB2E4...`);
450
+ for (const f of largeBinaries) {
451
+ const uploadUrl = urlByPath.get(f.path);
452
+ if (!uploadUrl) {
453
+ error(`presign \uC751\uB2F5\uC5D0 \uC5C5\uB85C\uB4DC URL \uC774 \uC5C6\uC2B5\uB2C8\uB2E4: ${f.path}`);
454
+ process.exit(1);
455
+ }
456
+ const bytes = Buffer.from(f.content, "base64");
457
+ try {
458
+ await putToPresignedUrl(uploadUrl, bytes, guessMimeType(f.path), timeoutMs);
459
+ } catch (err) {
460
+ error(`\uC5C5\uB85C\uB4DC \uC2E4\uD328 (${f.path}): ${err instanceof Error ? err.message : err}`);
461
+ process.exit(1);
462
+ }
463
+ success(`\uC9C1\uC811 \uC5C5\uB85C\uB4DC: ${f.path} (${formatSize(bytes.length)})`);
464
+ f.pre_uploaded = true;
465
+ f.size = bytes.length;
466
+ f.content = "";
467
+ }
468
+ }
284
469
  var DEFAULT_REQUEST_TIMEOUT_MS = 6e4;
285
470
  async function makeRequest(url, method, headers, body, reqOpts = {}) {
286
471
  return new Promise((resolve2, reject) => {
@@ -387,12 +572,14 @@ async function deploy(directory, config, isDev = false, deployOpts = {}) {
387
572
  const timeoutMs = computeDeployTimeout(totalSize, deployOpts.timeoutMs);
388
573
  log(`${colors.dim}\uD0C0\uC784\uC544\uC6C3: ${Math.round(timeoutMs / 1e3)}\uCD08${colors.reset}`);
389
574
  if (isDev) {
575
+ validateFileSizes(files);
390
576
  await fullDeploy(baseStorageUrl, headers, files, envLabel, "deploy/dev", timeoutMs);
391
577
  return;
392
578
  }
393
579
  try {
394
580
  const manifest = await tryFetchManifest(baseStorageUrl, headers);
395
581
  if (!manifest) {
582
+ validateFileSizes(files);
396
583
  await fullDeploy(baseStorageUrl, headers, files, envLabel, "deploy", timeoutMs);
397
584
  return;
398
585
  }
@@ -404,6 +591,7 @@ ${colors.cyan}URL: https://${config.storageId}.web.connectbase.world${colors.res
404
591
  `);
405
592
  return;
406
593
  }
594
+ await offloadLargeBinaries(baseStorageUrl, headers, diff, envLabel, timeoutMs);
407
595
  info(`\uBCC0\uACBD: ${colors.green}+${diff.upsert.length}${colors.reset} / ${colors.red}-${diff.delete.length}${colors.reset} (\uC804\uCCB4 ${files.length}\uAC1C \uC911)`);
408
596
  const uploadSize = diff.upsert.reduce((s, f) => s + f.content.length, 0);
409
597
  info(`\uC5C5\uB85C\uB4DC \uD06C\uAE30: ${(uploadSize / 1024).toFixed(1)} KB`);
@@ -2628,6 +2816,9 @@ main().catch((err) => {
2628
2816
  // Annotate the CommonJS export names for ESM import in node:
2629
2817
  0 && (module.exports = {
2630
2818
  computeDeployDiff,
2819
+ guessMimeType,
2820
+ isAllowedDeployExt,
2821
+ isBinaryDeployExt,
2631
2822
  makeRequest,
2632
2823
  normalizeRelativePath,
2633
2824
  parseArgs,
package/dist/index.d.mts CHANGED
@@ -2402,14 +2402,15 @@ declare class StorageAPI {
2402
2402
  * 파일 목록 조회
2403
2403
  *
2404
2404
  * @param storageId - 파일 스토리지 ID
2405
- * @param parentId - 부모 폴더 ID (미지정루트 폴더)
2405
+ * @param parentId - 부모 폴더 ID. 지정해당 폴더의 **직계 자식만** 반환한다.
2406
+ * 미지정 시 스토리지 **전체 파일 트리**를 flat 배열로 반환한다.
2406
2407
  *
2407
2408
  * @example
2408
2409
  * ```ts
2409
- * // 루트 폴더의 파일 목록
2410
+ * // 스토리지 전체 파일 목록 (flat 트리)
2410
2411
  * const files = await cb.storage.getFiles('storage-id')
2411
2412
  *
2412
- * // 특정 폴더의 하위 파일 목록
2413
+ * // 특정 폴더의 직계 자식만 (대용량 버킷에서 전체 트리 전송 회피)
2413
2414
  * const subFiles = await cb.storage.getFiles('storage-id', 'folder-id')
2414
2415
  * ```
2415
2416
  */
@@ -7370,6 +7371,12 @@ interface KnowledgeSearchRequest {
7370
7371
  top_k?: number;
7371
7372
  /** Agentic Search 활성화 — AI 가 쿼리를 자동 생성하여 다중 검색 수행 */
7372
7373
  agentic?: boolean;
7374
+ /**
7375
+ * Hybrid 검색 — BM25 키워드 + 벡터 시맨틱 검색을 RRF 로 융합.
7376
+ * 미설정(기본): 콘솔 > AI 에서 embedding 을 켠 앱은 자동 적용.
7377
+ * `false`: 키워드 전용 검색 강제. embedding 미설정 앱에서는 무시(BM25 only).
7378
+ */
7379
+ hybrid?: boolean;
7373
7380
  /**
7374
7381
  * metadata 기반 필터 (옵셔널). 키 형식 `metadata.<path>` 또는 raw key.
7375
7382
  * 값에 `AUTH_MEMBER_ID_TOKEN` 사용 시 서버가 인증된 AppMember ID 로 치환.
@@ -7655,6 +7662,11 @@ interface AIChatRequest {
7655
7662
  knowledgeBaseId?: string;
7656
7663
  topK?: number;
7657
7664
  agentic?: boolean;
7665
+ /**
7666
+ * KB 검색 hybrid(BM25 + 벡터 시맨틱, RRF 융합) 제어. 미설정(기본)이면 콘솔 > AI 에서
7667
+ * embedding 을 켠 앱은 자동 적용, `false` 면 키워드 전용 검색 강제.
7668
+ */
7669
+ hybrid?: boolean;
7658
7670
  toolGroupId?: string;
7659
7671
  /**
7660
7672
  * 0 보다 크면, 서버측 도구 그룹 실행(`toolGroupId` 사용 시)에서 각 도구 결과를 모델에
package/dist/index.d.ts CHANGED
@@ -2402,14 +2402,15 @@ declare class StorageAPI {
2402
2402
  * 파일 목록 조회
2403
2403
  *
2404
2404
  * @param storageId - 파일 스토리지 ID
2405
- * @param parentId - 부모 폴더 ID (미지정루트 폴더)
2405
+ * @param parentId - 부모 폴더 ID. 지정해당 폴더의 **직계 자식만** 반환한다.
2406
+ * 미지정 시 스토리지 **전체 파일 트리**를 flat 배열로 반환한다.
2406
2407
  *
2407
2408
  * @example
2408
2409
  * ```ts
2409
- * // 루트 폴더의 파일 목록
2410
+ * // 스토리지 전체 파일 목록 (flat 트리)
2410
2411
  * const files = await cb.storage.getFiles('storage-id')
2411
2412
  *
2412
- * // 특정 폴더의 하위 파일 목록
2413
+ * // 특정 폴더의 직계 자식만 (대용량 버킷에서 전체 트리 전송 회피)
2413
2414
  * const subFiles = await cb.storage.getFiles('storage-id', 'folder-id')
2414
2415
  * ```
2415
2416
  */
@@ -7370,6 +7371,12 @@ interface KnowledgeSearchRequest {
7370
7371
  top_k?: number;
7371
7372
  /** Agentic Search 활성화 — AI 가 쿼리를 자동 생성하여 다중 검색 수행 */
7372
7373
  agentic?: boolean;
7374
+ /**
7375
+ * Hybrid 검색 — BM25 키워드 + 벡터 시맨틱 검색을 RRF 로 융합.
7376
+ * 미설정(기본): 콘솔 > AI 에서 embedding 을 켠 앱은 자동 적용.
7377
+ * `false`: 키워드 전용 검색 강제. embedding 미설정 앱에서는 무시(BM25 only).
7378
+ */
7379
+ hybrid?: boolean;
7373
7380
  /**
7374
7381
  * metadata 기반 필터 (옵셔널). 키 형식 `metadata.<path>` 또는 raw key.
7375
7382
  * 값에 `AUTH_MEMBER_ID_TOKEN` 사용 시 서버가 인증된 AppMember ID 로 치환.
@@ -7655,6 +7662,11 @@ interface AIChatRequest {
7655
7662
  knowledgeBaseId?: string;
7656
7663
  topK?: number;
7657
7664
  agentic?: boolean;
7665
+ /**
7666
+ * KB 검색 hybrid(BM25 + 벡터 시맨틱, RRF 융합) 제어. 미설정(기본)이면 콘솔 > AI 에서
7667
+ * embedding 을 켠 앱은 자동 적용, `false` 면 키워드 전용 검색 강제.
7668
+ */
7669
+ hybrid?: boolean;
7658
7670
  toolGroupId?: string;
7659
7671
  /**
7660
7672
  * 0 보다 크면, 서버측 도구 그룹 실행(`toolGroupId` 사용 시)에서 각 도구 결과를 모델에
package/dist/index.js CHANGED
@@ -2279,14 +2279,15 @@ var StorageAPI = class {
2279
2279
  * 파일 목록 조회
2280
2280
  *
2281
2281
  * @param storageId - 파일 스토리지 ID
2282
- * @param parentId - 부모 폴더 ID (미지정루트 폴더)
2282
+ * @param parentId - 부모 폴더 ID. 지정해당 폴더의 **직계 자식만** 반환한다.
2283
+ * 미지정 시 스토리지 **전체 파일 트리**를 flat 배열로 반환한다.
2283
2284
  *
2284
2285
  * @example
2285
2286
  * ```ts
2286
- * // 루트 폴더의 파일 목록
2287
+ * // 스토리지 전체 파일 목록 (flat 트리)
2287
2288
  * const files = await cb.storage.getFiles('storage-id')
2288
2289
  *
2289
- * // 특정 폴더의 하위 파일 목록
2290
+ * // 특정 폴더의 직계 자식만 (대용량 버킷에서 전체 트리 전송 회피)
2290
2291
  * const subFiles = await cb.storage.getFiles('storage-id', 'folder-id')
2291
2292
  * ```
2292
2293
  */
package/dist/index.mjs CHANGED
@@ -2234,14 +2234,15 @@ var StorageAPI = class {
2234
2234
  * 파일 목록 조회
2235
2235
  *
2236
2236
  * @param storageId - 파일 스토리지 ID
2237
- * @param parentId - 부모 폴더 ID (미지정루트 폴더)
2237
+ * @param parentId - 부모 폴더 ID. 지정해당 폴더의 **직계 자식만** 반환한다.
2238
+ * 미지정 시 스토리지 **전체 파일 트리**를 flat 배열로 반환한다.
2238
2239
  *
2239
2240
  * @example
2240
2241
  * ```ts
2241
- * // 루트 폴더의 파일 목록
2242
+ * // 스토리지 전체 파일 목록 (flat 트리)
2242
2243
  * const files = await cb.storage.getFiles('storage-id')
2243
2244
  *
2244
- * // 특정 폴더의 하위 파일 목록
2245
+ * // 특정 폴더의 직계 자식만 (대용량 버킷에서 전체 트리 전송 회피)
2245
2246
  * const subFiles = await cb.storage.getFiles('storage-id', 'folder-id')
2246
2247
  * ```
2247
2248
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "connectbase-client",
3
- "version": "3.39.2",
3
+ "version": "3.42.0",
4
4
  "description": "Connect Base JavaScript/TypeScript SDK for browser and Node.js",
5
5
  "repository": {
6
6
  "type": "git",