@tnotesjs/core 0.0.7 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +6 -3
  2. package/dist/{chunk-JDVN2QYW.js → chunk-JUMVJKWV.js} +615 -614
  3. package/dist/cli/index.js +125 -125
  4. package/dist/vitepress/config/index.js +182 -207
  5. package/package.json +15 -3
  6. package/types/shims.d.ts +3 -4
  7. package/vitepress/components/CodeBlockFullscreen/CodeBlockFullscreen.vue +2 -1
  8. package/vitepress/components/CodeBlockFullscreen/index.ts +2 -1
  9. package/vitepress/components/Discussions/Discussions.vue +3 -2
  10. package/vitepress/components/EnWordList/EnWordList.vue +2 -1
  11. package/vitepress/components/Layout/AboutModal.vue +1 -1
  12. package/vitepress/components/Layout/AboutPanel.vue +8 -3
  13. package/vitepress/components/Layout/ContentCollapse.vue +2 -1
  14. package/vitepress/components/Layout/CustomSidebar.vue +7 -4
  15. package/vitepress/components/Layout/DocBeforeControls.vue +4 -4
  16. package/vitepress/components/Layout/DocFooter.vue +2 -1
  17. package/vitepress/components/Layout/ImagePreview.vue +2 -1
  18. package/vitepress/components/Layout/Layout.vue +17 -20
  19. package/vitepress/components/Layout/NoteStatus.vue +2 -1
  20. package/vitepress/components/Layout/SidebarItems.vue +7 -2
  21. package/vitepress/components/Layout/ToggleFullContent.vue +2 -1
  22. package/vitepress/components/Layout/ToggleSidebar.vue +2 -1
  23. package/vitepress/components/Layout/composables/useCollapseControl.ts +1 -1
  24. package/vitepress/components/Layout/composables/useNoteConfig.ts +2 -4
  25. package/vitepress/components/Layout/composables/useNoteSave.ts +3 -2
  26. package/vitepress/components/Layout/composables/useRedirect.ts +8 -5
  27. package/vitepress/components/Layout/composables/useVSCodeIntegration.ts +7 -4
  28. package/vitepress/components/Layout/homeReadme.data.ts +1 -1
  29. package/vitepress/components/LoadingPage/LoadingPage.vue +1 -1
  30. package/vitepress/components/MarkMap/MarkMap.vue +5 -4
  31. package/vitepress/components/Mermaid/Mermaid.vue +4 -2
  32. package/vitepress/components/NotesTable/NotesTable.vue +2 -1
  33. package/vitepress/components/Settings/Settings.vue +2 -1
  34. package/vitepress/components/SidebarCard/MindMapView.vue +10 -9
  35. package/vitepress/components/SidebarCard/SidebarCard.vue +4 -6
  36. package/vitepress/config/index.ts +7 -5
  37. package/vitepress/configs/head.config.ts +2 -1
  38. package/vitepress/configs/markdown-it.d.ts +6 -3
  39. package/vitepress/configs/markdown.config.ts +6 -5
  40. package/vitepress/configs/theme.config.ts +1 -1
  41. package/vitepress/plugins/buildProgressPlugin.ts +2 -38
  42. package/vitepress/plugins/getNoteByConfigIdPlugin.ts +3 -2
  43. package/vitepress/plugins/renameNotePlugin.ts +2 -1
  44. package/vitepress/plugins/updateConfigPlugin.ts +2 -1
  45. package/vitepress/theme/index.ts +5 -3
@@ -3,40 +3,6 @@ import {
3
3
  getConfigManager
4
4
  } from "./chunk-NASIL5FY.js";
5
5
 
6
- // commands/models.ts
7
- var COMMAND_NAMES = {
8
- BUILD: "build",
9
- CREATE_NOTES: "create-notes",
10
- DEV: "dev",
11
- FIX_TIMESTAMPS: "fix-timestamps",
12
- HELP: "help",
13
- PREVIEW: "preview",
14
- PULL: "pull",
15
- PUSH: "push",
16
- RENAME_NOTE: "rename-note",
17
- UPDATE: "update",
18
- UPDATE_COMPLETED_COUNT: "update-completed-count",
19
- UPDATE_NOTE_CONFIG: "update-note-config"
20
- };
21
- var COMMAND_DESCRIPTIONS = {
22
- [COMMAND_NAMES.DEV]: "\u542F\u52A8\u77E5\u8BC6\u5E93\u5F00\u53D1\u670D\u52A1",
23
- [COMMAND_NAMES.BUILD]: "\u6784\u5EFA\u77E5\u8BC6\u5E93",
24
- [COMMAND_NAMES.PREVIEW]: "\u9884\u89C8\u6784\u5EFA\u540E\u7684\u77E5\u8BC6\u5E93",
25
- [COMMAND_NAMES.UPDATE]: "\u6839\u636E\u7B14\u8BB0\u5185\u5BB9\u66F4\u65B0\u77E5\u8BC6\u5E93",
26
- [COMMAND_NAMES.UPDATE_COMPLETED_COUNT]: "\u66F4\u65B0\u5B8C\u6210\u7B14\u8BB0\u6570\u91CF\u5386\u53F2\u8BB0\u5F55\uFF08\u8FD1 1 \u5E74\uFF0C\u6700\u8FD1 12 \u4E2A\u6708\uFF09",
27
- [COMMAND_NAMES.CREATE_NOTES]: "\u65B0\u5EFA\u7B14\u8BB0\uFF08\u652F\u6301\u6279\u91CF\u521B\u5EFA\uFF09",
28
- [COMMAND_NAMES.PUSH]: "\u5C06\u77E5\u8BC6\u5E93\u63A8\u9001\u5230 GitHub",
29
- [COMMAND_NAMES.PULL]: "\u5C06 GitHub \u7684\u77E5\u8BC6\u5E93\u62C9\u4E0B\u6765",
30
- [COMMAND_NAMES.FIX_TIMESTAMPS]: "\u4FEE\u590D\u6240\u6709\u7B14\u8BB0\u7684\u65F6\u95F4\u6233\uFF08\u57FA\u4E8E git \u5386\u53F2\uFF09",
31
- [COMMAND_NAMES.UPDATE_NOTE_CONFIG]: "\u66F4\u65B0\u7B14\u8BB0\u914D\u7F6E\u6587\u4EF6",
32
- [COMMAND_NAMES.RENAME_NOTE]: "\u91CD\u547D\u540D\u7B14\u8BB0",
33
- [COMMAND_NAMES.HELP]: "\u663E\u793A\u5E2E\u52A9\u4FE1\u606F"
34
- };
35
- var COMMAND_OPTIONS = {
36
- QUIET: "quiet",
37
- FORCE: "force"
38
- };
39
-
40
6
  // utils/errorHandler.ts
