@im-nobsidian/core 0.1.11 → 0.1.12
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/dist/index.d.ts +201 -25
- package/dist/index.js +1042 -481
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { readFile, mkdir, writeFile, unlink, rename, stat, readdir } from 'fs/promises';
|
|
3
|
-
import { join, dirname, relative } from 'path';
|
|
3
|
+
import { join, dirname, relative, sep } from 'path';
|
|
4
4
|
import Database from 'better-sqlite3';
|
|
5
5
|
import { randomUUID, createHash } from 'crypto';
|
|
6
6
|
import { Client, LogLevel } from '@notionhq/client';
|
|
@@ -49,10 +49,28 @@ var ConfigSchema = z.object({
|
|
|
49
49
|
imageDownload: z.enum(["immediate", "lazy", "skip"]).default("immediate")
|
|
50
50
|
}),
|
|
51
51
|
advanced: z.object({
|
|
52
|
+
// Notion API 클라이언트 동시성/타임아웃
|
|
52
53
|
concurrency: z.number().min(1).max(10).default(3),
|
|
53
|
-
maxRetries: z.number().min(0).max(10).default(5),
|
|
54
54
|
timeoutMs: z.number().min(5e3).max(6e4).default(3e4),
|
|
55
|
-
|
|
55
|
+
// 페이지네이션/블록 배치 (Notion API children append 상한 = 100)
|
|
56
|
+
pageSize: z.number().min(1).max(100).default(100),
|
|
57
|
+
batchSize: z.number().min(1).max(100).default(100),
|
|
58
|
+
// Rate limit 게이트 (요청 간 최소 간격, ms)
|
|
59
|
+
rateLimitIntervalMs: z.number().min(0).max(5e3).default(350),
|
|
60
|
+
// API 재시도 (지수 백오프: retryBaseDelayMs * retryBackoffFactor^attempt)
|
|
61
|
+
maxRetries: z.number().min(0).max(10).default(5),
|
|
62
|
+
retryBaseDelayMs: z.number().min(100).max(1e4).default(1e3),
|
|
63
|
+
retryBackoffFactor: z.number().min(1).max(10).default(2),
|
|
64
|
+
// 오케스트레이터 단계 간 재시도 대기 (ms)
|
|
65
|
+
retryWaitMs: z.number().min(0).max(3e4).default(2e3),
|
|
66
|
+
// 미디어(이미지/파일) 다운로드 동시성/재시도
|
|
67
|
+
mediaConcurrency: z.number().min(1).max(10).default(3),
|
|
68
|
+
mediaMaxRetries: z.number().min(0).max(10).default(3),
|
|
69
|
+
mediaRetryBaseMs: z.number().min(100).max(1e4).default(1e3),
|
|
70
|
+
// 비마크다운 파일(첨부) 업로드/다운로드 동시성
|
|
71
|
+
fileConcurrency: z.number().min(1).max(10).default(2),
|
|
72
|
+
// 다운로드 파일 크기 상한 (bytes, 기본 100MB)
|
|
73
|
+
maxFileSizeBytes: z.number().min(1024).default(100 * 1024 * 1024)
|
|
56
74
|
})
|
|
57
75
|
});
|
|
58
76
|
var DEFAULT_CONFIG = {
|
|
@@ -84,13 +102,37 @@ var DEFAULT_CONFIG = {
|
|
|
84
102
|
},
|
|
85
103
|
advanced: {
|
|
86
104
|
concurrency: 3,
|
|
87
|
-
maxRetries: 5,
|
|
88
105
|
timeoutMs: 3e4,
|
|
89
|
-
|
|
106
|
+
pageSize: 100,
|
|
107
|
+
batchSize: 100,
|
|
108
|
+
rateLimitIntervalMs: 350,
|
|
109
|
+
maxRetries: 5,
|
|
110
|
+
retryBaseDelayMs: 1e3,
|
|
111
|
+
retryBackoffFactor: 2,
|
|
112
|
+
retryWaitMs: 2e3,
|
|
113
|
+
mediaConcurrency: 3,
|
|
114
|
+
mediaMaxRetries: 3,
|
|
115
|
+
mediaRetryBaseMs: 1e3,
|
|
116
|
+
fileConcurrency: 2,
|
|
117
|
+
maxFileSizeBytes: 100 * 1024 * 1024
|
|
90
118
|
}
|
|
91
119
|
};
|
|
120
|
+
|
|
121
|
+
// src/constants/paths.ts
|
|
122
|
+
var INTERNAL_DIR = ".im-nobsidian";
|
|
92
123
|
var CONFIG_FILE = "config.json";
|
|
93
|
-
var
|
|
124
|
+
var STATE_DB_FILE = "sync.db";
|
|
125
|
+
var DB_VIEWS_FILE = "db-views.json";
|
|
126
|
+
var IGNORE_FILE = ".im-nobsidian-ignore";
|
|
127
|
+
var GITIGNORE_ENTRY = `${INTERNAL_DIR}/`;
|
|
128
|
+
var DB_VIEWS_PATH = `${INTERNAL_DIR}/${DB_VIEWS_FILE}`;
|
|
129
|
+
var STATE_DB_PATH = `${INTERNAL_DIR}/${STATE_DB_FILE}`;
|
|
130
|
+
var INTERNAL_DIR_GLOB = `**/${INTERNAL_DIR}/**`;
|
|
131
|
+
function isInternalPath(path) {
|
|
132
|
+
return path === INTERNAL_DIR || path.startsWith(`${INTERNAL_DIR}/`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/config/manager.ts
|
|
94
136
|
var ConfigManager = class {
|
|
95
137
|
constructor(vaultRoot) {
|
|
96
138
|
this.vaultRoot = vaultRoot;
|
|
@@ -98,13 +140,13 @@ var ConfigManager = class {
|
|
|
98
140
|
vaultRoot;
|
|
99
141
|
config = null;
|
|
100
142
|
get configDir() {
|
|
101
|
-
return join(this.vaultRoot,
|
|
143
|
+
return join(this.vaultRoot, INTERNAL_DIR);
|
|
102
144
|
}
|
|
103
145
|
get configPath() {
|
|
104
146
|
return join(this.configDir, CONFIG_FILE);
|
|
105
147
|
}
|
|
106
148
|
get dbPath() {
|
|
107
|
-
return join(this.configDir,
|
|
149
|
+
return join(this.configDir, STATE_DB_FILE);
|
|
108
150
|
}
|
|
109
151
|
async load() {
|
|
110
152
|
try {
|
|
@@ -152,7 +194,7 @@ var ConfigManager = class {
|
|
|
152
194
|
}
|
|
153
195
|
async ensureGitignore() {
|
|
154
196
|
const gitignorePath = join(this.vaultRoot, ".gitignore");
|
|
155
|
-
const entry =
|
|
197
|
+
const entry = GITIGNORE_ENTRY;
|
|
156
198
|
try {
|
|
157
199
|
const content = await readFile(gitignorePath, "utf-8");
|
|
158
200
|
if (!content.includes(entry)) {
|
|
@@ -432,6 +474,9 @@ var StateDB = class _StateDB {
|
|
|
432
474
|
"UPDATE sync_state SET notion_parent_id = ?, updated_at = datetime('now') WHERE id = ?"
|
|
433
475
|
).run(parentId, id);
|
|
434
476
|
}
|
|
477
|
+
updatePath(id, newPath) {
|
|
478
|
+
this.db.prepare("UPDATE sync_state SET obsidian_path = ?, updated_at = datetime('now') WHERE id = ?").run(newPath, id);
|
|
479
|
+
}
|
|
435
480
|
delete(id) {
|
|
436
481
|
this.db.prepare("DELETE FROM sync_state WHERE id = ?").run(id);
|
|
437
482
|
}
|
|
@@ -454,6 +499,9 @@ var StateDB = class _StateDB {
|
|
|
454
499
|
VALUES (?, ?, ?, ?, datetime('now'))`
|
|
455
500
|
).run(entry.obsidianPath, entry.notionPageId, entry.title, JSON.stringify(entry.aliases));
|
|
456
501
|
}
|
|
502
|
+
deleteWikilink(obsidianPath) {
|
|
503
|
+
this.db.prepare("DELETE FROM wikilink_map WHERE obsidian_path = ?").run(obsidianPath);
|
|
504
|
+
}
|
|
457
505
|
// --- sync_metadata ---
|
|
458
506
|
getMeta(key) {
|
|
459
507
|
const row = this.db.prepare("SELECT value FROM sync_metadata WHERE key = ?").get(key);
|
|
@@ -558,7 +606,7 @@ var StateDB = class _StateDB {
|
|
|
558
606
|
};
|
|
559
607
|
|
|
560
608
|
// src/notion/property-mapper.ts
|
|
561
|
-
var DATE_REGEX = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2})
|
|
609
|
+
var DATE_REGEX = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
|
|
562
610
|
var WIKILINK_REGEX = /^\[\[(.+?)(?:\|.+?)?\]\]$/;
|
|
563
611
|
var PropertyMapper = class {
|
|
564
612
|
schema = /* @__PURE__ */ new Map();
|
|
@@ -606,13 +654,18 @@ var PropertyMapper = class {
|
|
|
606
654
|
switch (type) {
|
|
607
655
|
case "rich_text":
|
|
608
656
|
return { rich_text: [{ text: { content: String(value) } }] };
|
|
609
|
-
case "number":
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
657
|
+
case "number": {
|
|
658
|
+
const num = typeof value === "number" ? value : Number(value);
|
|
659
|
+
return Number.isFinite(num) ? { number: num } : null;
|
|
660
|
+
}
|
|
661
|
+
case "select": {
|
|
662
|
+
const name = String(value);
|
|
663
|
+
return name.trim() ? { select: { name } } : null;
|
|
664
|
+
}
|
|
613
665
|
case "multi_select": {
|
|
614
666
|
const items = Array.isArray(value) ? value : String(value).split(",").map((s) => s.trim());
|
|
615
|
-
|
|
667
|
+
const names = items.map((name) => String(name)).filter((name) => name.trim());
|
|
668
|
+
return { multi_select: names.map((name) => ({ name })) };
|
|
616
669
|
}
|
|
617
670
|
case "checkbox":
|
|
618
671
|
return { checkbox: Boolean(value) };
|
|
@@ -633,8 +686,10 @@ var PropertyMapper = class {
|
|
|
633
686
|
return { email: String(value) };
|
|
634
687
|
case "phone_number":
|
|
635
688
|
return { phone_number: String(value) };
|
|
636
|
-
case "status":
|
|
637
|
-
|
|
689
|
+
case "status": {
|
|
690
|
+
const name = String(value);
|
|
691
|
+
return name.trim() ? { status: { name } } : null;
|
|
692
|
+
}
|
|
638
693
|
case "relation": {
|
|
639
694
|
const items = Array.isArray(value) ? value : [value];
|
|
640
695
|
const ids = [];
|
|
@@ -664,14 +719,14 @@ var PropertyMapper = class {
|
|
|
664
719
|
const filesArr = fileList.map((f) => {
|
|
665
720
|
if (typeof f === "object" && f !== null && "url" in f) {
|
|
666
721
|
const obj = f;
|
|
667
|
-
|
|
722
|
+
const url2 = String(obj.url ?? "");
|
|
723
|
+
if (!url2) return null;
|
|
724
|
+
return { type: "external", name: obj.name ?? "file", external: { url: url2 } };
|
|
668
725
|
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
};
|
|
674
|
-
});
|
|
726
|
+
const url = String(f);
|
|
727
|
+
if (!url) return null;
|
|
728
|
+
return { type: "external", name: url, external: { url } };
|
|
729
|
+
}).filter(Boolean);
|
|
675
730
|
return { files: filesArr };
|
|
676
731
|
}
|
|
677
732
|
case "created_time":
|
|
@@ -754,10 +809,9 @@ var PropertyMapper = class {
|
|
|
754
809
|
}
|
|
755
810
|
case "files": {
|
|
756
811
|
const files = prop.files;
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
})) ?? [];
|
|
812
|
+
const urls = (files ?? []).map((f) => f.type === "file" ? f.file?.url : f.external?.url).filter((u) => typeof u === "string" && u.length > 0);
|
|
813
|
+
if (urls.length === 0) return [];
|
|
814
|
+
return urls.length === 1 ? urls[0] : urls;
|
|
761
815
|
}
|
|
762
816
|
case "formula": {
|
|
763
817
|
const formula = prop.formula;
|
|
@@ -804,6 +858,22 @@ var PropertyMapper = class {
|
|
|
804
858
|
}
|
|
805
859
|
};
|
|
806
860
|
|
|
861
|
+
// src/utils/logger.ts
|
|
862
|
+
var consoleLogger = {
|
|
863
|
+
warn: (msg, ...args) => console.warn(msg, ...args),
|
|
864
|
+
error: (msg, ...args) => console.error(msg, ...args),
|
|
865
|
+
info: (msg, ...args) => console.info(msg, ...args),
|
|
866
|
+
debug: () => {
|
|
867
|
+
}
|
|
868
|
+
};
|
|
869
|
+
var currentLogger = consoleLogger;
|
|
870
|
+
function setLogger(logger) {
|
|
871
|
+
currentLogger = logger;
|
|
872
|
+
}
|
|
873
|
+
function getLogger() {
|
|
874
|
+
return currentLogger;
|
|
875
|
+
}
|
|
876
|
+
|
|
807
877
|
// src/notion/client.ts
|
|
808
878
|
var NotionClient = class _NotionClient {
|
|
809
879
|
client;
|
|
@@ -811,6 +881,12 @@ var NotionClient = class _NotionClient {
|
|
|
811
881
|
token;
|
|
812
882
|
propertyMapper = new PropertyMapper();
|
|
813
883
|
customFetch;
|
|
884
|
+
minRequestInterval;
|
|
885
|
+
maxRetries;
|
|
886
|
+
retryBaseDelayMs;
|
|
887
|
+
retryBackoffFactor;
|
|
888
|
+
defaultPageSize;
|
|
889
|
+
batchSize;
|
|
814
890
|
constructor(options) {
|
|
815
891
|
this.client = new Client({
|
|
816
892
|
auth: options.token,
|
|
@@ -821,6 +897,28 @@ var NotionClient = class _NotionClient {
|
|
|
821
897
|
this.token = options.token;
|
|
822
898
|
this.customFetch = options.fetch;
|
|
823
899
|
this.sema = new Sema(options.concurrency ?? 3);
|
|
900
|
+
this.minRequestInterval = options.rateLimitIntervalMs ?? 350;
|
|
901
|
+
this.maxRetries = options.maxRetries ?? 5;
|
|
902
|
+
this.retryBaseDelayMs = options.retryBaseDelayMs ?? 1e3;
|
|
903
|
+
this.retryBackoffFactor = options.retryBackoffFactor ?? 2;
|
|
904
|
+
this.defaultPageSize = options.pageSize ?? 100;
|
|
905
|
+
this.batchSize = options.batchSize ?? 100;
|
|
906
|
+
}
|
|
907
|
+
/** config.advanced 의 운영 튜닝값으로 클라이언트를 생성한다 (매직넘버 단일 진실원). */
|
|
908
|
+
static fromConfig(config, fetch2) {
|
|
909
|
+
const a = config.advanced;
|
|
910
|
+
return new _NotionClient({
|
|
911
|
+
token: config.notion.token,
|
|
912
|
+
concurrency: a.concurrency,
|
|
913
|
+
timeoutMs: a.timeoutMs,
|
|
914
|
+
rateLimitIntervalMs: a.rateLimitIntervalMs,
|
|
915
|
+
maxRetries: a.maxRetries,
|
|
916
|
+
retryBaseDelayMs: a.retryBaseDelayMs,
|
|
917
|
+
retryBackoffFactor: a.retryBackoffFactor,
|
|
918
|
+
pageSize: a.pageSize,
|
|
919
|
+
batchSize: a.batchSize,
|
|
920
|
+
...fetch2 ? { fetch: fetch2 } : {}
|
|
921
|
+
});
|
|
824
922
|
}
|
|
825
923
|
getInternalClient() {
|
|
826
924
|
return this.client;
|
|
@@ -979,7 +1077,7 @@ var NotionClient = class _NotionClient {
|
|
|
979
1077
|
() => this.client.dataSources.query({
|
|
980
1078
|
data_source_id: dsId,
|
|
981
1079
|
start_cursor: options?.startCursor,
|
|
982
|
-
page_size: options?.pageSize ??
|
|
1080
|
+
page_size: options?.pageSize ?? this.defaultPageSize,
|
|
983
1081
|
filter: options?.filter
|
|
984
1082
|
})
|
|
985
1083
|
);
|
|
@@ -994,7 +1092,7 @@ var NotionClient = class _NotionClient {
|
|
|
994
1092
|
do {
|
|
995
1093
|
const response = await this.queryDatabase(databaseId, {
|
|
996
1094
|
startCursor: cursor,
|
|
997
|
-
pageSize:
|
|
1095
|
+
pageSize: this.defaultPageSize,
|
|
998
1096
|
filter
|
|
999
1097
|
});
|
|
1000
1098
|
all.push(...response.results);
|
|
@@ -1035,7 +1133,7 @@ var NotionClient = class _NotionClient {
|
|
|
1035
1133
|
() => this.client.blocks.children.list({
|
|
1036
1134
|
block_id: blockId,
|
|
1037
1135
|
start_cursor: options?.startCursor,
|
|
1038
|
-
page_size: options?.pageSize ??
|
|
1136
|
+
page_size: options?.pageSize ?? this.defaultPageSize
|
|
1039
1137
|
})
|
|
1040
1138
|
);
|
|
1041
1139
|
return {
|
|
@@ -1083,9 +1181,9 @@ var NotionClient = class _NotionClient {
|
|
|
1083
1181
|
return allBlocks;
|
|
1084
1182
|
}
|
|
1085
1183
|
async appendChildren(blockId, children) {
|
|
1086
|
-
const
|
|
1087
|
-
for (let i = 0; i < children.length; i +=
|
|
1088
|
-
const batch = children.slice(i, i +
|
|
1184
|
+
const batchSize = this.batchSize;
|
|
1185
|
+
for (let i = 0; i < children.length; i += batchSize) {
|
|
1186
|
+
const batch = children.slice(i, i + batchSize);
|
|
1089
1187
|
await this.withRateLimit(
|
|
1090
1188
|
() => this.client.blocks.children.append({
|
|
1091
1189
|
block_id: blockId,
|
|
@@ -1124,7 +1222,7 @@ var NotionClient = class _NotionClient {
|
|
|
1124
1222
|
query: params.query,
|
|
1125
1223
|
filter: params.filter,
|
|
1126
1224
|
start_cursor: params.startCursor,
|
|
1127
|
-
page_size: params.pageSize ??
|
|
1225
|
+
page_size: params.pageSize ?? this.defaultPageSize
|
|
1128
1226
|
})
|
|
1129
1227
|
);
|
|
1130
1228
|
return {
|
|
@@ -1142,7 +1240,7 @@ var NotionClient = class _NotionClient {
|
|
|
1142
1240
|
filter: { property: "object", value: "page" },
|
|
1143
1241
|
sort: { direction: "descending", timestamp: "last_edited_time" },
|
|
1144
1242
|
start_cursor: cursor,
|
|
1145
|
-
page_size:
|
|
1243
|
+
page_size: this.defaultPageSize
|
|
1146
1244
|
})
|
|
1147
1245
|
);
|
|
1148
1246
|
for (const page of response.results) {
|
|
@@ -1156,12 +1254,43 @@ var NotionClient = class _NotionClient {
|
|
|
1156
1254
|
} while (cursor);
|
|
1157
1255
|
return results;
|
|
1158
1256
|
}
|
|
1257
|
+
/**
|
|
1258
|
+
* 통합이 접근 가능한 모든 페이지를 search API로 일괄 조회한다(요청당 최대 100건).
|
|
1259
|
+
* 블록 트리를 페이지별로 재귀 순회하는 getChildPagesRecursive 대비 API 호출 수가
|
|
1260
|
+
* 페이지 수의 수십분의 1로 줄어든다. search API는 휴지통(in_trash) 페이지를 반환하지 않는다.
|
|
1261
|
+
* 반환 객체에는 parent 정보가 포함되어 호출 측에서 root subtree ancestry 필터링이 가능하다.
|
|
1262
|
+
*/
|
|
1263
|
+
async searchAllPages() {
|
|
1264
|
+
const results = [];
|
|
1265
|
+
let cursor;
|
|
1266
|
+
do {
|
|
1267
|
+
const response = await this.withRateLimit(
|
|
1268
|
+
() => this.client.search({
|
|
1269
|
+
filter: { property: "object", value: "page" },
|
|
1270
|
+
start_cursor: cursor,
|
|
1271
|
+
page_size: this.defaultPageSize
|
|
1272
|
+
})
|
|
1273
|
+
);
|
|
1274
|
+
for (const page of response.results) {
|
|
1275
|
+
if ("properties" in page) {
|
|
1276
|
+
results.push(page);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
cursor = response.next_cursor ?? void 0;
|
|
1280
|
+
} while (cursor);
|
|
1281
|
+
return results;
|
|
1282
|
+
}
|
|
1159
1283
|
async getChildPages(parentId) {
|
|
1160
1284
|
const blocks = await this.fetchAllChildrenDeep(parentId);
|
|
1161
1285
|
const childPageBlocks = blocks.filter((b) => b.type === "child_page");
|
|
1162
1286
|
if (childPageBlocks.length === 0) return [];
|
|
1163
1287
|
const pages = await Promise.all(childPageBlocks.map((b) => this.getPage(b.id)));
|
|
1164
|
-
return pages;
|
|
1288
|
+
return pages.filter((p) => !_NotionClient.isTrashedOrArchived(p));
|
|
1289
|
+
}
|
|
1290
|
+
/** 페이지가 휴지통(in_trash)이거나 보관(archived) 상태인지 판정한다. */
|
|
1291
|
+
static isTrashedOrArchived(page) {
|
|
1292
|
+
const inTrash = page.in_trash === true;
|
|
1293
|
+
return inTrash || page.archived === true;
|
|
1165
1294
|
}
|
|
1166
1295
|
async getChildDatabaseIds(parentId) {
|
|
1167
1296
|
const children = await this.fetchAllChildren(parentId);
|
|
@@ -1173,7 +1302,15 @@ var NotionClient = class _NotionClient {
|
|
|
1173
1302
|
while (currentLevel.length > 0) {
|
|
1174
1303
|
const nextLevel = [];
|
|
1175
1304
|
for (const id of currentLevel) {
|
|
1176
|
-
|
|
1305
|
+
let children;
|
|
1306
|
+
try {
|
|
1307
|
+
children = await this.getChildPages(id);
|
|
1308
|
+
} catch (error) {
|
|
1309
|
+
getLogger().warn(
|
|
1310
|
+
`[Im-Nobsidian] \uD558\uC704 \uD398\uC774\uC9C0 \uC2A4\uCE94 \uAC74\uB108\uB700 (${id}): ${error instanceof Error ? error.message : String(error)}`
|
|
1311
|
+
);
|
|
1312
|
+
continue;
|
|
1313
|
+
}
|
|
1177
1314
|
for (const child of children) {
|
|
1178
1315
|
all.push(child);
|
|
1179
1316
|
nextLevel.push(child.id);
|
|
@@ -1192,7 +1329,7 @@ var NotionClient = class _NotionClient {
|
|
|
1192
1329
|
() => this.client.views.list({
|
|
1193
1330
|
database_id: databaseId,
|
|
1194
1331
|
start_cursor: cursor,
|
|
1195
|
-
page_size:
|
|
1332
|
+
page_size: this.defaultPageSize
|
|
1196
1333
|
})
|
|
1197
1334
|
);
|
|
1198
1335
|
const viewRefs = response.results;
|
|
@@ -1269,7 +1406,6 @@ var NotionClient = class _NotionClient {
|
|
|
1269
1406
|
}
|
|
1270
1407
|
// ─── Internal ───
|
|
1271
1408
|
lastRequestTime = 0;
|
|
1272
|
-
minRequestInterval = 350;
|
|
1273
1409
|
async withRateLimit(fn) {
|
|
1274
1410
|
await this.sema.acquire();
|
|
1275
1411
|
try {
|
|
@@ -1288,8 +1424,8 @@ var NotionClient = class _NotionClient {
|
|
|
1288
1424
|
try {
|
|
1289
1425
|
return await fn();
|
|
1290
1426
|
} catch (error) {
|
|
1291
|
-
if (isRetryable(error) && attempt <
|
|
1292
|
-
const baseDelay = extractRetryAfter(error) ??
|
|
1427
|
+
if (isRetryable(error) && attempt < this.maxRetries) {
|
|
1428
|
+
const baseDelay = extractRetryAfter(error) ?? this.retryBaseDelayMs * Math.pow(this.retryBackoffFactor, attempt);
|
|
1293
1429
|
const jitter = baseDelay * (0.5 + Math.random() * 0.5);
|
|
1294
1430
|
await sleep(jitter);
|
|
1295
1431
|
return this.executeWithRetry(fn, attempt + 1);
|
|
@@ -1837,23 +1973,29 @@ var ChangeDetector = class {
|
|
|
1837
1973
|
}
|
|
1838
1974
|
};
|
|
1839
1975
|
|
|
1840
|
-
// src/
|
|
1841
|
-
var
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
}
|
|
1847
|
-
};
|
|
1848
|
-
var currentLogger = consoleLogger;
|
|
1849
|
-
function setLogger(logger) {
|
|
1850
|
-
currentLogger = logger;
|
|
1976
|
+
// src/constants/markers.ts
|
|
1977
|
+
var MARKER_BRAND = "im-nobsidian";
|
|
1978
|
+
var WIKILINK_PROTOCOL = `${MARKER_BRAND}://wikilink/`;
|
|
1979
|
+
var EMBED_PROTOCOL = `${MARKER_BRAND}://embed/`;
|
|
1980
|
+
function spacedMarker(body) {
|
|
1981
|
+
return `%% ${MARKER_BRAND}:${body} %%`;
|
|
1851
1982
|
}
|
|
1852
|
-
|
|
1853
|
-
|
|
1983
|
+
var SPACED_END = spacedMarker("end");
|
|
1984
|
+
function compactMarker(body) {
|
|
1985
|
+
return `%%${MARKER_BRAND}:${body}%%`;
|
|
1854
1986
|
}
|
|
1987
|
+
var TOGGLE_START = compactMarker("toggle:start");
|
|
1988
|
+
var TOGGLE_END = compactMarker("toggle:end");
|
|
1989
|
+
var COLUMN_LIST_START = compactMarker("column-list:start");
|
|
1990
|
+
var COLUMN_LIST_END = compactMarker("column-list:end");
|
|
1991
|
+
var COLUMN_SEP = compactMarker("column");
|
|
1992
|
+
var TOC_MARKER = compactMarker("toc");
|
|
1993
|
+
var PROPERTIES_TAG = `# ${MARKER_BRAND}:properties`;
|
|
1994
|
+
var MARKER_BRAND_RE = MARKER_BRAND;
|
|
1855
1995
|
|
|
1856
1996
|
// src/converter/pipeline.ts
|
|
1997
|
+
var INLINE_DB_PROBE = new RegExp(`%% ${MARKER_BRAND_RE}:inline-db`);
|
|
1998
|
+
var TOGGLE_HEADING_PROBE = new RegExp(`%% ${MARKER_BRAND_RE}:toggle-heading`);
|
|
1857
1999
|
var ConversionPipeline = class {
|
|
1858
2000
|
preProcessors = [];
|
|
1859
2001
|
postProcessors = [];
|
|
@@ -1866,9 +2008,9 @@ var ConversionPipeline = class {
|
|
|
1866
2008
|
this.postProcessors.sort((a, b) => a.order - b.order);
|
|
1867
2009
|
}
|
|
1868
2010
|
selectPath(content) {
|
|
1869
|
-
const hasInlineDb =
|
|
2011
|
+
const hasInlineDb = INLINE_DB_PROBE.test(content);
|
|
1870
2012
|
const hasColumnLayout = />\s*\[!col\]/.test(content);
|
|
1871
|
-
const hasToggleHeading =
|
|
2013
|
+
const hasToggleHeading = TOGGLE_HEADING_PROBE.test(content);
|
|
1872
2014
|
if (hasInlineDb || hasColumnLayout || hasToggleHeading) {
|
|
1873
2015
|
return "block-api";
|
|
1874
2016
|
}
|
|
@@ -1965,7 +2107,7 @@ var WikilinkResolver = class {
|
|
|
1965
2107
|
startIndex: 0
|
|
1966
2108
|
});
|
|
1967
2109
|
const encodedTarget = encodeURIComponent(target);
|
|
1968
|
-
return `[${label}](
|
|
2110
|
+
return `[${label}](${WIKILINK_PROTOCOL}${encodedTarget})`;
|
|
1969
2111
|
}
|
|
1970
2112
|
);
|
|
1971
2113
|
return {
|
|
@@ -2012,8 +2154,8 @@ var CalloutTransformer = class {
|
|
|
2012
2154
|
const emoji = CALLOUT_EMOJI_MAP[type.toLowerCase()] ?? "\u{1F4DD}";
|
|
2013
2155
|
const titleText = title?.trim() || type;
|
|
2014
2156
|
const foldMeta = foldable ? `
|
|
2015
|
-
|
|
2016
|
-
|
|
2157
|
+
${spacedMarker(`callout:type=${type}&foldable=${foldable === "+" ? "open" : "closed"}`)}` : `
|
|
2158
|
+
${spacedMarker(`callout:type=${type}`)}`;
|
|
2017
2159
|
return `> ${emoji} **${titleText}**${foldMeta}`;
|
|
2018
2160
|
}
|
|
2019
2161
|
);
|
|
@@ -2063,16 +2205,16 @@ var EmbedResolver = class {
|
|
|
2063
2205
|
if (isPush && !isExternalUrl(target)) {
|
|
2064
2206
|
const fileName = target.split("/").pop() ?? target;
|
|
2065
2207
|
return `> \u{1F4CE} ${fileName}
|
|
2066
|
-
>
|
|
2208
|
+
> ${spacedMarker(`local-image:${target}`)}`;
|
|
2067
2209
|
}
|
|
2068
2210
|
return `})`;
|
|
2069
2211
|
}
|
|
2070
2212
|
const encoded = encodeURIComponent(target);
|
|
2071
|
-
return `[${target}](
|
|
2213
|
+
return `[${target}](${EMBED_PROTOCOL}${encoded})`;
|
|
2072
2214
|
});
|
|
2073
2215
|
content = content.replace(MARKDOWN_IMAGE_REGEX, (_match, alt, url) => {
|
|
2074
2216
|
if (isVideoUrl(url)) {
|
|
2075
|
-
return
|
|
2217
|
+
return `${spacedMarker(`embed:type=video&url=${encodeURIComponent(url)}`)}
|
|
2076
2218
|
[${alt || "Video"}](${url})`;
|
|
2077
2219
|
}
|
|
2078
2220
|
if (isExternalUrl(url)) {
|
|
@@ -2097,7 +2239,7 @@ function isExternalUrl(url) {
|
|
|
2097
2239
|
}
|
|
2098
2240
|
|
|
2099
2241
|
// src/converter/pre-processors/preserve-marker.ts
|
|
2100
|
-
var MARKER_REGEX =
|
|
2242
|
+
var MARKER_REGEX = new RegExp(`%% ${MARKER_BRAND_RE}:(\\w[\\w-]*):([^\\s]+?) %%`, "g");
|
|
2101
2243
|
var PreserveMarkerCollector = class {
|
|
2102
2244
|
name = "PreserveMarkerCollector";
|
|
2103
2245
|
order = 70;
|
|
@@ -2167,7 +2309,7 @@ var PropertiesTableInjector = class {
|
|
|
2167
2309
|
}
|
|
2168
2310
|
const yamlStr = matter7.stringify("", filteredProps).trim();
|
|
2169
2311
|
const yamlBody = yamlStr.slice(4, -3).trim();
|
|
2170
|
-
const codeBlock = "```yaml\n
|
|
2312
|
+
const codeBlock = "```yaml\n" + PROPERTIES_TAG + "\n" + yamlBody + "\n```";
|
|
2171
2313
|
const content = codeBlock + "\n\n---\n\n" + input.content;
|
|
2172
2314
|
return { content, metadata: input.metadata };
|
|
2173
2315
|
}
|
|
@@ -2176,7 +2318,10 @@ var PropertiesTableInjector = class {
|
|
|
2176
2318
|
// src/converter/pre-processors/html-annotation.ts
|
|
2177
2319
|
var UNDERLINE_REGEX = /<u>([\s\S]*?)<\/u>/g;
|
|
2178
2320
|
var COLOR_SPAN_REGEX = /<span class="notion-(\w+?)(?:-bg)?">([\s\S]*?)<\/span>/g;
|
|
2179
|
-
var IM_NOBSIDIAN_COLOR_REGEX =
|
|
2321
|
+
var IM_NOBSIDIAN_COLOR_REGEX = new RegExp(
|
|
2322
|
+
`%% ${MARKER_BRAND_RE}:color:(\\w+(?:_background)?) %%([\\s\\S]*?)%% ${MARKER_BRAND_RE}:end %%`,
|
|
2323
|
+
"g"
|
|
2324
|
+
);
|
|
2180
2325
|
var HtmlAnnotationStripper = class {
|
|
2181
2326
|
name = "HtmlAnnotationStripper";
|
|
2182
2327
|
order = 5;
|
|
@@ -2194,8 +2339,8 @@ var HtmlAnnotationStripper = class {
|
|
|
2194
2339
|
|
|
2195
2340
|
// src/converter/post-processors/mention-to-wikilink.ts
|
|
2196
2341
|
var NOTION_LINK_REGEX = /\[([^\]]+)\]\((?:https:\/\/(?:www\.)?notion\.so\/|notion:\/\/)([a-f0-9-]+)\)/g;
|
|
2197
|
-
var WIKILINK_PRESERVE_REGEX =
|
|
2198
|
-
var EMBED_PRESERVE_REGEX =
|
|
2342
|
+
var WIKILINK_PRESERVE_REGEX = new RegExp(`\\[([^\\]]+)\\]\\(${WIKILINK_PROTOCOL}([^)]+)\\)`, "g");
|
|
2343
|
+
var EMBED_PRESERVE_REGEX = new RegExp(`\\[([^\\]]+)\\]\\(${EMBED_PROTOCOL}([^)]+)\\)`, "g");
|
|
2199
2344
|
var MentionToWikilink = class {
|
|
2200
2345
|
name = "MentionToWikilink";
|
|
2201
2346
|
order = 10;
|
|
@@ -2244,7 +2389,7 @@ var PreserveMarkerInjector = class {
|
|
|
2244
2389
|
}
|
|
2245
2390
|
formatMarker(marker) {
|
|
2246
2391
|
const params = Object.entries(marker.params).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join("&");
|
|
2247
|
-
return
|
|
2392
|
+
return spacedMarker(`${marker.type}:${params}`);
|
|
2248
2393
|
}
|
|
2249
2394
|
};
|
|
2250
2395
|
|
|
@@ -2263,7 +2408,10 @@ var EMOJI_TYPE_MAP = {
|
|
|
2263
2408
|
"\u{1F4CC}": "example",
|
|
2264
2409
|
"\u{1F4AC}": "quote"
|
|
2265
2410
|
};
|
|
2266
|
-
var CALLOUT_PRESERVE_REGEX =
|
|
2411
|
+
var CALLOUT_PRESERVE_REGEX = new RegExp(
|
|
2412
|
+
`%% ${MARKER_BRAND_RE}:callout:type=(\\w+)(?:&foldable=(open|closed))? %%`,
|
|
2413
|
+
"g"
|
|
2414
|
+
);
|
|
2267
2415
|
var CalloutRestorer = class {
|
|
2268
2416
|
name = "CalloutRestorer";
|
|
2269
2417
|
order = 20;
|
|
@@ -2296,7 +2444,10 @@ function escapeRegex(str) {
|
|
|
2296
2444
|
}
|
|
2297
2445
|
|
|
2298
2446
|
// src/converter/post-processors/color-annotator.ts
|
|
2299
|
-
var COLOR_MARKER_REGEX =
|
|
2447
|
+
var COLOR_MARKER_REGEX = new RegExp(
|
|
2448
|
+
`%% ${MARKER_BRAND_RE}:color:(\\w+(?:_background)?) %%([\\s\\S]*?)%% ${MARKER_BRAND_RE}:end %%`,
|
|
2449
|
+
"g"
|
|
2450
|
+
);
|
|
2300
2451
|
var ColorAnnotator = class {
|
|
2301
2452
|
name = "ColorAnnotator";
|
|
2302
2453
|
order = 30;
|
|
@@ -2371,7 +2522,10 @@ function normalizeDate(dateStr) {
|
|
|
2371
2522
|
}
|
|
2372
2523
|
|
|
2373
2524
|
// src/converter/post-processors/local-image-restorer.ts
|
|
2374
|
-
var LOCAL_IMAGE_MARKER_REGEX =
|
|
2525
|
+
var LOCAL_IMAGE_MARKER_REGEX = new RegExp(
|
|
2526
|
+
`>\\s*\u{1F4CE}\\s*[^\\n]*\\n>\\s*%%\\s*${MARKER_BRAND_RE}:local-image:([^\\s]+)\\s*%%`,
|
|
2527
|
+
"g"
|
|
2528
|
+
);
|
|
2375
2529
|
var LocalImageRestorer = class {
|
|
2376
2530
|
name = "LocalImageRestorer";
|
|
2377
2531
|
order = 15;
|
|
@@ -2385,7 +2539,9 @@ var LocalImageRestorer = class {
|
|
|
2385
2539
|
return { content, metadata: input.metadata };
|
|
2386
2540
|
}
|
|
2387
2541
|
};
|
|
2388
|
-
var YAML_PROPERTIES_REGEX =
|
|
2542
|
+
var YAML_PROPERTIES_REGEX = new RegExp(
|
|
2543
|
+
"```yaml\\n# " + MARKER_BRAND_RE + ":properties\\n([\\s\\S]*?)```\\n*(?:---\\n*)?"
|
|
2544
|
+
);
|
|
2389
2545
|
var LEGACY_TABLE_REGEX = /^\s*\| Property\s*\| Value\s*\|\n\s*\|\s*-{3,}\s*\|\s*-{3,}\s*\|\n((?:\|[^\n]+\|\n?)+)\n*---\n*/;
|
|
2390
2546
|
var PropertiesTableRestorer = class {
|
|
2391
2547
|
name = "PropertiesTableRestorer";
|
|
@@ -2502,11 +2658,6 @@ function createDefaultPipeline(options) {
|
|
|
2502
2658
|
}
|
|
2503
2659
|
var TRULY_UNSUPPORTED_BLOCK_TYPES = ["unsupported", "template"];
|
|
2504
2660
|
var DIVIDER_PLACEHOLDER = "\u200B%%IM-NOBSIDIAN_DIVIDER%%\u200B";
|
|
2505
|
-
var TOGGLE_START = "%%im-nobsidian:toggle:start%%";
|
|
2506
|
-
var TOGGLE_END = "%%im-nobsidian:toggle:end%%";
|
|
2507
|
-
var COLUMN_LIST_START = "%%im-nobsidian:column-list:start%%";
|
|
2508
|
-
var COLUMN_SEP = "%%im-nobsidian:column%%";
|
|
2509
|
-
var COLUMN_LIST_END = "%%im-nobsidian:column-list:end%%";
|
|
2510
2661
|
var VIDEO_URL_PATTERNS = [
|
|
2511
2662
|
/^https?:\/\/(www\.)?youtube\.com\/watch/,
|
|
2512
2663
|
/^https?:\/\/youtu\.be\//,
|
|
@@ -2552,7 +2703,7 @@ function richTextToMarkdown(richText) {
|
|
|
2552
2703
|
if (t.href) text = `[${text}](${t.href})`;
|
|
2553
2704
|
const color = a?.color;
|
|
2554
2705
|
if (color && color !== "default") {
|
|
2555
|
-
text =
|
|
2706
|
+
text = `${spacedMarker(`color:${color}`)}${text}${SPACED_END}`;
|
|
2556
2707
|
}
|
|
2557
2708
|
return text;
|
|
2558
2709
|
}).join("");
|
|
@@ -2588,9 +2739,9 @@ var BlockConverter = class {
|
|
|
2588
2739
|
this.n2m.setCustomTransformer(blockType, (block) => {
|
|
2589
2740
|
const id = block.id ?? "unknown";
|
|
2590
2741
|
return Promise.resolve(
|
|
2591
|
-
`> [
|
|
2742
|
+
`> [!${MARKER_BRAND}-unsupported] Notion \uC804\uC6A9 \uBE14\uB85D
|
|
2592
2743
|
> type: ${blockType}, id: ${id}
|
|
2593
|
-
>
|
|
2744
|
+
> ${compactMarker(`unsupported:type=${blockType}&id=${id}`)}`
|
|
2594
2745
|
);
|
|
2595
2746
|
});
|
|
2596
2747
|
}
|
|
@@ -2740,7 +2891,7 @@ ${COLUMN_LIST_END}`;
|
|
|
2740
2891
|
const b = block;
|
|
2741
2892
|
const title = b.child_database?.title ?? "Database";
|
|
2742
2893
|
return `> [!database] ${title}
|
|
2743
|
-
>
|
|
2894
|
+
> ${compactMarker(`child-database:id=${b.id}&title=${encodeURIComponent(title)}`)}`;
|
|
2744
2895
|
});
|
|
2745
2896
|
}
|
|
2746
2897
|
registerLinkToPageTransformer() {
|
|
@@ -2766,7 +2917,7 @@ ${COLUMN_LIST_END}`;
|
|
|
2766
2917
|
registerTocTransformer() {
|
|
2767
2918
|
if (!this.n2m) return;
|
|
2768
2919
|
this.n2m.setCustomTransformer("table_of_contents", async () => {
|
|
2769
|
-
return
|
|
2920
|
+
return TOC_MARKER;
|
|
2770
2921
|
});
|
|
2771
2922
|
}
|
|
2772
2923
|
registerBreadcrumbTransformer() {
|
|
@@ -3180,26 +3331,34 @@ var EXTENSION_TO_MIME = {
|
|
|
3180
3331
|
".tiff": "image/tiff"
|
|
3181
3332
|
};
|
|
3182
3333
|
var ImageHandler = class {
|
|
3183
|
-
constructor(vaultFs, attachmentFolder = "attachments", notionClient, customFetch) {
|
|
3334
|
+
constructor(vaultFs, attachmentFolder = "attachments", notionClient, customFetch, options) {
|
|
3184
3335
|
this.vaultFs = vaultFs;
|
|
3185
3336
|
this.attachmentFolder = attachmentFolder;
|
|
3186
3337
|
this.notionClient = notionClient;
|
|
3187
3338
|
this.customFetch = customFetch;
|
|
3339
|
+
this.concurrency = options?.concurrency ?? 3;
|
|
3340
|
+
this.maxRetries = options?.maxRetries ?? 3;
|
|
3341
|
+
this.retryBaseMs = options?.retryBaseMs ?? 1e3;
|
|
3342
|
+
this.maxFileSizeBytes = options?.maxFileSizeBytes ?? 100 * 1024 * 1024;
|
|
3188
3343
|
}
|
|
3189
3344
|
vaultFs;
|
|
3190
3345
|
attachmentFolder;
|
|
3191
3346
|
notionClient;
|
|
3192
3347
|
customFetch;
|
|
3348
|
+
concurrency;
|
|
3349
|
+
maxRetries;
|
|
3350
|
+
retryBaseMs;
|
|
3351
|
+
maxFileSizeBytes;
|
|
3193
3352
|
get fetchFn() {
|
|
3194
3353
|
return this.customFetch ?? globalThis.fetch;
|
|
3195
3354
|
}
|
|
3196
3355
|
async downloadImage(url, pageTitle) {
|
|
3197
|
-
const maxRetries =
|
|
3356
|
+
const maxRetries = this.maxRetries;
|
|
3198
3357
|
let lastError = null;
|
|
3199
3358
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
3200
3359
|
try {
|
|
3201
3360
|
if (attempt > 0) {
|
|
3202
|
-
await new Promise((r) => setTimeout(r,
|
|
3361
|
+
await new Promise((r) => setTimeout(r, this.retryBaseMs * attempt));
|
|
3203
3362
|
getLogger().info(
|
|
3204
3363
|
`[Im-Nobsidian] \uC774\uBBF8\uC9C0 \uB2E4\uC6B4\uB85C\uB4DC \uC7AC\uC2DC\uB3C4 (${attempt + 1}/${maxRetries}): ${pageTitle}`
|
|
3205
3364
|
);
|
|
@@ -3233,7 +3392,7 @@ var ImageHandler = class {
|
|
|
3233
3392
|
if (notionMatches.length === 0 && internalMatches.length === 0) {
|
|
3234
3393
|
return { content: markdown, downloads: [] };
|
|
3235
3394
|
}
|
|
3236
|
-
const sema = new Sema(
|
|
3395
|
+
const sema = new Sema(this.concurrency);
|
|
3237
3396
|
const downloads = [];
|
|
3238
3397
|
let result = markdown;
|
|
3239
3398
|
const httpResults = await Promise.allSettled(
|
|
@@ -3342,7 +3501,7 @@ var ImageHandler = class {
|
|
|
3342
3501
|
if (totalMatches === 0) {
|
|
3343
3502
|
return { content: markdown, downloads: [] };
|
|
3344
3503
|
}
|
|
3345
|
-
const sema = new Sema(
|
|
3504
|
+
const sema = new Sema(this.concurrency);
|
|
3346
3505
|
const downloads = [];
|
|
3347
3506
|
let result = markdown;
|
|
3348
3507
|
for (const match of notionHttpMatches) {
|
|
@@ -3437,12 +3596,12 @@ var ImageHandler = class {
|
|
|
3437
3596
|
}
|
|
3438
3597
|
}
|
|
3439
3598
|
async downloadFile(url, _pageTitle, caption) {
|
|
3440
|
-
const maxRetries =
|
|
3599
|
+
const maxRetries = this.maxRetries;
|
|
3441
3600
|
let lastError = null;
|
|
3442
3601
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
3443
3602
|
try {
|
|
3444
3603
|
if (attempt > 0) {
|
|
3445
|
-
await new Promise((r) => setTimeout(r,
|
|
3604
|
+
await new Promise((r) => setTimeout(r, this.retryBaseMs * attempt));
|
|
3446
3605
|
getLogger().info(
|
|
3447
3606
|
`[Im-Nobsidian] \uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uC7AC\uC2DC\uB3C4 (${attempt + 1}/${maxRetries}): ${caption}`
|
|
3448
3607
|
);
|
|
@@ -3452,7 +3611,7 @@ var ImageHandler = class {
|
|
|
3452
3611
|
throw new Error(`\uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328: ${response.status} ${response.statusText}`);
|
|
3453
3612
|
}
|
|
3454
3613
|
const contentLength = response.headers.get("content-length");
|
|
3455
|
-
const MAX_FILE_SIZE =
|
|
3614
|
+
const MAX_FILE_SIZE = this.maxFileSizeBytes;
|
|
3456
3615
|
if (contentLength && parseInt(contentLength, 10) > MAX_FILE_SIZE) {
|
|
3457
3616
|
const sizeMB = Math.round(parseInt(contentLength, 10) / 1024 / 1024);
|
|
3458
3617
|
getLogger().debug(`[Im-Nobsidian] \uD30C\uC77C \uC2A4\uD0B5 \u2014 \uB108\uBB34 \uD07C (${sizeMB}MB): ${caption}`);
|
|
@@ -3601,15 +3760,16 @@ function getFolderPath(filePath) {
|
|
|
3601
3760
|
return parts.length > 1 ? parts.slice(0, -1).join("/") : "";
|
|
3602
3761
|
}
|
|
3603
3762
|
var FileHandler = class {
|
|
3604
|
-
constructor(vaultFs, notionClient, stateDb) {
|
|
3763
|
+
constructor(vaultFs, notionClient, stateDb, concurrency = 2) {
|
|
3605
3764
|
this.vaultFs = vaultFs;
|
|
3606
3765
|
this.notionClient = notionClient;
|
|
3607
3766
|
this.stateDb = stateDb;
|
|
3767
|
+
this.sema = new Sema(concurrency);
|
|
3608
3768
|
}
|
|
3609
3769
|
vaultFs;
|
|
3610
3770
|
notionClient;
|
|
3611
3771
|
stateDb;
|
|
3612
|
-
sema
|
|
3772
|
+
sema;
|
|
3613
3773
|
async pushFilesForFolder(folderPageId, folderPath) {
|
|
3614
3774
|
const allFiles = await this.vaultFs.listNonMarkdownFiles();
|
|
3615
3775
|
const folderFiles = allFiles.filter((f) => getFolderPath(f.path) === folderPath);
|
|
@@ -3764,6 +3924,43 @@ var FileHandler = class {
|
|
|
3764
3924
|
}
|
|
3765
3925
|
};
|
|
3766
3926
|
|
|
3927
|
+
// src/sync/conflict-detector.ts
|
|
3928
|
+
function resolvePullConflict(input) {
|
|
3929
|
+
const { record, localContent, remoteContent, remoteChange, strategy } = input;
|
|
3930
|
+
const localHash = computeHash(localContent);
|
|
3931
|
+
const localModified = localHash !== record.contentHash;
|
|
3932
|
+
if (!localModified) {
|
|
3933
|
+
return { action: "write", localHash };
|
|
3934
|
+
}
|
|
3935
|
+
if (strategy === "remote-first") {
|
|
3936
|
+
return { action: "write", localHash };
|
|
3937
|
+
}
|
|
3938
|
+
if (strategy === "local-first") {
|
|
3939
|
+
return { action: "skip", localHash };
|
|
3940
|
+
}
|
|
3941
|
+
if (localContent === remoteContent) {
|
|
3942
|
+
return { action: "write", localHash };
|
|
3943
|
+
}
|
|
3944
|
+
const baseContent = record.baseSnapshot?.toString("utf-8") ?? null;
|
|
3945
|
+
if (baseContent !== null && remoteContent === baseContent) {
|
|
3946
|
+
return { action: "skip", localHash };
|
|
3947
|
+
}
|
|
3948
|
+
const conflict = {
|
|
3949
|
+
syncRecord: record,
|
|
3950
|
+
localChange: {
|
|
3951
|
+
path: record.obsidianPath,
|
|
3952
|
+
type: "modified",
|
|
3953
|
+
currentHash: localHash,
|
|
3954
|
+
previousHash: record.contentHash
|
|
3955
|
+
},
|
|
3956
|
+
remoteChange,
|
|
3957
|
+
baseContent,
|
|
3958
|
+
localContent,
|
|
3959
|
+
remoteContent
|
|
3960
|
+
};
|
|
3961
|
+
return { action: "conflict", conflict, localHash };
|
|
3962
|
+
}
|
|
3963
|
+
|
|
3767
3964
|
// src/utils/sanitize.ts
|
|
3768
3965
|
var INVALID_CHARS = /[<>:"/\\|?*\x00-\x1f]/g;
|
|
3769
3966
|
var TRAILING_DOTS = /\.+$/;
|
|
@@ -3825,12 +4022,13 @@ var BaseFileGenerator = class {
|
|
|
3825
4022
|
this.writeFormulas(lines, options.viewsConfig.views);
|
|
3826
4023
|
this.writePropertyDisplayNames(lines, options.schema);
|
|
3827
4024
|
this.writeViews(lines, options.viewsConfig.views, options.schema);
|
|
3828
|
-
return lines.join("\n");
|
|
4025
|
+
return lines.join("\n") + "\n";
|
|
3829
4026
|
}
|
|
3830
4027
|
writeFilters(lines, folderPath) {
|
|
3831
4028
|
lines.push("filters:");
|
|
3832
4029
|
lines.push(" and:");
|
|
3833
4030
|
lines.push(` - file.inFolder("${folderPath}")`);
|
|
4031
|
+
lines.push(` - file.ext == "md"`);
|
|
3834
4032
|
lines.push("");
|
|
3835
4033
|
}
|
|
3836
4034
|
needsContentCoverFormula(views) {
|
|
@@ -3869,25 +4067,25 @@ var BaseFileGenerator = class {
|
|
|
3869
4067
|
if (view.order && view.order.length > 0) {
|
|
3870
4068
|
lines.push(" order:");
|
|
3871
4069
|
for (const col of view.order) {
|
|
3872
|
-
lines.push(` - ${col}`);
|
|
4070
|
+
lines.push(` - ${this.propRef(col)}`);
|
|
3873
4071
|
}
|
|
3874
4072
|
}
|
|
3875
4073
|
if (view.sort && view.sort.length > 0) {
|
|
3876
4074
|
lines.push(" sort:");
|
|
3877
4075
|
for (const s of view.sort) {
|
|
3878
|
-
lines.push(` -
|
|
4076
|
+
lines.push(` - property: ${this.propRef(s.property)}`);
|
|
3879
4077
|
lines.push(` direction: ${s.direction}`);
|
|
3880
4078
|
}
|
|
3881
4079
|
}
|
|
3882
4080
|
if (view.groupBy) {
|
|
3883
4081
|
lines.push(" groupBy:");
|
|
3884
|
-
lines.push(` property: ${view.groupBy.property}`);
|
|
4082
|
+
lines.push(` property: ${this.propRef(view.groupBy.property)}`);
|
|
3885
4083
|
if (view.groupBy.direction) {
|
|
3886
4084
|
lines.push(` direction: ${view.groupBy.direction}`);
|
|
3887
4085
|
}
|
|
3888
4086
|
}
|
|
3889
4087
|
if (view.image) {
|
|
3890
|
-
lines.push(` image: ${view.image}`);
|
|
4088
|
+
lines.push(` image: ${this.propRef(view.image)}`);
|
|
3891
4089
|
}
|
|
3892
4090
|
}
|
|
3893
4091
|
}
|
|
@@ -3901,7 +4099,9 @@ var BaseFileGenerator = class {
|
|
|
3901
4099
|
if (view.properties && view.properties.length > 0) {
|
|
3902
4100
|
const visible = view.properties.filter((p) => p.visible !== false).map((p) => {
|
|
3903
4101
|
const name = p.propertyName ?? this.resolvePropertyName(p.propertyId, schema);
|
|
3904
|
-
|
|
4102
|
+
if (!name) return null;
|
|
4103
|
+
if (schema[name]?.type === "title") return null;
|
|
4104
|
+
return this.toNoteRef(name);
|
|
3905
4105
|
}).filter((name) => !!name);
|
|
3906
4106
|
if (visible.length > 0) {
|
|
3907
4107
|
result.order = ["file.name", ...visible];
|
|
@@ -3909,17 +4109,17 @@ var BaseFileGenerator = class {
|
|
|
3909
4109
|
}
|
|
3910
4110
|
if (view.sorts && view.sorts.length > 0) {
|
|
3911
4111
|
result.sort = view.sorts.map((s) => {
|
|
3912
|
-
let
|
|
4112
|
+
let property;
|
|
3913
4113
|
if (s.property) {
|
|
3914
4114
|
const propName = this.resolvePropertyName(s.property, schema) ?? s.property;
|
|
3915
|
-
|
|
4115
|
+
property = this.toNoteRef(propName);
|
|
3916
4116
|
} else if (s.timestamp) {
|
|
3917
|
-
|
|
4117
|
+
property = s.timestamp === "created_time" ? "file.ctime" : "file.mtime";
|
|
3918
4118
|
} else {
|
|
3919
4119
|
return null;
|
|
3920
4120
|
}
|
|
3921
4121
|
return {
|
|
3922
|
-
|
|
4122
|
+
property,
|
|
3923
4123
|
direction: s.direction === "ascending" ? "ASC" : "DESC"
|
|
3924
4124
|
};
|
|
3925
4125
|
}).filter((s) => s !== null);
|
|
@@ -3948,12 +4148,29 @@ var BaseFileGenerator = class {
|
|
|
3948
4148
|
toNoteRef(propertyName) {
|
|
3949
4149
|
return propertyName;
|
|
3950
4150
|
}
|
|
4151
|
+
/**
|
|
4152
|
+
* 속성 참조(order/sort/groupBy/image)를 YAML로 안전하게 출력한다.
|
|
4153
|
+
* `file.name`·`formula.coverImage`·`note.cover` 같은 접두사 식별자와 공백 포함 일반 속성명은
|
|
4154
|
+
* 평문 스칼라로 안전하므로 그대로 두고, 콜론·해시·따옴표 등 YAML 메타문자가 있을 때만 인용한다.
|
|
4155
|
+
*/
|
|
4156
|
+
propRef(ref) {
|
|
4157
|
+
if (/^[A-Za-z0-9_][A-Za-z0-9_. ]*$/.test(ref) && !ref.endsWith(" ")) return ref;
|
|
4158
|
+
return `"${ref.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
4159
|
+
}
|
|
3951
4160
|
resolvePropertyName(propertyId, schema) {
|
|
4161
|
+
const target = this.decodeId(propertyId);
|
|
3952
4162
|
for (const [name, prop] of Object.entries(schema)) {
|
|
3953
|
-
if (prop.id ===
|
|
4163
|
+
if (this.decodeId(prop.id) === target) return name;
|
|
3954
4164
|
}
|
|
3955
4165
|
return void 0;
|
|
3956
4166
|
}
|
|
4167
|
+
decodeId(id) {
|
|
4168
|
+
try {
|
|
4169
|
+
return decodeURIComponent(id);
|
|
4170
|
+
} catch {
|
|
4171
|
+
return id;
|
|
4172
|
+
}
|
|
4173
|
+
}
|
|
3957
4174
|
yamlKey(key) {
|
|
3958
4175
|
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) return key;
|
|
3959
4176
|
return `"${key.replace(/"/g, '\\"')}"`;
|
|
@@ -3979,8 +4196,6 @@ var NOTION_PDF_RE = /[\t ]*<pdf src="([^"]*)">([\s\S]*?)<\/pdf>/g;
|
|
|
3979
4196
|
var NOTION_FILE_RE = /[\t ]*<file src="([^"]*)">([\s\S]*?)<\/file>/g;
|
|
3980
4197
|
var NOTION_TAB_RE = /<tab title="([^"]*)">([\s\S]*?)<\/tab>/g;
|
|
3981
4198
|
var NOTION_UNDERLINE_RE = /<span underline="true">([\s\S]*?)<\/span>/g;
|
|
3982
|
-
var TOGGLE_START2 = "%%im-nobsidian:toggle:start%%";
|
|
3983
|
-
var TOGGLE_END2 = "%%im-nobsidian:toggle:end%%";
|
|
3984
4199
|
function notionEnhancedToObsidian(enhanced) {
|
|
3985
4200
|
let result = enhanced;
|
|
3986
4201
|
result = convertSyncedBlockRef(result);
|
|
@@ -4004,9 +4219,13 @@ function notionEnhancedToObsidian(enhanced) {
|
|
|
4004
4219
|
result = removeEmptyBlocks(result);
|
|
4005
4220
|
result = unescapePipes(result);
|
|
4006
4221
|
result = unescapeNotionChars(result);
|
|
4222
|
+
result = unescapeWikilinkBrackets(result);
|
|
4007
4223
|
result = ensureCalloutContinuity(result);
|
|
4008
4224
|
return result;
|
|
4009
4225
|
}
|
|
4226
|
+
function unescapeWikilinkBrackets(content) {
|
|
4227
|
+
return content.replace(/\\\[\\\[([\s\S]*?)\\\]\\\]/g, "[[$1]]");
|
|
4228
|
+
}
|
|
4010
4229
|
function obsidianToNotionEnhanced(obsidian) {
|
|
4011
4230
|
let result = obsidian;
|
|
4012
4231
|
result = convertTogglesToHtml(result);
|
|
@@ -4016,8 +4235,31 @@ function obsidianToNotionEnhanced(obsidian) {
|
|
|
4016
4235
|
result = restoreUnknownBlocks(result);
|
|
4017
4236
|
result = restoreColorSpans(result);
|
|
4018
4237
|
result = restoreUnderlineSpans(result);
|
|
4238
|
+
result = convertMentionPageIdToUrl(result);
|
|
4239
|
+
result = restoreWikilinkPreserveLinks(result);
|
|
4019
4240
|
return result;
|
|
4020
4241
|
}
|
|
4242
|
+
var MENTION_PAGE_ID_RE = /<mention-page id="([^"]+)">[\s\S]*?<\/mention-page>/g;
|
|
4243
|
+
var WIKILINK_PRESERVE_LINK_RE = new RegExp(
|
|
4244
|
+
`\\[([^\\]]+)\\]\\(${WIKILINK_PROTOCOL}([^)]+)\\)`,
|
|
4245
|
+
"g"
|
|
4246
|
+
);
|
|
4247
|
+
function convertMentionPageIdToUrl(content) {
|
|
4248
|
+
return content.replace(MENTION_PAGE_ID_RE, (_match, id) => {
|
|
4249
|
+
const nohyph = id.replace(/-/g, "");
|
|
4250
|
+
return `<mention-page url="https://www.notion.so/${nohyph}"/>`;
|
|
4251
|
+
});
|
|
4252
|
+
}
|
|
4253
|
+
function restoreWikilinkPreserveLinks(content) {
|
|
4254
|
+
return content.replace(WIKILINK_PRESERVE_LINK_RE, (_match, label, enc) => {
|
|
4255
|
+
let target = enc;
|
|
4256
|
+
try {
|
|
4257
|
+
target = decodeURIComponent(enc);
|
|
4258
|
+
} catch {
|
|
4259
|
+
}
|
|
4260
|
+
return target === label ? `[[${target}]]` : `[[${target}|${label}]]`;
|
|
4261
|
+
});
|
|
4262
|
+
}
|
|
4021
4263
|
function normalizeCodeBlockToggles(content) {
|
|
4022
4264
|
return content.replace(
|
|
4023
4265
|
/^- (.+)\n\t```(\w*)\n([\s\S]*?)\n\t```$/gm,
|
|
@@ -4088,12 +4330,14 @@ function preserveUnknownBlocks(content) {
|
|
|
4088
4330
|
const typeMatch = /type="([^"]*)"/.exec(attrs);
|
|
4089
4331
|
const altMatch = /alt="([^"]*)"/.exec(attrs);
|
|
4090
4332
|
const blockType = altMatch?.[1] ?? typeMatch?.[1] ?? "unknown";
|
|
4091
|
-
return
|
|
4333
|
+
return compactMarker(`unknown:id=${id}&type=${blockType}`);
|
|
4092
4334
|
});
|
|
4093
4335
|
result = result.replace(NOTION_UNKNOWN_URL_RE, (_match, url, attrs) => {
|
|
4094
4336
|
const altMatch = /alt="([^"]*)"/.exec(attrs);
|
|
4095
4337
|
const blockType = altMatch?.[1] ?? "bookmark";
|
|
4096
|
-
|
|
4338
|
+
const marker = compactMarker(`unknown:id=${encodeURIComponent(url)}&type=${blockType}`);
|
|
4339
|
+
const label = blockType === "embed" ? "\u{1F517} Embed" : blockType === "bookmark" ? "\u{1F516} Bookmark" : `\u{1F517} ${blockType}`;
|
|
4340
|
+
return `[${label}](${url})${marker}`;
|
|
4097
4341
|
});
|
|
4098
4342
|
return result;
|
|
4099
4343
|
}
|
|
@@ -4141,7 +4385,7 @@ ${indented}`;
|
|
|
4141
4385
|
}
|
|
4142
4386
|
function convertUnderlineSpans(content) {
|
|
4143
4387
|
return content.replace(NOTION_UNDERLINE_RE, (_match, text) => {
|
|
4144
|
-
return
|
|
4388
|
+
return `${compactMarker("underline")}${text}%%/underline%%`;
|
|
4145
4389
|
});
|
|
4146
4390
|
}
|
|
4147
4391
|
function convertTogglesToHtml(content) {
|
|
@@ -4161,14 +4405,14 @@ ${bodyText}
|
|
|
4161
4405
|
</details>`;
|
|
4162
4406
|
});
|
|
4163
4407
|
}
|
|
4164
|
-
const startRe = new RegExp(escapeRegex3(
|
|
4165
|
-
const endRe = new RegExp(escapeRegex3(
|
|
4408
|
+
const startRe = new RegExp(escapeRegex3(TOGGLE_START), "g");
|
|
4409
|
+
const endRe = new RegExp(escapeRegex3(TOGGLE_END), "g");
|
|
4166
4410
|
safety = 0;
|
|
4167
|
-
while (result.includes(
|
|
4168
|
-
const startIdx = result.indexOf(
|
|
4169
|
-
const endIdx = findMatchingEnd(result, startIdx +
|
|
4411
|
+
while (result.includes(TOGGLE_START) && safety++ < 100) {
|
|
4412
|
+
const startIdx = result.indexOf(TOGGLE_START);
|
|
4413
|
+
const endIdx = findMatchingEnd(result, startIdx + TOGGLE_START.length);
|
|
4170
4414
|
if (endIdx === -1) break;
|
|
4171
|
-
const inner = result.slice(startIdx +
|
|
4415
|
+
const inner = result.slice(startIdx + TOGGLE_START.length, endIdx).trim();
|
|
4172
4416
|
const titleMatch = /^- (.+)$/m.exec(inner);
|
|
4173
4417
|
const title = titleMatch ? titleMatch[1] : "";
|
|
4174
4418
|
const body = inner.split("\n").slice(1).map((line) => line.replace(/^ {2}/, "")).join("\n").trim();
|
|
@@ -4178,7 +4422,7 @@ ${bodyText}
|
|
|
4178
4422
|
${body}
|
|
4179
4423
|
|
|
4180
4424
|
</details>`;
|
|
4181
|
-
result = result.slice(0, startIdx) + html + result.slice(endIdx +
|
|
4425
|
+
result = result.slice(0, startIdx) + html + result.slice(endIdx + TOGGLE_END.length);
|
|
4182
4426
|
}
|
|
4183
4427
|
result = result.replace(startRe, "").replace(endRe, "");
|
|
4184
4428
|
return result;
|
|
@@ -4187,16 +4431,16 @@ function findMatchingEnd(content, startFrom) {
|
|
|
4187
4431
|
let depth = 1;
|
|
4188
4432
|
let pos = startFrom;
|
|
4189
4433
|
while (pos < content.length && depth > 0) {
|
|
4190
|
-
const nextStart = content.indexOf(
|
|
4191
|
-
const nextEnd = content.indexOf(
|
|
4434
|
+
const nextStart = content.indexOf(TOGGLE_START, pos);
|
|
4435
|
+
const nextEnd = content.indexOf(TOGGLE_END, pos);
|
|
4192
4436
|
if (nextEnd === -1) return -1;
|
|
4193
4437
|
if (nextStart !== -1 && nextStart < nextEnd) {
|
|
4194
4438
|
depth++;
|
|
4195
|
-
pos = nextStart +
|
|
4439
|
+
pos = nextStart + TOGGLE_START.length;
|
|
4196
4440
|
} else {
|
|
4197
4441
|
depth--;
|
|
4198
4442
|
if (depth === 0) return nextEnd;
|
|
4199
|
-
pos = nextEnd +
|
|
4443
|
+
pos = nextEnd + TOGGLE_END.length;
|
|
4200
4444
|
}
|
|
4201
4445
|
}
|
|
4202
4446
|
return -1;
|
|
@@ -4269,7 +4513,7 @@ function convertPageLinks(content) {
|
|
|
4269
4513
|
function convertColorSpans(content) {
|
|
4270
4514
|
return content.replace(
|
|
4271
4515
|
NOTION_COLOR_SPAN_RE,
|
|
4272
|
-
(_match, color, text) =>
|
|
4516
|
+
(_match, color, text) => `${compactMarker(`color:${color}`)}${text}%%/color%%`
|
|
4273
4517
|
);
|
|
4274
4518
|
}
|
|
4275
4519
|
function convertDatabaseBlocks(content) {
|
|
@@ -4370,7 +4614,10 @@ function restoreTabBlocks(content) {
|
|
|
4370
4614
|
return `<tab title="${title}">${unquoted}</tab>`;
|
|
4371
4615
|
});
|
|
4372
4616
|
}
|
|
4373
|
-
var OBSIDIAN_UNKNOWN_RE =
|
|
4617
|
+
var OBSIDIAN_UNKNOWN_RE = new RegExp(
|
|
4618
|
+
`(?:\\[[^\\]]*\\]\\([^)]*\\))?%%${MARKER_BRAND_RE}:unknown:id=([^&]+)&type=([^%]+)%%`,
|
|
4619
|
+
"g"
|
|
4620
|
+
);
|
|
4374
4621
|
function restoreUnknownBlocks(content) {
|
|
4375
4622
|
return content.replace(OBSIDIAN_UNKNOWN_RE, (_match, id, type) => {
|
|
4376
4623
|
const decoded = decodeURIComponent(id);
|
|
@@ -4380,13 +4627,19 @@ function restoreUnknownBlocks(content) {
|
|
|
4380
4627
|
return `<unknown id="${id}" type="${type}"/>`;
|
|
4381
4628
|
});
|
|
4382
4629
|
}
|
|
4383
|
-
var OBSIDIAN_COLOR_RE =
|
|
4630
|
+
var OBSIDIAN_COLOR_RE = new RegExp(
|
|
4631
|
+
`%%${MARKER_BRAND_RE}:color:([^%]+)%%([\\s\\S]*?)%%\\/color%%`,
|
|
4632
|
+
"g"
|
|
4633
|
+
);
|
|
4384
4634
|
function restoreColorSpans(content) {
|
|
4385
4635
|
return content.replace(OBSIDIAN_COLOR_RE, (_match, color, text) => {
|
|
4386
4636
|
return `<span color="${color}">${text}</span>`;
|
|
4387
4637
|
});
|
|
4388
4638
|
}
|
|
4389
|
-
var OBSIDIAN_UNDERLINE_RE =
|
|
4639
|
+
var OBSIDIAN_UNDERLINE_RE = new RegExp(
|
|
4640
|
+
`%%${MARKER_BRAND_RE}:underline%%([\\s\\S]*?)%%\\/underline%%`,
|
|
4641
|
+
"g"
|
|
4642
|
+
);
|
|
4390
4643
|
function restoreUnderlineSpans(content) {
|
|
4391
4644
|
return content.replace(OBSIDIAN_UNDERLINE_RE, (_match, text) => {
|
|
4392
4645
|
return `<span underline="true">${text}</span>`;
|
|
@@ -4452,16 +4705,18 @@ var DatabaseSyncer = class {
|
|
|
4452
4705
|
async pullAll() {
|
|
4453
4706
|
const databases = this.config.notion.databases;
|
|
4454
4707
|
if (!databases || databases.length === 0) {
|
|
4455
|
-
return { created: 0, updated: 0, failed: [] };
|
|
4708
|
+
return { created: 0, updated: 0, conflicts: [], failed: [] };
|
|
4456
4709
|
}
|
|
4457
4710
|
let created = 0;
|
|
4458
4711
|
let updated = 0;
|
|
4712
|
+
const conflicts = [];
|
|
4459
4713
|
const failed = [];
|
|
4460
4714
|
for (const dbConfig of databases) {
|
|
4461
4715
|
try {
|
|
4462
4716
|
const result = await this.pullDatabase(dbConfig);
|
|
4463
4717
|
created += result.created;
|
|
4464
4718
|
updated += result.updated;
|
|
4719
|
+
conflicts.push(...result.conflicts);
|
|
4465
4720
|
failed.push(...result.failed);
|
|
4466
4721
|
} catch (error) {
|
|
4467
4722
|
getLogger().warn(`[DB Sync] DB ${dbConfig.databaseId} pull \uC2E4\uD328:`, error);
|
|
@@ -4472,12 +4727,12 @@ var DatabaseSyncer = class {
|
|
|
4472
4727
|
});
|
|
4473
4728
|
}
|
|
4474
4729
|
}
|
|
4475
|
-
return { created, updated, failed };
|
|
4730
|
+
return { created, updated, conflicts, failed };
|
|
4476
4731
|
}
|
|
4477
4732
|
async pushAll() {
|
|
4478
4733
|
const databases = this.config.notion.databases;
|
|
4479
4734
|
if (!databases || databases.length === 0) {
|
|
4480
|
-
return { created: 0, updated: 0, failed: [] };
|
|
4735
|
+
return { created: 0, updated: 0, conflicts: [], failed: [] };
|
|
4481
4736
|
}
|
|
4482
4737
|
let created = 0;
|
|
4483
4738
|
let updated = 0;
|
|
@@ -4497,7 +4752,7 @@ var DatabaseSyncer = class {
|
|
|
4497
4752
|
});
|
|
4498
4753
|
}
|
|
4499
4754
|
}
|
|
4500
|
-
return { created, updated, failed };
|
|
4755
|
+
return { created, updated, conflicts: [], failed };
|
|
4501
4756
|
}
|
|
4502
4757
|
async pullDatabase(dbConfig) {
|
|
4503
4758
|
const schema = await this.notionClient.getDatabaseSchema(dbConfig.databaseId);
|
|
@@ -4511,14 +4766,19 @@ var DatabaseSyncer = class {
|
|
|
4511
4766
|
);
|
|
4512
4767
|
let created = 0;
|
|
4513
4768
|
let updated = 0;
|
|
4769
|
+
const conflicts = [];
|
|
4514
4770
|
const failed = [];
|
|
4515
4771
|
for (const page of pages) {
|
|
4516
4772
|
try {
|
|
4517
4773
|
const record = this.stateDb.getByNotionId(page.id);
|
|
4518
4774
|
if (record) {
|
|
4519
4775
|
if (page.last_edited_time === record.notionLastEdited) continue;
|
|
4520
|
-
await this.pullDatabasePage(page, dbConfig);
|
|
4521
|
-
|
|
4776
|
+
const outcome = await this.pullDatabasePage(page, dbConfig);
|
|
4777
|
+
if (outcome.action === "written") {
|
|
4778
|
+
updated++;
|
|
4779
|
+
} else if (outcome.action === "conflict") {
|
|
4780
|
+
conflicts.push(outcome.conflict);
|
|
4781
|
+
}
|
|
4522
4782
|
} else {
|
|
4523
4783
|
await this.pullDatabasePage(page, dbConfig);
|
|
4524
4784
|
created++;
|
|
@@ -4531,12 +4791,14 @@ var DatabaseSyncer = class {
|
|
|
4531
4791
|
});
|
|
4532
4792
|
}
|
|
4533
4793
|
}
|
|
4534
|
-
getLogger().debug(
|
|
4535
|
-
|
|
4794
|
+
getLogger().debug(
|
|
4795
|
+
`[DB Sync] ${dbConfig.localFolder}: ${created} \uC0DD\uC131, ${updated} \uC5C5\uB370\uC774\uD2B8, ${conflicts.length} \uCDA9\uB3CC`
|
|
4796
|
+
);
|
|
4797
|
+
return { created, updated, conflicts, failed };
|
|
4536
4798
|
}
|
|
4537
4799
|
async pullDatabaseViews(dbConfig, force = false) {
|
|
4538
4800
|
try {
|
|
4539
|
-
const configPath =
|
|
4801
|
+
const configPath = DB_VIEWS_PATH;
|
|
4540
4802
|
if (!force) {
|
|
4541
4803
|
try {
|
|
4542
4804
|
const existing = await this.vaultFs.readFile(configPath);
|
|
@@ -4546,7 +4808,7 @@ var DatabaseSyncer = class {
|
|
|
4546
4808
|
}
|
|
4547
4809
|
}
|
|
4548
4810
|
const viewsConfig = await this.notionClient.getDatabaseViewsConfig(dbConfig.databaseId);
|
|
4549
|
-
await this.vaultFs.ensureFolder(
|
|
4811
|
+
await this.vaultFs.ensureFolder(INTERNAL_DIR);
|
|
4550
4812
|
let allViewsConfigs = {};
|
|
4551
4813
|
try {
|
|
4552
4814
|
const existing = await this.vaultFs.readFile(configPath);
|
|
@@ -4658,6 +4920,33 @@ var DatabaseSyncer = class {
|
|
|
4658
4920
|
},
|
|
4659
4921
|
{ properties }
|
|
4660
4922
|
);
|
|
4923
|
+
if (existingRecord) {
|
|
4924
|
+
let localContent = "";
|
|
4925
|
+
try {
|
|
4926
|
+
localContent = await this.vaultFs.readFile(filePath);
|
|
4927
|
+
} catch {
|
|
4928
|
+
localContent = "";
|
|
4929
|
+
}
|
|
4930
|
+
const resolution = resolvePullConflict({
|
|
4931
|
+
record: existingRecord,
|
|
4932
|
+
localContent,
|
|
4933
|
+
remoteContent: finalContent,
|
|
4934
|
+
remoteChange: {
|
|
4935
|
+
pageId: page.id,
|
|
4936
|
+
type: "modified",
|
|
4937
|
+
lastEdited: page.last_edited_time,
|
|
4938
|
+
previousEdited: existingRecord.notionLastEdited
|
|
4939
|
+
},
|
|
4940
|
+
strategy: this.config.sync.conflictStrategy
|
|
4941
|
+
});
|
|
4942
|
+
if (resolution.action === "skip") {
|
|
4943
|
+
return { action: "skipped", path: filePath };
|
|
4944
|
+
}
|
|
4945
|
+
if (resolution.action === "conflict") {
|
|
4946
|
+
this.stateDb.updateStatus(existingRecord.id, "conflict");
|
|
4947
|
+
return { action: "conflict", path: filePath, conflict: resolution.conflict };
|
|
4948
|
+
}
|
|
4949
|
+
}
|
|
4661
4950
|
await this.vaultFs.ensureFolder(dbConfig.localFolder);
|
|
4662
4951
|
await this.vaultFs.writeFile(filePath, finalContent);
|
|
4663
4952
|
const hash = computeHash(finalContent);
|
|
@@ -4681,7 +4970,7 @@ var DatabaseSyncer = class {
|
|
|
4681
4970
|
aliases: extractAliases(properties)
|
|
4682
4971
|
});
|
|
4683
4972
|
});
|
|
4684
|
-
return filePath;
|
|
4973
|
+
return { action: "written", path: filePath };
|
|
4685
4974
|
}
|
|
4686
4975
|
async pushDatabase(dbConfig) {
|
|
4687
4976
|
const schema = await this.notionClient.getDatabaseSchema(dbConfig.databaseId);
|
|
@@ -4689,6 +4978,8 @@ var DatabaseSyncer = class {
|
|
|
4689
4978
|
const allFiles = await this.vaultFs.listMarkdownFiles();
|
|
4690
4979
|
const prefix = dbConfig.localFolder.endsWith("/") ? dbConfig.localFolder : dbConfig.localFolder + "/";
|
|
4691
4980
|
const dbFiles = allFiles.filter((f) => f.path.startsWith(prefix));
|
|
4981
|
+
const livePaths = new Set(dbFiles.map((f) => f.path));
|
|
4982
|
+
const orphanByHash = this.buildOrphanHashIndex(dbConfig.databaseId, livePaths);
|
|
4692
4983
|
let created = 0;
|
|
4693
4984
|
let updated = 0;
|
|
4694
4985
|
const failed = [];
|
|
@@ -4706,19 +4997,50 @@ var DatabaseSyncer = class {
|
|
|
4706
4997
|
const notionProps = this.propertyMapper.toNotionProperties(frontmatter, title);
|
|
4707
4998
|
const enhanced = obsidianToNotionEnhanced(body.trim());
|
|
4708
4999
|
if (record?.notionPageId) {
|
|
4709
|
-
await this.notionClient.updatePageProperties(record.notionPageId, notionProps);
|
|
4710
5000
|
try {
|
|
5001
|
+
await this.notionClient.updatePageProperties(record.notionPageId, notionProps);
|
|
4711
5002
|
await this.notionClient.replacePageMarkdown(record.notionPageId, enhanced);
|
|
4712
|
-
} catch {
|
|
5003
|
+
} catch (error) {
|
|
5004
|
+
this.stateDb.updateStatus(record.id, "error");
|
|
5005
|
+
failed.push({
|
|
5006
|
+
path: file.path,
|
|
5007
|
+
operation: "update",
|
|
5008
|
+
error: error instanceof Error ? error.message : String(error)
|
|
5009
|
+
});
|
|
5010
|
+
continue;
|
|
4713
5011
|
}
|
|
4714
5012
|
const updatedPage = await this.notionClient.getPage(record.notionPageId);
|
|
4715
5013
|
this.stateDb.transaction(() => {
|
|
4716
5014
|
this.stateDb.updateHash(record.id, hash, Buffer.from(content, "utf-8"));
|
|
4717
5015
|
this.stateDb.updateStatus(record.id, "synced");
|
|
4718
5016
|
this.stateDb.setNotionLastEdited(record.id, updatedPage.last_edited_time);
|
|
5017
|
+
this.stateDb.upsertWikilink({
|
|
5018
|
+
obsidianPath: file.path,
|
|
5019
|
+
notionPageId: record.notionPageId,
|
|
5020
|
+
title,
|
|
5021
|
+
aliases: extractAliases(frontmatter)
|
|
5022
|
+
});
|
|
4719
5023
|
});
|
|
4720
5024
|
updated++;
|
|
4721
5025
|
} else {
|
|
5026
|
+
const renamed = orphanByHash.get(hash);
|
|
5027
|
+
if (renamed?.notionPageId) {
|
|
5028
|
+
const movedPageId = renamed.notionPageId;
|
|
5029
|
+
orphanByHash.delete(hash);
|
|
5030
|
+
this.stateDb.transaction(() => {
|
|
5031
|
+
this.stateDb.updatePath(renamed.id, file.path);
|
|
5032
|
+
this.stateDb.updateHash(renamed.id, hash, Buffer.from(content, "utf-8"));
|
|
5033
|
+
this.stateDb.updateStatus(renamed.id, "synced");
|
|
5034
|
+
this.stateDb.upsertWikilink({
|
|
5035
|
+
obsidianPath: file.path,
|
|
5036
|
+
notionPageId: movedPageId,
|
|
5037
|
+
title,
|
|
5038
|
+
aliases: extractAliases(frontmatter)
|
|
5039
|
+
});
|
|
5040
|
+
});
|
|
5041
|
+
updated++;
|
|
5042
|
+
continue;
|
|
5043
|
+
}
|
|
4722
5044
|
const page = await this.notionClient.createPageWithMarkdown({
|
|
4723
5045
|
parentId: dbConfig.databaseId,
|
|
4724
5046
|
parentType: "database",
|
|
@@ -4757,7 +5079,21 @@ var DatabaseSyncer = class {
|
|
|
4757
5079
|
}
|
|
4758
5080
|
}
|
|
4759
5081
|
getLogger().debug(`[DB Sync] ${dbConfig.localFolder}: ${created} \uC0DD\uC131, ${updated} \uC5C5\uB370\uC774\uD2B8`);
|
|
4760
|
-
return { created, updated, failed };
|
|
5082
|
+
return { created, updated, conflicts: [], failed };
|
|
5083
|
+
}
|
|
5084
|
+
/**
|
|
5085
|
+
* rename 감지를 위한 고아 레코드 인덱스(내용 해시 → 레코드)를 만든다.
|
|
5086
|
+
* 고아 = 해당 DB 소속 db-row 레코드 중 로컬 파일이 더 이상 존재하지 않는(livePaths 에 없는) 것.
|
|
5087
|
+
* 같은 해시가 여러 고아에 걸리면 첫 항목을 유지한다(희박한 케이스).
|
|
5088
|
+
*/
|
|
5089
|
+
buildOrphanHashIndex(databaseId, livePaths) {
|
|
5090
|
+
const index = /* @__PURE__ */ new Map();
|
|
5091
|
+
for (const record of this.stateDb.getAll()) {
|
|
5092
|
+
if (record.fileType === "db-row" && record.notionParentId === databaseId && record.notionPageId && !livePaths.has(record.obsidianPath) && !index.has(record.contentHash)) {
|
|
5093
|
+
index.set(record.contentHash, record);
|
|
5094
|
+
}
|
|
5095
|
+
}
|
|
5096
|
+
return index;
|
|
4761
5097
|
}
|
|
4762
5098
|
};
|
|
4763
5099
|
function extractAliases(properties) {
|
|
@@ -4774,6 +5110,26 @@ function extractTitleFromPath(filePath) {
|
|
|
4774
5110
|
return filename.replace(/\.md$/, "");
|
|
4775
5111
|
}
|
|
4776
5112
|
|
|
5113
|
+
// src/utils/pool.ts
|
|
5114
|
+
async function runPool(items, worker, options) {
|
|
5115
|
+
const total = items.length;
|
|
5116
|
+
if (total === 0) return;
|
|
5117
|
+
const workerCount = Math.max(1, Math.min(Math.floor(options.concurrency) || 1, total));
|
|
5118
|
+
let cursor = 0;
|
|
5119
|
+
const runWorker = async () => {
|
|
5120
|
+
for (; ; ) {
|
|
5121
|
+
if (options.signal?.aborted) return;
|
|
5122
|
+
const index = cursor++;
|
|
5123
|
+
if (index >= total) return;
|
|
5124
|
+
try {
|
|
5125
|
+
await worker(items[index], index);
|
|
5126
|
+
} catch {
|
|
5127
|
+
}
|
|
5128
|
+
}
|
|
5129
|
+
};
|
|
5130
|
+
await Promise.all(Array.from({ length: workerCount }, () => runWorker()));
|
|
5131
|
+
}
|
|
5132
|
+
|
|
4777
5133
|
// src/sync/orchestrator.ts
|
|
4778
5134
|
var SyncOrchestrator = class {
|
|
4779
5135
|
constructor(config, stateDb, notionClient, vaultFs, customFetch) {
|
|
@@ -4790,9 +5146,20 @@ var SyncOrchestrator = class {
|
|
|
4790
5146
|
vaultFs,
|
|
4791
5147
|
config.paths.attachments,
|
|
4792
5148
|
notionClient,
|
|
4793
|
-
customFetch
|
|
5149
|
+
customFetch,
|
|
5150
|
+
{
|
|
5151
|
+
concurrency: config.advanced.mediaConcurrency,
|
|
5152
|
+
maxRetries: config.advanced.mediaMaxRetries,
|
|
5153
|
+
retryBaseMs: config.advanced.mediaRetryBaseMs,
|
|
5154
|
+
maxFileSizeBytes: config.advanced.maxFileSizeBytes
|
|
5155
|
+
}
|
|
5156
|
+
);
|
|
5157
|
+
this.fileHandler = new FileHandler(
|
|
5158
|
+
vaultFs,
|
|
5159
|
+
notionClient,
|
|
5160
|
+
stateDb,
|
|
5161
|
+
config.advanced.fileConcurrency
|
|
4794
5162
|
);
|
|
4795
|
-
this.fileHandler = new FileHandler(vaultFs, notionClient, stateDb);
|
|
4796
5163
|
this.propertyMapper = new PropertyMapper();
|
|
4797
5164
|
this.databaseSyncer = new DatabaseSyncer(
|
|
4798
5165
|
config,
|
|
@@ -4822,6 +5189,11 @@ var SyncOrchestrator = class {
|
|
|
4822
5189
|
dbSchemaLoaded = false;
|
|
4823
5190
|
_pullImageCount = 0;
|
|
4824
5191
|
_pullFileCount = 0;
|
|
5192
|
+
// pull 중 각 페이지 markdown(<database url=.../>)에서 추출한 인라인 DB 참조.
|
|
5193
|
+
// key: 하이픈 제거 databaseId, value: 그 DB가 박힌 부모 페이지 id.
|
|
5194
|
+
// 블록 트리 재귀 없이 markdown 신호만으로 컬럼/synced_block 등 깊이 중첩된
|
|
5195
|
+
// child_database 까지 발견해 폴더+.base 동기화 대상으로 등록한다.
|
|
5196
|
+
_inlineDbRefs = /* @__PURE__ */ new Map();
|
|
4825
5197
|
async push(options) {
|
|
4826
5198
|
const startTime = Date.now();
|
|
4827
5199
|
if (this.config.sync.direction === "pull") {
|
|
@@ -4880,87 +5252,68 @@ var SyncOrchestrator = class {
|
|
|
4880
5252
|
for (const folderPath of sortedFolders) {
|
|
4881
5253
|
await this.ensureFolderPage(folderPath);
|
|
4882
5254
|
}
|
|
4883
|
-
const
|
|
4884
|
-
let created = 0;
|
|
4885
|
-
let updated = 0;
|
|
4886
|
-
let deleted = 0;
|
|
5255
|
+
const counts = { created: 0, updated: 0, deleted: 0 };
|
|
4887
5256
|
const failed = [];
|
|
4888
5257
|
let completed = 0;
|
|
4889
5258
|
const total = filtered.length;
|
|
4890
|
-
const
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
case "deleted":
|
|
4910
|
-
await this.pushDelete(change.path);
|
|
4911
|
-
deleted++;
|
|
4912
|
-
break;
|
|
4913
|
-
}
|
|
4914
|
-
} catch (error) {
|
|
4915
|
-
failed.push({
|
|
4916
|
-
path: change.path,
|
|
4917
|
-
operation: change.type === "created" ? "create" : change.type === "deleted" ? "delete" : "update",
|
|
4918
|
-
error: error instanceof Error ? error.message : String(error)
|
|
4919
|
-
});
|
|
4920
|
-
} finally {
|
|
4921
|
-
sema.release();
|
|
5259
|
+
const opOf = (change) => change.type === "created" ? "create" : change.type === "deleted" ? "delete" : "update";
|
|
5260
|
+
const applyPushChange = async (change) => {
|
|
5261
|
+
switch (change.type) {
|
|
5262
|
+
case "created":
|
|
5263
|
+
await this.pushCreate(change.path);
|
|
5264
|
+
counts.created++;
|
|
5265
|
+
break;
|
|
5266
|
+
case "modified":
|
|
5267
|
+
await this.pushUpdate(change.path);
|
|
5268
|
+
counts.updated++;
|
|
5269
|
+
break;
|
|
5270
|
+
case "moved":
|
|
5271
|
+
await this.pushMove(change.path);
|
|
5272
|
+
counts.updated++;
|
|
5273
|
+
break;
|
|
5274
|
+
case "deleted":
|
|
5275
|
+
await this.pushDelete(change.path);
|
|
5276
|
+
counts.deleted++;
|
|
5277
|
+
break;
|
|
4922
5278
|
}
|
|
4923
|
-
}
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
const change = filtered.find((c) => c.path === target.path);
|
|
4931
|
-
if (!change) {
|
|
4932
|
-
failed.push(target);
|
|
4933
|
-
continue;
|
|
4934
|
-
}
|
|
5279
|
+
};
|
|
5280
|
+
const retryQueue = [];
|
|
5281
|
+
await runPool(
|
|
5282
|
+
filtered,
|
|
5283
|
+
async (change) => {
|
|
5284
|
+
if (options?.signal?.aborted) return;
|
|
5285
|
+
options?.onProgress?.(++completed, total, { path: change.path, operation: opOf(change) });
|
|
4935
5286
|
try {
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
created++;
|
|
4940
|
-
break;
|
|
4941
|
-
case "modified":
|
|
4942
|
-
await this.pushUpdate(change.path);
|
|
4943
|
-
updated++;
|
|
4944
|
-
break;
|
|
4945
|
-
case "moved":
|
|
4946
|
-
await this.pushMove(change.path);
|
|
4947
|
-
updated++;
|
|
4948
|
-
break;
|
|
4949
|
-
case "deleted":
|
|
4950
|
-
await this.pushDelete(change.path);
|
|
4951
|
-
deleted++;
|
|
4952
|
-
break;
|
|
4953
|
-
}
|
|
4954
|
-
getLogger().info(`[Im-Nobsidian] \uC7AC\uC2DC\uB3C4 \uC131\uACF5: ${change.path}`);
|
|
4955
|
-
} catch (error) {
|
|
4956
|
-
failed.push({
|
|
4957
|
-
path: change.path,
|
|
4958
|
-
operation: target.operation,
|
|
4959
|
-
error: error instanceof Error ? error.message : String(error)
|
|
4960
|
-
});
|
|
4961
|
-
getLogger().warn(`[Im-Nobsidian] \uC7AC\uC2DC\uB3C4 \uC2E4\uD328: ${change.path}`);
|
|
5287
|
+
await applyPushChange(change);
|
|
5288
|
+
} catch {
|
|
5289
|
+
retryQueue.push(change);
|
|
4962
5290
|
}
|
|
4963
|
-
}
|
|
5291
|
+
},
|
|
5292
|
+
{ concurrency: this.config.advanced.concurrency, signal: options?.signal }
|
|
5293
|
+
);
|
|
5294
|
+
if (retryQueue.length > 0) {
|
|
5295
|
+
const retryWaitMs = this.config.advanced.retryWaitMs;
|
|
5296
|
+
getLogger().info(
|
|
5297
|
+
`[Im-Nobsidian] Push ${retryQueue.length}\uAC74 \uC7AC\uC2DC\uB3C4 (${retryWaitMs / 1e3}\uCD08 \uD6C4)`
|
|
5298
|
+
);
|
|
5299
|
+
await new Promise((r) => setTimeout(r, retryWaitMs));
|
|
5300
|
+
await runPool(
|
|
5301
|
+
retryQueue,
|
|
5302
|
+
async (change) => {
|
|
5303
|
+
try {
|
|
5304
|
+
await applyPushChange(change);
|
|
5305
|
+
getLogger().info(`[Im-Nobsidian] \uC7AC\uC2DC\uB3C4 \uC131\uACF5: ${change.path}`);
|
|
5306
|
+
} catch (error) {
|
|
5307
|
+
failed.push({
|
|
5308
|
+
path: change.path,
|
|
5309
|
+
operation: opOf(change),
|
|
5310
|
+
error: error instanceof Error ? error.message : String(error)
|
|
5311
|
+
});
|
|
5312
|
+
getLogger().warn(`[Im-Nobsidian] \uC7AC\uC2DC\uB3C4 \uC2E4\uD328: ${change.path}`);
|
|
5313
|
+
}
|
|
5314
|
+
},
|
|
5315
|
+
{ concurrency: this.config.advanced.concurrency, signal: options?.signal }
|
|
5316
|
+
);
|
|
4964
5317
|
}
|
|
4965
5318
|
if (this.config.sync.syncFiles !== false) {
|
|
4966
5319
|
try {
|
|
@@ -4975,8 +5328,8 @@ var SyncOrchestrator = class {
|
|
|
4975
5328
|
if ((this.config.notion.databases?.length ?? 0) > 0) {
|
|
4976
5329
|
try {
|
|
4977
5330
|
const dbResult = await this.databaseSyncer.pushAll();
|
|
4978
|
-
created += dbResult.created;
|
|
4979
|
-
updated += dbResult.updated;
|
|
5331
|
+
counts.created += dbResult.created;
|
|
5332
|
+
counts.updated += dbResult.updated;
|
|
4980
5333
|
failed.push(...dbResult.failed);
|
|
4981
5334
|
} catch (error) {
|
|
4982
5335
|
getLogger().warn("[Im-Nobsidian] DB Push \uC911 \uC624\uB958:", error);
|
|
@@ -4985,7 +5338,13 @@ var SyncOrchestrator = class {
|
|
|
4985
5338
|
this.stateDb.setMeta("last_push_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
4986
5339
|
this.stateDb.setMeta("last_sync_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
4987
5340
|
this.stateDb.setMeta("push_in_progress", "");
|
|
4988
|
-
return {
|
|
5341
|
+
return {
|
|
5342
|
+
created: counts.created,
|
|
5343
|
+
updated: counts.updated,
|
|
5344
|
+
deleted: counts.deleted,
|
|
5345
|
+
failed,
|
|
5346
|
+
duration: Date.now() - startTime
|
|
5347
|
+
};
|
|
4989
5348
|
}
|
|
4990
5349
|
async pull(options) {
|
|
4991
5350
|
const startTime = Date.now();
|
|
@@ -5005,11 +5364,10 @@ var SyncOrchestrator = class {
|
|
|
5005
5364
|
return emptyResult;
|
|
5006
5365
|
}
|
|
5007
5366
|
this.cleanupInterruptedSync();
|
|
5008
|
-
|
|
5009
|
-
let updated = 0;
|
|
5010
|
-
let deleted = 0;
|
|
5367
|
+
const counts = { created: 0, updated: 0, deleted: 0 };
|
|
5011
5368
|
this._pullImageCount = 0;
|
|
5012
5369
|
this._pullFileCount = 0;
|
|
5370
|
+
this._inlineDbRefs.clear();
|
|
5013
5371
|
const conflicts = [];
|
|
5014
5372
|
const writtenPaths = [];
|
|
5015
5373
|
const failed = [];
|
|
@@ -5021,7 +5379,7 @@ var SyncOrchestrator = class {
|
|
|
5021
5379
|
return record && options.paths.some((p) => record.obsidianPath.startsWith(p));
|
|
5022
5380
|
}) : remoteChanges;
|
|
5023
5381
|
if (filtered.length === 0 && (this.config.notion.databases?.length ?? 0) === 0) {
|
|
5024
|
-
const hasDbResults = await this.pullDiscoveredDatabases(writtenPaths, failed);
|
|
5382
|
+
const hasDbResults = await this.pullDiscoveredDatabases(writtenPaths, failed, conflicts);
|
|
5025
5383
|
if (!hasDbResults.created && !hasDbResults.updated) {
|
|
5026
5384
|
this.stateDb.setMeta("last_pull_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
5027
5385
|
this.stateDb.setMeta("last_sync_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
@@ -5035,7 +5393,7 @@ var SyncOrchestrator = class {
|
|
|
5035
5393
|
created: hasDbResults.created,
|
|
5036
5394
|
updated: hasDbResults.updated,
|
|
5037
5395
|
deleted: 0,
|
|
5038
|
-
conflicts
|
|
5396
|
+
conflicts,
|
|
5039
5397
|
writtenPaths,
|
|
5040
5398
|
failed,
|
|
5041
5399
|
duration: Date.now() - startTime,
|
|
@@ -5073,113 +5431,94 @@ var SyncOrchestrator = class {
|
|
|
5073
5431
|
};
|
|
5074
5432
|
}
|
|
5075
5433
|
this.stateDb.setMeta("pull_in_progress", "true");
|
|
5076
|
-
const sema = new Sema(this.config.advanced.concurrency);
|
|
5077
|
-
let pullCompleted = 0;
|
|
5078
5434
|
const pullTotal = filtered.length;
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5435
|
+
let pullCompleted = 0;
|
|
5436
|
+
const toFailure = (change, error) => {
|
|
5437
|
+
const record = this.stateDb.getByNotionId(change.pageId);
|
|
5438
|
+
return {
|
|
5439
|
+
path: record?.obsidianPath ?? change.pageId,
|
|
5440
|
+
operation: change.type === "created" ? "create" : change.type === "deleted" ? "delete" : "update",
|
|
5441
|
+
error: error instanceof Error ? error.message : String(error)
|
|
5442
|
+
};
|
|
5443
|
+
};
|
|
5444
|
+
const applyChange = async (change) => {
|
|
5445
|
+
switch (change.type) {
|
|
5446
|
+
case "created": {
|
|
5447
|
+
const path = await this.pullCreate(change.pageId);
|
|
5448
|
+
writtenPaths.push(path);
|
|
5449
|
+
counts.created++;
|
|
5450
|
+
return path;
|
|
5451
|
+
}
|
|
5452
|
+
case "modified": {
|
|
5453
|
+
const result = await this.pullUpdate(change);
|
|
5454
|
+
if (result.conflict) {
|
|
5455
|
+
conflicts.push(result.conflict);
|
|
5456
|
+
} else if (result.path) {
|
|
5457
|
+
writtenPaths.push(result.path);
|
|
5458
|
+
counts.updated++;
|
|
5102
5459
|
}
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
break;
|
|
5460
|
+
return result.path;
|
|
5461
|
+
}
|
|
5462
|
+
case "deleted": {
|
|
5463
|
+
const record = this.stateDb.getByNotionId(change.pageId);
|
|
5464
|
+
const path = await this.pullDelete(change.pageId);
|
|
5465
|
+
if (path) {
|
|
5466
|
+
counts.deleted++;
|
|
5467
|
+
return path;
|
|
5112
5468
|
}
|
|
5469
|
+
return record?.obsidianPath;
|
|
5113
5470
|
}
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
const op = change.type === "created" ? "create" : change.type === "deleted" ? "delete" : "update";
|
|
5117
|
-
options?.onProgress?.(++pullCompleted, pullTotal, { path: displayPath, operation: op });
|
|
5118
|
-
} catch (error) {
|
|
5119
|
-
const record = this.stateDb.getByNotionId(change.pageId);
|
|
5120
|
-
failed.push({
|
|
5121
|
-
path: record?.obsidianPath ?? change.pageId,
|
|
5122
|
-
operation: change.type === "created" ? "create" : change.type === "deleted" ? "delete" : "update",
|
|
5123
|
-
error: error instanceof Error ? error.message : String(error)
|
|
5124
|
-
});
|
|
5125
|
-
} finally {
|
|
5126
|
-
sema.release();
|
|
5471
|
+
default:
|
|
5472
|
+
return void 0;
|
|
5127
5473
|
}
|
|
5128
|
-
}
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
for (const target of retryTargets) {
|
|
5135
|
-
const change = filtered.find((c) => {
|
|
5136
|
-
const record = this.stateDb.getByNotionId(c.pageId);
|
|
5137
|
-
return (record?.obsidianPath ?? c.pageId) === target.path;
|
|
5138
|
-
});
|
|
5139
|
-
if (!change) {
|
|
5140
|
-
failed.push(target);
|
|
5141
|
-
continue;
|
|
5142
|
-
}
|
|
5474
|
+
};
|
|
5475
|
+
const retryQueue = [];
|
|
5476
|
+
await runPool(
|
|
5477
|
+
filtered,
|
|
5478
|
+
async (change) => {
|
|
5479
|
+
if (options?.signal?.aborted) return;
|
|
5143
5480
|
try {
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
case "modified": {
|
|
5152
|
-
const result = await this.pullUpdate(change);
|
|
5153
|
-
if (result.conflict) {
|
|
5154
|
-
conflicts.push(result.conflict);
|
|
5155
|
-
} else if (result.path) {
|
|
5156
|
-
writtenPaths.push(result.path);
|
|
5157
|
-
updated++;
|
|
5158
|
-
}
|
|
5159
|
-
break;
|
|
5160
|
-
}
|
|
5161
|
-
case "deleted": {
|
|
5162
|
-
const path = await this.pullDelete(change.pageId);
|
|
5163
|
-
if (path) deleted++;
|
|
5164
|
-
break;
|
|
5165
|
-
}
|
|
5166
|
-
}
|
|
5167
|
-
getLogger().info(`[Im-Nobsidian] \uC7AC\uC2DC\uB3C4 \uC131\uACF5: ${target.path}`);
|
|
5168
|
-
} catch (error) {
|
|
5169
|
-
failed.push({
|
|
5170
|
-
path: target.path,
|
|
5171
|
-
operation: target.operation,
|
|
5172
|
-
error: error instanceof Error ? error.message : String(error)
|
|
5173
|
-
});
|
|
5174
|
-
getLogger().warn(`[Im-Nobsidian] \uC7AC\uC2DC\uB3C4 \uC2E4\uD328: ${target.path}`);
|
|
5481
|
+
const resultPath = await applyChange(change);
|
|
5482
|
+
const record = this.stateDb.getByNotionId(change.pageId);
|
|
5483
|
+
const displayPath = resultPath ?? record?.obsidianPath ?? change.pageId;
|
|
5484
|
+
const op = change.type === "created" ? "create" : change.type === "deleted" ? "delete" : "update";
|
|
5485
|
+
options?.onProgress?.(++pullCompleted, pullTotal, { path: displayPath, operation: op });
|
|
5486
|
+
} catch {
|
|
5487
|
+
retryQueue.push(change);
|
|
5175
5488
|
}
|
|
5176
|
-
}
|
|
5489
|
+
},
|
|
5490
|
+
{ concurrency: this.config.advanced.concurrency, signal: options?.signal }
|
|
5491
|
+
);
|
|
5492
|
+
if (retryQueue.length > 0) {
|
|
5493
|
+
const retryWaitMs = this.config.advanced.retryWaitMs;
|
|
5494
|
+
getLogger().info(
|
|
5495
|
+
`[Im-Nobsidian] Pull ${retryQueue.length}\uAC74 \uC7AC\uC2DC\uB3C4 (${retryWaitMs / 1e3}\uCD08 \uD6C4)`
|
|
5496
|
+
);
|
|
5497
|
+
await new Promise((r) => setTimeout(r, retryWaitMs));
|
|
5498
|
+
await runPool(
|
|
5499
|
+
retryQueue,
|
|
5500
|
+
async (change) => {
|
|
5501
|
+
try {
|
|
5502
|
+
await applyChange(change);
|
|
5503
|
+
const record = this.stateDb.getByNotionId(change.pageId);
|
|
5504
|
+
getLogger().info(
|
|
5505
|
+
`[Im-Nobsidian] \uC7AC\uC2DC\uB3C4 \uC131\uACF5: ${record?.obsidianPath ?? change.pageId}`
|
|
5506
|
+
);
|
|
5507
|
+
} catch (error) {
|
|
5508
|
+
const failure = toFailure(change, error);
|
|
5509
|
+
failed.push(failure);
|
|
5510
|
+
getLogger().warn(`[Im-Nobsidian] \uC7AC\uC2DC\uB3C4 \uC2E4\uD328: ${failure.path}`);
|
|
5511
|
+
}
|
|
5512
|
+
},
|
|
5513
|
+
{ concurrency: this.config.advanced.concurrency, signal: options?.signal }
|
|
5514
|
+
);
|
|
5177
5515
|
}
|
|
5178
5516
|
if ((this.config.notion.databases?.length ?? 0) > 0) {
|
|
5179
5517
|
try {
|
|
5180
5518
|
const dbResult = await this.databaseSyncer.pullAll();
|
|
5181
|
-
created += dbResult.created;
|
|
5182
|
-
updated += dbResult.updated;
|
|
5519
|
+
counts.created += dbResult.created;
|
|
5520
|
+
counts.updated += dbResult.updated;
|
|
5521
|
+
conflicts.push(...dbResult.conflicts);
|
|
5183
5522
|
failed.push(...dbResult.failed);
|
|
5184
5523
|
if (dbResult.created + dbResult.updated > 0) {
|
|
5185
5524
|
const dbPaths = this.stateDb.getByStatus("synced").filter((r) => r.fileType === "db-row").map((r) => r.obsidianPath);
|
|
@@ -5190,9 +5529,9 @@ var SyncOrchestrator = class {
|
|
|
5190
5529
|
}
|
|
5191
5530
|
}
|
|
5192
5531
|
{
|
|
5193
|
-
const dbDiscovery = await this.pullDiscoveredDatabases(writtenPaths, failed);
|
|
5194
|
-
created += dbDiscovery.created;
|
|
5195
|
-
updated += dbDiscovery.updated;
|
|
5532
|
+
const dbDiscovery = await this.pullDiscoveredDatabases(writtenPaths, failed, conflicts);
|
|
5533
|
+
counts.created += dbDiscovery.created;
|
|
5534
|
+
counts.updated += dbDiscovery.updated;
|
|
5196
5535
|
}
|
|
5197
5536
|
const linkTargetPaths = writtenPaths.length > 0 ? writtenPaths : [];
|
|
5198
5537
|
const linkCount = linkTargetPaths.length > 0 ? await this.resolveNotionLinks(linkTargetPaths) : 0;
|
|
@@ -5200,9 +5539,9 @@ var SyncOrchestrator = class {
|
|
|
5200
5539
|
this.stateDb.setMeta("last_sync_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
5201
5540
|
this.stateDb.setMeta("pull_in_progress", "");
|
|
5202
5541
|
return {
|
|
5203
|
-
created,
|
|
5204
|
-
updated,
|
|
5205
|
-
deleted,
|
|
5542
|
+
created: counts.created,
|
|
5543
|
+
updated: counts.updated,
|
|
5544
|
+
deleted: counts.deleted,
|
|
5206
5545
|
conflicts,
|
|
5207
5546
|
writtenPaths,
|
|
5208
5547
|
failed,
|
|
@@ -5334,7 +5673,48 @@ var SyncOrchestrator = class {
|
|
|
5334
5673
|
}
|
|
5335
5674
|
return allDbs;
|
|
5336
5675
|
}
|
|
5337
|
-
|
|
5676
|
+
/**
|
|
5677
|
+
* 자동 발견된 child_database 1개를 DatabaseSyncer 설정으로 변환한다.
|
|
5678
|
+
* 블록 스캔/markdown 양쪽 발견 경로에서 공통으로 사용한다.
|
|
5679
|
+
* - localFolder: 부모 페이지의 obsidianPath 기준으로 폴더를 잡아 원본 중첩 구조를 보존
|
|
5680
|
+
* (예: 부모 "AI Engineer (1).md" + DB "Minirecord Project" → "AI Engineer (1)/Minirecord-Project")
|
|
5681
|
+
* - 제목 취득 실패 시 부모 폴더명 기반 안전 이름으로 폴백
|
|
5682
|
+
*/
|
|
5683
|
+
async buildDiscoveredDbConfig(dbId, parentPageId) {
|
|
5684
|
+
try {
|
|
5685
|
+
const dbTitle = await this.notionClient.getDatabaseTitle(dbId);
|
|
5686
|
+
const parentEntry = parentPageId ? this.stateDb.getByNotionId(parentPageId) : null;
|
|
5687
|
+
let parentFolder = "";
|
|
5688
|
+
if (parentEntry?.obsidianPath) {
|
|
5689
|
+
const obsPath = parentEntry.obsidianPath;
|
|
5690
|
+
if (obsPath.endsWith(".md")) {
|
|
5691
|
+
const parts = obsPath.split("/");
|
|
5692
|
+
parts.pop();
|
|
5693
|
+
parentFolder = parts.length > 0 ? parts.join("/") : obsPath.replace(/\.md$/, "");
|
|
5694
|
+
} else {
|
|
5695
|
+
parentFolder = obsPath;
|
|
5696
|
+
}
|
|
5697
|
+
}
|
|
5698
|
+
let safeName;
|
|
5699
|
+
if (dbTitle) {
|
|
5700
|
+
safeName = dbTitle.replace(/[^a-zA-Z0-9가-힣\s_-]/g, "").replace(/\s+/g, "-");
|
|
5701
|
+
} else {
|
|
5702
|
+
const parentName = parentFolder.split("/").pop() || "";
|
|
5703
|
+
safeName = parentName ? `${parentName}-DB` : `db-${dbId.replace(/-/g, "").slice(0, 8)}`;
|
|
5704
|
+
}
|
|
5705
|
+
if (!safeName) safeName = `db-${dbId.replace(/-/g, "").slice(0, 8)}`;
|
|
5706
|
+
if (!parentFolder) parentFolder = "databases";
|
|
5707
|
+
return {
|
|
5708
|
+
databaseId: dbId,
|
|
5709
|
+
localFolder: `${parentFolder}/${safeName}`,
|
|
5710
|
+
titleProperty: "Name"
|
|
5711
|
+
};
|
|
5712
|
+
} catch (error) {
|
|
5713
|
+
getLogger().warn(`[Im-Nobsidian] \uC790\uB3D9 \uBC1C\uACAC DB ${dbId} \uC124\uC815 \uC0DD\uC131 \uC2E4\uD328:`, error);
|
|
5714
|
+
return null;
|
|
5715
|
+
}
|
|
5716
|
+
}
|
|
5717
|
+
async pullDiscoveredDatabases(writtenPaths, failed, conflicts) {
|
|
5338
5718
|
if (this.isDatabaseMode) return { created: 0, updated: 0 };
|
|
5339
5719
|
let created = 0;
|
|
5340
5720
|
let updated = 0;
|
|
@@ -5348,53 +5728,45 @@ var SyncOrchestrator = class {
|
|
|
5348
5728
|
dbConfigs = [];
|
|
5349
5729
|
}
|
|
5350
5730
|
}
|
|
5731
|
+
const configuredIds = new Set(
|
|
5732
|
+
(this.config.notion.databases ?? []).map((d) => d.databaseId.replace(/-/g, ""))
|
|
5733
|
+
);
|
|
5734
|
+
const knownIds = /* @__PURE__ */ new Set([
|
|
5735
|
+
...configuredIds,
|
|
5736
|
+
...dbConfigs.map((c) => c.databaseId.replace(/-/g, ""))
|
|
5737
|
+
]);
|
|
5738
|
+
let changed = false;
|
|
5351
5739
|
if (dbConfigs.length === 0) {
|
|
5352
|
-
const
|
|
5353
|
-
const
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
let parentFolder = "";
|
|
5362
|
-
if (parentEntry?.obsidianPath) {
|
|
5363
|
-
const obsPath = parentEntry.obsidianPath;
|
|
5364
|
-
if (obsPath.endsWith(".md")) {
|
|
5365
|
-
const parts = obsPath.split("/");
|
|
5366
|
-
parts.pop();
|
|
5367
|
-
parentFolder = parts.length > 0 ? parts.join("/") : obsPath.replace(/\.md$/, "");
|
|
5368
|
-
} else {
|
|
5369
|
-
parentFolder = obsPath;
|
|
5370
|
-
}
|
|
5371
|
-
}
|
|
5372
|
-
let safeName;
|
|
5373
|
-
if (dbTitle) {
|
|
5374
|
-
safeName = dbTitle.replace(/[^a-zA-Z0-9가-힣\s_-]/g, "").replace(/\s+/g, "-");
|
|
5375
|
-
} else {
|
|
5376
|
-
const parentName = parentFolder.split("/").pop() || "";
|
|
5377
|
-
safeName = parentName ? `${parentName}-DB` : `db-${dbId.slice(0, 8)}`;
|
|
5378
|
-
}
|
|
5379
|
-
if (!parentFolder) parentFolder = "databases";
|
|
5380
|
-
dbConfigs.push({
|
|
5381
|
-
databaseId: dbId,
|
|
5382
|
-
localFolder: `${parentFolder}/${safeName}`,
|
|
5383
|
-
titleProperty: "Name"
|
|
5384
|
-
});
|
|
5385
|
-
} catch (error) {
|
|
5386
|
-
getLogger().warn(`[Im-Nobsidian] \uC790\uB3D9 \uBC1C\uACAC DB ${dbId} \uB3D9\uAE30\uD654 \uC2E4\uD328:`, error);
|
|
5740
|
+
const discovered = await this.discoverChildDatabases();
|
|
5741
|
+
for (const { dbId, parentPageId } of discovered) {
|
|
5742
|
+
const nohyph = dbId.replace(/-/g, "");
|
|
5743
|
+
if (knownIds.has(nohyph)) continue;
|
|
5744
|
+
const cfg = await this.buildDiscoveredDbConfig(dbId, parentPageId);
|
|
5745
|
+
if (cfg) {
|
|
5746
|
+
dbConfigs.push(cfg);
|
|
5747
|
+
knownIds.add(nohyph);
|
|
5748
|
+
changed = true;
|
|
5387
5749
|
}
|
|
5388
5750
|
}
|
|
5389
|
-
|
|
5390
|
-
|
|
5751
|
+
}
|
|
5752
|
+
for (const [nohyph, parentPageId] of this._inlineDbRefs) {
|
|
5753
|
+
if (knownIds.has(nohyph)) continue;
|
|
5754
|
+
const cfg = await this.buildDiscoveredDbConfig(normalizeNotionId(nohyph), parentPageId);
|
|
5755
|
+
if (cfg) {
|
|
5756
|
+
dbConfigs.push(cfg);
|
|
5757
|
+
knownIds.add(nohyph);
|
|
5758
|
+
changed = true;
|
|
5391
5759
|
}
|
|
5392
5760
|
}
|
|
5761
|
+
if (changed && dbConfigs.length > 0) {
|
|
5762
|
+
this.stateDb.setMeta("discovered_dbs", JSON.stringify(dbConfigs));
|
|
5763
|
+
}
|
|
5393
5764
|
for (const dbConfig of dbConfigs) {
|
|
5394
5765
|
try {
|
|
5395
5766
|
const dbResult = await this.databaseSyncer.pullDatabase(dbConfig);
|
|
5396
5767
|
created += dbResult.created;
|
|
5397
5768
|
updated += dbResult.updated;
|
|
5769
|
+
conflicts.push(...dbResult.conflicts);
|
|
5398
5770
|
failed.push(...dbResult.failed);
|
|
5399
5771
|
if (dbResult.created + dbResult.updated > 0) {
|
|
5400
5772
|
const dbPaths = this.stateDb.getByStatus("synced").filter((r) => r.fileType === "db-row").map((r) => r.obsidianPath);
|
|
@@ -5438,7 +5810,7 @@ var SyncOrchestrator = class {
|
|
|
5438
5810
|
});
|
|
5439
5811
|
content = resolved;
|
|
5440
5812
|
const resolved2 = content.replace(
|
|
5441
|
-
/\[([^\]]+)\]\(\/([a-f0-9]{32})
|
|
5813
|
+
/\[([^\]]+)\]\(\/(?:p\/)?([a-f0-9]{32})(?:\?[^)]*)?\)/g,
|
|
5442
5814
|
(_match, text, id) => {
|
|
5443
5815
|
const title = idToTitle.get(id);
|
|
5444
5816
|
if (title) {
|
|
@@ -5465,6 +5837,11 @@ var SyncOrchestrator = class {
|
|
|
5465
5837
|
this.dbSchemaLoaded = true;
|
|
5466
5838
|
}
|
|
5467
5839
|
async pushCreate(path) {
|
|
5840
|
+
const existingRecord = this.stateDb.getByPath(path);
|
|
5841
|
+
if (existingRecord?.notionPageId) {
|
|
5842
|
+
await this.pushUpdate(path);
|
|
5843
|
+
return;
|
|
5844
|
+
}
|
|
5468
5845
|
const content = await this.vaultFs.readFile(path);
|
|
5469
5846
|
const title = extractTitle(path);
|
|
5470
5847
|
const parentId = await this.resolveNotionParent(path);
|
|
@@ -5499,6 +5876,20 @@ var SyncOrchestrator = class {
|
|
|
5499
5876
|
conversionResult.content,
|
|
5500
5877
|
effectiveProperties
|
|
5501
5878
|
);
|
|
5879
|
+
this.stateDb.upsert({
|
|
5880
|
+
obsidianPath: path,
|
|
5881
|
+
notionPageId: page.id,
|
|
5882
|
+
notionParentId: parentId,
|
|
5883
|
+
contentHash: "",
|
|
5884
|
+
notionLastEdited: page.last_edited_time,
|
|
5885
|
+
localLastModified: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5886
|
+
syncDirection: "both",
|
|
5887
|
+
fileType: this.isFolderNote(path) ? "folder-note" : "file",
|
|
5888
|
+
status: "pending",
|
|
5889
|
+
baseSnapshot: null,
|
|
5890
|
+
localMtime: null,
|
|
5891
|
+
localFileSize: null
|
|
5892
|
+
});
|
|
5502
5893
|
await this.imageHandler.uploadAndAppendImages(page.id, conversionResult.images);
|
|
5503
5894
|
const hash = computeHash(content);
|
|
5504
5895
|
const fileStat = await this.vaultFs.getFileStat(path);
|
|
@@ -5596,7 +5987,10 @@ var SyncOrchestrator = class {
|
|
|
5596
5987
|
throw error;
|
|
5597
5988
|
}
|
|
5598
5989
|
}
|
|
5599
|
-
this.stateDb.
|
|
5990
|
+
this.stateDb.transaction(() => {
|
|
5991
|
+
this.stateDb.delete(record.id);
|
|
5992
|
+
this.stateDb.deleteWikilink(record.obsidianPath);
|
|
5993
|
+
});
|
|
5600
5994
|
} else {
|
|
5601
5995
|
this.stateDb.updateStatus(record.id, "pending");
|
|
5602
5996
|
}
|
|
@@ -5639,7 +6033,9 @@ var SyncOrchestrator = class {
|
|
|
5639
6033
|
} while (cursor);
|
|
5640
6034
|
remotePages = allPages;
|
|
5641
6035
|
} else {
|
|
5642
|
-
|
|
6036
|
+
const allAccessible = await this.notionClient.searchAllPages();
|
|
6037
|
+
const underRoot = await this.filterPagesUnderRoot(allAccessible);
|
|
6038
|
+
remotePages = underRoot.map((p) => ({ id: p.id, last_edited_time: p.last_edited_time }));
|
|
5643
6039
|
}
|
|
5644
6040
|
for (const page of remotePages) {
|
|
5645
6041
|
const record = this.stateDb.getByNotionId(page.id);
|
|
@@ -5711,12 +6107,84 @@ var SyncOrchestrator = class {
|
|
|
5711
6107
|
if (notionIdsEqual(parentId, this.config.notion.rootPageId)) return true;
|
|
5712
6108
|
return !!this.stateDb.getByNotionId(parentId);
|
|
5713
6109
|
}
|
|
6110
|
+
/**
|
|
6111
|
+
* search API로 받은 전체 접근 가능 페이지 중 rootPageId 하위(자손)만 남긴다.
|
|
6112
|
+
* 각 페이지의 parent 체인을 root에 닿을 때까지 거슬러 올라가며 판정한다.
|
|
6113
|
+
* - 부모가 이미 받은 페이지면 추가 API 호출 없이 메모리에서 해석(공통 경로)
|
|
6114
|
+
* - 부모가 검색 결과에 없으면(데이터베이스·미공유 조상 등) getPage로 1회 조회 후 캐시
|
|
6115
|
+
* - 조회 불가/순환/깊이 초과 시 보수적으로 root 하위가 아님으로 판정
|
|
6116
|
+
* 판정 결과는 체인 전체에 메모이즈해 형제 페이지 처리 시 재사용한다.
|
|
6117
|
+
*/
|
|
6118
|
+
async filterPagesUnderRoot(allPages) {
|
|
6119
|
+
const root = normalizeNotionId(this.config.notion.rootPageId);
|
|
6120
|
+
const byId = /* @__PURE__ */ new Map();
|
|
6121
|
+
for (const p of allPages) byId.set(normalizeNotionId(p.id), p);
|
|
6122
|
+
const verdict = /* @__PURE__ */ new Map();
|
|
6123
|
+
const isUnderRoot = async (start) => {
|
|
6124
|
+
const chain = [];
|
|
6125
|
+
let current = start;
|
|
6126
|
+
let result = false;
|
|
6127
|
+
while (current) {
|
|
6128
|
+
const id = normalizeNotionId(current.id);
|
|
6129
|
+
if (id === root) {
|
|
6130
|
+
result = true;
|
|
6131
|
+
break;
|
|
6132
|
+
}
|
|
6133
|
+
const memo = verdict.get(id);
|
|
6134
|
+
if (memo !== void 0) {
|
|
6135
|
+
result = memo;
|
|
6136
|
+
break;
|
|
6137
|
+
}
|
|
6138
|
+
if (chain.includes(id)) {
|
|
6139
|
+
result = false;
|
|
6140
|
+
break;
|
|
6141
|
+
}
|
|
6142
|
+
chain.push(id);
|
|
6143
|
+
const parentId = await this.extractParentId(current);
|
|
6144
|
+
if (!parentId) {
|
|
6145
|
+
result = false;
|
|
6146
|
+
break;
|
|
6147
|
+
}
|
|
6148
|
+
const pid = normalizeNotionId(parentId);
|
|
6149
|
+
if (pid === root) {
|
|
6150
|
+
result = true;
|
|
6151
|
+
break;
|
|
6152
|
+
}
|
|
6153
|
+
const memoParent = verdict.get(pid);
|
|
6154
|
+
if (memoParent !== void 0) {
|
|
6155
|
+
result = memoParent;
|
|
6156
|
+
break;
|
|
6157
|
+
}
|
|
6158
|
+
let parentPage = byId.get(pid) ?? null;
|
|
6159
|
+
if (!parentPage) {
|
|
6160
|
+
try {
|
|
6161
|
+
parentPage = await this.notionClient.getPage(parentId);
|
|
6162
|
+
byId.set(pid, parentPage);
|
|
6163
|
+
} catch {
|
|
6164
|
+
result = false;
|
|
6165
|
+
break;
|
|
6166
|
+
}
|
|
6167
|
+
}
|
|
6168
|
+
current = parentPage;
|
|
6169
|
+
}
|
|
6170
|
+
for (const id of chain) verdict.set(id, result);
|
|
6171
|
+
return result;
|
|
6172
|
+
};
|
|
6173
|
+
const out = [];
|
|
6174
|
+
for (const p of allPages) {
|
|
6175
|
+
if (normalizeNotionId(p.id) === root) continue;
|
|
6176
|
+
if (await isUnderRoot(p)) out.push(p);
|
|
6177
|
+
}
|
|
6178
|
+
return out;
|
|
6179
|
+
}
|
|
5714
6180
|
async pullCreate(pageId) {
|
|
5715
6181
|
const page = await this.notionClient.getPage(pageId);
|
|
5716
6182
|
const title = this.notionClient.extractTitle(page);
|
|
5717
6183
|
const safeName = sanitizeFileName(title);
|
|
5718
6184
|
const parentPath = await this.resolveParentPath(page);
|
|
5719
|
-
const firstChildren = await this.notionClient.listChildren(pageId, {
|
|
6185
|
+
const firstChildren = await this.notionClient.listChildren(pageId, {
|
|
6186
|
+
pageSize: this.config.advanced.pageSize
|
|
6187
|
+
});
|
|
5720
6188
|
const hasChildPages = firstChildren.results.some((b) => "type" in b && b.type === "child_page");
|
|
5721
6189
|
const markdown = await this.fetchPageMarkdown(pageId);
|
|
5722
6190
|
const hasContent = markdown.trim().length > 0;
|
|
@@ -5836,46 +6304,45 @@ var SyncOrchestrator = class {
|
|
|
5836
6304
|
} catch {
|
|
5837
6305
|
localContent = "";
|
|
5838
6306
|
}
|
|
5839
|
-
const
|
|
5840
|
-
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
|
|
5846
|
-
|
|
5847
|
-
|
|
5848
|
-
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
|
|
5852
|
-
previousHash: record.contentHash
|
|
5853
|
-
},
|
|
5854
|
-
remoteChange: change,
|
|
5855
|
-
baseContent: record.baseSnapshot?.toString("utf-8") ?? null,
|
|
5856
|
-
localContent,
|
|
5857
|
-
remoteContent
|
|
5858
|
-
};
|
|
5859
|
-
this.stateDb.updateStatus(record.id, "conflict");
|
|
5860
|
-
return { conflict };
|
|
5861
|
-
}
|
|
6307
|
+
const resolution = resolvePullConflict({
|
|
6308
|
+
record,
|
|
6309
|
+
localContent,
|
|
6310
|
+
remoteContent,
|
|
6311
|
+
remoteChange: change,
|
|
6312
|
+
strategy: this.config.sync.conflictStrategy
|
|
6313
|
+
});
|
|
6314
|
+
if (resolution.action === "skip") {
|
|
6315
|
+
return { path: record.obsidianPath };
|
|
6316
|
+
}
|
|
6317
|
+
if (resolution.action === "conflict") {
|
|
6318
|
+
this.stateDb.updateStatus(record.id, "conflict");
|
|
6319
|
+
return { conflict: resolution.conflict };
|
|
5862
6320
|
}
|
|
5863
6321
|
await this.vaultFs.writeFile(record.obsidianPath, remoteContent);
|
|
5864
6322
|
const updateStat = await this.vaultFs.getFileStat(record.obsidianPath);
|
|
5865
6323
|
const newHash = computeHash(remoteContent);
|
|
5866
|
-
this.stateDb.
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
6324
|
+
this.stateDb.transaction(() => {
|
|
6325
|
+
this.stateDb.upsert({
|
|
6326
|
+
obsidianPath: record.obsidianPath,
|
|
6327
|
+
notionPageId: change.pageId,
|
|
6328
|
+
notionParentId: record.notionParentId,
|
|
6329
|
+
contentHash: newHash,
|
|
6330
|
+
notionLastEdited: page.last_edited_time,
|
|
6331
|
+
localLastModified: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6332
|
+
syncDirection: record.syncDirection,
|
|
6333
|
+
fileType: record.fileType,
|
|
6334
|
+
status: "synced",
|
|
6335
|
+
baseSnapshot: Buffer.from(remoteContent, "utf-8"),
|
|
6336
|
+
localMtime: updateStat?.mtime ?? null,
|
|
6337
|
+
localFileSize: updateStat?.size ?? null
|
|
6338
|
+
});
|
|
6339
|
+
const aliases = extractAliases2(properties);
|
|
6340
|
+
this.stateDb.upsertWikilink({
|
|
6341
|
+
obsidianPath: record.obsidianPath,
|
|
6342
|
+
notionPageId: change.pageId,
|
|
6343
|
+
title,
|
|
6344
|
+
aliases
|
|
6345
|
+
});
|
|
5879
6346
|
});
|
|
5880
6347
|
return { path: record.obsidianPath };
|
|
5881
6348
|
}
|
|
@@ -5888,7 +6355,10 @@ var SyncOrchestrator = class {
|
|
|
5888
6355
|
} catch {
|
|
5889
6356
|
}
|
|
5890
6357
|
}
|
|
5891
|
-
this.stateDb.
|
|
6358
|
+
this.stateDb.transaction(() => {
|
|
6359
|
+
this.stateDb.delete(record.id);
|
|
6360
|
+
this.stateDb.deleteWikilink(record.obsidianPath);
|
|
6361
|
+
});
|
|
5892
6362
|
return record.obsidianPath;
|
|
5893
6363
|
}
|
|
5894
6364
|
async ensureFolderPage(folderPath) {
|
|
@@ -5896,7 +6366,12 @@ var SyncOrchestrator = class {
|
|
|
5896
6366
|
const folderNoteRecord = this.stateDb.getByPath(folderNotePath);
|
|
5897
6367
|
if (folderNoteRecord?.notionPageId) {
|
|
5898
6368
|
const existing2 = this.stateDb.getByPath(folderPath);
|
|
5899
|
-
if (existing2)
|
|
6369
|
+
if (existing2) {
|
|
6370
|
+
this.stateDb.transaction(() => {
|
|
6371
|
+
this.stateDb.delete(existing2.id);
|
|
6372
|
+
this.stateDb.deleteWikilink(existing2.obsidianPath);
|
|
6373
|
+
});
|
|
6374
|
+
}
|
|
5900
6375
|
return;
|
|
5901
6376
|
}
|
|
5902
6377
|
const existing = this.stateDb.getByPath(folderPath);
|
|
@@ -5955,7 +6430,10 @@ var SyncOrchestrator = class {
|
|
|
5955
6430
|
const folderNotePath = `${folder.obsidianPath}/${folderName}.md`;
|
|
5956
6431
|
const noteRecord = this.stateDb.getByPath(folderNotePath);
|
|
5957
6432
|
if (noteRecord?.notionPageId) {
|
|
5958
|
-
this.stateDb.
|
|
6433
|
+
this.stateDb.transaction(() => {
|
|
6434
|
+
this.stateDb.delete(folder.id);
|
|
6435
|
+
this.stateDb.deleteWikilink(folder.obsidianPath);
|
|
6436
|
+
});
|
|
5959
6437
|
}
|
|
5960
6438
|
}
|
|
5961
6439
|
}
|
|
@@ -6041,12 +6519,38 @@ var SyncOrchestrator = class {
|
|
|
6041
6519
|
if (this.config.conversion.preferMarkdownApi !== false) {
|
|
6042
6520
|
try {
|
|
6043
6521
|
const result = await this.notionClient.getPageMarkdown(pageId);
|
|
6044
|
-
|
|
6522
|
+
this.collectInlineDbRefs(pageId, result.markdown);
|
|
6523
|
+
return this.resolveNotionIdWikilinks(notionEnhancedToObsidian(result.markdown));
|
|
6045
6524
|
} catch {
|
|
6046
6525
|
}
|
|
6047
6526
|
}
|
|
6048
6527
|
return this.blockConverter.notionBlocksToMarkdown(pageId);
|
|
6049
6528
|
}
|
|
6529
|
+
// Markdown API는 인라인 데이터베이스를 다음처럼 렌더한다(컬럼/synced_block 내부 포함):
|
|
6530
|
+
// <database url="https://www.notion.so/<id-nohyph>" inline="true"
|
|
6531
|
+
// data-source-url="collection://<ds-id>">Title</database>
|
|
6532
|
+
// url 안의 32자리 hex 가 databaseId 이므로, 별도 블록 트리 재귀 없이 이 태그만 파싱해
|
|
6533
|
+
// 깊이 중첩된 child_database 까지 폴더+.base 동기화 대상으로 등록한다.
|
|
6534
|
+
collectInlineDbRefs(parentPageId, rawMarkdown) {
|
|
6535
|
+
const re = /<database\b[^>]*\burl="https:\/\/www\.notion\.so\/([a-f0-9]{32})"[^>]*>/g;
|
|
6536
|
+
let m;
|
|
6537
|
+
while ((m = re.exec(rawMarkdown)) !== null) {
|
|
6538
|
+
const dbId = m[1];
|
|
6539
|
+
if (dbId) this._inlineDbRefs.set(dbId, parentPageId);
|
|
6540
|
+
}
|
|
6541
|
+
}
|
|
6542
|
+
// pull 시 url 기반 page mention 은 `[[notion:<id>]]` 로 1차 변환된다(notionEnhancedToObsidian).
|
|
6543
|
+
// 이를 state DB 역조회로 원래 `[[제목]]` 위키링크로 복원해 push↔pull 라운드트립을 수렴시킨다.
|
|
6544
|
+
// 볼트 밖/미추적 페이지면 `[[notion:<id>]]` 를 그대로 두어 정보 손실을 막는다.
|
|
6545
|
+
resolveNotionIdWikilinks(markdown) {
|
|
6546
|
+
return markdown.replace(/\[\[notion:([a-f0-9]{32})\]\]/g, (match, id) => {
|
|
6547
|
+
const record = this.stateDb.getByNotionId(normalizeNotionId(id));
|
|
6548
|
+
if (!record?.obsidianPath) return match;
|
|
6549
|
+
const base = record.obsidianPath.split("/").pop() ?? record.obsidianPath;
|
|
6550
|
+
const title = base.replace(/\.md$/, "");
|
|
6551
|
+
return `[[${title}]]`;
|
|
6552
|
+
});
|
|
6553
|
+
}
|
|
6050
6554
|
async extractParentId(page) {
|
|
6051
6555
|
const parent = page.parent;
|
|
6052
6556
|
if (parent.type === "page_id") return parent.page_id ?? null;
|
|
@@ -6123,7 +6627,7 @@ var NodeVaultFS = class {
|
|
|
6123
6627
|
excludePatterns;
|
|
6124
6628
|
loadImNobsidianIgnore() {
|
|
6125
6629
|
try {
|
|
6126
|
-
const ignorePath = join(this.rootPath,
|
|
6630
|
+
const ignorePath = join(this.rootPath, IGNORE_FILE);
|
|
6127
6631
|
const content = readFileSync(ignorePath, "utf-8");
|
|
6128
6632
|
return content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
6129
6633
|
} catch {
|
|
@@ -6276,8 +6780,8 @@ var NodeVaultFS = class {
|
|
|
6276
6780
|
};
|
|
6277
6781
|
|
|
6278
6782
|
// src/converter/pre-processors/inline-db.ts
|
|
6279
|
-
var INLINE_DB_START =
|
|
6280
|
-
var INLINE_DB_END =
|
|
6783
|
+
var INLINE_DB_START = new RegExp(`%% ${MARKER_BRAND_RE}:inline-db:([^\\s]+) %%`, "g");
|
|
6784
|
+
var INLINE_DB_END = new RegExp(`%% ${MARKER_BRAND_RE}:end %%`, "g");
|
|
6281
6785
|
var InlineDBParser = class {
|
|
6282
6786
|
name = "InlineDBParser";
|
|
6283
6787
|
order = 40;
|
|
@@ -6327,82 +6831,126 @@ function threeWayMerge(base, local, remote) {
|
|
|
6327
6831
|
const baseLines = base.split("\n");
|
|
6328
6832
|
const localLines = local.split("\n");
|
|
6329
6833
|
const remoteLines = remote.split("\n");
|
|
6330
|
-
const
|
|
6331
|
-
|
|
6834
|
+
const hunks = [
|
|
6835
|
+
...computeHunks(baseLines, localLines, 0),
|
|
6836
|
+
...computeHunks(baseLines, remoteLines, 2)
|
|
6837
|
+
];
|
|
6838
|
+
hunks.sort((a, b) => a.baseStart - b.baseStart || a.side - b.side);
|
|
6332
6839
|
const merged = [];
|
|
6333
6840
|
const conflicts = [];
|
|
6334
|
-
let
|
|
6335
|
-
let
|
|
6336
|
-
while (
|
|
6337
|
-
const
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
merged.push(...remoteChange.newLines);
|
|
6363
|
-
baseIdx = remoteChange.baseEnd;
|
|
6364
|
-
remoteDiff.splice(remoteDiff.indexOf(remoteChange), 1);
|
|
6841
|
+
let cursor = 0;
|
|
6842
|
+
let i = 0;
|
|
6843
|
+
while (i < hunks.length) {
|
|
6844
|
+
const first = hunks[i];
|
|
6845
|
+
if (first.baseStart > cursor) {
|
|
6846
|
+
merged.push(...baseLines.slice(cursor, first.baseStart));
|
|
6847
|
+
}
|
|
6848
|
+
const regionStart = first.baseStart;
|
|
6849
|
+
let regionEnd = first.baseEnd;
|
|
6850
|
+
const group = [first];
|
|
6851
|
+
i++;
|
|
6852
|
+
while (i < hunks.length) {
|
|
6853
|
+
const next = hunks[i];
|
|
6854
|
+
const overlaps = next.baseStart < regionEnd || regionEnd === regionStart && next.baseStart === regionStart;
|
|
6855
|
+
if (!overlaps) break;
|
|
6856
|
+
regionEnd = Math.max(regionEnd, next.baseEnd);
|
|
6857
|
+
group.push(next);
|
|
6858
|
+
i++;
|
|
6859
|
+
}
|
|
6860
|
+
const baseRegion = baseLines.slice(regionStart, regionEnd);
|
|
6861
|
+
const localRegion = applyHunks(baseLines, group, 0, regionStart, regionEnd);
|
|
6862
|
+
const remoteRegion = applyHunks(baseLines, group, 2, regionStart, regionEnd);
|
|
6863
|
+
if (arraysEqual(localRegion, baseRegion)) {
|
|
6864
|
+
merged.push(...remoteRegion);
|
|
6865
|
+
} else if (arraysEqual(remoteRegion, baseRegion)) {
|
|
6866
|
+
merged.push(...localRegion);
|
|
6867
|
+
} else if (arraysEqual(localRegion, remoteRegion)) {
|
|
6868
|
+
merged.push(...localRegion);
|
|
6365
6869
|
} else {
|
|
6366
|
-
|
|
6367
|
-
|
|
6368
|
-
|
|
6369
|
-
|
|
6870
|
+
conflicts.push({ localLines: localRegion, remoteLines: remoteRegion });
|
|
6871
|
+
merged.push("<<<<<<< LOCAL");
|
|
6872
|
+
merged.push(...localRegion);
|
|
6873
|
+
merged.push("=======");
|
|
6874
|
+
merged.push(...remoteRegion);
|
|
6875
|
+
merged.push(">>>>>>> REMOTE");
|
|
6370
6876
|
}
|
|
6877
|
+
cursor = regionEnd;
|
|
6878
|
+
}
|
|
6879
|
+
if (cursor < baseLines.length) {
|
|
6880
|
+
merged.push(...baseLines.slice(cursor));
|
|
6371
6881
|
}
|
|
6372
6882
|
return {
|
|
6373
|
-
success,
|
|
6883
|
+
success: conflicts.length === 0,
|
|
6374
6884
|
merged: merged.join("\n"),
|
|
6375
6885
|
conflicts
|
|
6376
6886
|
};
|
|
6377
6887
|
}
|
|
6378
|
-
function
|
|
6888
|
+
function computeHunks(base, modified, side) {
|
|
6889
|
+
const matches = lcsMatches(base, modified);
|
|
6379
6890
|
const hunks = [];
|
|
6380
|
-
let
|
|
6381
|
-
let
|
|
6382
|
-
|
|
6383
|
-
|
|
6384
|
-
|
|
6385
|
-
|
|
6386
|
-
|
|
6387
|
-
|
|
6388
|
-
|
|
6389
|
-
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
}
|
|
6393
|
-
baseIdx++;
|
|
6394
|
-
hunks.push({ baseStart: hunkStart, baseEnd: baseIdx, newLines });
|
|
6891
|
+
let prevBase = -1;
|
|
6892
|
+
let prevMod = -1;
|
|
6893
|
+
const emitGap = (curBase, curMod) => {
|
|
6894
|
+
const baseLen = curBase - (prevBase + 1);
|
|
6895
|
+
const modLen = curMod - (prevMod + 1);
|
|
6896
|
+
if (baseLen > 0 || modLen > 0) {
|
|
6897
|
+
hunks.push({
|
|
6898
|
+
baseStart: prevBase + 1,
|
|
6899
|
+
baseEnd: curBase,
|
|
6900
|
+
content: modified.slice(prevMod + 1, curMod),
|
|
6901
|
+
side
|
|
6902
|
+
});
|
|
6395
6903
|
}
|
|
6904
|
+
};
|
|
6905
|
+
for (const [b, m] of matches) {
|
|
6906
|
+
emitGap(b, m);
|
|
6907
|
+
prevBase = b;
|
|
6908
|
+
prevMod = m;
|
|
6396
6909
|
}
|
|
6397
|
-
|
|
6398
|
-
hunks.push({
|
|
6399
|
-
baseStart: baseIdx,
|
|
6400
|
-
baseEnd: base.length,
|
|
6401
|
-
newLines: modified.slice(modIdx)
|
|
6402
|
-
});
|
|
6403
|
-
}
|
|
6910
|
+
emitGap(base.length, modified.length);
|
|
6404
6911
|
return hunks;
|
|
6405
6912
|
}
|
|
6913
|
+
function applyHunks(baseLines, group, side, regionStart, regionEnd) {
|
|
6914
|
+
const out = [];
|
|
6915
|
+
let b = regionStart;
|
|
6916
|
+
for (const hunk of group) {
|
|
6917
|
+
if (hunk.side !== side) continue;
|
|
6918
|
+
if (hunk.baseStart > b) out.push(...baseLines.slice(b, hunk.baseStart));
|
|
6919
|
+
out.push(...hunk.content);
|
|
6920
|
+
b = hunk.baseEnd;
|
|
6921
|
+
}
|
|
6922
|
+
if (b < regionEnd) out.push(...baseLines.slice(b, regionEnd));
|
|
6923
|
+
return out;
|
|
6924
|
+
}
|
|
6925
|
+
function lcsMatches(a, b) {
|
|
6926
|
+
const n = a.length;
|
|
6927
|
+
const m = b.length;
|
|
6928
|
+
const dp = new Array(n + 1);
|
|
6929
|
+
for (let i2 = 0; i2 <= n; i2++) dp[i2] = new Uint32Array(m + 1);
|
|
6930
|
+
for (let i2 = n - 1; i2 >= 0; i2--) {
|
|
6931
|
+
const row = dp[i2];
|
|
6932
|
+
const next = dp[i2 + 1];
|
|
6933
|
+
const ai = a[i2];
|
|
6934
|
+
for (let j2 = m - 1; j2 >= 0; j2--) {
|
|
6935
|
+
row[j2] = ai === b[j2] ? next[j2 + 1] + 1 : Math.max(next[j2], row[j2 + 1]);
|
|
6936
|
+
}
|
|
6937
|
+
}
|
|
6938
|
+
const matches = [];
|
|
6939
|
+
let i = 0;
|
|
6940
|
+
let j = 0;
|
|
6941
|
+
while (i < n && j < m) {
|
|
6942
|
+
if (a[i] === b[j]) {
|
|
6943
|
+
matches.push([i, j]);
|
|
6944
|
+
i++;
|
|
6945
|
+
j++;
|
|
6946
|
+
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
6947
|
+
i++;
|
|
6948
|
+
} else {
|
|
6949
|
+
j++;
|
|
6950
|
+
}
|
|
6951
|
+
}
|
|
6952
|
+
return matches;
|
|
6953
|
+
}
|
|
6406
6954
|
function arraysEqual(a, b) {
|
|
6407
6955
|
if (a.length !== b.length) return false;
|
|
6408
6956
|
return a.every((val, idx) => val === b[idx]);
|
|
@@ -6621,7 +7169,12 @@ var FileWatcher = class {
|
|
|
6621
7169
|
watcher = null;
|
|
6622
7170
|
start() {
|
|
6623
7171
|
this.watcher = watch(this.rootPath, {
|
|
6624
|
-
|
|
7172
|
+
// 이벤트 경로를 rootPath 기준 "볼트 상대경로"로 방출한다. 동기화 엔진의 변경
|
|
7173
|
+
// 감지·paths 필터는 모두 볼트 상대경로(listMarkdownFileStats 기준)를 쓰므로,
|
|
7174
|
+
// 절대경로를 그대로 넘기면 push 의 startsWith 스코프 필터에서 누락돼 변경 파일이
|
|
7175
|
+
// 실제로 동기화되지 않는다(증분 watch 의 핵심 버그).
|
|
7176
|
+
cwd: this.rootPath,
|
|
7177
|
+
ignored: [/(^|[/\\])\./, "**/node_modules/**", INTERNAL_DIR_GLOB],
|
|
6625
7178
|
persistent: true,
|
|
6626
7179
|
ignoreInitial: true,
|
|
6627
7180
|
awaitWriteFinish: {
|
|
@@ -6629,9 +7182,13 @@ var FileWatcher = class {
|
|
|
6629
7182
|
pollInterval: 100
|
|
6630
7183
|
}
|
|
6631
7184
|
});
|
|
6632
|
-
this.watcher.on("add", (path) => this.callback("add", path));
|
|
6633
|
-
this.watcher.on("change", (path) => this.callback("change", path));
|
|
6634
|
-
this.watcher.on("unlink", (path) => this.callback("unlink", path));
|
|
7185
|
+
this.watcher.on("add", (path) => this.callback("add", this.toVaultPath(path)));
|
|
7186
|
+
this.watcher.on("change", (path) => this.callback("change", this.toVaultPath(path)));
|
|
7187
|
+
this.watcher.on("unlink", (path) => this.callback("unlink", this.toVaultPath(path)));
|
|
7188
|
+
}
|
|
7189
|
+
// 볼트 경로는 항상 "/" 구분자를 사용한다. Windows 백슬래시를 정규화.
|
|
7190
|
+
toVaultPath(path) {
|
|
7191
|
+
return sep === "/" ? path : path.split(sep).join("/");
|
|
6635
7192
|
}
|
|
6636
7193
|
async stop() {
|
|
6637
7194
|
if (this.watcher) {
|
|
@@ -6711,16 +7268,19 @@ var WatchSyncService = class {
|
|
|
6711
7268
|
this.syncQueued = true;
|
|
6712
7269
|
return;
|
|
6713
7270
|
}
|
|
7271
|
+
this.clearDebounce();
|
|
7272
|
+
if (this.pendingChanges.size === 0) return;
|
|
6714
7273
|
this.syncing = true;
|
|
6715
|
-
const
|
|
7274
|
+
const batch = new Map(this.pendingChanges);
|
|
6716
7275
|
this.pendingChanges.clear();
|
|
6717
7276
|
try {
|
|
6718
7277
|
this.options.onSyncStart?.();
|
|
6719
|
-
const result = await this.orchestrator.sync({
|
|
6720
|
-
paths: changedPaths.length > 0 ? changedPaths : void 0
|
|
6721
|
-
});
|
|
7278
|
+
const result = await this.orchestrator.sync({ paths: [...batch.keys()] });
|
|
6722
7279
|
this.options.onSyncComplete?.(result);
|
|
6723
7280
|
} catch (error) {
|
|
7281
|
+
for (const [path, event] of batch) {
|
|
7282
|
+
if (!this.pendingChanges.has(path)) this.pendingChanges.set(path, event);
|
|
7283
|
+
}
|
|
6724
7284
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
6725
7285
|
this.options.onSyncError?.(err);
|
|
6726
7286
|
} finally {
|
|
@@ -6728,6 +7288,8 @@ var WatchSyncService = class {
|
|
|
6728
7288
|
if (this.syncQueued) {
|
|
6729
7289
|
this.syncQueued = false;
|
|
6730
7290
|
void this.executeSync();
|
|
7291
|
+
} else if (this.pendingChanges.size > 0) {
|
|
7292
|
+
this.scheduleSync();
|
|
6731
7293
|
}
|
|
6732
7294
|
}
|
|
6733
7295
|
}
|
|
@@ -6898,7 +7460,6 @@ function parseDate(value) {
|
|
|
6898
7460
|
const match = /^(\d{4}-\d{2}-\d{2})/.exec(value);
|
|
6899
7461
|
return match?.[1] ?? null;
|
|
6900
7462
|
}
|
|
6901
|
-
var DB_VIEWS_PATH = ".im-nobsidian/db-views.json";
|
|
6902
7463
|
var ViewDataProvider = class {
|
|
6903
7464
|
constructor(vaultFs) {
|
|
6904
7465
|
this.vaultFs = vaultFs;
|
|
@@ -7105,6 +7666,6 @@ ${lines.join("\n")}
|
|
|
7105
7666
|
}`;
|
|
7106
7667
|
}
|
|
7107
7668
|
|
|
7108
|
-
export { BaseFileGenerator, BlockConverter, CalloutRestorer, CalloutTransformer, ChangeDetector, ColorAnnotator, ConfigManager, ConfigSchema, ConflictResolver, ConversionPipeline, DEFAULT_CONFIG, DatabaseSyncer, EmbedResolver, EntryEditor, FileHandler, FileWatcher, FrontmatterExtractor, FrontmatterGenerator, HtmlAnnotationStripper, ImageHandler, InlineDBParser, LocalImageRestorer, MathNormalizer, MentionToWikilink, NodeVaultFS, NotionBlockBuilder, NotionClient, PreserveMarkerCollector, PreserveMarkerInjector, PropertiesTableInjector, PropertiesTableRestorer, PropertyMapper, StateDB, SyncOrchestrator, TreeMapper, UnsupportedBlockStripper, ViewDataProvider, WatchSyncService, WikilinkResolver, computeBufferHash, computeHash, createDefaultPipeline, extractCalendarEntries, filterByMonth, filterEntries, generateColorCSS, generateId, getLogger, getNotionBgColor, getNotionColor, getVisibleProperties, groupEntries, normalizeNotionId, notionIdsEqual, sanitizeFileName, setLogger, sortEntries, threeWayMerge };
|
|
7669
|
+
export { BaseFileGenerator, BlockConverter, CONFIG_FILE, CalloutRestorer, CalloutTransformer, ChangeDetector, ColorAnnotator, ConfigManager, ConfigSchema, ConflictResolver, ConversionPipeline, DB_VIEWS_FILE, DB_VIEWS_PATH, DEFAULT_CONFIG, DatabaseSyncer, EmbedResolver, EntryEditor, FileHandler, FileWatcher, FrontmatterExtractor, FrontmatterGenerator, GITIGNORE_ENTRY, HtmlAnnotationStripper, IGNORE_FILE, INTERNAL_DIR, INTERNAL_DIR_GLOB, ImageHandler, InlineDBParser, LocalImageRestorer, MARKER_BRAND, MathNormalizer, MentionToWikilink, NodeVaultFS, NotionBlockBuilder, NotionClient, PreserveMarkerCollector, PreserveMarkerInjector, PropertiesTableInjector, PropertiesTableRestorer, PropertyMapper, STATE_DB_FILE, STATE_DB_PATH, StateDB, SyncOrchestrator, TreeMapper, UnsupportedBlockStripper, ViewDataProvider, WatchSyncService, WikilinkResolver, computeBufferHash, computeHash, createDefaultPipeline, extractCalendarEntries, filterByMonth, filterEntries, generateColorCSS, generateId, getLogger, getNotionBgColor, getNotionColor, getVisibleProperties, groupEntries, isInternalPath, normalizeNotionId, notionIdsEqual, sanitizeFileName, setLogger, sortEntries, threeWayMerge };
|
|
7109
7670
|
//# sourceMappingURL=index.js.map
|
|
7110
7671
|
//# sourceMappingURL=index.js.map
|