docuking-mcp 2.12.0 → 2.14.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 +206 -189
- package/package.json +1 -1
package/handlers/sync.js
CHANGED
|
@@ -806,82 +806,23 @@ docuking_init을 먼저 실행하세요.`,
|
|
|
806
806
|
}
|
|
807
807
|
|
|
808
808
|
// ========================================
|
|
809
|
-
// 4.
|
|
809
|
+
// 4. 자동 삭제 비활성화 (2026-01-17 버그 수정)
|
|
810
|
+
// ========================================
|
|
811
|
+
// 이전 로직: "서버에 있고 로컬에 없으면 삭제"
|
|
812
|
+
// 문제: 다른 프로젝트에서 Pull하면 서버 파일이 삭제됨
|
|
813
|
+
// 해결: 자동 삭제 제거, docuking_delete로만 명시적 삭제
|
|
814
|
+
//
|
|
815
|
+
// 삭제는 명시적으로만:
|
|
816
|
+
// - 사용자가 "이 파일 삭제해줘" 요청 시
|
|
817
|
+
// - docuking_delete MCP 도구 사용
|
|
810
818
|
// ========================================
|
|
811
|
-
// 단, 협업자 폴더(yy_Coworker_*)는 삭제하지 않음 (오너가 협업자 파일을 삭제하면 안됨)
|
|
812
|
-
// source: 'web' 파일은 MCP에서 삭제 시도하지 않음 (클라이언트에서 필터링)
|
|
813
|
-
// 중요: localPathsBeforePull 사용 (Pull 전에 수집한 목록)
|
|
814
|
-
// processedLocalPaths는 Pull로 내려받은 파일도 포함하므로 사용 금지
|
|
815
819
|
let deleted = 0;
|
|
816
820
|
const deletedFilePaths = [];
|
|
817
|
-
let protectedFiles = [];
|
|
818
|
-
const webProtectedPaths = []; // MCP에서 미리 필터링한 웹 파일
|
|
819
|
-
|
|
820
|
-
if (serverAllPaths.length > 0 && !isCoworker) {
|
|
821
|
-
// 오너만 삭제 수행
|
|
822
|
-
// yy_Coworker_*로 시작하는 경로는 삭제 대상에서 제외
|
|
823
|
-
// localPathsBeforePull: Pull 전에 수집한 로컬 파일 목록 (좀비 파일 방지)
|
|
824
|
-
//
|
|
825
|
-
// ★ 핵심: source: 'web' 파일은 삭제 대상에서 미리 제외!
|
|
826
|
-
// 이렇게 하면 웹에서 생성한 파일이 절대 삭제되지 않음
|
|
827
|
-
const pathsToDelete = serverAllPaths.filter(p => {
|
|
828
|
-
// 1. 로컬에 있으면 제외 (삭제 대상 아님)
|
|
829
|
-
if (localPathsBeforePull.has(p)) return false;
|
|
830
|
-
|
|
831
|
-
// 2. 협업자 폴더는 제외
|
|
832
|
-
if (p.startsWith('yy_Coworker_')) return false;
|
|
833
|
-
|
|
834
|
-
// 3. source: 'web' 파일은 제외 (웹에서 생성된 파일 보호)
|
|
835
|
-
const meta = serverPathToMeta[p];
|
|
836
|
-
if (meta && meta.source === 'web') {
|
|
837
|
-
webProtectedPaths.push(p);
|
|
838
|
-
return false;
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
return true;
|
|
842
|
-
});
|
|
843
|
-
|
|
844
|
-
// 웹 보호 파일 로그
|
|
845
|
-
if (webProtectedPaths.length > 0) {
|
|
846
|
-
console.error(`[DocuKing] source:web 파일 ${webProtectedPaths.length}개 삭제에서 제외:`, webProtectedPaths.slice(0, 5));
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
console.error(`[DocuKing] soft-delete 대상: ${pathsToDelete.length}개 (서버에만 있고 Pull 전 로컬에 없던 파일, source:local만)`);
|
|
850
|
-
|
|
851
|
-
if (pathsToDelete.length > 0) {
|
|
852
|
-
try {
|
|
853
|
-
// soft-delete API 호출 (deleted_at 설정, 3일 후 hard delete)
|
|
854
|
-
const deleteResponse = await fetch(`${API_ENDPOINT}/files/soft-delete`, {
|
|
855
|
-
method: 'POST',
|
|
856
|
-
headers: {
|
|
857
|
-
'Content-Type': 'application/json',
|
|
858
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
859
|
-
},
|
|
860
|
-
body: JSON.stringify({
|
|
861
|
-
projectId,
|
|
862
|
-
paths: pathsToDelete,
|
|
863
|
-
}),
|
|
864
|
-
});
|
|
865
|
-
|
|
866
|
-
if (deleteResponse.ok) {
|
|
867
|
-
const deleteResult = await deleteResponse.json();
|
|
868
|
-
deleted = deleteResult.deleted || 0;
|
|
869
|
-
deletedFilePaths.push(...(deleteResult.deletedPaths || []));
|
|
870
|
-
protectedFiles = deleteResult.protected || [];
|
|
871
|
-
|
|
872
|
-
// 보호된 파일이 있으면 로그 출력 (Backend에서 추가로 보호한 파일)
|
|
873
|
-
if (protectedFiles.length > 0) {
|
|
874
|
-
console.error(`[DocuKing] Backend에서 추가 보호된 파일 ${protectedFiles.length}개:`, protectedFiles.slice(0, 5));
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
} catch (e) {
|
|
878
|
-
console.error('[DocuKing] 파일 soft-delete 실패:', e.message);
|
|
879
|
-
}
|
|
880
|
-
}
|
|
821
|
+
let protectedFiles = [];
|
|
881
822
|
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
823
|
+
// 자동 삭제 로직 비활성화 - 아래 코드는 주석 처리
|
|
824
|
+
// "로컬에 없으면 삭제" 로직은 위험하므로 제거
|
|
825
|
+
console.error(`[DocuKing] 자동 삭제 비활성화됨 (명시적 삭제만 허용: docuking_delete 사용)`);
|
|
885
826
|
|
|
886
827
|
// 5. 빈 폴더 생성
|
|
887
828
|
let createdEmptyFolders = 0;
|
|
@@ -1227,14 +1168,48 @@ export async function handlePullInternal(args) {
|
|
|
1227
1168
|
const localIndex = loadIndex(localPath);
|
|
1228
1169
|
const syncedFiles = []; // 다운로드/업데이트된 파일 목록
|
|
1229
1170
|
|
|
1230
|
-
|
|
1171
|
+
// ========================================
|
|
1172
|
+
// 폴더 먼저 처리
|
|
1173
|
+
// ========================================
|
|
1174
|
+
const folders = sortedFiles.filter(f => f.type === 'folder');
|
|
1175
|
+
const files_only = sortedFiles.filter(f => f.type !== 'folder');
|
|
1176
|
+
|
|
1177
|
+
for (const folder of folders) {
|
|
1231
1178
|
current++;
|
|
1232
|
-
const
|
|
1179
|
+
const serverMeta = serverPathToMeta[folder.path];
|
|
1180
|
+
let fullPath;
|
|
1181
|
+
if (folder.path.startsWith('xx_') ||
|
|
1182
|
+
folder.path.startsWith('yy_All_Docu/') ||
|
|
1183
|
+
folder.path.startsWith('yy_All_Docu') ||
|
|
1184
|
+
folder.path.startsWith('yy_Coworker_') ||
|
|
1185
|
+
folder.path.startsWith('zz_ai_')) {
|
|
1186
|
+
fullPath = path.join(localPath, folder.path);
|
|
1187
|
+
} else {
|
|
1188
|
+
fullPath = path.join(mainFolderPath, folder.path);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
if (fs.existsSync(fullPath)) {
|
|
1192
|
+
results.push({ type: 'skip', path: folder.path, itemType: 'folder' });
|
|
1193
|
+
skipped++;
|
|
1194
|
+
} else {
|
|
1195
|
+
fs.mkdirSync(fullPath, { recursive: true });
|
|
1196
|
+
results.push({ type: 'download', path: folder.path, itemType: 'folder' });
|
|
1197
|
+
foldersCreated++;
|
|
1198
|
+
downloaded++;
|
|
1199
|
+
if (serverMeta && serverMeta.source === 'web') {
|
|
1200
|
+
filesToMarkAsPulled.push(folder.path);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1233
1204
|
|
|
1234
|
-
|
|
1235
|
-
|
|
1205
|
+
// ========================================
|
|
1206
|
+
// 파일 처리 - Batch API 사용 (200개씩)
|
|
1207
|
+
// ========================================
|
|
1208
|
+
const BATCH_SIZE = 200;
|
|
1236
1209
|
|
|
1237
|
-
|
|
1210
|
+
// 먼저 로컬 해시 기반으로 스킵할 파일 필터링
|
|
1211
|
+
const filesToDownload = [];
|
|
1212
|
+
for (const file of files_only) {
|
|
1238
1213
|
let fullPath;
|
|
1239
1214
|
if (file.path.startsWith('xx_') ||
|
|
1240
1215
|
file.path.startsWith('yy_All_Docu/') ||
|
|
@@ -1246,146 +1221,188 @@ export async function handlePullInternal(args) {
|
|
|
1246
1221
|
fullPath = path.join(mainFolderPath, file.path);
|
|
1247
1222
|
}
|
|
1248
1223
|
|
|
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++;
|
|
1224
|
+
const serverHash = serverPathToHash[file.path];
|
|
1264
1225
|
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1226
|
+
// 로컬 파일 해시 비교
|
|
1227
|
+
if (fs.existsSync(fullPath)) {
|
|
1228
|
+
try {
|
|
1229
|
+
const localStat = fs.statSync(fullPath);
|
|
1230
|
+
if (localStat.isDirectory()) {
|
|
1231
|
+
results.push({ type: 'skip', path: file.path, error: '같은 이름의 폴더 존재' });
|
|
1232
|
+
skipped++;
|
|
1233
|
+
current++;
|
|
1234
|
+
continue;
|
|
1269
1235
|
}
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
`${API_ENDPOINT}/files?projectId=${projectId}&path=${encodeURIComponent(file.path)}`,
|
|
1278
|
-
{
|
|
1279
|
-
headers: {
|
|
1280
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
1281
|
-
},
|
|
1236
|
+
const localBuffer = fs.readFileSync(fullPath);
|
|
1237
|
+
const localHash = crypto.createHash('sha256').update(localBuffer).digest('hex');
|
|
1238
|
+
if (localHash === serverHash) {
|
|
1239
|
+
results.push({ type: 'skip', path: file.path });
|
|
1240
|
+
skipped++;
|
|
1241
|
+
current++;
|
|
1242
|
+
continue;
|
|
1282
1243
|
}
|
|
1283
|
-
);
|
|
1284
1244
|
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1245
|
+
// ========================================
|
|
1246
|
+
// [버그 수정] 로컬 파일 우선 정책
|
|
1247
|
+
// 해시가 다르면 → 로컬 수정이 있다는 뜻
|
|
1248
|
+
// 서버 파일로 덮어쓰지 않고 로컬 유지
|
|
1249
|
+
// 다음 Push에서 로컬 → 서버로 업로드됨
|
|
1250
|
+
// ========================================
|
|
1251
|
+
console.error(`[DocuKing] 로컬 우선: ${file.path} (해시 다름 → 로컬 유지, Push 시 업로드됨)`);
|
|
1252
|
+
results.push({ type: 'skip', path: file.path, reason: 'local-modified' });
|
|
1253
|
+
skipped++;
|
|
1254
|
+
current++;
|
|
1288
1255
|
continue;
|
|
1256
|
+
} catch (e) {
|
|
1257
|
+
// 해시 계산 실패 시 다운로드 대상
|
|
1289
1258
|
}
|
|
1259
|
+
}
|
|
1290
1260
|
|
|
1291
|
-
|
|
1261
|
+
// 로컬에 파일이 없는 경우만 다운로드
|
|
1262
|
+
filesToDownload.push({ ...file, fullPath });
|
|
1263
|
+
}
|
|
1292
1264
|
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1265
|
+
console.error(`[DocuKing] Batch Pull: ${filesToDownload.length}개 파일 다운로드 필요 (${Math.ceil(filesToDownload.length / BATCH_SIZE)}개 배치)`);
|
|
1266
|
+
|
|
1267
|
+
// Batch API로 파일 다운로드
|
|
1268
|
+
for (let i = 0; i < filesToDownload.length; i += BATCH_SIZE) {
|
|
1269
|
+
const batch = filesToDownload.slice(i, i + BATCH_SIZE);
|
|
1270
|
+
const batchPaths = batch.map(f => f.path);
|
|
1271
|
+
const batchNum = Math.floor(i / BATCH_SIZE) + 1;
|
|
1272
|
+
const totalBatches = Math.ceil(filesToDownload.length / BATCH_SIZE);
|
|
1273
|
+
|
|
1274
|
+
console.error(`[DocuKing] Batch ${batchNum}/${totalBatches}: ${batch.length}개 파일 다운로드 중...`);
|
|
1275
|
+
|
|
1276
|
+
try {
|
|
1277
|
+
const batchResponse = await fetch(`${API_ENDPOINT}/files/batch-pull`, {
|
|
1278
|
+
method: 'POST',
|
|
1279
|
+
headers: {
|
|
1280
|
+
'Content-Type': 'application/json',
|
|
1281
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
1282
|
+
},
|
|
1283
|
+
body: JSON.stringify({
|
|
1284
|
+
projectId,
|
|
1285
|
+
paths: batchPaths,
|
|
1286
|
+
}),
|
|
1287
|
+
});
|
|
1296
1288
|
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1289
|
+
if (!batchResponse.ok) {
|
|
1290
|
+
const errorText = await batchResponse.text();
|
|
1291
|
+
console.error(`[DocuKing] Batch Pull 실패: ${errorText}`);
|
|
1292
|
+
// 배치 실패 시 모든 파일 실패 처리
|
|
1293
|
+
for (const file of batch) {
|
|
1294
|
+
results.push({ type: 'fail', path: file.path, error: 'Batch request failed' });
|
|
1295
|
+
failed++;
|
|
1296
|
+
current++;
|
|
1297
|
+
}
|
|
1298
|
+
continue;
|
|
1303
1299
|
}
|
|
1304
|
-
const serverHash = crypto.createHash('sha256').update(serverBuffer).digest('hex');
|
|
1305
1300
|
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1301
|
+
const batchData = await batchResponse.json();
|
|
1302
|
+
const filesData = batchData.files || [];
|
|
1303
|
+
|
|
1304
|
+
// 파일별 처리
|
|
1305
|
+
for (const file of batch) {
|
|
1306
|
+
current++;
|
|
1307
|
+
const fileData = filesData.find(f => f.path === file.path);
|
|
1308
|
+
const serverMeta = serverPathToMeta[file.path];
|
|
1309
|
+
|
|
1310
|
+
if (!fileData || fileData.error) {
|
|
1311
|
+
results.push({ type: 'fail', path: file.path, error: fileData?.error || 'Not found' });
|
|
1312
|
+
failed++;
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1309
1315
|
|
|
1310
|
-
if (localExists) {
|
|
1311
1316
|
try {
|
|
1312
|
-
const
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1317
|
+
const content = fileData.content || '';
|
|
1318
|
+
const encoding = fileData.encoding || 'utf-8';
|
|
1319
|
+
|
|
1320
|
+
// 서버 파일 버퍼
|
|
1321
|
+
let serverBuffer;
|
|
1322
|
+
if (encoding === 'base64') {
|
|
1323
|
+
serverBuffer = Buffer.from(content, 'base64');
|
|
1324
|
+
} else {
|
|
1325
|
+
serverBuffer = Buffer.from(content, 'utf-8');
|
|
1318
1326
|
}
|
|
1319
|
-
const
|
|
1320
|
-
localHash = crypto.createHash('sha256').update(localBuffer).digest('hex');
|
|
1321
|
-
} catch (e) {
|
|
1322
|
-
localExists = false;
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1327
|
+
const serverHash = fileData.hash || crypto.createHash('sha256').update(serverBuffer).digest('hex');
|
|
1325
1328
|
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
// 변경 없음 - 스킵
|
|
1329
|
-
results.push({ type: 'skip', path: file.path });
|
|
1330
|
-
skipped++;
|
|
1331
|
-
continue;
|
|
1332
|
-
}
|
|
1329
|
+
// 로컬 존재 여부
|
|
1330
|
+
const localExists = fs.existsSync(file.fullPath);
|
|
1333
1331
|
|
|
1334
|
-
|
|
1335
|
-
|
|
1332
|
+
// 디렉토리 생성
|
|
1333
|
+
fs.mkdirSync(path.dirname(file.fullPath), { recursive: true });
|
|
1336
1334
|
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1335
|
+
// 파일 저장
|
|
1336
|
+
if (encoding === 'base64') {
|
|
1337
|
+
fs.writeFileSync(file.fullPath, serverBuffer);
|
|
1338
|
+
} else {
|
|
1339
|
+
fs.writeFileSync(file.fullPath, content, 'utf-8');
|
|
1340
|
+
}
|
|
1343
1341
|
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1342
|
+
if (localExists) {
|
|
1343
|
+
results.push({ type: 'update', path: file.path });
|
|
1344
|
+
updated++;
|
|
1345
|
+
} else {
|
|
1346
|
+
results.push({ type: 'download', path: file.path });
|
|
1347
|
+
downloaded++;
|
|
1348
|
+
}
|
|
1351
1349
|
|
|
1352
|
-
|
|
1353
|
-
|
|
1350
|
+
// 인덱스에 기록
|
|
1351
|
+
syncedFiles.push({ serverPath: file.path, fullPath: file.fullPath, hash: serverHash });
|
|
1354
1352
|
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1353
|
+
// source: 'web' 파일이면 mark-as-pulled 대상
|
|
1354
|
+
if (serverMeta && serverMeta.source === 'web') {
|
|
1355
|
+
filesToMarkAsPulled.push(file.path);
|
|
1356
|
+
}
|
|
1357
|
+
} catch (e) {
|
|
1358
|
+
results.push({ type: 'fail', path: file.path, error: e.message });
|
|
1359
|
+
failed++;
|
|
1360
|
+
}
|
|
1358
1361
|
}
|
|
1359
1362
|
} catch (e) {
|
|
1360
|
-
|
|
1361
|
-
|
|
1363
|
+
console.error(`[DocuKing] Batch ${batchNum} 네트워크 오류: ${e.message}`);
|
|
1364
|
+
// 배치 실패 시 개별 요청으로 폴백
|
|
1365
|
+
for (const file of batch) {
|
|
1366
|
+
current++;
|
|
1367
|
+
try {
|
|
1368
|
+
const response = await fetch(
|
|
1369
|
+
`${API_ENDPOINT}/files?projectId=${projectId}&path=${encodeURIComponent(file.path)}`,
|
|
1370
|
+
{ headers: { 'Authorization': `Bearer ${apiKey}` } }
|
|
1371
|
+
);
|
|
1372
|
+
if (!response.ok) {
|
|
1373
|
+
results.push({ type: 'fail', path: file.path, error: await response.text() });
|
|
1374
|
+
failed++;
|
|
1375
|
+
continue;
|
|
1376
|
+
}
|
|
1377
|
+
const data = await response.json();
|
|
1378
|
+
const content = data.file?.content || data.content || '';
|
|
1379
|
+
const encoding = data.file?.encoding || data.encoding || 'utf-8';
|
|
1380
|
+
let serverBuffer = encoding === 'base64' ? Buffer.from(content, 'base64') : Buffer.from(content, 'utf-8');
|
|
1381
|
+
fs.mkdirSync(path.dirname(file.fullPath), { recursive: true });
|
|
1382
|
+
fs.writeFileSync(file.fullPath, encoding === 'base64' ? serverBuffer : content, encoding === 'base64' ? undefined : 'utf-8');
|
|
1383
|
+
results.push({ type: 'download', path: file.path });
|
|
1384
|
+
downloaded++;
|
|
1385
|
+
} catch (fallbackErr) {
|
|
1386
|
+
results.push({ type: 'fail', path: file.path, error: fallbackErr.message });
|
|
1387
|
+
failed++;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1362
1390
|
}
|
|
1363
1391
|
}
|
|
1364
1392
|
|
|
1365
1393
|
// ========================================
|
|
1366
|
-
// mark-as-pulled
|
|
1367
|
-
//
|
|
1394
|
+
// mark-as-pulled 비활성화 (2026-01-17 정책 변경)
|
|
1395
|
+
// ========================================
|
|
1396
|
+
// 이전 로직: source를 'web' → 'local'로 변경
|
|
1397
|
+
// 문제: source는 "출생신고"로, 한 번 정해지면 불변이어야 함
|
|
1398
|
+
// 변경하면 "출처 위조"가 됨
|
|
1399
|
+
//
|
|
1400
|
+
// 새 정책: source는 절대 변경하지 않음
|
|
1401
|
+
// - web에서 태어난 파일은 Pull해도 source: 'web' 유지
|
|
1402
|
+
// - local에서 태어난 파일은 Push해도 source: 'local' 유지
|
|
1368
1403
|
// ========================================
|
|
1369
1404
|
if (filesToMarkAsPulled.length > 0) {
|
|
1370
|
-
|
|
1371
|
-
const markResponse = await fetch(`${API_ENDPOINT}/files/mark-as-pulled`, {
|
|
1372
|
-
method: 'POST',
|
|
1373
|
-
headers: {
|
|
1374
|
-
'Content-Type': 'application/json',
|
|
1375
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
1376
|
-
},
|
|
1377
|
-
body: JSON.stringify({
|
|
1378
|
-
projectId,
|
|
1379
|
-
paths: filesToMarkAsPulled,
|
|
1380
|
-
}),
|
|
1381
|
-
});
|
|
1382
|
-
|
|
1383
|
-
if (markResponse.ok) {
|
|
1384
|
-
console.error(`[DocuKing] mark-as-pulled 완료: ${filesToMarkAsPulled.length}개`);
|
|
1385
|
-
}
|
|
1386
|
-
} catch (e) {
|
|
1387
|
-
console.error('[DocuKing] mark-as-pulled 실패:', e.message);
|
|
1388
|
-
}
|
|
1405
|
+
console.error(`[DocuKing] mark-as-pulled 비활성화됨 (source 불변 정책): ${filesToMarkAsPulled.length}개 파일의 source 유지`);
|
|
1389
1406
|
}
|
|
1390
1407
|
|
|
1391
1408
|
// ========================================
|