41
7
  var TNotesError = class _TNotesError extends Error {
42
8
  constructor(message, code = "UNKNOWN" /* UNKNOWN */, context) {
@@ -399,8 +365,8 @@ var genHierarchicalSidebar = (itemList, titles, titlesNotesCount, sidebarIsColla
399
365
  };
400
366
 
401
367
  // utils/getChangedIds.ts
402
- import path from "path";
403
368
  import { execSync } from "child_process";
369
+ import path from "path";
404
370
  function getChangedIds() {
405
371
  const changedFiles = execSync(
406
372
  `git diff --name-only HEAD -- "notes/[0-9][0-9][0-9][0-9]*/README.md"`
@@ -419,7 +385,7 @@ function createAddNumberToTitle() {
419
385
  const titleNumbers = Array(7).fill(0);
420
386
  return function addNumberToTitle(title) {
421
387
  const match = title.match(
422
- /^(\#+)\s*((\d+(\.\d*)?(\.\d*)?(\.\d*)?(\.\d*)?(\.\d*)?)\.\s*)?(.*)/
388
+ /^(#+)\s*((\d+(\.\d*)?(\.\d*)?(\.\d*)?(\.\d*)?(\.\d*)?)\.\s*)?(.*)/
423
389
  );
424
390
  const plainTitle = match ? match[9].trim() : title.trim();
425
391
  const level = title.indexOf(" ");
@@ -1006,7 +972,7 @@ function buildNoteLink(note, repoOwner, repoName2) {
1006
972
  }
1007
973
  function updateNoteStatus(note) {
1008
974
  let status = " ";
1009
- let deprecatedMark = "";
975
+ const deprecatedMark = "";
1010
976
  if (note.config) {
1011
977
  if (note.config.done) {
1012
978
  status = "x";
@@ -1140,6 +1106,40 @@ function validateNoteTitle(title) {
1140
1106
  return { valid: true };
1141
1107
  }
1142
1108
 
1109
+ // commands/models.ts
1110
+ var COMMAND_NAMES = {
1111
+ BUILD: "build",
1112
+ CREATE_NOTES: "create-notes",
1113
+ DEV: "dev",
1114
+ FIX_TIMESTAMPS: "fix-timestamps",
1115
+ HELP: "help",
1116
+ PREVIEW: "preview",
1117
+ PULL: "pull",
1118
+ PUSH: "push",
1119
+ RENAME_NOTE: "rename-note",
1120
+ UPDATE: "update",
1121
+ UPDATE_COMPLETED_COUNT: "update-completed-count",
1122
+ UPDATE_NOTE_CONFIG: "update-note-config"
1123
+ };
1124
+ var COMMAND_DESCRIPTIONS = {
1125
+ [COMMAND_NAMES.DEV]: "\u542F\u52A8\u77E5\u8BC6\u5E93\u5F00\u53D1\u670D\u52A1",
1126
+ [COMMAND_NAMES.BUILD]: "\u6784\u5EFA\u77E5\u8BC6\u5E93",
1127
+ [COMMAND_NAMES.PREVIEW]: "\u9884\u89C8\u6784\u5EFA\u540E\u7684\u77E5\u8BC6\u5E93",
1128
+ [COMMAND_NAMES.UPDATE]: "\u6839\u636E\u7B14\u8BB0\u5185\u5BB9\u66F4\u65B0\u77E5\u8BC6\u5E93",
1129
+ [COMMAND_NAMES.UPDATE_COMPLETED_COUNT]: "\u66F4\u65B0\u5B8C\u6210\u7B14\u8BB0\u6570\u91CF\u5386\u53F2\u8BB0\u5F55\uFF08\u8FD1 1 \u5E74\uFF0C\u6700\u8FD1 12 \u4E2A\u6708\uFF09",
1130
+ [COMMAND_NAMES.CREATE_NOTES]: "\u65B0\u5EFA\u7B14\u8BB0\uFF08\u652F\u6301\u6279\u91CF\u521B\u5EFA\uFF09",
1131
+ [COMMAND_NAMES.PUSH]: "\u5C06\u77E5\u8BC6\u5E93\u63A8\u9001\u5230 GitHub",
1132
+ [COMMAND_NAMES.PULL]: "\u5C06 GitHub \u7684\u77E5\u8BC6\u5E93\u62C9\u4E0B\u6765",
1133
+ [COMMAND_NAMES.FIX_TIMESTAMPS]: "\u4FEE\u590D\u6240\u6709\u7B14\u8BB0\u7684\u65F6\u95F4\u6233\uFF08\u57FA\u4E8E git \u5386\u53F2\uFF09",
1134
+ [COMMAND_NAMES.UPDATE_NOTE_CONFIG]: "\u66F4\u65B0\u7B14\u8BB0\u914D\u7F6E\u6587\u4EF6",
1135
+ [COMMAND_NAMES.RENAME_NOTE]: "\u91CD\u547D\u540D\u7B14\u8BB0",
1136
+ [COMMAND_NAMES.HELP]: "\u663E\u793A\u5E2E\u52A9\u4FE1\u606F"
1137
+ };
1138
+ var COMMAND_OPTIONS = {
1139
+ QUIET: "quiet",
1140
+ FORCE: "force"
1141
+ };
1142
+
1143
1143
  // core/NoteIndexCache.ts
1144
1144
  import { join as join2 } from "path";
1145
1145
  var NoteIndexCache = class _NoteIndexCache {
@@ -1338,7 +1338,7 @@ var TocGenerator = class {
1338
1338
  titles.push(numberedTitle);
1339
1339
  lines[i] = numberedTitle;
1340
1340
  } else if (isUnnumberedHeader) {
1341
- const match = line.match(/^(\#+)\s*(\d+(\.\d+)*\.\s*)?(.*)/);
1341
+ const match = line.match(/^(#+)\s*(\d+(\.\d+)*\.\s*)?(.*)/);
1342
1342
  if (match) {
1343
1343
  const headerSymbol = match[1];
1344
1344
  const plainTitle = match[4];
@@ -1373,7 +1373,7 @@ var TocGenerator = class {
1373
1373
  });
1374
1374
  }
1375
1375
  if (noteConfig.yuque.length > 0) {
1376
- noteConfig.yuque.forEach((slug, i) => {
1376
+ noteConfig.yuque.forEach((slug) => {
1377
1377
  yuqueTOCItems.push(
1378
1378
  ` - [TNotes.yuque.${repoName2.replace(
1379
1379
  "TNotes.",
@@ -1416,9 +1416,9 @@ var TocGenerator = class {
1416
1416
  * 更新首页目录
1417
1417
  * @param lines - 首页内容行数组
1418
1418
  * @param titles - 标题数组
1419
- * @param titlesNotesCount - 每个标题下的笔记数量
1419
+ * @param _titlesNotesCount - 每个标题下的笔记数量
1420
1420
  */
1421
- updateHomeToc(lines, titles, titlesNotesCount) {
1421
+ updateHomeToc(lines, titles, _titlesNotesCount) {
1422
1422
  let startLineIdx = -1, endLineIdx = -1;
1423
1423
  lines.forEach((line, idx) => {
1424
1424
  if (line.startsWith(NOTES_TOC_START_TAG)) startLineIdx = idx;
@@ -1740,7 +1740,6 @@ var GitManager = class {
1740
1740
  didStash = await this.stash("Auto-stash before pull");
1741
1741
  }
1742
1742
  try {
1743
- const status = await this.getStatus();
1744
1743
  const beforeCommit = await runCommand("git rev-parse HEAD", this.dir);
1745
1744
  this.logger.info("\u6B63\u5728\u62C9\u53D6\u8FDC\u7A0B\u66F4\u65B0...");
1746
1745
  const cmd = `git pull ${rebase ? "--rebase" : ""} ${autostash ? "--autostash" : ""}`.trim();
@@ -2054,41 +2053,165 @@ var ProcessManager = class {
2054
2053
  }
2055
2054
  };
2056
2055
 
2057
- // commands/BaseCommand.ts
2058
- var BaseCommand = class {
2059
- constructor(name) {
2060
- this.name = name;
2061
- this.logger = logger.child(name);
2056
+ // services/file-watcher/configChangeHandler.ts
2057
+ var ConfigChangeHandler = class {
2058
+ constructor(config2) {
2059
+ this.config = config2;
2062
2060
  }
2063
- logger;
2064
- options = {};
2065
- /** 命令描述(从静态配置读取) */
2066
- get description() {
2067
- return COMMAND_DESCRIPTIONS[this.name];
2061
+ async handle(events) {
2062
+ if (events.length === 0) return [];
2063
+ const changedIndexes = [];
2064
+ const { state, noteService, noteIndexCache, logger: logger2 } = this.config;
2065
+ for (const change of events) {
2066
+ if (noteService.shouldIgnoreConfigChange(change.path)) {
2067
+ logger2.debug(`\u5FFD\u7565 API \u5199\u5165\u7684\u914D\u7F6E\u6587\u4EF6: ${change.path}`);
2068
+ continue;
2069
+ }
2070
+ const snapshot = state.readConfigSnapshot(change.path);
2071
+ if (!snapshot) continue;
2072
+ const cached = state.getConfigSnapshot(change.path);
2073
+ state.setConfigSnapshot(change.path, snapshot);
2074
+ noteIndexCache.updateConfig(change.noteIndex, snapshot);
2075
+ if (!cached) continue;
2076
+ const statusChanged = cached.done !== snapshot.done;
2077
+ const otherChanged = cached.enableDiscussions !== snapshot.enableDiscussions || cached.description !== snapshot.description;
2078
+ if (statusChanged) {
2079
+ changedIndexes.push(change.noteIndex);
2080
+ logger2.info(`\u68C0\u6D4B\u5230\u914D\u7F6E\u72B6\u6001\u53D8\u5316: done(${cached.done}\u2192${snapshot.done})`);
2081
+ } else if (otherChanged) {
2082
+ logger2.info("\u68C0\u6D4B\u5230\u914D\u7F6E\u975E\u72B6\u6001\u5B57\u6BB5\u53D8\u5316\uFF0C\u5DF2\u5237\u65B0\u7F13\u5B58\uFF08\u65E0\u9700\u5168\u5C40\u66F4\u65B0\uFF09");
2083
+ }
2084
+ }
2085
+ return changedIndexes;
2086
+ }
2087
+ };
2088
+
2089
+ // services/file-watcher/eventScheduler.ts
2090
+ var DEFAULT_DEBOUNCE_MS = 1e3;
2091
+ var DEFAULT_BATCH_WINDOW_MS = 1e3;
2092
+ var DEFAULT_BATCH_THRESHOLD = 3;
2093
+ var DEFAULT_BATCH_BUFFER_MS = 2e3;
2094
+ var EventScheduler = class {
2095
+ constructor(config2) {
2096
+ this.config = config2;
2097
+ this.debounceMs = config2.debounceMs ?? DEFAULT_DEBOUNCE_MS;
2098
+ this.batchWindowMs = config2.batchWindowMs ?? DEFAULT_BATCH_WINDOW_MS;
2099
+ this.batchThreshold = config2.batchThreshold ?? DEFAULT_BATCH_THRESHOLD;
2100
+ this.batchBufferMs = config2.batchBufferMs ?? DEFAULT_BATCH_BUFFER_MS;
2101
+ }
2102
+ /** 待处理的文件变更事件队列 */
2103
+ pendingEvents = /* @__PURE__ */ new Map();
2104
+ /** 防抖定时器 */
2105
+ updateTimer = null;
2106
+ /** 批量更新恢复定时器 */
2107
+ batchResumeTimer = null;
2108
+ /** 记录最近的变更时间戳 */
2109
+ recentChanges = [];
2110
+ /** 标记是否正在更新,避免循环触发 - 类似一把更新行为锁 */
2111
+ isUpdating = false;
2112
+ debounceMs;
2113
+ batchWindowMs;
2114
+ batchThreshold;
2115
+ batchBufferMs;
2116
+ /**
2117
+ * 设置更新状态锁,用于防止在执行耗时更新操作时被新的文件变更事件打断
2118
+ *
2119
+ * @param flag - true 表示正在更新(锁定),false 表示更新完成(解锁)
2120
+ */
2121
+ setUpdating(flag) {
2122
+ this.isUpdating = flag;
2068
2123
  }
2069
2124
  /**
2070
- * 设置命令选项
2125
+ * 获取当前是否处于更新锁定状态
2126
+ *
2127
+ * @returns true 表示正在执行更新操作(事件处理被暂停),false 表示空闲可处理新事件
2071
2128
  */
2072
- setOptions(options) {
2073
- this.options = { ...this.options, ...options };
2129
+ getUpdating() {
2130
+ return this.isUpdating;
2074
2131
  }
2075
2132
  /**
2076
- * 执行命令(带错误处理)
2133
+ * 将文件变更事件加入待处理队列,并启动防抖定时器
2134
+ *
2135
+ * - 若同一文件路径的事件已存在,则忽略重复事件(去重)
2136
+ * - 每次新事件都会重置防抖计时器,确保在变更停止后才触发处理
2137
+ *
2138
+ * @param event 文件变更事件
2077
2139
  */
2078
- async execute() {
2079
- const startTime = Date.now();
2080
- try {
2081
- this.logger.start(this.description);
2082
- await this.run();
2083
- const duration = Date.now() - startTime;
2084
- this.logger.done(`\u547D\u4EE4\u6267\u884C\u8017\u65F6\uFF1A${duration} ms`);
2085
- } catch (error) {
2086
- handleError(error);
2087
- throw error;
2140
+ enqueue(event) {
2141
+ if (this.pendingEvents.has(event.path)) return;
2142
+ this.pendingEvents.set(event.path, event);
2143
+ if (this.updateTimer) clearTimeout(this.updateTimer);
2144
+ this.updateTimer = setTimeout(() => this.flush(), this.debounceMs);
2145
+ }
2146
+ /**
2147
+ * 立即触发事件队列的处理(防抖到期或手动调用)
2148
+ *
2149
+ * - 若当前正在更新(isUpdating 为 true),则跳过以避免重复处理
2150
+ * - 清空待处理事件队列,并通过 onFlush 回调交由上层服务处理
2151
+ * - 处理开始后会锁定更新状态,防止处理过程中被新事件打断
2152
+ */
2153
+ flush() {
2154
+ if (this.isUpdating) return;
2155
+ if (this.pendingEvents.size === 0) return;
2156
+ const events = Array.from(this.pendingEvents.values());
2157
+ this.pendingEvents.clear();
2158
+ this.isUpdating = true;
2159
+ this.config.onFlush(events);
2160
+ }
2161
+ /**
2162
+ * 记录当前变更时间并检测是否触发批量更新模式
2163
+ *
2164
+ * - 维护一个滑动时间窗口(BATCH_UPDATE_WINDOW_MS)内的变更记录
2165
+ * - 若短时间内(1秒内)变更次数达到阈值(BATCH_UPDATE_THRESHOLD = 3),则判定为批量操作
2166
+ * - 触发批量模式后:
2167
+ * 1. 清空当前待处理事件队列,避免重复处理
2168
+ * 2. 锁定更新状态(isUpdating = true)
2169
+ * 3. 暂停监听服务,并在延迟(窗口 + 缓冲时间)后自动恢复
2170
+ *
2171
+ * @param now 当前时间戳(默认使用 Date.now())
2172
+ * @returns true 表示已触发批量更新模式,false 表示仍处于普通监听模式
2173
+ */
2174
+ recordChangeAndDetectBatch(now = Date.now()) {
2175
+ this.recentChanges.push(now);
2176
+ this.recentChanges = this.recentChanges.filter(
2177
+ (t) => now - t < this.batchWindowMs
2178
+ );
2179
+ if (this.recentChanges.length < this.batchThreshold) return false;
2180
+ this.pendingEvents.clear();
2181
+ this.recentChanges = [];
2182
+ this.isUpdating = true;
2183
+ this.config.onPauseForBatch();
2184
+ this.batchResumeTimer = setTimeout(() => {
2185
+ this.batchResumeTimer = null;
2186
+ this.isUpdating = false;
2187
+ this.config.reinit();
2188
+ this.config.onResumeAfterBatch();
2189
+ }, this.batchWindowMs + this.batchBufferMs);
2190
+ return true;
2191
+ }
2192
+ /**
2193
+ * 清理所有定时器,释放资源
2194
+ *
2195
+ * 在服务停止时调用,防止定时器在服务销毁后仍然触发回调
2196
+ */
2197
+ clearTimers() {
2198
+ if (this.updateTimer) {
2199
+ clearTimeout(this.updateTimer);
2200
+ this.updateTimer = null;
2201
+ }
2202
+ if (this.batchResumeTimer) {
2203
+ clearTimeout(this.batchResumeTimer);
2204
+ this.batchResumeTimer = null;
2088
2205
  }
2206
+ this.pendingEvents.clear();
2207
+ this.recentChanges = [];
2089
2208
  }
2090
2209
  };
2091
2210
 
2211
+ // services/file-watcher/folderChangeHandler.ts
2212
+ import { existsSync as existsSync3, promises as fsPromises } from "fs";
2213
+ import { join as join3 } from "path";
2214
+
2092
2215
  // services/file-watcher/internal.ts
2093
2216
  var WATCH_EVENT_TYPES = {
2094
2217
  README: "readme",
@@ -2104,448 +2227,11 @@ async function safeExecute(label, fn, logger2) {
2104
2227
  }
2105
2228
  }
2106
2229
 
2107
- // services/file-watcher/watchState.ts
2108
- import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync as readdirSync2, statSync } from "fs";
2109
- import { createHash } from "crypto";
2110
- import { join as join3 } from "path";
2111
- var WatchState = class {
2112
- constructor(config2) {
2113
- this.config = config2;
2114
- }
2115
- /** 文件哈希缓存 */
2116
- fileHashes = /* @__PURE__ */ new Map();
2117
- /** 笔记目录缓存 */
2118
- noteDirCache = /* @__PURE__ */ new Set();
2119
- /** 笔记配置缓存 */
2120
- configCache = /* @__PURE__ */ new Map();
2121
- /**
2122
- * 获取指定文件的 MD5 哈希值,若文件不存在或读取失败返回 null
2123
- *
2124
- * @param filePath 文件路径
2125
- * @returns 文件哈希
2126
- */
2127
- getFileHash(filePath) {
2128
- try {
2129
- if (!existsSync3(filePath)) return null;
2130
- const content = readFileSync3(filePath, "utf-8");
2131
- if (content.length === 0) return null;
2132
- return createHash("md5").update(content).digest("hex");
2133
- } catch {
2134
- return null;
2135
- }
2136
- }
2137
- /**
2138
- * 更新文件哈希缓存,只有当文件内容发生变化时才更新并返回 true
2139
- *
2140
- * @param filePath 文件路径
2141
- * @returns 是否发生变化
2142
- */
2143
- updateFileHash(filePath) {
2144
- const current = this.getFileHash(filePath);
2145
- if (!current) return false;
2146
- const prev = this.fileHashes.get(filePath);
2147
- if (prev === current) return false;
2148
- this.fileHashes.set(filePath, current);
2149
- return true;
2150
- }
2151
- /**
2152
- * 检查指定名称的笔记目录是否已存在于缓存中
2153
- *
2154
- * @param name 笔记目录名称
2155
- * @returns 若存在则返回 true,否则返回 false
2156
- */
2157
- hasNoteDir(name) {
2158
- return this.noteDirCache.has(name);
2159
- }
2160
- /**
2161
- * 将指定名称的笔记目录添加到缓存中
2162
- *
2163
- * @param name 笔记目录名称
2164
- */
2165
- addNoteDir(name) {
2166
- this.noteDirCache.add(name);
2167
- }
2168
- /**
2169
- * 从缓存中移除指定名称的笔记目录
2170
- *
2171
- * @param name 笔记目录名称
2172
- */
2173
- deleteNoteDir(name) {
2174
- this.noteDirCache.delete(name);
2175
- }
2176
- /**
2177
- * 清空所有缓存数据,包括文件哈希、笔记目录和配置快照
2178
- */
2179
- clearAll() {
2180
- this.fileHashes.clear();
2181
- this.noteDirCache.clear();
2182
- this.configCache.clear();
2183
- }
2184
- /**
2185
- * 清除指定笔记目录相关的缓存数据,包括 README.md 和 .tnotes.json 的文件哈希及配置快照
2186
- *
2187
- * @param noteDirName 笔记目录名称
2188
- */
2189
- clearNoteCaches(noteDirName) {
2190
- const readmePath = join3(this.config.notesDir, noteDirName, "README.md");
2191
- const configPath = join3(this.config.notesDir, noteDirName, ".tnotes.json");
2192
- this.fileHashes.delete(readmePath);
2193
- this.fileHashes.delete(configPath);
2194
- this.configCache.delete(configPath);
2195
- }
2196
- /**
2197
- * 获取指定配置文件路径对应的配置快照
2198
- *
2199
- * @param configPath 配置文件路径(通常为 .tnotes.json 的绝对路径)
2200
- * @returns 配置快照,若不存在则返回 undefined
2201
- */
2202
- getConfigSnapshot(configPath) {
2203
- return this.configCache.get(configPath);
2204
- }
2205
- /**
2206
- * 设置指定配置文件路径的配置快照到缓存中
2207
- *
2208
- * @param configPath 配置文件路径(通常为 .tnotes.json 的绝对路径)
2209
- * @param snapshot 配置快照对象
2210
- */
2211
- setConfigSnapshot(configPath, snapshot) {
2212
- this.configCache.set(configPath, snapshot);
2213
- }
2214
- /**
2215
- * 读取指定配置文件的快照
2216
- *
2217
- * 解析 .tnotes.json 配置文件,提取 done、enableDiscussions、description 字段。
2218
- *
2219
- * @param configPath 配置文件路径(通常为 .tnotes.json 的绝对路径)
2220
- * @returns 配置快照,若文件不存在或解析失败则返回 null
2221
- */
2222
- readConfigSnapshot(configPath) {
2223
- try {
2224
- if (!existsSync3(configPath)) return null;
2225
- const content = readFileSync3(configPath, "utf-8");
2226
- const config2 = JSON.parse(content);
2227
- return {
2228
- done: Boolean(config2.done),
2229
- enableDiscussions: Boolean(config2.enableDiscussions),
2230
- description: config2.description || ""
2231
- };
2232
- } catch (error) {
2233
- this.config.logger.error(`[\u8BFB\u53D6\u914D\u7F6E\u5FEB\u7167] ${error}`);
2234
- return null;
2235
- }
2236
- }
2237
- /**
2238
- * 从磁盘初始化监听状态缓存:
2239
- * 遍历笔记根目录下的所有子目录,将每个笔记目录的 README.md 和 .tnotes.json
2240
- * 的哈希值及配置快照加载到缓存中。
2241
- */
2242
- initializeFromDisk() {
2243
- try {
2244
- const noteDirs = readdirSync2(this.config.notesDir);
2245
- this.clearAll();
2246
- for (const noteDir of noteDirs) {
2247
- const noteDirPath = join3(this.config.notesDir, noteDir);
2248
- if (!statSync(noteDirPath).isDirectory()) continue;
2249
- this.noteDirCache.add(noteDir);
2250
- const readmePath = join3(noteDirPath, "README.md");
2251
- const readmeHash = this.getFileHash(readmePath);
2252
- if (readmeHash) this.fileHashes.set(readmePath, readmeHash);
2253
- const configPath = join3(noteDirPath, ".tnotes.json");
2254
- const configHash = this.getFileHash(configPath);
2255
- if (configHash) {
2256
- this.fileHashes.set(configPath, configHash);
2257
- const snapshot = this.readConfigSnapshot(configPath);
2258
- if (snapshot) this.configCache.set(configPath, snapshot);
2259
- }
2260
- }
2261
- } catch (error) {
2262
- this.config.logger.warn(
2263
- `[initializeFromDisk] \u521D\u59CB\u5316\u76D1\u542C\u72B6\u6001\u5931\u8D25: ${error}`
2264
- );
2265
- }
2266
- }
2267
- };
2268
-
2269
- // services/file-watcher/eventScheduler.ts
2270
- var DEFAULT_DEBOUNCE_MS = 1e3;
2271
- var DEFAULT_BATCH_WINDOW_MS = 1e3;
2272
- var DEFAULT_BATCH_THRESHOLD = 3;
2273
- var DEFAULT_BATCH_BUFFER_MS = 2e3;
2274
- var EventScheduler = class {
2275
- constructor(config2) {
2276
- this.config = config2;
2277
- this.debounceMs = config2.debounceMs ?? DEFAULT_DEBOUNCE_MS;
2278
- this.batchWindowMs = config2.batchWindowMs ?? DEFAULT_BATCH_WINDOW_MS;
2279
- this.batchThreshold = config2.batchThreshold ?? DEFAULT_BATCH_THRESHOLD;
2280
- this.batchBufferMs = config2.batchBufferMs ?? DEFAULT_BATCH_BUFFER_MS;
2281
- }
2282
- /** 待处理的文件变更事件队列 */
2283
- pendingEvents = /* @__PURE__ */ new Map();
2284
- /** 防抖定时器 */
2285
- updateTimer = null;
2286
- /** 批量更新恢复定时器 */
2287
- batchResumeTimer = null;
2288
- /** 记录最近的变更时间戳 */
2289
- recentChanges = [];
2290
- /** 标记是否正在更新,避免循环触发 - 类似一把更新行为锁 */
2291
- isUpdating = false;
2292
- debounceMs;
2293
- batchWindowMs;
2294
- batchThreshold;
2295
- batchBufferMs;
2296
- /**
2297
- * 设置更新状态锁,用于防止在执行耗时更新操作时被新的文件变更事件打断
2298
- *
2299
- * @param flag - true 表示正在更新(锁定),false 表示更新完成(解锁)
2300
- */
2301
- setUpdating(flag) {
2302
- this.isUpdating = flag;
2303
- }
2304
- /**
2305
- * 获取当前是否处于更新锁定状态
2306
- *
2307
- * @returns true 表示正在执行更新操作(事件处理被暂停),false 表示空闲可处理新事件
2308
- */
2309
- getUpdating() {
2310
- return this.isUpdating;
2311
- }
2312
- /**
2313
- * 将文件变更事件加入待处理队列,并启动防抖定时器
2314
- *
2315
- * - 若同一文件路径的事件已存在,则忽略重复事件(去重)
2316
- * - 每次新事件都会重置防抖计时器,确保在变更停止后才触发处理
2317
- *
2318
- * @param event 文件变更事件
2319
- */
2320
- enqueue(event) {
2321
- if (this.pendingEvents.has(event.path)) return;
2322
- this.pendingEvents.set(event.path, event);
2323
- if (this.updateTimer) clearTimeout(this.updateTimer);
2324
- this.updateTimer = setTimeout(() => this.flush(), this.debounceMs);
2325
- }
2326
- /**
2327
- * 立即触发事件队列的处理(防抖到期或手动调用)
2328
- *
2329
- * - 若当前正在更新(isUpdating 为 true),则跳过以避免重复处理
2330
- * - 清空待处理事件队列,并通过 onFlush 回调交由上层服务处理
2331
- * - 处理开始后会锁定更新状态,防止处理过程中被新事件打断
2332
- */
2333
- flush() {
2334
- if (this.isUpdating) return;
2335
- if (this.pendingEvents.size === 0) return;
2336
- const events = Array.from(this.pendingEvents.values());
2337
- this.pendingEvents.clear();
2338
- this.isUpdating = true;
2339
- this.config.onFlush(events);
2340
- }
2341
- /**
2342
- * 记录当前变更时间并检测是否触发批量更新模式
2343
- *
2344
- * - 维护一个滑动时间窗口(BATCH_UPDATE_WINDOW_MS)内的变更记录
2345
- * - 若短时间内(1秒内)变更次数达到阈值(BATCH_UPDATE_THRESHOLD = 3),则判定为批量操作
2346
- * - 触发批量模式后:
2347
- * 1. 清空当前待处理事件队列,避免重复处理
2348
- * 2. 锁定更新状态(isUpdating = true)
2349
- * 3. 暂停监听服务,并在延迟(窗口 + 缓冲时间)后自动恢复
2350
- *
2351
- * @param now 当前时间戳(默认使用 Date.now())
2352
- * @returns true 表示已触发批量更新模式,false 表示仍处于普通监听模式
2353
- */
2354
- recordChangeAndDetectBatch(now = Date.now()) {
2355
- this.recentChanges.push(now);
2356
- this.recentChanges = this.recentChanges.filter(
2357
- (t) => now - t < this.batchWindowMs
2358
- );
2359
- if (this.recentChanges.length < this.batchThreshold) return false;
2360
- this.pendingEvents.clear();
2361
- this.recentChanges = [];
2362
- this.isUpdating = true;
2363
- this.config.onPauseForBatch();
2364
- this.batchResumeTimer = setTimeout(() => {
2365
- this.batchResumeTimer = null;
2366
- this.isUpdating = false;
2367
- this.config.reinit();
2368
- this.config.onResumeAfterBatch();
2369
- }, this.batchWindowMs + this.batchBufferMs);
2370
- return true;
2371
- }
2372
- /**
2373
- * 清理所有定时器,释放资源
2374
- *
2375
- * 在服务停止时调用,防止定时器在服务销毁后仍然触发回调
2376
- */
2377
- clearTimers() {
2378
- if (this.updateTimer) {
2379
- clearTimeout(this.updateTimer);
2380
- this.updateTimer = null;
2381
- }
2382
- if (this.batchResumeTimer) {
2383
- clearTimeout(this.batchResumeTimer);
2384
- this.batchResumeTimer = null;
2385
- }
2386
- this.pendingEvents.clear();
2387
- this.recentChanges = [];
2388
- }
2389
- };
2390
-
2391
- // services/file-watcher/renameDetector.ts
2392
- import { existsSync as existsSync4 } from "fs";
2393
- import { join as join4 } from "path";
2394
- var FOLDER_RENAME_DETECT_WINDOW_MS = 500;
2395
- var RenameDetector = class {
2396
- constructor(config2) {
2397
- this.config = config2;
2398
- }
2399
- /** 待处理的文件夹重命名 */
2400
- pendingFolderRename = null;
2401
- /** 文件夹重命名检测定时器 */
2402
- folderRenameTimer = null;
2403
- handleFsRename(folderName) {
2404
- const { notesDir, dirCache, logger: logger2, onDelete, onRename } = this.config;
2405
- const folderPath = join4(notesDir, folderName);
2406
- const folderExists = existsSync4(folderPath);
2407
- const noteIndex = NoteManager.extractNoteIndex(folderName);
2408
- if (!noteIndex) {
2409
- logger2.warn(`\u65E0\u6CD5\u4ECE\u6587\u4EF6\u5939\u540D\u79F0\u63D0\u53D6\u7B14\u8BB0\u7D22\u5F15: ${folderName}`);
2410
- return;
2411
- }
2412
- if (!folderExists) {
2413
- if (dirCache.has(folderName)) {
2414
- logger2.info(`\u68C0\u6D4B\u5230\u6587\u4EF6\u5939\u5220\u9664/\u91CD\u547D\u540D: ${folderName}`);
2415
- this.pendingFolderRename = { oldName: folderName, time: Date.now() };
2416
- if (this.folderRenameTimer) clearTimeout(this.folderRenameTimer);
2417
- this.folderRenameTimer = setTimeout(() => {
2418
- if (this.pendingFolderRename) {
2419
- logger2.warn(`\u68C0\u6D4B\u5230\u7B14\u8BB0\u5220\u9664: ${this.pendingFolderRename.oldName}`);
2420
- onDelete(this.pendingFolderRename.oldName);
2421
- }
2422
- this.pendingFolderRename = null;
2423
- this.folderRenameTimer = null;
2424
- }, FOLDER_RENAME_DETECT_WINDOW_MS);
2425
- }
2426
- return;
2427
- }
2428
- if (!dirCache.has(folderName)) {
2429
- logger2.info(`\u68C0\u6D4B\u5230\u6587\u4EF6\u5939\u521B\u5EFA/\u91CD\u547D\u540D: ${folderName}`);
2430
- if (this.pendingFolderRename && Date.now() - this.pendingFolderRename.time < FOLDER_RENAME_DETECT_WINDOW_MS) {
2431
- const oldName = this.pendingFolderRename.oldName;
2432
- const oldNoteIndex = NoteManager.extractNoteIndex(oldName);
2433
- if (oldNoteIndex && oldNoteIndex === noteIndex) {
2434
- logger2.info(`\u68C0\u6D4B\u5230\u6587\u4EF6\u5939\u91CD\u547D\u540D: ${oldName} \u2192 ${folderName}`);
2435
- if (this.folderRenameTimer) {
2436
- clearTimeout(this.folderRenameTimer);
2437
- this.folderRenameTimer = null;
2438
- }
2439
- onRename(oldName, folderName);
2440
- this.pendingFolderRename = null;
2441
- } else if (oldNoteIndex && oldNoteIndex !== noteIndex) {
2442
- logger2.warn(`\u7D22\u5F15\u51B2\u7A81\uFF0C\u56DE\u9000: ${oldName} -> ${folderName}`);
2443
- }
2444
- }
2445
- dirCache.add(folderName);
2446
- if (this.pendingFolderRename) {
2447
- dirCache.delete(this.pendingFolderRename.oldName);
2448
- }
2449
- }
2450
- }
2451
- clearTimers() {
2452
- if (this.folderRenameTimer) {
2453
- clearTimeout(this.folderRenameTimer);
2454
- this.folderRenameTimer = null;
2455
- }
2456
- this.pendingFolderRename = null;
2457
- }
2458
- };
2459
-
2460
- // services/file-watcher/configChangeHandler.ts
2461
- var ConfigChangeHandler = class {
2462
- constructor(config2) {
2463
- this.config = config2;
2464
- }
2465
- async handle(events) {
2466
- if (events.length === 0) return [];
2467
- const changedIndexes = [];
2468
- const { state, noteService, noteIndexCache, logger: logger2 } = this.config;
2469
- for (const change of events) {
2470
- if (noteService.shouldIgnoreConfigChange(change.path)) {
2471
- logger2.debug(`\u5FFD\u7565 API \u5199\u5165\u7684\u914D\u7F6E\u6587\u4EF6: ${change.path}`);
2472
- continue;
2473
- }
2474
- const snapshot = state.readConfigSnapshot(change.path);
2475
- if (!snapshot) continue;
2476
- const cached = state.getConfigSnapshot(change.path);
2477
- state.setConfigSnapshot(change.path, snapshot);
2478
- noteIndexCache.updateConfig(change.noteIndex, snapshot);
2479
- if (!cached) continue;
2480
- const statusChanged = cached.done !== snapshot.done;
2481
- const otherChanged = cached.enableDiscussions !== snapshot.enableDiscussions || cached.description !== snapshot.description;
2482
- if (statusChanged) {
2483
- changedIndexes.push(change.noteIndex);
2484
- logger2.info(`\u68C0\u6D4B\u5230\u914D\u7F6E\u72B6\u6001\u53D8\u5316: done(${cached.done}\u2192${snapshot.done})`);
2485
- } else if (otherChanged) {
2486
- logger2.info("\u68C0\u6D4B\u5230\u914D\u7F6E\u975E\u72B6\u6001\u5B57\u6BB5\u53D8\u5316\uFF0C\u5DF2\u5237\u65B0\u7F13\u5B58\uFF08\u65E0\u9700\u5168\u5C40\u66F4\u65B0\uFF09");
2487
- }
2488
- }
2489
- return changedIndexes;
2490
- }
2491
- };
2492
-
2493
- // services/file-watcher/readmeChangeHandler.ts
2494
- var ReadmeChangeHandler = class {
2495
- constructor(config2) {
2496
- this.config = config2;
2497
- }
2498
- async handle(events) {
2499
- if (events.length === 0) return;
2500
- const indexes = [...new Set(events.map((c) => c.noteIndex))];
2501
- for (const noteIndex of indexes) {
2502
- const noteInfo = this.config.noteService.getNoteByIndex(noteIndex);
2503
- if (noteInfo) {
2504
- await this.config.noteService.fixNoteTitle(noteInfo);
2505
- }
2506
- }
2507
- }
2508
- };
2509
-
2510
- // services/file-watcher/globalUpdateCoordinator.ts
2511
- var GlobalUpdateCoordinator = class {
2512
- constructor(config2) {
2513
- this.config = config2;
2514
- }
2515
- async applyConfigUpdates(changedNoteIndexes) {
2516
- if (changedNoteIndexes.length === 0) return;
2517
- const { readmeService, noteIndexCache, logger: logger2 } = this.config;
2518
- logger2.info("\u68C0\u6D4B\u5230\u7B14\u8BB0\u72B6\u6001\u53D8\u5316\uFF0C\u589E\u91CF\u66F4\u65B0\u5168\u5C40\u6587\u4EF6...");
2519
- for (const noteIndex of changedNoteIndexes) {
2520
- await safeExecute(
2521
- `\u589E\u91CF\u66F4\u65B0 ${noteIndex}`,
2522
- async () => {
2523
- const item = noteIndexCache.getByNoteIndex(noteIndex);
2524
- await readmeService.updateNoteInReadme(
2525
- noteIndex,
2526
- item?.noteConfig || {}
2527
- );
2528
- logger2.info(`\u589E\u91CF\u66F4\u65B0 README \u4E2D\u7684\u7B14\u8BB0: ${noteIndex}`);
2529
- },
2530
- logger2
2531
- );
2532
- }
2533
- await readmeService.regenerateSidebar();
2534
- }
2535
- async updateNoteReadmesOnly(events) {
2536
- const noteIndexesToUpdate = [...new Set(events.map((c) => c.noteIndex))];
2537
- if (noteIndexesToUpdate.length === 0) return;
2538
- await this.config.readmeService.updateNoteReadmesOnly(noteIndexesToUpdate);
2539
- }
2540
- };
2541
-
2542
- // services/file-watcher/folderChangeHandler.ts
2543
- import { existsSync as existsSync5, promises as fsPromises } from "fs";
2544
- import { join as join5 } from "path";
2545
- var RENAME_REVERT_DELAY_MS = 2e3;
2546
- var DELETE_REINIT_DELAY_MS = 1e3;
2547
- var UPDATE_UNLOCK_DELAY_MS = 500;
2548
- var FolderChangeHandler = class {
2230
+ // services/file-watcher/folderChangeHandler.ts
2231
+ var RENAME_REVERT_DELAY_MS = 2e3;
2232
+ var DELETE_REINIT_DELAY_MS = 1e3;
2233
+ var UPDATE_UNLOCK_DELAY_MS = 500;
2234
+ var FolderChangeHandler = class {
2549
2235
  constructor(config2) {
2550
2236
  this.config = config2;
2551
2237
  }
@@ -2672,104 +2358,396 @@ var FolderChangeHandler = class {
2672
2358
  logger2.error(`\u672A\u627E\u5230\u65B0\u7B14\u8BB0: ${newNoteIndex}`);
2673
2359
  }
2674
2360
  }
2675
- async revertFolderRename(oldName, newName) {
2676
- const { notesDir, scheduler, watchState, logger: logger2 } = this.config;
2677
- try {
2678
- const oldPath = join5(notesDir, oldName);
2679
- const newPath = join5(notesDir, newName);
2680
- if (existsSync5(newPath)) {
2681
- scheduler.setUpdating(true);
2682
- await fsPromises.rename(newPath, oldPath);
2683
- logger2.warn(`\u6587\u4EF6\u5939\u5DF2\u56DE\u9000: ${newName} \u2192 ${oldName}`);
2684
- this.scheduleTimer(() => {
2685
- scheduler.setUpdating(false);
2686
- watchState.initializeFromDisk();
2687
- }, RENAME_REVERT_DELAY_MS);
2688
- }
2689
- } catch (error) {
2690
- logger2.error(`\u56DE\u9000\u6587\u4EF6\u5939\u91CD\u547D\u540D\u5931\u8D25: ${error}`);
2691
- }
2361
+ async revertFolderRename(oldName, newName) {
2362
+ const { notesDir, scheduler, watchState, logger: logger2 } = this.config;
2363
+ try {
2364
+ const oldPath = join3(notesDir, oldName);
2365
+ const newPath = join3(notesDir, newName);
2366
+ if (existsSync3(newPath)) {
2367
+ scheduler.setUpdating(true);
2368
+ await fsPromises.rename(newPath, oldPath);
2369
+ logger2.warn(`\u6587\u4EF6\u5939\u5DF2\u56DE\u9000: ${newName} \u2192 ${oldName}`);
2370
+ this.scheduleTimer(() => {
2371
+ scheduler.setUpdating(false);
2372
+ watchState.initializeFromDisk();
2373
+ }, RENAME_REVERT_DELAY_MS);
2374
+ }
2375
+ } catch (error) {
2376
+ logger2.error(`\u56DE\u9000\u6587\u4EF6\u5939\u91CD\u547D\u540D\u5931\u8D25: ${error}`);
2377
+ }
2378
+ }
2379
+ extractNoteIndexOrWarn(name) {
2380
+ const noteIndex = name.match(/^(\d{4})/)?.[1] || null;
2381
+ if (!noteIndex) {
2382
+ this.config.logger.warn(`\u65E0\u6CD5\u4ECE\u6587\u4EF6\u5939\u540D\u79F0\u63D0\u53D6\u7B14\u8BB0\u7D22\u5F15: ${name}`);
2383
+ }
2384
+ return noteIndex;
2385
+ }
2386
+ // #endregion - 私有实现
2387
+ };
2388
+
2389
+ // services/file-watcher/fsWatcherAdapter.ts
2390
+ import { watch } from "fs";
2391
+ import { basename, dirname, join as join4, sep } from "path";
2392
+ var FsWatcherAdapter = class {
2393
+ constructor(config2) {
2394
+ this.config = config2;
2395
+ }
2396
+ /** 文件系统监听器实例 */
2397
+ watcher = null;
2398
+ start() {
2399
+ const { logger: logger2 } = this.config;
2400
+ if (this.watcher) {
2401
+ logger2.warn("\u6587\u4EF6\u76D1\u542C\u670D\u52A1\u5DF2\u542F\u52A8");
2402
+ return;
2403
+ }
2404
+ this.watcher = watch(
2405
+ this.config.notesDir,
2406
+ { recursive: true },
2407
+ (eventType, filename) => this.handleFsEvent(eventType, filename)
2408
+ );
2409
+ logger2.debug(`\u6587\u4EF6\u76D1\u542C\u5DF2\u542F\u52A8\uFF0C\u76D1\u542C\u76EE\u5F55\uFF1A${this.config.notesDir}`);
2410
+ }
2411
+ stop() {
2412
+ if (!this.watcher) return;
2413
+ this.watcher.close();
2414
+ this.watcher = null;
2415
+ }
2416
+ isWatching() {
2417
+ return this.watcher !== null;
2418
+ }
2419
+ handleFsEvent(eventType, filename) {
2420
+ const { isUpdating, onRename, onNoteEvent } = this.config;
2421
+ if (!filename || // 忽略无文件变更
2422
+ isUpdating()) {
2423
+ return;
2424
+ }
2425
+ if (eventType === "rename" && // 检测文件夹 rename 事件
2426
+ !filename.includes(sep)) {
2427
+ onRename(filename);
2428
+ return;
2429
+ }
2430
+ const baseFilename = basename(filename);
2431
+ if (baseFilename !== "README.md" && baseFilename !== ".tnotes.json") {
2432
+ return;
2433
+ }
2434
+ const fullPath = join4(this.config.notesDir, filename);
2435
+ const event = this.buildWatchEvent(fullPath, filename);
2436
+ if (!event) {
2437
+ return;
2438
+ }
2439
+ onNoteEvent(event);
2440
+ }
2441
+ buildWatchEvent(fullPath, filename) {
2442
+ const noteDirName = basename(dirname(fullPath));
2443
+ const noteIndex = NoteManager.extractNoteIndex(noteDirName);
2444
+ if (!noteIndex) {
2445
+ NoteManager.warnInvalidNoteIndex(noteDirName);
2446
+ return null;
2447
+ }
2448
+ const fileType = filename.endsWith("README.md") ? WATCH_EVENT_TYPES.README : WATCH_EVENT_TYPES.CONFIG;
2449
+ return {
2450
+ path: fullPath,
2451
+ type: fileType,
2452
+ noteIndex,
2453
+ noteDirName,
2454
+ noteDirPath: dirname(fullPath)
2455
+ };
2456
+ }
2457
+ };
2458
+
2459
+ // services/file-watcher/globalUpdateCoordinator.ts
2460
+ var GlobalUpdateCoordinator = class {
2461
+ constructor(config2) {
2462
+ this.config = config2;
2463
+ }
2464
+ async applyConfigUpdates(changedNoteIndexes) {
2465
+ if (changedNoteIndexes.length === 0) return;
2466
+ const { readmeService, noteIndexCache, logger: logger2 } = this.config;
2467
+ logger2.info("\u68C0\u6D4B\u5230\u7B14\u8BB0\u72B6\u6001\u53D8\u5316\uFF0C\u589E\u91CF\u66F4\u65B0\u5168\u5C40\u6587\u4EF6...");
2468
+ for (const noteIndex of changedNoteIndexes) {
2469
+ await safeExecute(
2470
+ `\u589E\u91CF\u66F4\u65B0 ${noteIndex}`,
2471
+ async () => {
2472
+ const item = noteIndexCache.getByNoteIndex(noteIndex);
2473
+ await readmeService.updateNoteInReadme(
2474
+ noteIndex,
2475
+ item?.noteConfig || {}
2476
+ );
2477
+ logger2.info(`\u589E\u91CF\u66F4\u65B0 README \u4E2D\u7684\u7B14\u8BB0: ${noteIndex}`);
2478
+ },
2479
+ logger2
2480
+ );
2481
+ }
2482
+ await readmeService.regenerateSidebar();
2483
+ }
2484
+ async updateNoteReadmesOnly(events) {
2485
+ const noteIndexesToUpdate = [...new Set(events.map((c) => c.noteIndex))];
2486
+ if (noteIndexesToUpdate.length === 0) return;
2487
+ await this.config.readmeService.updateNoteReadmesOnly(noteIndexesToUpdate);
2488
+ }
2489
+ };
2490
+
2491
+ // services/file-watcher/readmeChangeHandler.ts
2492
+ var ReadmeChangeHandler = class {
2493
+ constructor(config2) {
2494
+ this.config = config2;
2495
+ }
2496
+ async handle(events) {
2497
+ if (events.length === 0) return;
2498
+ const indexes = [...new Set(events.map((c) => c.noteIndex))];
2499
+ for (const noteIndex of indexes) {
2500
+ const noteInfo = this.config.noteService.getNoteByIndex(noteIndex);
2501
+ if (noteInfo) {
2502
+ await this.config.noteService.fixNoteTitle(noteInfo);
2503
+ }
2504
+ }
2505
+ }
2506
+ };
2507
+
2508
+ // services/file-watcher/renameDetector.ts
2509
+ import { existsSync as existsSync4 } from "fs";
2510
+ import { join as join5 } from "path";
2511
+ var FOLDER_RENAME_DETECT_WINDOW_MS = 500;
2512
+ var RenameDetector = class {
2513
+ constructor(config2) {
2514
+ this.config = config2;
2515
+ }
2516
+ /** 待处理的文件夹重命名 */
2517
+ pendingFolderRename = null;
2518
+ /** 文件夹重命名检测定时器 */
2519
+ folderRenameTimer = null;
2520
+ handleFsRename(folderName) {
2521
+ const { notesDir, dirCache, logger: logger2, onDelete, onRename } = this.config;
2522
+ const folderPath = join5(notesDir, folderName);
2523
+ const folderExists = existsSync4(folderPath);
2524
+ const noteIndex = NoteManager.extractNoteIndex(folderName);
2525
+ if (!noteIndex) {
2526
+ logger2.warn(`\u65E0\u6CD5\u4ECE\u6587\u4EF6\u5939\u540D\u79F0\u63D0\u53D6\u7B14\u8BB0\u7D22\u5F15: ${folderName}`);
2527
+ return;
2528
+ }
2529
+ if (!folderExists) {
2530
+ if (dirCache.has(folderName)) {
2531
+ logger2.info(`\u68C0\u6D4B\u5230\u6587\u4EF6\u5939\u5220\u9664/\u91CD\u547D\u540D: ${folderName}`);
2532
+ this.pendingFolderRename = { oldName: folderName, time: Date.now() };
2533
+ if (this.folderRenameTimer) clearTimeout(this.folderRenameTimer);
2534
+ this.folderRenameTimer = setTimeout(() => {
2535
+ if (this.pendingFolderRename) {
2536
+ logger2.warn(`\u68C0\u6D4B\u5230\u7B14\u8BB0\u5220\u9664: ${this.pendingFolderRename.oldName}`);
2537
+ onDelete(this.pendingFolderRename.oldName);
2538
+ }
2539
+ this.pendingFolderRename = null;
2540
+ this.folderRenameTimer = null;
2541
+ }, FOLDER_RENAME_DETECT_WINDOW_MS);
2542
+ }
2543
+ return;
2544
+ }
2545
+ if (!dirCache.has(folderName)) {
2546
+ logger2.info(`\u68C0\u6D4B\u5230\u6587\u4EF6\u5939\u521B\u5EFA/\u91CD\u547D\u540D: ${folderName}`);
2547
+ if (this.pendingFolderRename && Date.now() - this.pendingFolderRename.time < FOLDER_RENAME_DETECT_WINDOW_MS) {
2548
+ const oldName = this.pendingFolderRename.oldName;
2549
+ const oldNoteIndex = NoteManager.extractNoteIndex(oldName);
2550
+ if (oldNoteIndex && oldNoteIndex === noteIndex) {
2551
+ logger2.info(`\u68C0\u6D4B\u5230\u6587\u4EF6\u5939\u91CD\u547D\u540D: ${oldName} \u2192 ${folderName}`);
2552
+ if (this.folderRenameTimer) {
2553
+ clearTimeout(this.folderRenameTimer);
2554
+ this.folderRenameTimer = null;
2555
+ }
2556
+ onRename(oldName, folderName);
2557
+ this.pendingFolderRename = null;
2558
+ } else if (oldNoteIndex && oldNoteIndex !== noteIndex) {
2559
+ logger2.warn(`\u7D22\u5F15\u51B2\u7A81\uFF0C\u56DE\u9000: ${oldName} -> ${folderName}`);
2560
+ }
2561
+ }
2562
+ dirCache.add(folderName);
2563
+ if (this.pendingFolderRename) {
2564
+ dirCache.delete(this.pendingFolderRename.oldName);
2565
+ }
2566
+ }
2567
+ }
2568
+ clearTimers() {
2569
+ if (this.folderRenameTimer) {
2570
+ clearTimeout(this.folderRenameTimer);
2571
+ this.folderRenameTimer = null;
2572
+ }
2573
+ this.pendingFolderRename = null;
2574
+ }
2575
+ };
2576
+
2577
+ // services/file-watcher/watchState.ts
2578
+ import { createHash } from "crypto";
2579
+ import { existsSync as existsSync5, readFileSync as readFileSync3, readdirSync as readdirSync2, statSync } from "fs";
2580
+ import { join as join6 } from "path";
2581
+ var WatchState = class {
2582
+ constructor(config2) {
2583
+ this.config = config2;
2584
+ }
2585
+ /** 文件哈希缓存 */
2586
+ fileHashes = /* @__PURE__ */ new Map();
2587
+ /** 笔记目录缓存 */
2588
+ noteDirCache = /* @__PURE__ */ new Set();
2589
+ /** 笔记配置缓存 */
2590
+ configCache = /* @__PURE__ */ new Map();
2591
+ /**
2592
+ * 获取指定文件的 MD5 哈希值,若文件不存在或读取失败返回 null
2593
+ *
2594
+ * @param filePath 文件路径
2595
+ * @returns 文件哈希
2596
+ */
2597
+ getFileHash(filePath) {
2598
+ try {
2599
+ if (!existsSync5(filePath)) return null;
2600
+ const content = readFileSync3(filePath, "utf-8");
2601
+ if (content.length === 0) return null;
2602
+ return createHash("md5").update(content).digest("hex");
2603
+ } catch {
2604
+ return null;
2605
+ }
2606
+ }
2607
+ /**
2608
+ * 更新文件哈希缓存,只有当文件内容发生变化时才更新并返回 true
2609
+ *
2610
+ * @param filePath 文件路径
2611
+ * @returns 是否发生变化
2612
+ */
2613
+ updateFileHash(filePath) {
2614
+ const current = this.getFileHash(filePath);
2615
+ if (!current) return false;
2616
+ const prev = this.fileHashes.get(filePath);
2617
+ if (prev === current) return false;
2618
+ this.fileHashes.set(filePath, current);
2619
+ return true;
2620
+ }
2621
+ /**
2622
+ * 检查指定名称的笔记目录是否已存在于缓存中
2623
+ *
2624
+ * @param name 笔记目录名称
2625
+ * @returns 若存在则返回 true,否则返回 false
2626
+ */
2627
+ hasNoteDir(name) {
2628
+ return this.noteDirCache.has(name);
2692
2629
  }
2693
- extractNoteIndexOrWarn(name) {
2694
- const noteIndex = name.match(/^(\d{4})/)?.[1] || null;
2695
- if (!noteIndex) {
2696
- this.config.logger.warn(`\u65E0\u6CD5\u4ECE\u6587\u4EF6\u5939\u540D\u79F0\u63D0\u53D6\u7B14\u8BB0\u7D22\u5F15: ${name}`);
2697
- }
2698
- return noteIndex;
2630
+ /**
2631
+ * 将指定名称的笔记目录添加到缓存中
2632
+ *
2633
+ * @param name 笔记目录名称
2634
+ */
2635
+ addNoteDir(name) {
2636
+ this.noteDirCache.add(name);
2699
2637
  }
2700
- // #endregion - 私有实现
2701
- };
2702
-
2703
- // services/file-watcher/fsWatcherAdapter.ts
2704
- import { watch } from "fs";
2705
- import { basename, dirname, join as join6, sep } from "path";
2706
- var FsWatcherAdapter = class {
2707
- constructor(config2) {
2708
- this.config = config2;
2638
+ /**
2639
+ * 从缓存中移除指定名称的笔记目录
2640
+ *
2641
+ * @param name 笔记目录名称
2642
+ */
2643
+ deleteNoteDir(name) {
2644
+ this.noteDirCache.delete(name);
2709
2645
  }
2710
- /** 文件系统监听器实例 */
2711
- watcher = null;
2712
- start() {
2713
- const { logger: logger2 } = this.config;
2714
- if (this.watcher) {
2715
- logger2.warn("\u6587\u4EF6\u76D1\u542C\u670D\u52A1\u5DF2\u542F\u52A8");
2716
- return;
2717
- }
2718
- this.watcher = watch(
2719
- this.config.notesDir,
2720
- { recursive: true },
2721
- (eventType, filename) => this.handleFsEvent(eventType, filename)
2722
- );
2723
- logger2.debug(`\u6587\u4EF6\u76D1\u542C\u5DF2\u542F\u52A8\uFF0C\u76D1\u542C\u76EE\u5F55\uFF1A${this.config.notesDir}`);
2646
+ /**
2647
+ * 清空所有缓存数据,包括文件哈希、笔记目录和配置快照
2648
+ */
2649
+ clearAll() {
2650
+ this.fileHashes.clear();
2651
+ this.noteDirCache.clear();
2652
+ this.configCache.clear();
2724
2653
  }
2725
- stop() {
2726
- if (!this.watcher) return;
2727
- this.watcher.close();
2728
- this.watcher = null;
2654
+ /**
2655
+ * 清除指定笔记目录相关的缓存数据,包括 README.md 和 .tnotes.json 的文件哈希及配置快照
2656
+ *
2657
+ * @param noteDirName 笔记目录名称
2658
+ */
2659
+ clearNoteCaches(noteDirName) {
2660
+ const readmePath = join6(this.config.notesDir, noteDirName, "README.md");
2661
+ const configPath = join6(this.config.notesDir, noteDirName, ".tnotes.json");
2662
+ this.fileHashes.delete(readmePath);
2663
+ this.fileHashes.delete(configPath);
2664
+ this.configCache.delete(configPath);
2729
2665
  }
2730
- isWatching() {
2731
- return this.watcher !== null;
2666
+ /**
2667
+ * 获取指定配置文件路径对应的配置快照
2668
+ *
2669
+ * @param configPath 配置文件路径(通常为 .tnotes.json 的绝对路径)
2670
+ * @returns 配置快照,若不存在则返回 undefined
2671
+ */
2672
+ getConfigSnapshot(configPath) {
2673
+ return this.configCache.get(configPath);
2732
2674
  }
2733
- handleFsEvent(eventType, filename) {
2734
- const { isUpdating, onRename, onNoteEvent } = this.config;
2735
- if (!filename || // 忽略无文件变更
2736
- isUpdating()) {
2737
- return;
2738
- }
2739
- if (eventType === "rename" && // 检测文件夹 rename 事件
2740
- !filename.includes(sep)) {
2741
- onRename(filename);
2742
- return;
2743
- }
2744
- const baseFilename = basename(filename);
2745
- if (baseFilename !== "README.md" && baseFilename !== ".tnotes.json") {
2746
- return;
2747
- }
2748
- const fullPath = join6(this.config.notesDir, filename);
2749
- const event = this.buildWatchEvent(fullPath, filename);
2750
- if (!event) {
2751
- return;
2752
- }
2753
- onNoteEvent(event);
2675
+ /**
2676
+ * 设置指定配置文件路径的配置快照到缓存中
2677
+ *
2678
+ * @param configPath 配置文件路径(通常为 .tnotes.json 的绝对路径)
2679
+ * @param snapshot 配置快照对象
2680
+ */
2681
+ setConfigSnapshot(configPath, snapshot) {
2682
+ this.configCache.set(configPath, snapshot);
2754
2683
  }
2755
- buildWatchEvent(fullPath, filename) {
2756
- const noteDirName = basename(dirname(fullPath));
2757
- const noteIndex = NoteManager.extractNoteIndex(noteDirName);
2758
- if (!noteIndex) {
2759
- NoteManager.warnInvalidNoteIndex(noteDirName);
2684
+ /**
2685
+ * 读取指定配置文件的快照
2686
+ *
2687
+ * 解析 .tnotes.json 配置文件,提取 done、enableDiscussions、description 字段。
2688
+ *
2689
+ * @param configPath 配置文件路径(通常为 .tnotes.json 的绝对路径)
2690
+ * @returns 配置快照,若文件不存在或解析失败则返回 null
2691
+ */
2692
+ readConfigSnapshot(configPath) {
2693
+ try {
2694
+ if (!existsSync5(configPath)) return null;
2695
+ const content = readFileSync3(configPath, "utf-8");
2696
+ const config2 = JSON.parse(content);
2697
+ return {
2698
+ done: Boolean(config2.done),
2699
+ enableDiscussions: Boolean(config2.enableDiscussions),
2700
+ description: config2.description || ""
2701
+ };
2702
+ } catch (error) {
2703
+ this.config.logger.error(`[\u8BFB\u53D6\u914D\u7F6E\u5FEB\u7167] ${error}`);
2760
2704
  return null;
2761
2705
  }
2762
- const fileType = filename.endsWith("README.md") ? WATCH_EVENT_TYPES.README : WATCH_EVENT_TYPES.CONFIG;
2763
- return {
2764
- path: fullPath,
2765
- type: fileType,
2766
- noteIndex,
2767
- noteDirName,
2768
- noteDirPath: dirname(fullPath)
2769
- };
2706
+ }
2707
+ /**
2708
+ * 从磁盘初始化监听状态缓存:
2709
+ * 遍历笔记根目录下的所有子目录,将每个笔记目录的 README.md 和 .tnotes.json
2710
+ * 的哈希值及配置快照加载到缓存中。
2711
+ */
2712
+ initializeFromDisk() {
2713
+ try {
2714
+ const noteDirs = readdirSync2(this.config.notesDir);
2715
+ this.clearAll();
2716
+ for (const noteDir of noteDirs) {
2717
+ const noteDirPath = join6(this.config.notesDir, noteDir);
2718
+ if (!statSync(noteDirPath).isDirectory()) continue;
2719
+ this.noteDirCache.add(noteDir);
2720
+ const readmePath = join6(noteDirPath, "README.md");
2721
+ const readmeHash = this.getFileHash(readmePath);
2722
+ if (readmeHash) this.fileHashes.set(readmePath, readmeHash);
2723
+ const configPath = join6(noteDirPath, ".tnotes.json");
2724
+ const configHash = this.getFileHash(configPath);
2725
+ if (configHash) {
2726
+ this.fileHashes.set(configPath, configHash);
2727
+ const snapshot = this.readConfigSnapshot(configPath);
2728
+ if (snapshot) this.configCache.set(configPath, snapshot);
2729
+ }
2730
+ }
2731
+ } catch (error) {
2732
+ this.config.logger.warn(
2733
+ `[initializeFromDisk] \u521D\u59CB\u5316\u76D1\u542C\u72B6\u6001\u5931\u8D25: ${error}`
2734
+ );
2735
+ }
2770
2736
  }
2771
2737
  };
2772
2738
 
2739
+ // services/note/service.ts
2740
+ import { writeFileSync as writeFileSync4, readFileSync as readFileSync5 } from "fs";
2741
+ import { join as join7 } from "path";
2742
+ import { v4 as uuidv4 } from "uuid";
2743
+
2744
+ // config/templates.ts
2745
+ function generateNoteTitle(noteIndex, title, repoUrl) {
2746
+ const dirName = `${noteIndex}. ${title}`;
2747
+ const encodedDirName = encodeURIComponent(dirName);
2748
+ return `# [${dirName}](${repoUrl}/${encodedDirName})`;
2749
+ }
2750
+
2773
2751
  // services/readme/service.ts
2774
2752
  import {
2775
2753
  existsSync as existsSync6,
@@ -3091,18 +3069,6 @@ var ReadmeService = class _ReadmeService {
3091
3069
  }
3092
3070
  };
3093
3071
 
3094
- // services/note/service.ts
3095
- import { writeFileSync as writeFileSync4, readFileSync as readFileSync5 } from "fs";
3096
- import { join as join7 } from "path";
3097
- import { v4 as uuidv4 } from "uuid";
3098
-
3099
- // config/templates.ts
3100
- function generateNoteTitle(noteIndex, title, repoUrl) {
3101
- const dirName = `${noteIndex}. ${title}`;
3102
- const encodedDirName = encodeURIComponent(dirName);
3103
- return `# [${dirName}](${repoUrl}/${encodedDirName})`;
3104
- }
3105
-
3106
3072
  // services/note/service.ts
3107
3073
  var NEW_NOTES_README_MD_TEMPLATE = `
3108
3074
  <!-- region:toc -->
@@ -3626,6 +3592,7 @@ var GitService = class {
3626
3592
  };
3627
3593
 
3628
3594
  // services/timestamp/service.ts
3595
+ import { execSync as execSync3 } from "child_process";
3629
3596
  import {
3630
3597
  existsSync as existsSync7,
3631
3598
  readFileSync as readFileSync6,
@@ -3634,7 +3601,6 @@ import {
3634
3601
  statSync as statSync2
3635
3602
  } from "fs";
3636
3603
  import { join as join8 } from "path";
3637
- import { execSync as execSync3 } from "child_process";
3638
3604
  var BIRTH_DATE = (/* @__PURE__ */ new Date("1999-06-29T00:00:00+08:00")).getTime();
3639
3605
  var TimestampService = class {
3640
3606
  noteManager;
@@ -4146,6 +4112,41 @@ var VitepressService = class _VitepressService {
4146
4112
  }
4147
4113
  };
4148
4114
 
4115
+ // commands/BaseCommand.ts
4116
+ var BaseCommand = class {
4117
+ constructor(name) {
4118
+ this.name = name;
4119
+ this.logger = logger.child(name);
4120
+ }
4121
+ logger;
4122
+ options = {};
4123
+ /** 命令描述(从静态配置读取) */
4124
+ get description() {
4125
+ return COMMAND_DESCRIPTIONS[this.name];
4126
+ }
4127
+ /**
4128
+ * 设置命令选项
4129
+ */
4130
+ setOptions(options) {
4131
+ this.options = { ...this.options, ...options };
4132
+ }
4133
+ /**
4134
+ * 执行命令(带错误处理)
4135
+ */
4136
+ async execute() {
4137
+ const startTime = Date.now();
4138
+ try {
4139
+ this.logger.start(this.description);
4140
+ await this.run();
4141
+ const duration = Date.now() - startTime;
4142
+ this.logger.done(`\u547D\u4EE4\u6267\u884C\u8017\u65F6\uFF1A${duration} ms`);
4143
+ } catch (error) {
4144
+ handleError(error);
4145
+ throw error;
4146
+ }
4147
+ }
4148
+ };
4149
+
4149
4150
  // commands/note/UpdateNoteConfigCommand.ts
4150
4151
  var UpdateNoteConfigCommand = class extends BaseCommand {
4151
4152
  noteService;
@@ -4295,20 +4296,16 @@ var RenameNoteCommand = class extends BaseCommand {
4295
4296
  };
4296
4297
 
4297
4298
  export {
4298
- COMMAND_NAMES,
4299
- COMMAND_DESCRIPTIONS,
4300
- COMMAND_OPTIONS,
4299
+ ROOT_DIR_PATH,
4300
+ ROOT_CONFIG_PATH,
4301
4301
  handleError,
4302
4302
  generateAnchor,
4303
4303
  logger,
4304
4304
  createLogger,
4305
4305
  parseArgs,
4306
4306
  parseReadmeCompletedNotes,
4307
- ROOT_DIR_PATH,
4308
- ROOT_CONFIG_PATH,
4309
- NoteManager,
4310
4307
  runCommand,
4311
- BaseCommand,
4308
+ NoteManager,
4312
4309
  NoteIndexCache,
4313
4310
  ReadmeService,
4314
4311
  NoteService,
@@ -4316,6 +4313,10 @@ export {
4316
4313
  GitService,
4317
4314
  TimestampService,
4318
4315
  VitepressService,
4316
+ COMMAND_NAMES,
4317
+ COMMAND_DESCRIPTIONS,
4318
+ COMMAND_OPTIONS,
4319
+ BaseCommand,
4319
4320
  UpdateNoteConfigCommand,
4320
4321
  RenameNoteCommand
4321
4322
  };