@im-nobsidian/core 0.1.10 → 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 +1054 -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() {
|
|
@@ -2849,6 +3000,7 @@ ${COLUMN_LIST_END}`;
|
|
|
2849
3000
|
let processed = this.extractToggleBlocks(markdown);
|
|
2850
3001
|
processed = this.extractColumnBlocks(processed);
|
|
2851
3002
|
processed = this.replaceDividers(processed);
|
|
3003
|
+
processed = processed.replace(/<unknown[^>]*\/>/g, "");
|
|
2852
3004
|
return processed;
|
|
2853
3005
|
}
|
|
2854
3006
|
replaceDividers(markdown) {
|
|
@@ -3179,26 +3331,34 @@ var EXTENSION_TO_MIME = {
|
|
|
3179
3331
|
".tiff": "image/tiff"
|
|
3180
3332
|
};
|
|
3181
3333
|
var ImageHandler = class {
|
|
3182
|
-
constructor(vaultFs, attachmentFolder = "attachments", notionClient, customFetch) {
|
|
3334
|
+
constructor(vaultFs, attachmentFolder = "attachments", notionClient, customFetch, options) {
|
|
3183
3335
|
this.vaultFs = vaultFs;
|
|
3184
3336
|
this.attachmentFolder = attachmentFolder;
|
|
3185
3337
|
this.notionClient = notionClient;
|
|
3186
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;
|
|
3187
3343
|
}
|
|
3188
3344
|
vaultFs;
|
|
3189
3345
|
attachmentFolder;
|
|
3190
3346
|
notionClient;
|
|
3191
3347
|
customFetch;
|
|
3348
|
+
concurrency;
|
|
3349
|
+
maxRetries;
|
|
3350
|
+
retryBaseMs;
|
|
3351
|
+
maxFileSizeBytes;
|
|
3192
3352
|
get fetchFn() {
|
|
3193
3353
|
return this.customFetch ?? globalThis.fetch;
|
|
3194
3354
|
}
|
|
3195
3355
|
async downloadImage(url, pageTitle) {
|
|
3196
|
-
const maxRetries =
|
|
3356
|
+
const maxRetries = this.maxRetries;
|
|
3197
3357
|
let lastError = null;
|
|
3198
3358
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
3199
3359
|
try {
|
|
3200
3360
|
if (attempt > 0) {
|
|
3201
|
-
await new Promise((r) => setTimeout(r,
|
|
3361
|
+
await new Promise((r) => setTimeout(r, this.retryBaseMs * attempt));
|
|
3202
3362
|
getLogger().info(
|
|
3203
3363
|
`[Im-Nobsidian] \uC774\uBBF8\uC9C0 \uB2E4\uC6B4\uB85C\uB4DC \uC7AC\uC2DC\uB3C4 (${attempt + 1}/${maxRetries}): ${pageTitle}`
|
|
3204
3364
|
);
|
|
@@ -3232,7 +3392,7 @@ var ImageHandler = class {
|
|
|
3232
3392
|
if (notionMatches.length === 0 && internalMatches.length === 0) {
|
|
3233
3393
|
return { content: markdown, downloads: [] };
|
|
3234
3394
|
}
|
|
3235
|
-
const sema = new Sema(
|
|
3395
|
+
const sema = new Sema(this.concurrency);
|
|
3236
3396
|
const downloads = [];
|
|
3237
3397
|
let result = markdown;
|
|
3238
3398
|
const httpResults = await Promise.allSettled(
|
|
@@ -3341,7 +3501,7 @@ var ImageHandler = class {
|
|
|
3341
3501
|
if (totalMatches === 0) {
|
|
3342
3502
|
return { content: markdown, downloads: [] };
|
|
3343
3503
|
}
|
|
3344
|
-
const sema = new Sema(
|
|
3504
|
+
const sema = new Sema(this.concurrency);
|
|
3345
3505
|
const downloads = [];
|
|
3346
3506
|
let result = markdown;
|
|
3347
3507
|
for (const match of notionHttpMatches) {
|
|
@@ -3436,12 +3596,12 @@ var ImageHandler = class {
|
|
|
3436
3596
|
}
|
|
3437
3597
|
}
|
|
3438
3598
|
async downloadFile(url, _pageTitle, caption) {
|
|
3439
|
-
const maxRetries =
|
|
3599
|
+
const maxRetries = this.maxRetries;
|
|
3440
3600
|
let lastError = null;
|
|
3441
3601
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
3442
3602
|
try {
|
|
3443
3603
|
if (attempt > 0) {
|
|
3444
|
-
await new Promise((r) => setTimeout(r,
|
|
3604
|
+
await new Promise((r) => setTimeout(r, this.retryBaseMs * attempt));
|
|
3445
3605
|
getLogger().info(
|
|
3446
3606
|
`[Im-Nobsidian] \uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uC7AC\uC2DC\uB3C4 (${attempt + 1}/${maxRetries}): ${caption}`
|
|
3447
3607
|
);
|
|
@@ -3451,7 +3611,7 @@ var ImageHandler = class {
|
|
|
3451
3611
|
throw new Error(`\uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328: ${response.status} ${response.statusText}`);
|
|
3452
3612
|
}
|
|
3453
3613
|
const contentLength = response.headers.get("content-length");
|
|
3454
|
-
const MAX_FILE_SIZE =
|
|
3614
|
+
const MAX_FILE_SIZE = this.maxFileSizeBytes;
|
|
3455
3615
|
if (contentLength && parseInt(contentLength, 10) > MAX_FILE_SIZE) {
|
|
3456
3616
|
const sizeMB = Math.round(parseInt(contentLength, 10) / 1024 / 1024);
|
|
3457
3617
|
getLogger().debug(`[Im-Nobsidian] \uD30C\uC77C \uC2A4\uD0B5 \u2014 \uB108\uBB34 \uD07C (${sizeMB}MB): ${caption}`);
|
|
@@ -3600,15 +3760,16 @@ function getFolderPath(filePath) {
|
|
|
3600
3760
|
return parts.length > 1 ? parts.slice(0, -1).join("/") : "";
|
|
3601
3761
|
}
|
|
3602
3762
|
var FileHandler = class {
|
|
3603
|
-
constructor(vaultFs, notionClient, stateDb) {
|
|
3763
|
+
constructor(vaultFs, notionClient, stateDb, concurrency = 2) {
|
|
3604
3764
|
this.vaultFs = vaultFs;
|
|
3605
3765
|
this.notionClient = notionClient;
|
|
3606
3766
|
this.stateDb = stateDb;
|
|
3767
|
+
this.sema = new Sema(concurrency);
|
|
3607
3768
|
}
|
|
3608
3769
|
vaultFs;
|
|
3609
3770
|
notionClient;
|
|
3610
3771
|
stateDb;
|
|
3611
|
-
sema
|
|
3772
|
+
sema;
|
|
3612
3773
|
async pushFilesForFolder(folderPageId, folderPath) {
|
|
3613
3774
|
const allFiles = await this.vaultFs.listNonMarkdownFiles();
|
|
3614
3775
|
const folderFiles = allFiles.filter((f) => getFolderPath(f.path) === folderPath);
|
|
@@ -3763,6 +3924,43 @@ var FileHandler = class {
|
|
|
3763
3924
|
}
|
|
3764
3925
|
};
|
|
3765
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
|
+
|
|
3766
3964
|
// src/utils/sanitize.ts
|
|
3767
3965
|
var INVALID_CHARS = /[<>:"/\\|?*\x00-\x1f]/g;
|
|
3768
3966
|
var TRAILING_DOTS = /\.+$/;
|
|
@@ -3824,12 +4022,13 @@ var BaseFileGenerator = class {
|
|
|
3824
4022
|
this.writeFormulas(lines, options.viewsConfig.views);
|
|
3825
4023
|
this.writePropertyDisplayNames(lines, options.schema);
|
|
3826
4024
|
this.writeViews(lines, options.viewsConfig.views, options.schema);
|
|
3827
|
-
return lines.join("\n");
|
|
4025
|
+
return lines.join("\n") + "\n";
|
|
3828
4026
|
}
|
|
3829
4027
|
writeFilters(lines, folderPath) {
|
|
3830
4028
|
lines.push("filters:");
|
|
3831
4029
|
lines.push(" and:");
|
|
3832
4030
|
lines.push(` - file.inFolder("${folderPath}")`);
|
|
4031
|
+
lines.push(` - file.ext == "md"`);
|
|
3833
4032
|
lines.push("");
|
|
3834
4033
|
}
|
|
3835
4034
|
needsContentCoverFormula(views) {
|
|
@@ -3868,25 +4067,25 @@ var BaseFileGenerator = class {
|
|
|
3868
4067
|
if (view.order && view.order.length > 0) {
|
|
3869
4068
|
lines.push(" order:");
|
|
3870
4069
|
for (const col of view.order) {
|
|
3871
|
-
lines.push(` - ${col}`);
|
|
4070
|
+
lines.push(` - ${this.propRef(col)}`);
|
|
3872
4071
|
}
|
|
3873
4072
|
}
|
|
3874
4073
|
if (view.sort && view.sort.length > 0) {
|
|
3875
4074
|
lines.push(" sort:");
|
|
3876
4075
|
for (const s of view.sort) {
|
|
3877
|
-
lines.push(` -
|
|
4076
|
+
lines.push(` - property: ${this.propRef(s.property)}`);
|
|
3878
4077
|
lines.push(` direction: ${s.direction}`);
|
|
3879
4078
|
}
|
|
3880
4079
|
}
|
|
3881
4080
|
if (view.groupBy) {
|
|
3882
4081
|
lines.push(" groupBy:");
|
|
3883
|
-
lines.push(` property: ${view.groupBy.property}`);
|
|
4082
|
+
lines.push(` property: ${this.propRef(view.groupBy.property)}`);
|
|
3884
4083
|
if (view.groupBy.direction) {
|
|
3885
4084
|
lines.push(` direction: ${view.groupBy.direction}`);
|
|
3886
4085
|
}
|
|
3887
4086
|
}
|
|
3888
4087
|
if (view.image) {
|
|
3889
|
-
lines.push(` image: ${view.image}`);
|
|
4088
|
+
lines.push(` image: ${this.propRef(view.image)}`);
|
|
3890
4089
|
}
|
|
3891
4090
|
}
|
|
3892
4091
|
}
|
|
@@ -3900,7 +4099,9 @@ var BaseFileGenerator = class {
|
|
|
3900
4099
|
if (view.properties && view.properties.length > 0) {
|
|
3901
4100
|
const visible = view.properties.filter((p) => p.visible !== false).map((p) => {
|
|
3902
4101
|
const name = p.propertyName ?? this.resolvePropertyName(p.propertyId, schema);
|
|
3903
|
-
|
|
4102
|
+
if (!name) return null;
|
|
4103
|
+
if (schema[name]?.type === "title") return null;
|
|
4104
|
+
return this.toNoteRef(name);
|
|
3904
4105
|
}).filter((name) => !!name);
|
|
3905
4106
|
if (visible.length > 0) {
|
|
3906
4107
|
result.order = ["file.name", ...visible];
|
|
@@ -3908,17 +4109,17 @@ var BaseFileGenerator = class {
|
|
|
3908
4109
|
}
|
|
3909
4110
|
if (view.sorts && view.sorts.length > 0) {
|
|
3910
4111
|
result.sort = view.sorts.map((s) => {
|
|
3911
|
-
let
|
|
4112
|
+
let property;
|
|
3912
4113
|
if (s.property) {
|
|
3913
4114
|
const propName = this.resolvePropertyName(s.property, schema) ?? s.property;
|
|
3914
|
-
|
|
4115
|
+
property = this.toNoteRef(propName);
|
|
3915
4116
|
} else if (s.timestamp) {
|
|
3916
|
-
|
|
4117
|
+
property = s.timestamp === "created_time" ? "file.ctime" : "file.mtime";
|
|
3917
4118
|
} else {
|
|
3918
4119
|
return null;
|
|
3919
4120
|
}
|
|
3920
4121
|
return {
|
|
3921
|
-
|
|
4122
|
+
property,
|
|
3922
4123
|
direction: s.direction === "ascending" ? "ASC" : "DESC"
|
|
3923
4124
|
};
|
|
3924
4125
|
}).filter((s) => s !== null);
|
|
@@ -3947,12 +4148,29 @@ var BaseFileGenerator = class {
|
|
|
3947
4148
|
toNoteRef(propertyName) {
|
|
3948
4149
|
return propertyName;
|
|
3949
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
|
+
}
|
|
3950
4160
|
resolvePropertyName(propertyId, schema) {
|
|
4161
|
+
const target = this.decodeId(propertyId);
|
|
3951
4162
|
for (const [name, prop] of Object.entries(schema)) {
|
|
3952
|
-
if (prop.id ===
|
|
4163
|
+
if (this.decodeId(prop.id) === target) return name;
|
|
3953
4164
|
}
|
|
3954
4165
|
return void 0;
|
|
3955
4166
|
}
|
|
4167
|
+
decodeId(id) {
|
|
4168
|
+
try {
|
|
4169
|
+
return decodeURIComponent(id);
|
|
4170
|
+
} catch {
|
|
4171
|
+
return id;
|
|
4172
|
+
}
|
|
4173
|
+
}
|
|
3956
4174
|
yamlKey(key) {
|
|
3957
4175
|
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) return key;
|
|
3958
4176
|
return `"${key.replace(/"/g, '\\"')}"`;
|
|
@@ -3971,14 +4189,13 @@ var NOTION_PAGE_MENTION_RE = /<mention-page id="([^"]+)">([\s\S]*?)<\/mention-pa
|
|
|
3971
4189
|
var NOTION_USER_MENTION_RE = /<mention-user id="[^"]*">([^<]*)<\/mention-user>/g;
|
|
3972
4190
|
var NOTION_DATE_MENTION_RE = /<mention-date start="([^"]*)"(?: end="([^"]*)")?[^>]*\/>/g;
|
|
3973
4191
|
var NOTION_UNKNOWN_RE = /<unknown id="([^"]*)"([^>]*)\/>/g;
|
|
4192
|
+
var NOTION_UNKNOWN_URL_RE = /<unknown url="([^"]*)"([^>]*)\/>/g;
|
|
3974
4193
|
var NOTION_AUDIO_RE = /[\t ]*<audio src="([^"]*)">([\s\S]*?)<\/audio>/g;
|
|
3975
4194
|
var NOTION_VIDEO_RE = /[\t ]*<video src="([^"]*)">([\s\S]*?)<\/video>/g;
|
|
3976
4195
|
var NOTION_PDF_RE = /[\t ]*<pdf src="([^"]*)">([\s\S]*?)<\/pdf>/g;
|
|
3977
4196
|
var NOTION_FILE_RE = /[\t ]*<file src="([^"]*)">([\s\S]*?)<\/file>/g;
|
|
3978
4197
|
var NOTION_TAB_RE = /<tab title="([^"]*)">([\s\S]*?)<\/tab>/g;
|
|
3979
4198
|
var NOTION_UNDERLINE_RE = /<span underline="true">([\s\S]*?)<\/span>/g;
|
|
3980
|
-
var TOGGLE_START2 = "%%im-nobsidian:toggle:start%%";
|
|
3981
|
-
var TOGGLE_END2 = "%%im-nobsidian:toggle:end%%";
|
|
3982
4199
|
function notionEnhancedToObsidian(enhanced) {
|
|
3983
4200
|
let result = enhanced;
|
|
3984
4201
|
result = convertSyncedBlockRef(result);
|
|
@@ -4002,9 +4219,13 @@ function notionEnhancedToObsidian(enhanced) {
|
|
|
4002
4219
|
result = removeEmptyBlocks(result);
|
|
4003
4220
|
result = unescapePipes(result);
|
|
4004
4221
|
result = unescapeNotionChars(result);
|
|
4222
|
+
result = unescapeWikilinkBrackets(result);
|
|
4005
4223
|
result = ensureCalloutContinuity(result);
|
|
4006
4224
|
return result;
|
|
4007
4225
|
}
|
|
4226
|
+
function unescapeWikilinkBrackets(content) {
|
|
4227
|
+
return content.replace(/\\\[\\\[([\s\S]*?)\\\]\\\]/g, "[[$1]]");
|
|
4228
|
+
}
|
|
4008
4229
|
function obsidianToNotionEnhanced(obsidian) {
|
|
4009
4230
|
let result = obsidian;
|
|
4010
4231
|
result = convertTogglesToHtml(result);
|
|
@@ -4014,8 +4235,31 @@ function obsidianToNotionEnhanced(obsidian) {
|
|
|
4014
4235
|
result = restoreUnknownBlocks(result);
|
|
4015
4236
|
result = restoreColorSpans(result);
|
|
4016
4237
|
result = restoreUnderlineSpans(result);
|
|
4238
|
+
result = convertMentionPageIdToUrl(result);
|
|
4239
|
+
result = restoreWikilinkPreserveLinks(result);
|
|
4017
4240
|
return result;
|
|
4018
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
|
+
}
|
|
4019
4263
|
function normalizeCodeBlockToggles(content) {
|
|
4020
4264
|
return content.replace(
|
|
4021
4265
|
/^- (.+)\n\t```(\w*)\n([\s\S]*?)\n\t```$/gm,
|
|
@@ -4082,12 +4326,20 @@ function convertDateMentions(content) {
|
|
|
4082
4326
|
});
|
|
4083
4327
|
}
|
|
4084
4328
|
function preserveUnknownBlocks(content) {
|
|
4085
|
-
|
|
4329
|
+
let result = content.replace(NOTION_UNKNOWN_RE, (_match, id, attrs) => {
|
|
4086
4330
|
const typeMatch = /type="([^"]*)"/.exec(attrs);
|
|
4087
4331
|
const altMatch = /alt="([^"]*)"/.exec(attrs);
|
|
4088
4332
|
const blockType = altMatch?.[1] ?? typeMatch?.[1] ?? "unknown";
|
|
4089
|
-
return
|
|
4333
|
+
return compactMarker(`unknown:id=${id}&type=${blockType}`);
|
|
4090
4334
|
});
|
|
4335
|
+
result = result.replace(NOTION_UNKNOWN_URL_RE, (_match, url, attrs) => {
|
|
4336
|
+
const altMatch = /alt="([^"]*)"/.exec(attrs);
|
|
4337
|
+
const blockType = altMatch?.[1] ?? "bookmark";
|
|
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}`;
|
|
4341
|
+
});
|
|
4342
|
+
return result;
|
|
4091
4343
|
}
|
|
4092
4344
|
function convertSyncedBlockRef(content) {
|
|
4093
4345
|
let result = content.replace(
|
|
@@ -4133,7 +4385,7 @@ ${indented}`;
|
|
|
4133
4385
|
}
|
|
4134
4386
|
function convertUnderlineSpans(content) {
|
|
4135
4387
|
return content.replace(NOTION_UNDERLINE_RE, (_match, text) => {
|
|
4136
|
-
return
|
|
4388
|
+
return `${compactMarker("underline")}${text}%%/underline%%`;
|
|
4137
4389
|
});
|
|
4138
4390
|
}
|
|
4139
4391
|
function convertTogglesToHtml(content) {
|
|
@@ -4153,14 +4405,14 @@ ${bodyText}
|
|
|
4153
4405
|
</details>`;
|
|
4154
4406
|
});
|
|
4155
4407
|
}
|
|
4156
|
-
const startRe = new RegExp(escapeRegex3(
|
|
4157
|
-
const endRe = new RegExp(escapeRegex3(
|
|
4408
|
+
const startRe = new RegExp(escapeRegex3(TOGGLE_START), "g");
|
|
4409
|
+
const endRe = new RegExp(escapeRegex3(TOGGLE_END), "g");
|
|
4158
4410
|
safety = 0;
|
|
4159
|
-
while (result.includes(
|
|
4160
|
-
const startIdx = result.indexOf(
|
|
4161
|
-
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);
|
|
4162
4414
|
if (endIdx === -1) break;
|
|
4163
|
-
const inner = result.slice(startIdx +
|
|
4415
|
+
const inner = result.slice(startIdx + TOGGLE_START.length, endIdx).trim();
|
|
4164
4416
|
const titleMatch = /^- (.+)$/m.exec(inner);
|
|
4165
4417
|
const title = titleMatch ? titleMatch[1] : "";
|
|
4166
4418
|
const body = inner.split("\n").slice(1).map((line) => line.replace(/^ {2}/, "")).join("\n").trim();
|
|
@@ -4170,7 +4422,7 @@ ${bodyText}
|
|
|
4170
4422
|
${body}
|
|
4171
4423
|
|
|
4172
4424
|
</details>`;
|
|
4173
|
-
result = result.slice(0, startIdx) + html + result.slice(endIdx +
|
|
4425
|
+
result = result.slice(0, startIdx) + html + result.slice(endIdx + TOGGLE_END.length);
|
|
4174
4426
|
}
|
|
4175
4427
|
result = result.replace(startRe, "").replace(endRe, "");
|
|
4176
4428
|
return result;
|
|
@@ -4179,16 +4431,16 @@ function findMatchingEnd(content, startFrom) {
|
|
|
4179
4431
|
let depth = 1;
|
|
4180
4432
|
let pos = startFrom;
|
|
4181
4433
|
while (pos < content.length && depth > 0) {
|
|
4182
|
-
const nextStart = content.indexOf(
|
|
4183
|
-
const nextEnd = content.indexOf(
|
|
4434
|
+
const nextStart = content.indexOf(TOGGLE_START, pos);
|
|
4435
|
+
const nextEnd = content.indexOf(TOGGLE_END, pos);
|
|
4184
4436
|
if (nextEnd === -1) return -1;
|
|
4185
4437
|
if (nextStart !== -1 && nextStart < nextEnd) {
|
|
4186
4438
|
depth++;
|
|
4187
|
-
pos = nextStart +
|
|
4439
|
+
pos = nextStart + TOGGLE_START.length;
|
|
4188
4440
|
} else {
|
|
4189
4441
|
depth--;
|
|
4190
4442
|
if (depth === 0) return nextEnd;
|
|
4191
|
-
pos = nextEnd +
|
|
4443
|
+
pos = nextEnd + TOGGLE_END.length;
|
|
4192
4444
|
}
|
|
4193
4445
|
}
|
|
4194
4446
|
return -1;
|
|
@@ -4261,7 +4513,7 @@ function convertPageLinks(content) {
|
|
|
4261
4513
|
function convertColorSpans(content) {
|
|
4262
4514
|
return content.replace(
|
|
4263
4515
|
NOTION_COLOR_SPAN_RE,
|
|
4264
|
-
(_match, color, text) =>
|
|
4516
|
+
(_match, color, text) => `${compactMarker(`color:${color}`)}${text}%%/color%%`
|
|
4265
4517
|
);
|
|
4266
4518
|
}
|
|
4267
4519
|
function convertDatabaseBlocks(content) {
|
|
@@ -4362,19 +4614,32 @@ function restoreTabBlocks(content) {
|
|
|
4362
4614
|
return `<tab title="${title}">${unquoted}</tab>`;
|
|
4363
4615
|
});
|
|
4364
4616
|
}
|
|
4365
|
-
var OBSIDIAN_UNKNOWN_RE =
|
|
4617
|
+
var OBSIDIAN_UNKNOWN_RE = new RegExp(
|
|
4618
|
+
`(?:\\[[^\\]]*\\]\\([^)]*\\))?%%${MARKER_BRAND_RE}:unknown:id=([^&]+)&type=([^%]+)%%`,
|
|
4619
|
+
"g"
|
|
4620
|
+
);
|
|
4366
4621
|
function restoreUnknownBlocks(content) {
|
|
4367
4622
|
return content.replace(OBSIDIAN_UNKNOWN_RE, (_match, id, type) => {
|
|
4623
|
+
const decoded = decodeURIComponent(id);
|
|
4624
|
+
if (decoded.startsWith("http")) {
|
|
4625
|
+
return `<unknown url="${decoded}" alt="${type}"/>`;
|
|
4626
|
+
}
|
|
4368
4627
|
return `<unknown id="${id}" type="${type}"/>`;
|
|
4369
4628
|
});
|
|
4370
4629
|
}
|
|
4371
|
-
var OBSIDIAN_COLOR_RE =
|
|
4630
|
+
var OBSIDIAN_COLOR_RE = new RegExp(
|
|
4631
|
+
`%%${MARKER_BRAND_RE}:color:([^%]+)%%([\\s\\S]*?)%%\\/color%%`,
|
|
4632
|
+
"g"
|
|
4633
|
+
);
|
|
4372
4634
|
function restoreColorSpans(content) {
|
|
4373
4635
|
return content.replace(OBSIDIAN_COLOR_RE, (_match, color, text) => {
|
|
4374
4636
|
return `<span color="${color}">${text}</span>`;
|
|
4375
4637
|
});
|
|
4376
4638
|
}
|
|
4377
|
-
var OBSIDIAN_UNDERLINE_RE =
|
|
4639
|
+
var OBSIDIAN_UNDERLINE_RE = new RegExp(
|
|
4640
|
+
`%%${MARKER_BRAND_RE}:underline%%([\\s\\S]*?)%%\\/underline%%`,
|
|
4641
|
+
"g"
|
|
4642
|
+
);
|
|
4378
4643
|
function restoreUnderlineSpans(content) {
|
|
4379
4644
|
return content.replace(OBSIDIAN_UNDERLINE_RE, (_match, text) => {
|
|
4380
4645
|
return `<span underline="true">${text}</span>`;
|
|
@@ -4440,16 +4705,18 @@ var DatabaseSyncer = class {
|
|
|
4440
4705
|
async pullAll() {
|
|
4441
4706
|
const databases = this.config.notion.databases;
|
|
4442
4707
|
if (!databases || databases.length === 0) {
|
|
4443
|
-
return { created: 0, updated: 0, failed: [] };
|
|
4708
|
+
return { created: 0, updated: 0, conflicts: [], failed: [] };
|
|
4444
4709
|
}
|
|
4445
4710
|
let created = 0;
|
|
4446
4711
|
let updated = 0;
|
|
4712
|
+
const conflicts = [];
|
|
4447
4713
|
const failed = [];
|
|
4448
4714
|
for (const dbConfig of databases) {
|
|
4449
4715
|
try {
|
|
4450
4716
|
const result = await this.pullDatabase(dbConfig);
|
|
4451
4717
|
created += result.created;
|
|
4452
4718
|
updated += result.updated;
|
|
4719
|
+
conflicts.push(...result.conflicts);
|
|
4453
4720
|
failed.push(...result.failed);
|
|
4454
4721
|
} catch (error) {
|
|
4455
4722
|
getLogger().warn(`[DB Sync] DB ${dbConfig.databaseId} pull \uC2E4\uD328:`, error);
|
|
@@ -4460,12 +4727,12 @@ var DatabaseSyncer = class {
|
|
|
4460
4727
|
});
|
|
4461
4728
|
}
|
|
4462
4729
|
}
|
|
4463
|
-
return { created, updated, failed };
|
|
4730
|
+
return { created, updated, conflicts, failed };
|
|
4464
4731
|
}
|
|
4465
4732
|
async pushAll() {
|
|
4466
4733
|
const databases = this.config.notion.databases;
|
|
4467
4734
|
if (!databases || databases.length === 0) {
|
|
4468
|
-
return { created: 0, updated: 0, failed: [] };
|
|
4735
|
+
return { created: 0, updated: 0, conflicts: [], failed: [] };
|
|
4469
4736
|
}
|
|
4470
4737
|
let created = 0;
|
|
4471
4738
|
let updated = 0;
|
|
@@ -4485,7 +4752,7 @@ var DatabaseSyncer = class {
|
|
|
4485
4752
|
});
|
|
4486
4753
|
}
|
|
4487
4754
|
}
|
|
4488
|
-
return { created, updated, failed };
|
|
4755
|
+
return { created, updated, conflicts: [], failed };
|
|
4489
4756
|
}
|
|
4490
4757
|
async pullDatabase(dbConfig) {
|
|
4491
4758
|
const schema = await this.notionClient.getDatabaseSchema(dbConfig.databaseId);
|
|
@@ -4499,14 +4766,19 @@ var DatabaseSyncer = class {
|
|
|
4499
4766
|
);
|
|
4500
4767
|
let created = 0;
|
|
4501
4768
|
let updated = 0;
|
|
4769
|
+
const conflicts = [];
|
|
4502
4770
|
const failed = [];
|
|
4503
4771
|
for (const page of pages) {
|
|
4504
4772
|
try {
|
|
4505
4773
|
const record = this.stateDb.getByNotionId(page.id);
|
|
4506
4774
|
if (record) {
|
|
4507
4775
|
if (page.last_edited_time === record.notionLastEdited) continue;
|
|
4508
|
-
await this.pullDatabasePage(page, dbConfig);
|
|
4509
|
-
|
|
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
|
+
}
|
|
4510
4782
|
} else {
|
|
4511
4783
|
await this.pullDatabasePage(page, dbConfig);
|
|
4512
4784
|
created++;
|
|
@@ -4519,12 +4791,14 @@ var DatabaseSyncer = class {
|
|
|
4519
4791
|
});
|
|
4520
4792
|
}
|
|
4521
4793
|
}
|
|
4522
|
-
getLogger().debug(
|
|
4523
|
-
|
|
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 };
|
|
4524
4798
|
}
|
|
4525
4799
|
async pullDatabaseViews(dbConfig, force = false) {
|
|
4526
4800
|
try {
|
|
4527
|
-
const configPath =
|
|
4801
|
+
const configPath = DB_VIEWS_PATH;
|
|
4528
4802
|
if (!force) {
|
|
4529
4803
|
try {
|
|
4530
4804
|
const existing = await this.vaultFs.readFile(configPath);
|
|
@@ -4534,7 +4808,7 @@ var DatabaseSyncer = class {
|
|
|
4534
4808
|
}
|
|
4535
4809
|
}
|
|
4536
4810
|
const viewsConfig = await this.notionClient.getDatabaseViewsConfig(dbConfig.databaseId);
|
|
4537
|
-
await this.vaultFs.ensureFolder(
|
|
4811
|
+
await this.vaultFs.ensureFolder(INTERNAL_DIR);
|
|
4538
4812
|
let allViewsConfigs = {};
|
|
4539
4813
|
try {
|
|
4540
4814
|
const existing = await this.vaultFs.readFile(configPath);
|
|
@@ -4646,6 +4920,33 @@ var DatabaseSyncer = class {
|
|
|
4646
4920
|
},
|
|
4647
4921
|
{ properties }
|
|
4648
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
|
+
}
|
|
4649
4950
|
await this.vaultFs.ensureFolder(dbConfig.localFolder);
|
|
4650
4951
|
await this.vaultFs.writeFile(filePath, finalContent);
|
|
4651
4952
|
const hash = computeHash(finalContent);
|
|
@@ -4669,7 +4970,7 @@ var DatabaseSyncer = class {
|
|
|
4669
4970
|
aliases: extractAliases(properties)
|
|
4670
4971
|
});
|
|
4671
4972
|
});
|
|
4672
|
-
return filePath;
|
|
4973
|
+
return { action: "written", path: filePath };
|
|
4673
4974
|
}
|
|
4674
4975
|
async pushDatabase(dbConfig) {
|
|
4675
4976
|
const schema = await this.notionClient.getDatabaseSchema(dbConfig.databaseId);
|
|
@@ -4677,6 +4978,8 @@ var DatabaseSyncer = class {
|
|
|
4677
4978
|
const allFiles = await this.vaultFs.listMarkdownFiles();
|
|
4678
4979
|
const prefix = dbConfig.localFolder.endsWith("/") ? dbConfig.localFolder : dbConfig.localFolder + "/";
|
|
4679
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);
|
|
4680
4983
|
let created = 0;
|
|
4681
4984
|
let updated = 0;
|
|
4682
4985
|
const failed = [];
|
|
@@ -4694,19 +4997,50 @@ var DatabaseSyncer = class {
|
|
|
4694
4997
|
const notionProps = this.propertyMapper.toNotionProperties(frontmatter, title);
|
|
4695
4998
|
const enhanced = obsidianToNotionEnhanced(body.trim());
|
|
4696
4999
|
if (record?.notionPageId) {
|
|
4697
|
-
await this.notionClient.updatePageProperties(record.notionPageId, notionProps);
|
|
4698
5000
|
try {
|
|
5001
|
+
await this.notionClient.updatePageProperties(record.notionPageId, notionProps);
|
|
4699
5002
|
await this.notionClient.replacePageMarkdown(record.notionPageId, enhanced);
|
|
4700
|
-
} 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;
|
|
4701
5011
|
}
|
|
4702
5012
|
const updatedPage = await this.notionClient.getPage(record.notionPageId);
|
|
4703
5013
|
this.stateDb.transaction(() => {
|
|
4704
5014
|
this.stateDb.updateHash(record.id, hash, Buffer.from(content, "utf-8"));
|
|
4705
5015
|
this.stateDb.updateStatus(record.id, "synced");
|
|
4706
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
|
+
});
|
|
4707
5023
|
});
|
|
4708
5024
|
updated++;
|
|
4709
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
|
+
}
|
|
4710
5044
|
const page = await this.notionClient.createPageWithMarkdown({
|
|
4711
5045
|
parentId: dbConfig.databaseId,
|
|
4712
5046
|
parentType: "database",
|
|
@@ -4745,7 +5079,21 @@ var DatabaseSyncer = class {
|
|
|
4745
5079
|
}
|
|
4746
5080
|
}
|
|
4747
5081
|
getLogger().debug(`[DB Sync] ${dbConfig.localFolder}: ${created} \uC0DD\uC131, ${updated} \uC5C5\uB370\uC774\uD2B8`);
|
|
4748
|
-
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;
|
|
4749
5097
|
}
|
|
4750
5098
|
};
|
|
4751
5099
|
function extractAliases(properties) {
|
|
@@ -4762,6 +5110,26 @@ function extractTitleFromPath(filePath) {
|
|
|
4762
5110
|
return filename.replace(/\.md$/, "");
|
|
4763
5111
|
}
|
|
4764
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
|
+
|
|
4765
5133
|
// src/sync/orchestrator.ts
|
|
4766
5134
|
var SyncOrchestrator = class {
|
|
4767
5135
|
constructor(config, stateDb, notionClient, vaultFs, customFetch) {
|
|
@@ -4778,9 +5146,20 @@ var SyncOrchestrator = class {
|
|
|
4778
5146
|
vaultFs,
|
|
4779
5147
|
config.paths.attachments,
|
|
4780
5148
|
notionClient,
|
|
4781
|
-
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
|
|
4782
5162
|
);
|
|
4783
|
-
this.fileHandler = new FileHandler(vaultFs, notionClient, stateDb);
|
|
4784
5163
|
this.propertyMapper = new PropertyMapper();
|
|
4785
5164
|
this.databaseSyncer = new DatabaseSyncer(
|
|
4786
5165
|
config,
|
|
@@ -4810,6 +5189,11 @@ var SyncOrchestrator = class {
|
|
|
4810
5189
|
dbSchemaLoaded = false;
|
|
4811
5190
|
_pullImageCount = 0;
|
|
4812
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();
|
|
4813
5197
|
async push(options) {
|
|
4814
5198
|
const startTime = Date.now();
|
|
4815
5199
|
if (this.config.sync.direction === "pull") {
|
|
@@ -4868,87 +5252,68 @@ var SyncOrchestrator = class {
|
|
|
4868
5252
|
for (const folderPath of sortedFolders) {
|
|
4869
5253
|
await this.ensureFolderPage(folderPath);
|
|
4870
5254
|
}
|
|
4871
|
-
const
|
|
4872
|
-
let created = 0;
|
|
4873
|
-
let updated = 0;
|
|
4874
|
-
let deleted = 0;
|
|
5255
|
+
const counts = { created: 0, updated: 0, deleted: 0 };
|
|
4875
5256
|
const failed = [];
|
|
4876
5257
|
let completed = 0;
|
|
4877
5258
|
const total = filtered.length;
|
|
4878
|
-
const
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
case "deleted":
|
|
4898
|
-
await this.pushDelete(change.path);
|
|
4899
|
-
deleted++;
|
|
4900
|
-
break;
|
|
4901
|
-
}
|
|
4902
|
-
} catch (error) {
|
|
4903
|
-
failed.push({
|
|
4904
|
-
path: change.path,
|
|
4905
|
-
operation: change.type === "created" ? "create" : change.type === "deleted" ? "delete" : "update",
|
|
4906
|
-
error: error instanceof Error ? error.message : String(error)
|
|
4907
|
-
});
|
|
4908
|
-
} finally {
|
|
4909
|
-
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;
|
|
4910
5278
|
}
|
|
4911
|
-
}
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
const change = filtered.find((c) => c.path === target.path);
|
|
4919
|
-
if (!change) {
|
|
4920
|
-
failed.push(target);
|
|
4921
|
-
continue;
|
|
4922
|
-
}
|
|
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) });
|
|
4923
5286
|
try {
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
created++;
|
|
4928
|
-
break;
|
|
4929
|
-
case "modified":
|
|
4930
|
-
await this.pushUpdate(change.path);
|
|
4931
|
-
updated++;
|
|
4932
|
-
break;
|
|
4933
|
-
case "moved":
|
|
4934
|
-
await this.pushMove(change.path);
|
|
4935
|
-
updated++;
|
|
4936
|
-
break;
|
|
4937
|
-
case "deleted":
|
|
4938
|
-
await this.pushDelete(change.path);
|
|
4939
|
-
deleted++;
|
|
4940
|
-
break;
|
|
4941
|
-
}
|
|
4942
|
-
getLogger().info(`[Im-Nobsidian] \uC7AC\uC2DC\uB3C4 \uC131\uACF5: ${change.path}`);
|
|
4943
|
-
} catch (error) {
|
|
4944
|
-
failed.push({
|
|
4945
|
-
path: change.path,
|
|
4946
|
-
operation: target.operation,
|
|
4947
|
-
error: error instanceof Error ? error.message : String(error)
|
|
4948
|
-
});
|
|
4949
|
-
getLogger().warn(`[Im-Nobsidian] \uC7AC\uC2DC\uB3C4 \uC2E4\uD328: ${change.path}`);
|
|
5287
|
+
await applyPushChange(change);
|
|
5288
|
+
} catch {
|
|
5289
|
+
retryQueue.push(change);
|
|
4950
5290
|
}
|
|
4951
|
-
}
|
|
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
|
+
);
|
|
4952
5317
|
}
|
|
4953
5318
|
if (this.config.sync.syncFiles !== false) {
|
|
4954
5319
|
try {
|
|
@@ -4963,8 +5328,8 @@ var SyncOrchestrator = class {
|
|
|
4963
5328
|
if ((this.config.notion.databases?.length ?? 0) > 0) {
|
|
4964
5329
|
try {
|
|
4965
5330
|
const dbResult = await this.databaseSyncer.pushAll();
|
|
4966
|
-
created += dbResult.created;
|
|
4967
|
-
updated += dbResult.updated;
|
|
5331
|
+
counts.created += dbResult.created;
|
|
5332
|
+
counts.updated += dbResult.updated;
|
|
4968
5333
|
failed.push(...dbResult.failed);
|
|
4969
5334
|
} catch (error) {
|
|
4970
5335
|
getLogger().warn("[Im-Nobsidian] DB Push \uC911 \uC624\uB958:", error);
|
|
@@ -4973,7 +5338,13 @@ var SyncOrchestrator = class {
|
|
|
4973
5338
|
this.stateDb.setMeta("last_push_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
4974
5339
|
this.stateDb.setMeta("last_sync_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
4975
5340
|
this.stateDb.setMeta("push_in_progress", "");
|
|
4976
|
-
return {
|
|
5341
|
+
return {
|
|
5342
|
+
created: counts.created,
|
|
5343
|
+
updated: counts.updated,
|
|
5344
|
+
deleted: counts.deleted,
|
|
5345
|
+
failed,
|
|
5346
|
+
duration: Date.now() - startTime
|
|
5347
|
+
};
|
|
4977
5348
|
}
|
|
4978
5349
|
async pull(options) {
|
|
4979
5350
|
const startTime = Date.now();
|
|
@@ -4993,11 +5364,10 @@ var SyncOrchestrator = class {
|
|
|
4993
5364
|
return emptyResult;
|
|
4994
5365
|
}
|
|
4995
5366
|
this.cleanupInterruptedSync();
|
|
4996
|
-
|
|
4997
|
-
let updated = 0;
|
|
4998
|
-
let deleted = 0;
|
|
5367
|
+
const counts = { created: 0, updated: 0, deleted: 0 };
|
|
4999
5368
|
this._pullImageCount = 0;
|
|
5000
5369
|
this._pullFileCount = 0;
|
|
5370
|
+
this._inlineDbRefs.clear();
|
|
5001
5371
|
const conflicts = [];
|
|
5002
5372
|
const writtenPaths = [];
|
|
5003
5373
|
const failed = [];
|
|
@@ -5009,7 +5379,7 @@ var SyncOrchestrator = class {
|
|
|
5009
5379
|
return record && options.paths.some((p) => record.obsidianPath.startsWith(p));
|
|
5010
5380
|
}) : remoteChanges;
|
|
5011
5381
|
if (filtered.length === 0 && (this.config.notion.databases?.length ?? 0) === 0) {
|
|
5012
|
-
const hasDbResults = await this.pullDiscoveredDatabases(writtenPaths, failed);
|
|
5382
|
+
const hasDbResults = await this.pullDiscoveredDatabases(writtenPaths, failed, conflicts);
|
|
5013
5383
|
if (!hasDbResults.created && !hasDbResults.updated) {
|
|
5014
5384
|
this.stateDb.setMeta("last_pull_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
5015
5385
|
this.stateDb.setMeta("last_sync_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
@@ -5023,7 +5393,7 @@ var SyncOrchestrator = class {
|
|
|
5023
5393
|
created: hasDbResults.created,
|
|
5024
5394
|
updated: hasDbResults.updated,
|
|
5025
5395
|
deleted: 0,
|
|
5026
|
-
conflicts
|
|
5396
|
+
conflicts,
|
|
5027
5397
|
writtenPaths,
|
|
5028
5398
|
failed,
|
|
5029
5399
|
duration: Date.now() - startTime,
|
|
@@ -5061,113 +5431,94 @@ var SyncOrchestrator = class {
|
|
|
5061
5431
|
};
|
|
5062
5432
|
}
|
|
5063
5433
|
this.stateDb.setMeta("pull_in_progress", "true");
|
|
5064
|
-
const sema = new Sema(this.config.advanced.concurrency);
|
|
5065
|
-
let pullCompleted = 0;
|
|
5066
5434
|
const pullTotal = filtered.length;
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
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++;
|
|
5090
5459
|
}
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
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;
|
|
5100
5468
|
}
|
|
5469
|
+
return record?.obsidianPath;
|
|
5101
5470
|
}
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
const op = change.type === "created" ? "create" : change.type === "deleted" ? "delete" : "update";
|
|
5105
|
-
options?.onProgress?.(++pullCompleted, pullTotal, { path: displayPath, operation: op });
|
|
5106
|
-
} catch (error) {
|
|
5107
|
-
const record = this.stateDb.getByNotionId(change.pageId);
|
|
5108
|
-
failed.push({
|
|
5109
|
-
path: record?.obsidianPath ?? change.pageId,
|
|
5110
|
-
operation: change.type === "created" ? "create" : change.type === "deleted" ? "delete" : "update",
|
|
5111
|
-
error: error instanceof Error ? error.message : String(error)
|
|
5112
|
-
});
|
|
5113
|
-
} finally {
|
|
5114
|
-
sema.release();
|
|
5471
|
+
default:
|
|
5472
|
+
return void 0;
|
|
5115
5473
|
}
|
|
5116
|
-
}
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
for (const target of retryTargets) {
|
|
5123
|
-
const change = filtered.find((c) => {
|
|
5124
|
-
const record = this.stateDb.getByNotionId(c.pageId);
|
|
5125
|
-
return (record?.obsidianPath ?? c.pageId) === target.path;
|
|
5126
|
-
});
|
|
5127
|
-
if (!change) {
|
|
5128
|
-
failed.push(target);
|
|
5129
|
-
continue;
|
|
5130
|
-
}
|
|
5474
|
+
};
|
|
5475
|
+
const retryQueue = [];
|
|
5476
|
+
await runPool(
|
|
5477
|
+
filtered,
|
|
5478
|
+
async (change) => {
|
|
5479
|
+
if (options?.signal?.aborted) return;
|
|
5131
5480
|
try {
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
case "modified": {
|
|
5140
|
-
const result = await this.pullUpdate(change);
|
|
5141
|
-
if (result.conflict) {
|
|
5142
|
-
conflicts.push(result.conflict);
|
|
5143
|
-
} else if (result.path) {
|
|
5144
|
-
writtenPaths.push(result.path);
|
|
5145
|
-
updated++;
|
|
5146
|
-
}
|
|
5147
|
-
break;
|
|
5148
|
-
}
|
|
5149
|
-
case "deleted": {
|
|
5150
|
-
const path = await this.pullDelete(change.pageId);
|
|
5151
|
-
if (path) deleted++;
|
|
5152
|
-
break;
|
|
5153
|
-
}
|
|
5154
|
-
}
|
|
5155
|
-
getLogger().info(`[Im-Nobsidian] \uC7AC\uC2DC\uB3C4 \uC131\uACF5: ${target.path}`);
|
|
5156
|
-
} catch (error) {
|
|
5157
|
-
failed.push({
|
|
5158
|
-
path: target.path,
|
|
5159
|
-
operation: target.operation,
|
|
5160
|
-
error: error instanceof Error ? error.message : String(error)
|
|
5161
|
-
});
|
|
5162
|
-
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);
|
|
5163
5488
|
}
|
|
5164
|
-
}
|
|
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
|
+
);
|
|
5165
5515
|
}
|
|
5166
5516
|
if ((this.config.notion.databases?.length ?? 0) > 0) {
|
|
5167
5517
|
try {
|
|
5168
5518
|
const dbResult = await this.databaseSyncer.pullAll();
|
|
5169
|
-
created += dbResult.created;
|
|
5170
|
-
updated += dbResult.updated;
|
|
5519
|
+
counts.created += dbResult.created;
|
|
5520
|
+
counts.updated += dbResult.updated;
|
|
5521
|
+
conflicts.push(...dbResult.conflicts);
|
|
5171
5522
|
failed.push(...dbResult.failed);
|
|
5172
5523
|
if (dbResult.created + dbResult.updated > 0) {
|
|
5173
5524
|
const dbPaths = this.stateDb.getByStatus("synced").filter((r) => r.fileType === "db-row").map((r) => r.obsidianPath);
|
|
@@ -5178,9 +5529,9 @@ var SyncOrchestrator = class {
|
|
|
5178
5529
|
}
|
|
5179
5530
|
}
|
|
5180
5531
|
{
|
|
5181
|
-
const dbDiscovery = await this.pullDiscoveredDatabases(writtenPaths, failed);
|
|
5182
|
-
created += dbDiscovery.created;
|
|
5183
|
-
updated += dbDiscovery.updated;
|
|
5532
|
+
const dbDiscovery = await this.pullDiscoveredDatabases(writtenPaths, failed, conflicts);
|
|
5533
|
+
counts.created += dbDiscovery.created;
|
|
5534
|
+
counts.updated += dbDiscovery.updated;
|
|
5184
5535
|
}
|
|
5185
5536
|
const linkTargetPaths = writtenPaths.length > 0 ? writtenPaths : [];
|
|
5186
5537
|
const linkCount = linkTargetPaths.length > 0 ? await this.resolveNotionLinks(linkTargetPaths) : 0;
|
|
@@ -5188,9 +5539,9 @@ var SyncOrchestrator = class {
|
|
|
5188
5539
|
this.stateDb.setMeta("last_sync_at", (/* @__PURE__ */ new Date()).toISOString());
|
|
5189
5540
|
this.stateDb.setMeta("pull_in_progress", "");
|
|
5190
5541
|
return {
|
|
5191
|
-
created,
|
|
5192
|
-
updated,
|
|
5193
|
-
deleted,
|
|
5542
|
+
created: counts.created,
|
|
5543
|
+
updated: counts.updated,
|
|
5544
|
+
deleted: counts.deleted,
|
|
5194
5545
|
conflicts,
|
|
5195
5546
|
writtenPaths,
|
|
5196
5547
|
failed,
|
|
@@ -5322,7 +5673,48 @@ var SyncOrchestrator = class {
|
|
|
5322
5673
|
}
|
|
5323
5674
|
return allDbs;
|
|
5324
5675
|
}
|
|
5325
|
-
|
|
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) {
|
|
5326
5718
|
if (this.isDatabaseMode) return { created: 0, updated: 0 };
|
|
5327
5719
|
let created = 0;
|
|
5328
5720
|
let updated = 0;
|
|
@@ -5336,53 +5728,45 @@ var SyncOrchestrator = class {
|
|
|
5336
5728
|
dbConfigs = [];
|
|
5337
5729
|
}
|
|
5338
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;
|
|
5339
5739
|
if (dbConfigs.length === 0) {
|
|
5340
|
-
const
|
|
5341
|
-
const
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
let parentFolder = "";
|
|
5350
|
-
if (parentEntry?.obsidianPath) {
|
|
5351
|
-
const obsPath = parentEntry.obsidianPath;
|
|
5352
|
-
if (obsPath.endsWith(".md")) {
|
|
5353
|
-
const parts = obsPath.split("/");
|
|
5354
|
-
parts.pop();
|
|
5355
|
-
parentFolder = parts.length > 0 ? parts.join("/") : obsPath.replace(/\.md$/, "");
|
|
5356
|
-
} else {
|
|
5357
|
-
parentFolder = obsPath;
|
|
5358
|
-
}
|
|
5359
|
-
}
|
|
5360
|
-
let safeName;
|
|
5361
|
-
if (dbTitle) {
|
|
5362
|
-
safeName = dbTitle.replace(/[^a-zA-Z0-9가-힣\s_-]/g, "").replace(/\s+/g, "-");
|
|
5363
|
-
} else {
|
|
5364
|
-
const parentName = parentFolder.split("/").pop() || "";
|
|
5365
|
-
safeName = parentName ? `${parentName}-DB` : `db-${dbId.slice(0, 8)}`;
|
|
5366
|
-
}
|
|
5367
|
-
if (!parentFolder) parentFolder = "databases";
|
|
5368
|
-
dbConfigs.push({
|
|
5369
|
-
databaseId: dbId,
|
|
5370
|
-
localFolder: `${parentFolder}/${safeName}`,
|
|
5371
|
-
titleProperty: "Name"
|
|
5372
|
-
});
|
|
5373
|
-
} catch (error) {
|
|
5374
|
-
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;
|
|
5375
5749
|
}
|
|
5376
5750
|
}
|
|
5377
|
-
|
|
5378
|
-
|
|
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;
|
|
5379
5759
|
}
|
|
5380
5760
|
}
|
|
5761
|
+
if (changed && dbConfigs.length > 0) {
|
|
5762
|
+
this.stateDb.setMeta("discovered_dbs", JSON.stringify(dbConfigs));
|
|
5763
|
+
}
|
|
5381
5764
|
for (const dbConfig of dbConfigs) {
|
|
5382
5765
|
try {
|
|
5383
5766
|
const dbResult = await this.databaseSyncer.pullDatabase(dbConfig);
|
|
5384
5767
|
created += dbResult.created;
|
|
5385
5768
|
updated += dbResult.updated;
|
|
5769
|
+
conflicts.push(...dbResult.conflicts);
|
|
5386
5770
|
failed.push(...dbResult.failed);
|
|
5387
5771
|
if (dbResult.created + dbResult.updated > 0) {
|
|
5388
5772
|
const dbPaths = this.stateDb.getByStatus("synced").filter((r) => r.fileType === "db-row").map((r) => r.obsidianPath);
|
|
@@ -5426,7 +5810,7 @@ var SyncOrchestrator = class {
|
|
|
5426
5810
|
});
|
|
5427
5811
|
content = resolved;
|
|
5428
5812
|
const resolved2 = content.replace(
|
|
5429
|
-
/\[([^\]]+)\]\(\/([a-f0-9]{32})
|
|
5813
|
+
/\[([^\]]+)\]\(\/(?:p\/)?([a-f0-9]{32})(?:\?[^)]*)?\)/g,
|
|
5430
5814
|
(_match, text, id) => {
|
|
5431
5815
|
const title = idToTitle.get(id);
|
|
5432
5816
|
if (title) {
|
|
@@ -5453,6 +5837,11 @@ var SyncOrchestrator = class {
|
|
|
5453
5837
|
this.dbSchemaLoaded = true;
|
|
5454
5838
|
}
|
|
5455
5839
|
async pushCreate(path) {
|
|
5840
|
+
const existingRecord = this.stateDb.getByPath(path);
|
|
5841
|
+
if (existingRecord?.notionPageId) {
|
|
5842
|
+
await this.pushUpdate(path);
|
|
5843
|
+
return;
|
|
5844
|
+
}
|
|
5456
5845
|
const content = await this.vaultFs.readFile(path);
|
|
5457
5846
|
const title = extractTitle(path);
|
|
5458
5847
|
const parentId = await this.resolveNotionParent(path);
|
|
@@ -5487,6 +5876,20 @@ var SyncOrchestrator = class {
|
|
|
5487
5876
|
conversionResult.content,
|
|
5488
5877
|
effectiveProperties
|
|
5489
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
|
+
});
|
|
5490
5893
|
await this.imageHandler.uploadAndAppendImages(page.id, conversionResult.images);
|
|
5491
5894
|
const hash = computeHash(content);
|
|
5492
5895
|
const fileStat = await this.vaultFs.getFileStat(path);
|
|
@@ -5584,7 +5987,10 @@ var SyncOrchestrator = class {
|
|
|
5584
5987
|
throw error;
|
|
5585
5988
|
}
|
|
5586
5989
|
}
|
|
5587
|
-
this.stateDb.
|
|
5990
|
+
this.stateDb.transaction(() => {
|
|
5991
|
+
this.stateDb.delete(record.id);
|
|
5992
|
+
this.stateDb.deleteWikilink(record.obsidianPath);
|
|
5993
|
+
});
|
|
5588
5994
|
} else {
|
|
5589
5995
|
this.stateDb.updateStatus(record.id, "pending");
|
|
5590
5996
|
}
|
|
@@ -5627,7 +6033,9 @@ var SyncOrchestrator = class {
|
|
|
5627
6033
|
} while (cursor);
|
|
5628
6034
|
remotePages = allPages;
|
|
5629
6035
|
} else {
|
|
5630
|
-
|
|
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 }));
|
|
5631
6039
|
}
|
|
5632
6040
|
for (const page of remotePages) {
|
|
5633
6041
|
const record = this.stateDb.getByNotionId(page.id);
|
|
@@ -5699,12 +6107,84 @@ var SyncOrchestrator = class {
|
|
|
5699
6107
|
if (notionIdsEqual(parentId, this.config.notion.rootPageId)) return true;
|
|
5700
6108
|
return !!this.stateDb.getByNotionId(parentId);
|
|
5701
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
|
+
}
|
|
5702
6180
|
async pullCreate(pageId) {
|
|
5703
6181
|
const page = await this.notionClient.getPage(pageId);
|
|
5704
6182
|
const title = this.notionClient.extractTitle(page);
|
|
5705
6183
|
const safeName = sanitizeFileName(title);
|
|
5706
6184
|
const parentPath = await this.resolveParentPath(page);
|
|
5707
|
-
const firstChildren = await this.notionClient.listChildren(pageId, {
|
|
6185
|
+
const firstChildren = await this.notionClient.listChildren(pageId, {
|
|
6186
|
+
pageSize: this.config.advanced.pageSize
|
|
6187
|
+
});
|
|
5708
6188
|
const hasChildPages = firstChildren.results.some((b) => "type" in b && b.type === "child_page");
|
|
5709
6189
|
const markdown = await this.fetchPageMarkdown(pageId);
|
|
5710
6190
|
const hasContent = markdown.trim().length > 0;
|
|
@@ -5824,46 +6304,45 @@ var SyncOrchestrator = class {
|
|
|
5824
6304
|
} catch {
|
|
5825
6305
|
localContent = "";
|
|
5826
6306
|
}
|
|
5827
|
-
const
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
|
|
5834
|
-
|
|
5835
|
-
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
|
|
5840
|
-
previousHash: record.contentHash
|
|
5841
|
-
},
|
|
5842
|
-
remoteChange: change,
|
|
5843
|
-
baseContent: record.baseSnapshot?.toString("utf-8") ?? null,
|
|
5844
|
-
localContent,
|
|
5845
|
-
remoteContent
|
|
5846
|
-
};
|
|
5847
|
-
this.stateDb.updateStatus(record.id, "conflict");
|
|
5848
|
-
return { conflict };
|
|
5849
|
-
}
|
|
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 };
|
|
5850
6320
|
}
|
|
5851
6321
|
await this.vaultFs.writeFile(record.obsidianPath, remoteContent);
|
|
5852
6322
|
const updateStat = await this.vaultFs.getFileStat(record.obsidianPath);
|
|
5853
6323
|
const newHash = computeHash(remoteContent);
|
|
5854
|
-
this.stateDb.
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
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
|
+
});
|
|
5867
6346
|
});
|
|
5868
6347
|
return { path: record.obsidianPath };
|
|
5869
6348
|
}
|
|
@@ -5876,7 +6355,10 @@ var SyncOrchestrator = class {
|
|
|
5876
6355
|
} catch {
|
|
5877
6356
|
}
|
|
5878
6357
|
}
|
|
5879
|
-
this.stateDb.
|
|
6358
|
+
this.stateDb.transaction(() => {
|
|
6359
|
+
this.stateDb.delete(record.id);
|
|
6360
|
+
this.stateDb.deleteWikilink(record.obsidianPath);
|
|
6361
|
+
});
|
|
5880
6362
|
return record.obsidianPath;
|
|
5881
6363
|
}
|
|
5882
6364
|
async ensureFolderPage(folderPath) {
|
|
@@ -5884,7 +6366,12 @@ var SyncOrchestrator = class {
|
|
|
5884
6366
|
const folderNoteRecord = this.stateDb.getByPath(folderNotePath);
|
|
5885
6367
|
if (folderNoteRecord?.notionPageId) {
|
|
5886
6368
|
const existing2 = this.stateDb.getByPath(folderPath);
|
|
5887
|
-
if (existing2)
|
|
6369
|
+
if (existing2) {
|
|
6370
|
+
this.stateDb.transaction(() => {
|
|
6371
|
+
this.stateDb.delete(existing2.id);
|
|
6372
|
+
this.stateDb.deleteWikilink(existing2.obsidianPath);
|
|
6373
|
+
});
|
|
6374
|
+
}
|
|
5888
6375
|
return;
|
|
5889
6376
|
}
|
|
5890
6377
|
const existing = this.stateDb.getByPath(folderPath);
|
|
@@ -5943,7 +6430,10 @@ var SyncOrchestrator = class {
|
|
|
5943
6430
|
const folderNotePath = `${folder.obsidianPath}/${folderName}.md`;
|
|
5944
6431
|
const noteRecord = this.stateDb.getByPath(folderNotePath);
|
|
5945
6432
|
if (noteRecord?.notionPageId) {
|
|
5946
|
-
this.stateDb.
|
|
6433
|
+
this.stateDb.transaction(() => {
|
|
6434
|
+
this.stateDb.delete(folder.id);
|
|
6435
|
+
this.stateDb.deleteWikilink(folder.obsidianPath);
|
|
6436
|
+
});
|
|
5947
6437
|
}
|
|
5948
6438
|
}
|
|
5949
6439
|
}
|
|
@@ -6029,12 +6519,38 @@ var SyncOrchestrator = class {
|
|
|
6029
6519
|
if (this.config.conversion.preferMarkdownApi !== false) {
|
|
6030
6520
|
try {
|
|
6031
6521
|
const result = await this.notionClient.getPageMarkdown(pageId);
|
|
6032
|
-
|
|
6522
|
+
this.collectInlineDbRefs(pageId, result.markdown);
|
|
6523
|
+
return this.resolveNotionIdWikilinks(notionEnhancedToObsidian(result.markdown));
|
|
6033
6524
|
} catch {
|
|
6034
6525
|
}
|
|
6035
6526
|
}
|
|
6036
6527
|
return this.blockConverter.notionBlocksToMarkdown(pageId);
|
|
6037
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
|
+
}
|
|
6038
6554
|
async extractParentId(page) {
|
|
6039
6555
|
const parent = page.parent;
|
|
6040
6556
|
if (parent.type === "page_id") return parent.page_id ?? null;
|
|
@@ -6111,7 +6627,7 @@ var NodeVaultFS = class {
|
|
|
6111
6627
|
excludePatterns;
|
|
6112
6628
|
loadImNobsidianIgnore() {
|
|
6113
6629
|
try {
|
|
6114
|
-
const ignorePath = join(this.rootPath,
|
|
6630
|
+
const ignorePath = join(this.rootPath, IGNORE_FILE);
|
|
6115
6631
|
const content = readFileSync(ignorePath, "utf-8");
|
|
6116
6632
|
return content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
6117
6633
|
} catch {
|
|
@@ -6264,8 +6780,8 @@ var NodeVaultFS = class {
|
|
|
6264
6780
|
};
|
|
6265
6781
|
|
|
6266
6782
|
// src/converter/pre-processors/inline-db.ts
|
|
6267
|
-
var INLINE_DB_START =
|
|
6268
|
-
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");
|
|
6269
6785
|
var InlineDBParser = class {
|
|
6270
6786
|
name = "InlineDBParser";
|
|
6271
6787
|
order = 40;
|
|
@@ -6315,82 +6831,126 @@ function threeWayMerge(base, local, remote) {
|
|
|
6315
6831
|
const baseLines = base.split("\n");
|
|
6316
6832
|
const localLines = local.split("\n");
|
|
6317
6833
|
const remoteLines = remote.split("\n");
|
|
6318
|
-
const
|
|
6319
|
-
|
|
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);
|
|
6320
6839
|
const merged = [];
|
|
6321
6840
|
const conflicts = [];
|
|
6322
|
-
let
|
|
6323
|
-
let
|
|
6324
|
-
while (
|
|
6325
|
-
const
|
|
6326
|
-
|
|
6327
|
-
|
|
6328
|
-
|
|
6329
|
-
|
|
6330
|
-
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
|
|
6334
|
-
|
|
6335
|
-
|
|
6336
|
-
|
|
6337
|
-
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
merged.push(...remoteChange.newLines);
|
|
6351
|
-
baseIdx = remoteChange.baseEnd;
|
|
6352
|
-
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);
|
|
6353
6869
|
} else {
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
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");
|
|
6358
6876
|
}
|
|
6877
|
+
cursor = regionEnd;
|
|
6878
|
+
}
|
|
6879
|
+
if (cursor < baseLines.length) {
|
|
6880
|
+
merged.push(...baseLines.slice(cursor));
|
|
6359
6881
|
}
|
|
6360
6882
|
return {
|
|
6361
|
-
success,
|
|
6883
|
+
success: conflicts.length === 0,
|
|
6362
6884
|
merged: merged.join("\n"),
|
|
6363
6885
|
conflicts
|
|
6364
6886
|
};
|
|
6365
6887
|
}
|
|
6366
|
-
function
|
|
6888
|
+
function computeHunks(base, modified, side) {
|
|
6889
|
+
const matches = lcsMatches(base, modified);
|
|
6367
6890
|
const hunks = [];
|
|
6368
|
-
let
|
|
6369
|
-
let
|
|
6370
|
-
|
|
6371
|
-
|
|
6372
|
-
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
|
|
6376
|
-
|
|
6377
|
-
|
|
6378
|
-
|
|
6379
|
-
|
|
6380
|
-
}
|
|
6381
|
-
baseIdx++;
|
|
6382
|
-
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
|
+
});
|
|
6383
6903
|
}
|
|
6904
|
+
};
|
|
6905
|
+
for (const [b, m] of matches) {
|
|
6906
|
+
emitGap(b, m);
|
|
6907
|
+
prevBase = b;
|
|
6908
|
+
prevMod = m;
|
|
6384
6909
|
}
|
|
6385
|
-
|
|
6386
|
-
hunks.push({
|
|
6387
|
-
baseStart: baseIdx,
|
|
6388
|
-
baseEnd: base.length,
|
|
6389
|
-
newLines: modified.slice(modIdx)
|
|
6390
|
-
});
|
|
6391
|
-
}
|
|
6910
|
+
emitGap(base.length, modified.length);
|
|
6392
6911
|
return hunks;
|
|
6393
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
|
+
}
|
|
6394
6954
|
function arraysEqual(a, b) {
|
|
6395
6955
|
if (a.length !== b.length) return false;
|
|
6396
6956
|
return a.every((val, idx) => val === b[idx]);
|
|
@@ -6609,7 +7169,12 @@ var FileWatcher = class {
|
|
|
6609
7169
|
watcher = null;
|
|
6610
7170
|
start() {
|
|
6611
7171
|
this.watcher = watch(this.rootPath, {
|
|
6612
|
-
|
|
7172
|
+
// 이벤트 경로를 rootPath 기준 "볼트 상대경로"로 방출한다. 동기화 엔진의 변경
|
|
7173
|
+
// 감지·paths 필터는 모두 볼트 상대경로(listMarkdownFileStats 기준)를 쓰므로,
|
|
7174
|
+
// 절대경로를 그대로 넘기면 push 의 startsWith 스코프 필터에서 누락돼 변경 파일이
|
|
7175
|
+
// 실제로 동기화되지 않는다(증분 watch 의 핵심 버그).
|
|
7176
|
+
cwd: this.rootPath,
|
|
7177
|
+
ignored: [/(^|[/\\])\./, "**/node_modules/**", INTERNAL_DIR_GLOB],
|
|
6613
7178
|
persistent: true,
|
|
6614
7179
|
ignoreInitial: true,
|
|
6615
7180
|
awaitWriteFinish: {
|
|
@@ -6617,9 +7182,13 @@ var FileWatcher = class {
|
|
|
6617
7182
|
pollInterval: 100
|
|
6618
7183
|
}
|
|
6619
7184
|
});
|
|
6620
|
-
this.watcher.on("add", (path) => this.callback("add", path));
|
|
6621
|
-
this.watcher.on("change", (path) => this.callback("change", path));
|
|
6622
|
-
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("/");
|
|
6623
7192
|
}
|
|
6624
7193
|
async stop() {
|
|
6625
7194
|
if (this.watcher) {
|
|
@@ -6699,16 +7268,19 @@ var WatchSyncService = class {
|
|
|
6699
7268
|
this.syncQueued = true;
|
|
6700
7269
|
return;
|
|
6701
7270
|
}
|
|
7271
|
+
this.clearDebounce();
|
|
7272
|
+
if (this.pendingChanges.size === 0) return;
|
|
6702
7273
|
this.syncing = true;
|
|
6703
|
-
const
|
|
7274
|
+
const batch = new Map(this.pendingChanges);
|
|
6704
7275
|
this.pendingChanges.clear();
|
|
6705
7276
|
try {
|
|
6706
7277
|
this.options.onSyncStart?.();
|
|
6707
|
-
const result = await this.orchestrator.sync({
|
|
6708
|
-
paths: changedPaths.length > 0 ? changedPaths : void 0
|
|
6709
|
-
});
|
|
7278
|
+
const result = await this.orchestrator.sync({ paths: [...batch.keys()] });
|
|
6710
7279
|
this.options.onSyncComplete?.(result);
|
|
6711
7280
|
} catch (error) {
|
|
7281
|
+
for (const [path, event] of batch) {
|
|
7282
|
+
if (!this.pendingChanges.has(path)) this.pendingChanges.set(path, event);
|
|
7283
|
+
}
|
|
6712
7284
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
6713
7285
|
this.options.onSyncError?.(err);
|
|
6714
7286
|
} finally {
|
|
@@ -6716,6 +7288,8 @@ var WatchSyncService = class {
|
|
|
6716
7288
|
if (this.syncQueued) {
|
|
6717
7289
|
this.syncQueued = false;
|
|
6718
7290
|
void this.executeSync();
|
|
7291
|
+
} else if (this.pendingChanges.size > 0) {
|
|
7292
|
+
this.scheduleSync();
|
|
6719
7293
|
}
|
|
6720
7294
|
}
|
|
6721
7295
|
}
|
|
@@ -6886,7 +7460,6 @@ function parseDate(value) {
|
|
|
6886
7460
|
const match = /^(\d{4}-\d{2}-\d{2})/.exec(value);
|
|
6887
7461
|
return match?.[1] ?? null;
|
|
6888
7462
|
}
|
|
6889
|
-
var DB_VIEWS_PATH = ".im-nobsidian/db-views.json";
|
|
6890
7463
|
var ViewDataProvider = class {
|
|
6891
7464
|
constructor(vaultFs) {
|
|
6892
7465
|
this.vaultFs = vaultFs;
|
|
@@ -7093,6 +7666,6 @@ ${lines.join("\n")}
|
|
|
7093
7666
|
}`;
|
|
7094
7667
|
}
|
|
7095
7668
|
|
|
7096
|
-
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 };
|
|
7097
7670
|
//# sourceMappingURL=index.js.map
|
|
7098
7671
|
//# sourceMappingURL=index.js.map
|