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.
- package/handlers/sync.js +171 -97
- 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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
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
|
-
|
|
1308
|
-
|
|
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
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1378
|
+
// 디렉토리 생성
|
|
1379
|
+
fs.mkdirSync(path.dirname(file.fullPath), { recursive: true });
|
|
1336
1380
|
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
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
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
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
|
-
|
|
1396
|
+
// 인덱스에 기록
|
|
1397
|
+
syncedFiles.push({ serverPath: file.path, fullPath: file.fullPath, hash: serverHash });
|
|
1354
1398
|
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
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
|
-
|
|
1361
|
-
|
|
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
|
|