docuking-mcp 2.12.0 → 2.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.
Files changed (2) hide show
  1. package/handlers/sync.js +171 -97
  2. package/package.json +1 -1
package/handlers/sync.js CHANGED
@@ -1227,14 +1227,48 @@ export async function handlePullInternal(args) {
1227
1227
  const localIndex = loadIndex(localPath);
1228
1228
  const syncedFiles = []; // 다운로드/업데이트된 파일 목록
1229
1229
 
1230
- for (const file of sortedFiles) {
1230
+ // ========================================
1231
+ // 폴더 먼저 처리
1232
+ // ========================================
1233
+ const folders = sortedFiles.filter(f => f.type === 'folder');
1234
+ const files_only = sortedFiles.filter(f => f.type !== 'folder');
1235
+
1236
+ for (const folder of folders) {
1231
1237
  current++;
1232
- const progress = `${current}/${total}`;
1238
+ const serverMeta = serverPathToMeta[folder.path];
1239
+ let fullPath;
1240
+ if (folder.path.startsWith('xx_') ||
1241
+ folder.path.startsWith('yy_All_Docu/') ||
1242
+ folder.path.startsWith('yy_All_Docu') ||
1243
+ folder.path.startsWith('yy_Coworker_') ||
1244
+ folder.path.startsWith('zz_ai_')) {
1245
+ fullPath = path.join(localPath, folder.path);
1246
+ } else {
1247
+ fullPath = path.join(mainFolderPath, folder.path);
1248
+ }
1249
+
1250
+ if (fs.existsSync(fullPath)) {
1251
+ results.push({ type: 'skip', path: folder.path, itemType: 'folder' });
1252
+ skipped++;
1253
+ } else {
1254
+ fs.mkdirSync(fullPath, { recursive: true });
1255
+ results.push({ type: 'download', path: folder.path, itemType: 'folder' });
1256
+ foldersCreated++;
1257
+ downloaded++;
1258
+ if (serverMeta && serverMeta.source === 'web') {
1259
+ filesToMarkAsPulled.push(folder.path);
1260
+ }
1261
+ }
1262
+ }
1233
1263
 
1234
- // 서버 메타 정보 조회
1235
- const serverMeta = serverPathToMeta[file.path];
1264
+ // ========================================
1265
+ // 파일 처리 - Batch API 사용 (200개씩)
1266
+ // ========================================
1267
+ const BATCH_SIZE = 200;
1236
1268
 
1237
- // 서버 경로에 따라 로컬 저장 위치 결정
1269
+ // 먼저 로컬 해시 기반으로 스킵할 파일 필터링
1270
+ const filesToDownload = [];
1271
+ for (const file of files_only) {
1238
1272
  let fullPath;
1239
1273
  if (file.path.startsWith('xx_') ||
1240
1274
  file.path.startsWith('yy_All_Docu/') ||
@@ -1246,119 +1280,159 @@ export async function handlePullInternal(args) {
1246
1280
  fullPath = path.join(mainFolderPath, file.path);
1247
1281
  }
1248
1282
 
1249
- try {
1250
- // ========================================
1251
- // 폴더 처리 (type === 'folder')
1252
- // ========================================
1253
- if (file.type === 'folder') {
1254
- if (fs.existsSync(fullPath)) {
1255
- // 이미 있음 → 스킵
1256
- results.push({ type: 'skip', path: file.path, itemType: 'folder' });
1257
- skipped++;
1258
- } else {
1259
- // 폴더 생성
1260
- fs.mkdirSync(fullPath, { recursive: true });
1261
- results.push({ type: 'download', path: file.path, itemType: 'folder' });
1262
- foldersCreated++;
1263
- downloaded++;
1283
+ const serverHash = serverPathToHash[file.path];
1264
1284
 
1265
- // source: 'web' 폴더면 mark-as-pulled 대상
1266
- if (serverMeta && serverMeta.source === 'web') {
1267
- filesToMarkAsPulled.push(file.path);
1268
- }
1285
+ // 로컬 파일 해시 비교
1286
+ if (fs.existsSync(fullPath)) {
1287
+ try {
1288
+ const localStat = fs.statSync(fullPath);
1289
+ if (localStat.isDirectory()) {
1290
+ results.push({ type: 'skip', path: file.path, error: '같은 이름의 폴더 존재' });
1291
+ skipped++;
1292
+ current++;
1293
+ continue;
1269
1294
  }
1270
- continue;
1295
+ const localBuffer = fs.readFileSync(fullPath);
1296
+ const localHash = crypto.createHash('sha256').update(localBuffer).digest('hex');
1297
+ if (localHash === serverHash) {
1298
+ results.push({ type: 'skip', path: file.path });
1299
+ skipped++;
1300
+ current++;
1301
+ continue;
1302
+ }
1303
+ } catch (e) {
1304
+ // 해시 계산 실패 시 다운로드 대상
1271
1305
  }
1306
+ }
1272
1307
 
1273
- // ========================================
1274
- // 파일 처리 (type === 'file')
1275
- // ========================================
1276
- const response = await fetch(
1277
- `${API_ENDPOINT}/files?projectId=${projectId}&path=${encodeURIComponent(file.path)}`,
1278
- {
1279
- headers: {
1280
- 'Authorization': `Bearer ${apiKey}`,
1281
- },
1282
- }
1283
- );
1308
+ filesToDownload.push({ ...file, fullPath });
1309
+ }
1284
1310
 
1285
- if (!response.ok) {
1286
- results.push({ type: 'fail', path: file.path, error: await response.text() });
1287
- failed++;
1288
- continue;
1289
- }
1311
+ console.error(`[DocuKing] Batch Pull: ${filesToDownload.length}개 파일 다운로드 필요 (${Math.ceil(filesToDownload.length / BATCH_SIZE)}개 배치)`);
1290
1312
 
1291
- const data = await response.json();
1313
+ // Batch API로 파일 다운로드
1314
+ for (let i = 0; i < filesToDownload.length; i += BATCH_SIZE) {
1315
+ const batch = filesToDownload.slice(i, i + BATCH_SIZE);
1316
+ const batchPaths = batch.map(f => f.path);
1317
+ const batchNum = Math.floor(i / BATCH_SIZE) + 1;
1318
+ const totalBatches = Math.ceil(filesToDownload.length / BATCH_SIZE);
1292
1319
 
1293
- // 인코딩에 따라 저장
1294
- const content = data.file?.content || data.content || '';
1295
- const encoding = data.file?.encoding || data.encoding || 'utf-8';
1320
+ console.error(`[DocuKing] Batch ${batchNum}/${totalBatches}: ${batch.length}개 파일 다운로드 중...`);
1296
1321
 
1297
- // 서버 파일 해시 계산
1298
- let serverBuffer;
1299
- if (encoding === 'base64') {
1300
- serverBuffer = Buffer.from(content, 'base64');
1301
- } else {
1302
- serverBuffer = Buffer.from(content, 'utf-8');
1322
+ try {
1323
+ const batchResponse = await fetch(`${API_ENDPOINT}/files/batch-pull`, {
1324
+ method: 'POST',
1325
+ headers: {
1326
+ 'Content-Type': 'application/json',
1327
+ 'Authorization': `Bearer ${apiKey}`,
1328
+ },
1329
+ body: JSON.stringify({
1330
+ projectId,
1331
+ paths: batchPaths,
1332
+ }),
1333
+ });
1334
+
1335
+ if (!batchResponse.ok) {
1336
+ const errorText = await batchResponse.text();
1337
+ console.error(`[DocuKing] Batch Pull 실패: ${errorText}`);
1338
+ // 배치 실패 시 모든 파일 실패 처리
1339
+ for (const file of batch) {
1340
+ results.push({ type: 'fail', path: file.path, error: 'Batch request failed' });
1341
+ failed++;
1342
+ current++;
1343
+ }
1344
+ continue;
1303
1345
  }
1304
- const serverHash = crypto.createHash('sha256').update(serverBuffer).digest('hex');
1305
1346
 
1306
- // 로컬 파일 존재 여부 및 해시 비교
1307
- let localExists = fs.existsSync(fullPath);
1308
- let localHash = null;
1347
+ const batchData = await batchResponse.json();
1348
+ const filesData = batchData.files || [];
1349
+
1350
+ // 파일별 처리
1351
+ for (const file of batch) {
1352
+ current++;
1353
+ const fileData = filesData.find(f => f.path === file.path);
1354
+ const serverMeta = serverPathToMeta[file.path];
1355
+
1356
+ if (!fileData || fileData.error) {
1357
+ results.push({ type: 'fail', path: file.path, error: fileData?.error || 'Not found' });
1358
+ failed++;
1359
+ continue;
1360
+ }
1309
1361
 
1310
- if (localExists) {
1311
1362
  try {
1312
- const localStat = fs.statSync(fullPath);
1313
- if (localStat.isDirectory()) {
1314
- // 로컬에 같은 이름의 폴더가 있으면 건너뜀
1315
- results.push({ type: 'skip', path: file.path, error: '같은 이름의 폴더 존재' });
1316
- skipped++;
1317
- continue;
1363
+ const content = fileData.content || '';
1364
+ const encoding = fileData.encoding || 'utf-8';
1365
+
1366
+ // 서버 파일 버퍼
1367
+ let serverBuffer;
1368
+ if (encoding === 'base64') {
1369
+ serverBuffer = Buffer.from(content, 'base64');
1370
+ } else {
1371
+ serverBuffer = Buffer.from(content, 'utf-8');
1318
1372
  }
1319
- const localBuffer = fs.readFileSync(fullPath);
1320
- localHash = crypto.createHash('sha256').update(localBuffer).digest('hex');
1321
- } catch (e) {
1322
- localExists = false;
1323
- }
1324
- }
1373
+ const serverHash = fileData.hash || crypto.createHash('sha256').update(serverBuffer).digest('hex');
1325
1374
 
1326
- // 변경 감지
1327
- if (localExists && localHash === serverHash) {
1328
- // 변경 없음 - 스킵
1329
- results.push({ type: 'skip', path: file.path });
1330
- skipped++;
1331
- continue;
1332
- }
1375
+ // 로컬 존재 여부
1376
+ const localExists = fs.existsSync(file.fullPath);
1333
1377
 
1334
- // 디렉토리 생성
1335
- fs.mkdirSync(path.dirname(fullPath), { recursive: true });
1378
+ // 디렉토리 생성
1379
+ fs.mkdirSync(path.dirname(file.fullPath), { recursive: true });
1336
1380
 
1337
- // 파일 저장
1338
- if (encoding === 'base64') {
1339
- fs.writeFileSync(fullPath, serverBuffer);
1340
- } else {
1341
- fs.writeFileSync(fullPath, content, 'utf-8');
1342
- }
1381
+ // 파일 저장
1382
+ if (encoding === 'base64') {
1383
+ fs.writeFileSync(file.fullPath, serverBuffer);
1384
+ } else {
1385
+ fs.writeFileSync(file.fullPath, content, 'utf-8');
1386
+ }
1343
1387
 
1344
- if (localExists) {
1345
- results.push({ type: 'update', path: file.path });
1346
- updated++;
1347
- } else {
1348
- results.push({ type: 'download', path: file.path });
1349
- downloaded++;
1350
- }
1388
+ if (localExists) {
1389
+ results.push({ type: 'update', path: file.path });
1390
+ updated++;
1391
+ } else {
1392
+ results.push({ type: 'download', path: file.path });
1393
+ downloaded++;
1394
+ }
1351
1395
 
1352
- // 인덱스에 다운로드된 파일 기록
1353
- syncedFiles.push({ serverPath: file.path, fullPath, hash: serverHash });
1396
+ // 인덱스에 기록
1397
+ syncedFiles.push({ serverPath: file.path, fullPath: file.fullPath, hash: serverHash });
1354
1398
 
1355
- // source: 'web' 파일이면 mark-as-pulled 대상
1356
- if (serverMeta && serverMeta.source === 'web') {
1357
- filesToMarkAsPulled.push(file.path);
1399
+ // source: 'web' 파일이면 mark-as-pulled 대상
1400
+ if (serverMeta && serverMeta.source === 'web') {
1401
+ filesToMarkAsPulled.push(file.path);
1402
+ }
1403
+ } catch (e) {
1404
+ results.push({ type: 'fail', path: file.path, error: e.message });
1405
+ failed++;
1406
+ }
1358
1407
  }
1359
1408
  } catch (e) {
1360
- results.push({ type: 'fail', path: file.path, error: e.message });
1361
- failed++;
1409
+ console.error(`[DocuKing] Batch ${batchNum} 네트워크 오류: ${e.message}`);
1410
+ // 배치 실패 시 개별 요청으로 폴백
1411
+ for (const file of batch) {
1412
+ current++;
1413
+ try {
1414
+ const response = await fetch(
1415
+ `${API_ENDPOINT}/files?projectId=${projectId}&path=${encodeURIComponent(file.path)}`,
1416
+ { headers: { 'Authorization': `Bearer ${apiKey}` } }
1417
+ );
1418
+ if (!response.ok) {
1419
+ results.push({ type: 'fail', path: file.path, error: await response.text() });
1420
+ failed++;
1421
+ continue;
1422
+ }
1423
+ const data = await response.json();
1424
+ const content = data.file?.content || data.content || '';
1425
+ const encoding = data.file?.encoding || data.encoding || 'utf-8';
1426
+ let serverBuffer = encoding === 'base64' ? Buffer.from(content, 'base64') : Buffer.from(content, 'utf-8');
1427
+ fs.mkdirSync(path.dirname(file.fullPath), { recursive: true });
1428
+ fs.writeFileSync(file.fullPath, encoding === 'base64' ? serverBuffer : content, encoding === 'base64' ? undefined : 'utf-8');
1429
+ results.push({ type: 'download', path: file.path });
1430
+ downloaded++;
1431
+ } catch (fallbackErr) {
1432
+ results.push({ type: 'fail', path: file.path, error: fallbackErr.message });
1433
+ failed++;
1434
+ }
1435
+ }
1362
1436
  }
1363
1437
  }
1364
1438
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docuking-mcp",
3
- "version": "2.12.0",
3
+ "version": "2.13.0",
4
4
  "description": "DocuKing MCP Server - AI 시대의 문서 협업 플랫폼",
5
5
  "type": "module",
6
6
  "main": "index.js",