@tnotesjs/core 0.1.14 → 0.1.16

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 (117) hide show
  1. package/README.md +87 -87
  2. package/dist/{chunk-FVKQPBPP.js → chunk-4MO7F7Q4.js} +2005 -1769
  3. package/dist/cli/index.js +1 -1
  4. package/dist/vitepress/config/index.js +212 -4
  5. package/package.json +1 -1
  6. package/types/config.ts +61 -61
  7. package/types/index.ts +11 -11
  8. package/types/note.ts +33 -33
  9. package/vitepress/assets/icons/icon__check.svg +3 -3
  10. package/vitepress/assets/icons/icon__clipboard.svg +7 -7
  11. package/vitepress/assets/icons/icon__close.svg +1 -1
  12. package/vitepress/assets/icons/icon__collapse.svg +1 -1
  13. package/vitepress/assets/icons/icon__confirm.svg +1 -1
  14. package/vitepress/assets/icons/icon__copy.svg +4 -4
  15. package/vitepress/assets/icons/icon__focus.svg +1 -1
  16. package/vitepress/assets/icons/icon__fold.svg +3 -3
  17. package/vitepress/assets/icons/icon__folder.svg +1 -1
  18. package/vitepress/assets/icons/icon__fullscreen.svg +1 -1
  19. package/vitepress/assets/icons/icon__fullscreen_exit.svg +1 -1
  20. package/vitepress/assets/icons/icon__github.svg +3 -3
  21. package/vitepress/assets/icons/icon__mindmap.svg +1 -1
  22. package/vitepress/assets/icons/icon__next.svg +1 -1
  23. package/vitepress/assets/icons/icon__number_gray.svg +1 -1
  24. package/vitepress/assets/icons/icon__number_purple.svg +1 -1
  25. package/vitepress/assets/icons/icon__prev.svg +1 -1
  26. package/vitepress/assets/icons/icon__restore.svg +1 -1
  27. package/vitepress/assets/icons/icon__rotate.svg +4 -4
  28. package/vitepress/assets/icons/icon__search.svg +1 -1
  29. package/vitepress/assets/icons/icon__setting.svg +1 -0
  30. package/vitepress/assets/icons/icon__sidebar_collapsed.svg +1 -1
  31. package/vitepress/assets/icons/icon__sidebar_opened.svg +1 -1
  32. package/vitepress/assets/icons/icon__totop.svg +5 -5
  33. package/vitepress/assets/icons/icon__vscode.svg +5 -5
  34. package/vitepress/assets/icons/icon__zoom_fit.svg +1 -1
  35. package/vitepress/assets/icons/icon__zoom_in.svg +1 -1
  36. package/vitepress/assets/icons/icon__zoom_out.svg +1 -1
  37. package/vitepress/assets/icons/icon__zoom_reset.svg +1 -1
  38. package/vitepress/assets/icons/index.ts +39 -38
  39. package/vitepress/components/BilibiliOutsidePlayer/BilibiliOutsidePlayer.vue +20 -20
  40. package/vitepress/components/CodeBlockFullscreen/CodeBlockFullscreen.vue +373 -373
  41. package/vitepress/components/CodeBlockFullscreen/index.ts +115 -115
  42. package/vitepress/components/CodeBlockFullscreen/styles.css +64 -64
  43. package/vitepress/components/Discussions/Discussions.module.scss +32 -32
  44. package/vitepress/components/Discussions/Discussions.vue +211 -211
  45. package/vitepress/components/EnWordList/EnWordList.module.scss +124 -124
  46. package/vitepress/components/EnWordList/EnWordList.vue +543 -543
  47. package/vitepress/components/EnWordList/RightClickMenu.module.scss +22 -22
  48. package/vitepress/components/EnWordList/RightClickMenu.vue +66 -66
  49. package/vitepress/components/Footprints/Footprints.module.scss +93 -93
  50. package/vitepress/components/Footprints/Footprints.vue +377 -377
  51. package/vitepress/components/Layout/AboutModal.module.scss +233 -233
  52. package/vitepress/components/Layout/AboutModal.vue +105 -105
  53. package/vitepress/components/Layout/AboutPanel.vue +270 -270
  54. package/vitepress/components/Layout/ContentCollapse.vue +603 -603
  55. package/vitepress/components/Layout/CustomSidebar.vue +878 -608
  56. package/vitepress/components/Layout/DocBeforeControls.vue +149 -149
  57. package/vitepress/components/Layout/DocFooter.vue +233 -233
  58. package/vitepress/components/Layout/ImagePreview.module.scss +201 -201
  59. package/vitepress/components/Layout/ImagePreview.vue +281 -281
  60. package/vitepress/components/Layout/Layout.module.scss +661 -661
  61. package/vitepress/components/Layout/Layout.vue +618 -560
  62. package/vitepress/components/Layout/NavBarSettingsTrigger.vue +59 -0
  63. package/vitepress/components/Layout/NoteStatus.vue +140 -140
  64. package/vitepress/components/Layout/SidebarItems.vue +1083 -268
  65. package/vitepress/components/Layout/SidebarNavBefore.vue +171 -114
  66. package/vitepress/components/Layout/Swiper.vue +167 -167
  67. package/vitepress/components/Layout/ToggleFullContent.module.scss +11 -11
  68. package/vitepress/components/Layout/ToggleFullContent.vue +35 -35
  69. package/vitepress/components/Layout/ToggleSidebar.module.scss +11 -11
  70. package/vitepress/components/Layout/ToggleSidebar.vue +36 -36
  71. package/vitepress/components/Layout/composables/useCollapseControl.ts +88 -88
  72. package/vitepress/components/Layout/composables/useNoteConfig.ts +119 -119
  73. package/vitepress/components/Layout/composables/useNoteSave.ts +178 -178
  74. package/vitepress/components/Layout/composables/useNoteValidation.ts +85 -85
  75. package/vitepress/components/Layout/composables/useRedirect.ts +113 -113
  76. package/vitepress/components/Layout/composables/useVSCodeIntegration.ts +85 -85
  77. package/vitepress/components/Layout/homeReadme.data.ts +124 -124
  78. package/vitepress/components/MarkMap/MarkMap.module.scss +159 -159
  79. package/vitepress/components/MarkMap/MarkMap.vue +405 -405
  80. package/vitepress/components/Mermaid/Mermaid.module.scss +275 -275
  81. package/vitepress/components/Mermaid/Mermaid.vue +365 -365
  82. package/vitepress/components/NotesTable/NotesTable.module.scss +77 -77
  83. package/vitepress/components/NotesTable/NotesTable.vue +98 -98
  84. package/vitepress/components/NotesTable/README.md +67 -67
  85. package/vitepress/components/Settings/Settings.module.scss +375 -433
  86. package/vitepress/components/Settings/Settings.vue +376 -306
  87. package/vitepress/components/SidebarCard/MindMapView.vue +484 -484
  88. package/vitepress/components/SidebarCard/NotesTrendChart.vue +108 -108
  89. package/vitepress/components/SidebarCard/SidebarCard.vue +946 -946
  90. package/vitepress/components/Tooltip/Tooltip.vue +70 -70
  91. package/vitepress/components/constants.ts +109 -91
  92. package/vitepress/components/notesConfig.data.ts +73 -73
  93. package/vitepress/components/sidebar.data.ts +59 -59
  94. package/vitepress/components/tnotes-config.data.ts +21 -21
  95. package/vitepress/components/utils.ts +26 -26
  96. package/vitepress/config/index.ts +169 -167
  97. package/vitepress/configs/constants.ts +26 -26
  98. package/vitepress/configs/head.config.ts +25 -25
  99. package/vitepress/configs/index.ts +9 -9
  100. package/vitepress/configs/markdown-it.d.ts +23 -23
  101. package/vitepress/configs/markdown.config.ts +366 -366
  102. package/vitepress/configs/theme.config.ts +108 -108
  103. package/vitepress/plugins/buildProgressPlugin.ts +434 -434
  104. package/vitepress/plugins/getNoteByConfigIdPlugin.ts +107 -107
  105. package/vitepress/plugins/renameNotePlugin.ts +68 -68
  106. package/vitepress/plugins/sidebarStructurePlugin.ts +260 -0
  107. package/vitepress/plugins/updateConfigPlugin.ts +63 -63
  108. package/vitepress/theme/index.ts +137 -137
  109. package/vitepress/theme/styles/base.scss +50 -50
  110. package/vitepress/theme/styles/components/404.scss +31 -31
  111. package/vitepress/theme/styles/components/collapse.scss +175 -175
  112. package/vitepress/theme/styles/components/markmap.scss +101 -101
  113. package/vitepress/theme/styles/components/swiper.scss +255 -255
  114. package/vitepress/theme/styles/index.scss +25 -25
  115. package/vitepress/theme/styles/layout.scss +70 -62
  116. package/vitepress/theme/styles/utilities.scss +39 -39
  117. package/vitepress/theme/styles/vitepress-override.scss +25 -25
@@ -1282,6 +1282,14 @@ var NoteIndexCache = class _NoteIndexCache {
1282
1282
  }
1283
1283
  };
1284
1284
 
1285
+ // services/readme/service.ts
1286
+ import {
1287
+ existsSync as existsSync3,
1288
+ readFileSync as readFileSync3,
1289
+ writeFileSync as writeFileSync3,
1290
+ promises as fsPromises
1291
+ } from "fs";
1292
+
1285
1293
  // core/ReadmeGenerator.ts
1286
1294
  import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
1287
1295
 
@@ -1572,663 +1580,538 @@ var ReadmeGenerator = class {
1572
1580
  }
1573
1581
  };
1574
1582
 
1575
- // core/GitManager.ts
1576
- var GitManager = class {
1577
- logger;
1578
- dir;
1579
- constructor(dir, logger2) {
1580
- this.dir = dir;
1581
- this.logger = logger2?.child("git") || new Logger({ prefix: "git" });
1582
- }
1583
- /**
1584
- * 检查是否为有效的 Git 仓库
1585
- */
1586
- async isValidRepo() {
1587
- try {
1588
- const result = await runCommand(
1589
- "git rev-parse --is-inside-work-tree",
1590
- this.dir
1591
- );
1592
- return result.trim() === "true";
1593
- } catch {
1594
- return false;
1595
- }
1583
+ // services/readme/service.ts
1584
+ var ReadmeService = class _ReadmeService {
1585
+ static instance;
1586
+ noteManager;
1587
+ readmeGenerator;
1588
+ configManager;
1589
+ noteIndexCache;
1590
+ constructor() {
1591
+ this.noteManager = NoteManager.getInstance();
1592
+ this.readmeGenerator = new ReadmeGenerator();
1593
+ this.configManager = ConfigManager.getInstance();
1594
+ this.noteIndexCache = NoteIndexCache.getInstance();
1596
1595
  }
1597
- /**
1598
- * 确保是有效的 Git 仓库,否则抛出错误
1599
- */
1600
- async ensureValidRepo() {
1601
- if (!await this.isValidRepo()) {
1602
- throw createError.gitNotRepo(this.dir);
1596
+ static getInstance() {
1597
+ if (!_ReadmeService.instance) {
1598
+ _ReadmeService.instance = new _ReadmeService();
1603
1599
  }
1600
+ return _ReadmeService.instance;
1604
1601
  }
1605
1602
  /**
1606
- * 获取 Git 状态
1603
+ * 更新所有笔记的 README
1604
+ * @param options - 更新选项
1607
1605
  */
1608
- async getStatus() {
1609
- await this.ensureValidRepo();
1610
- const statusOutput = await runCommand(
1611
- "git -c core.quotePath=false status --porcelain",
1612
- this.dir
1613
- );
1614
- const lines = statusOutput.trim().split("\n").filter((line) => line);
1615
- const files = lines.map((line) => {
1616
- const statusCode = line.substring(0, 2);
1617
- let path2 = line.substring(3);
1618
- path2 = path2.replace(/^"(.*)"$/, "$1");
1619
- let status = "modified";
1620
- if (line.startsWith("??")) {
1621
- status = "untracked";
1622
- } else if (/^[MADRC]/.test(statusCode)) {
1623
- status = "staged";
1624
- } else if (/^.[MD]/.test(statusCode)) {
1625
- status = "unstaged";
1626
- }
1627
- return { path: path2, status, statusCode };
1628
- });
1629
- const staged = files.filter((f) => f.status === "staged").length;
1630
- const unstaged = files.filter((f) => f.status === "unstaged").length;
1631
- const untracked = files.filter((f) => f.status === "untracked").length;
1632
- const branch = await runCommand("git branch --show-current", this.dir);
1633
- let ahead = 0;
1634
- let behind = 0;
1635
- try {
1636
- const aheadBehind = await runCommand(
1637
- "git rev-list --left-right --count @{upstream}...HEAD",
1638
- this.dir
1606
+ async updateAllReadmes(options = {}) {
1607
+ const {
1608
+ updateSidebar = true,
1609
+ updateHome = true,
1610
+ notes: providedNotes
1611
+ } = options;
1612
+ logger.info("\u5F00\u59CB\u66F4\u65B0\u77E5\u8BC6\u5E93...");
1613
+ const notes = providedNotes ?? this.noteManager.scanNotes();
1614
+ logger.info(`\u626B\u63CF\u5230 ${notes.length} \u7BC7\u7B14\u8BB0`);
1615
+ const changedIndexes = await this.getChangedNoteIndexes();
1616
+ const shouldIncrementalUpdate = changedIndexes.size > 0 && changedIndexes.size < notes.length * 0.3;
1617
+ let notesToUpdate = notes;
1618
+ if (shouldIncrementalUpdate) {
1619
+ notesToUpdate = notes.filter((note) => changedIndexes.has(note.index));
1620
+ logger.info(
1621
+ `\u68C0\u6D4B\u5230 ${changedIndexes.size} \u7BC7\u7B14\u8BB0\u6709\u53D8\u66F4\uFF0C\u4F7F\u7528\u589E\u91CF\u66F4\u65B0\u6A21\u5F0F`
1639
1622
  );
1640
- const [behindStr, aheadStr] = aheadBehind.trim().split(" ");
1641
- behind = parseInt(behindStr) || 0;
1642
- ahead = parseInt(aheadStr) || 0;
1643
- } catch {
1623
+ } else {
1624
+ logger.info("\u4F7F\u7528\u5168\u91CF\u66F4\u65B0\u6A21\u5F0F");
1644
1625
  }
1645
- return {
1646
- hasChanges: lines.length > 0,
1647
- changedFiles: lines.length,
1648
- staged,
1649
- unstaged,
1650
- untracked,
1651
- branch: branch.trim(),
1652
- ahead,
1653
- behind,
1654
- files
1655
- };
1626
+ const startTime = Date.now();
1627
+ await this.updateNoteReadmesInParallel(notesToUpdate);
1628
+ const updateTime = Date.now() - startTime;
1629
+ logger.info(`\u66F4\u65B0\u4E86 ${notesToUpdate.length} \u7BC7\u7B14\u8BB0 (\u8017\u65F6 ${updateTime}ms)`);
1630
+ if (updateHome) {
1631
+ await this.updateHomeReadme(notes);
1632
+ }
1633
+ if (updateSidebar) {
1634
+ await this.updateSidebar(notes);
1635
+ }
1636
+ logger.info("\u77E5\u8BC6\u5E93\u66F4\u65B0\u5B8C\u6210\uFF01");
1656
1637
  }
1657
1638
  /**
1658
- * 获取远程仓库信息
1639
+ * 只更新指定笔记的 README(不更新 sidebar、home)
1640
+ * @param noteIndexes - 笔记索引数组,例如 ['0001', '0002']
1659
1641
  */
1660
- async getRemoteInfo() {
1661
- try {
1662
- await this.ensureValidRepo();
1663
- const remoteUrl = await runCommand(
1664
- "git config --get remote.origin.url",
1665
- this.dir
1666
- );
1667
- const url = remoteUrl.trim();
1668
- if (!url) return null;
1669
- const httpsMatch = url.match(
1670
- /https:\/\/(?:www\.)?github\.com\/([^/]+)\/(.+?)(?:\.git)?$/
1671
- );
1672
- if (httpsMatch) {
1673
- return {
1674
- url,
1675
- type: "https",
1676
- owner: httpsMatch[1],
1677
- repo: httpsMatch[2]
1678
- };
1642
+ async updateNoteReadmesOnly(noteIndexes) {
1643
+ if (noteIndexes.length === 0) return;
1644
+ const notesToUpdate = [];
1645
+ for (const noteIndex of noteIndexes) {
1646
+ const note = this.noteManager.getNoteByIndex(noteIndex);
1647
+ if (note) {
1648
+ notesToUpdate.push(note);
1649
+ } else {
1650
+ logger.warn(`\u7B14\u8BB0\u672A\u627E\u5230: ${noteIndex}`);
1679
1651
  }
1680
- const sshMatch = url.match(/git@github\.com:([^/]+)\/(.+?)(?:\.git)?$/);
1681
- if (sshMatch) {
1682
- return {
1683
- url,
1684
- type: "ssh",
1685
- owner: sshMatch[1],
1686
- repo: sshMatch[2]
1687
- };
1652
+ }
1653
+ if (notesToUpdate.length === 0) {
1654
+ logger.warn("\u6CA1\u6709\u627E\u5230\u9700\u8981\u66F4\u65B0\u7684\u7B14\u8BB0");
1655
+ return;
1656
+ }
1657
+ for (const note of notesToUpdate) {
1658
+ try {
1659
+ this.readmeGenerator.updateNoteReadme(note);
1660
+ } catch (error) {
1661
+ logger.error(`\u66F4\u65B0\u7B14\u8BB0 ${note.dirName} \u5931\u8D25`, error);
1688
1662
  }
1689
- return { url, type: "unknown" };
1690
- } catch {
1691
- return null;
1692
1663
  }
1693
1664
  }
1694
1665
  /**
1695
- * 检查是否有未提交的更改
1696
- */
1697
- async hasUncommittedChanges() {
1698
- const status = await this.getStatus();
1699
- return status.hasChanges;
1700
- }
1701
- /**
1702
- * Stash 当前更改
1666
+ * 获取变更的笔记索引集合
1667
+ * @returns 变更的笔记索引集合
1703
1668
  */
1704
- async stash(message) {
1669
+ async getChangedNoteIndexes() {
1705
1670
  try {
1706
- await this.ensureValidRepo();
1707
- const cmd = message ? `git stash push -m "${message}"` : "git stash push";
1708
- await runCommand(cmd, this.dir);
1709
- this.logger.info("Stashed uncommitted changes");
1710
- return true;
1671
+ return getChangedIds();
1711
1672
  } catch (error) {
1712
- this.logger.warn("Failed to stash changes");
1713
- return false;
1673
+ return /* @__PURE__ */ new Set();
1714
1674
  }
1715
1675
  }
1716
1676
  /**
1717
- * Pop stash
1677
+ * 并行更新多个笔记的 README
1678
+ * @param notes - 笔记信息数组
1718
1679
  */
1719
- async stashPop() {
1720
- try {
1721
- await this.ensureValidRepo();
1722
- await runCommand("git stash pop", this.dir);
1723
- this.logger.info("Restored stashed changes");
1724
- return true;
1725
- } catch (error) {
1726
- this.logger.warn("Failed to restore stashed changes");
1727
- return false;
1680
+ async updateNoteReadmesInParallel(notes) {
1681
+ const batchSize = 10;
1682
+ const batches = [];
1683
+ for (let i = 0; i < notes.length; i += batchSize) {
1684
+ batches.push(notes.slice(i, i + batchSize));
1685
+ }
1686
+ let successCount = 0;
1687
+ let failCount = 0;
1688
+ for (const batch of batches) {
1689
+ const results = await Promise.allSettled(
1690
+ batch.map(
1691
+ (note) => Promise.resolve().then(() => {
1692
+ this.readmeGenerator.updateNoteReadme(note);
1693
+ })
1694
+ )
1695
+ );
1696
+ for (const result of results) {
1697
+ if (result.status === "fulfilled") {
1698
+ successCount++;
1699
+ } else {
1700
+ failCount++;
1701
+ logger.error("\u66F4\u65B0\u7B14\u8BB0\u5931\u8D25", result.reason);
1702
+ }
1703
+ }
1704
+ }
1705
+ if (failCount > 0) {
1706
+ logger.warn(`\u66F4\u65B0\u5B8C\u6210\uFF1A\u6210\u529F ${successCount} \u7BC7\uFF0C\u5931\u8D25 ${failCount} \u7BC7`);
1728
1707
  }
1729
1708
  }
1730
1709
  /**
1731
- * 拉取远程更新
1710
+ * 更新侧边栏配置
1711
+ * @param notes - 笔记信息数组
1732
1712
  */
1733
- async pull(options) {
1734
- await this.ensureValidRepo();
1735
- const { rebase = true, autostash = true } = options || {};
1736
- const hasChanges = await this.hasUncommittedChanges();
1737
- let didStash = false;
1738
- if (hasChanges && !autostash) {
1739
- this.logger.warn("Repository has uncommitted changes");
1740
- didStash = await this.stash("Auto-stash before pull");
1713
+ async updateSidebar(notes) {
1714
+ if (!existsSync3(ROOT_README_PATH)) {
1715
+ logger.error("\u672A\u627E\u5230\u9996\u9875 README\uFF0C\u65E0\u6CD5\u751F\u6210\u4FA7\u8FB9\u680F");
1716
+ return;
1741
1717
  }
1742
- try {
1743
- const beforeCommit = await runCommand("git rev-parse HEAD", this.dir);
1744
- this.logger.info("\u6B63\u5728\u62C9\u53D6\u8FDC\u7A0B\u66F4\u65B0...");
1745
- const cmd = `git pull ${rebase ? "--rebase" : ""} ${autostash ? "--autostash" : ""}`.trim();
1746
- await runCommand(cmd, this.dir);
1747
- const afterCommit = await runCommand("git rev-parse HEAD", this.dir);
1748
- if (beforeCommit.trim() !== afterCommit.trim()) {
1749
- try {
1750
- const diffOutput = await runCommand(
1751
- `git diff --name-only ${beforeCommit.trim()}..${afterCommit.trim()}`,
1752
- this.dir
1753
- );
1754
- const changedFiles = diffOutput.trim().split("\n").filter((f) => f);
1755
- if (changedFiles.length > 0) {
1756
- console.log(` \u66F4\u65B0\u4E86 ${changedFiles.length} \u4E2A\u6587\u4EF6:`);
1757
- changedFiles.forEach((file, index) => {
1758
- console.log(` ${index + 1}. ${file}`);
1759
- });
1718
+ const content = readFileSync3(ROOT_README_PATH, "utf-8");
1719
+ const lines = content.split("\n");
1720
+ const itemList = [];
1721
+ const titles = [];
1722
+ const titlesNotesCount = [];
1723
+ let currentNoteCount = 0;
1724
+ let inTocRegion = false;
1725
+ for (const line of lines) {
1726
+ if (line.includes("<!-- region:toc -->")) {
1727
+ inTocRegion = true;
1728
+ continue;
1729
+ }
1730
+ if (line.includes("<!-- endregion:toc -->")) {
1731
+ inTocRegion = false;
1732
+ continue;
1733
+ }
1734
+ if (inTocRegion) {
1735
+ continue;
1736
+ }
1737
+ const parsed = parseNoteLine(line);
1738
+ if (parsed.isMatch && parsed.noteIndex) {
1739
+ const note = notes.find((n) => n.index === parsed.noteIndex);
1740
+ if (!note) {
1741
+ logger.warn(`\u672A\u627E\u5230\u7B14\u8BB0\u7D22\u5F15: ${parsed.noteIndex}`);
1742
+ continue;
1743
+ }
1744
+ let statusEmoji = "\u23F0 ";
1745
+ if (note?.config) {
1746
+ if (note.config.done) {
1747
+ statusEmoji = "\u2705 ";
1760
1748
  }
1761
- this.logger.success(`\u62C9\u53D6\u6210\u529F: ${changedFiles.length} \u4E2A\u6587\u4EF6\u5DF2\u66F4\u65B0`);
1762
- } catch {
1763
- this.logger.success("\u62C9\u53D6\u6210\u529F");
1764
1749
  }
1765
- } else {
1766
- this.logger.info("\u5DF2\u662F\u6700\u65B0\uFF0C\u6CA1\u6709\u9700\u8981\u62C9\u53D6\u7684\u66F4\u65B0");
1750
+ const sidebarShowNoteId2 = this.configManager.get("sidebarShowNoteId");
1751
+ let displayText = note.dirName;
1752
+ if (!sidebarShowNoteId2) {
1753
+ displayText = note.dirName.replace(/^\d{4}\.\s/, "");
1754
+ }
1755
+ itemList.push({
1756
+ text: statusEmoji + displayText,
1757
+ link: `/notes/${note.dirName}/README`
1758
+ });
1759
+ currentNoteCount++;
1760
+ continue;
1767
1761
  }
1768
- } catch (error) {
1769
- this.logger.error("\u62C9\u53D6\u5931\u8D25");
1770
- handleError(error);
1771
- throw error;
1772
- } finally {
1773
- if (didStash) {
1774
- await this.stashPop();
1762
+ const titleMatch = line.match(/^(#{2,})\s+(.+)$/);
1763
+ if (titleMatch) {
1764
+ if (titles.length > 0) {
1765
+ titlesNotesCount.push(currentNoteCount);
1766
+ }
1767
+ titles.push(line);
1768
+ currentNoteCount = 0;
1775
1769
  }
1776
1770
  }
1771
+ if (titles.length > 0) {
1772
+ titlesNotesCount.push(currentNoteCount);
1773
+ }
1774
+ const sidebarIsCollapsed = true;
1775
+ const hierarchicalSidebar = genHierarchicalSidebar(
1776
+ itemList,
1777
+ titles,
1778
+ titlesNotesCount,
1779
+ sidebarIsCollapsed
1780
+ );
1781
+ writeFileSync3(
1782
+ VP_SIDEBAR_PATH,
1783
+ JSON.stringify(hierarchicalSidebar, null, 2),
1784
+ "utf-8"
1785
+ );
1786
+ logger.info("\u5DF2\u66F4\u65B0\u4FA7\u8FB9\u680F\u914D\u7F6E");
1777
1787
  }
1778
1788
  /**
1779
- * 提交更改
1789
+ * 更新首页 README
1790
+ * @param notes - 笔记信息数组
1780
1791
  */
1781
- async commit(message) {
1782
- await this.ensureValidRepo();
1783
- try {
1784
- await runCommand(`git commit -m "${message}"`, this.dir);
1785
- this.logger.success(`Committed: ${message}`);
1786
- } catch (error) {
1787
- handleError(error);
1788
- throw error;
1789
- }
1792
+ async updateHomeReadme(notes) {
1793
+ this.readmeGenerator.updateHomeReadme(notes, ROOT_README_PATH);
1790
1794
  }
1791
1795
  /**
1792
- * 添加文件到暂存区
1796
+ * 增量更新首页 README 中的单个笔记
1797
+ * @param noteIndex - 笔记索引
1798
+ * @param updates - 需要更新的配置字段
1793
1799
  */
1794
- async add(files = ".") {
1795
- await this.ensureValidRepo();
1796
- const fileList = Array.isArray(files) ? files.join(" ") : files;
1797
- try {
1798
- await runCommand(`git add ${fileList}`, this.dir);
1799
- this.logger.info(`Staged changes: ${fileList}`);
1800
- } catch (error) {
1801
- handleError(error);
1802
- throw error;
1800
+ async updateNoteInReadme(noteIndex, updates) {
1801
+ const item = this.noteIndexCache.getByNoteIndex(noteIndex);
1802
+ if (!item) {
1803
+ logger.warn(`\u5C1D\u8BD5\u66F4\u65B0\u4E0D\u5B58\u5728\u7684\u7B14\u8BB0: ${noteIndex}`);
1804
+ return;
1805
+ }
1806
+ const content = await fsPromises.readFile(ROOT_README_PATH, "utf-8");
1807
+ const lines = content.split("\n");
1808
+ const repoOwner = this.configManager.get("author");
1809
+ const repoName2 = this.configManager.get("repoName");
1810
+ const mergedConfig = { ...item.noteConfig, ...updates };
1811
+ const tempNoteInfo = {
1812
+ index: noteIndex,
1813
+ dirName: item.folderName,
1814
+ path: "",
1815
+ readmePath: "",
1816
+ configPath: "",
1817
+ config: mergedConfig
1818
+ };
1819
+ let updated = false;
1820
+ for (let i = 0; i < lines.length; i++) {
1821
+ const parsed = parseNoteLine(lines[i]);
1822
+ if (parsed.noteIndex === noteIndex) {
1823
+ lines[i] = buildNoteLineMarkdown(tempNoteInfo, repoOwner, repoName2);
1824
+ updated = true;
1825
+ }
1826
+ }
1827
+ if (updated) {
1828
+ await fsPromises.writeFile(ROOT_README_PATH, lines.join("\n"), "utf-8");
1829
+ logger.info(`\u589E\u91CF\u66F4\u65B0 README.md \u4E2D\u7684\u7B14\u8BB0: ${noteIndex}`);
1830
+ } else {
1831
+ logger.warn(`README.md \u4E2D\u672A\u627E\u5230\u7B14\u8BB0: ${noteIndex}`);
1803
1832
  }
1804
1833
  }
1805
1834
  /**
1806
- * 推送到远程仓库
1835
+ * 从首页 README 中删除笔记
1836
+ * @param noteIndex - 笔记索引
1807
1837
  */
1808
- async push(options) {
1809
- await this.ensureValidRepo();
1810
- const { force = false, setUpstream = false } = options || {};
1811
- try {
1812
- const status = await this.getStatus();
1813
- this.logger.progress(`\u6B63\u5728\u63A8\u9001\u5230\u8FDC\u7A0B (${status.branch})...`);
1814
- let cmd = "git push";
1815
- if (force) cmd += " --force";
1816
- if (setUpstream) cmd += ` --set-upstream origin ${status.branch}`;
1817
- await runCommand(cmd, this.dir);
1818
- const remoteInfo = await this.getRemoteInfo();
1819
- if (remoteInfo) {
1820
- this.logger.success(`\u63A8\u9001\u6210\u529F \u2192 ${remoteInfo.owner}/${remoteInfo.repo}`);
1821
- } else {
1822
- this.logger.success("\u63A8\u9001\u6210\u529F");
1838
+ async deleteNoteFromReadme(noteIndex) {
1839
+ const content = await fsPromises.readFile(ROOT_README_PATH, "utf-8");
1840
+ const lines = content.split("\n");
1841
+ const linesToRemove = [];
1842
+ for (let i = 0; i < lines.length; i++) {
1843
+ const parsed = parseNoteLine(lines[i]);
1844
+ if (parsed.noteIndex === noteIndex) {
1845
+ linesToRemove.push(i);
1823
1846
  }
1824
- } catch (error) {
1825
- this.logger.error("\u63A8\u9001\u5931\u8D25");
1826
- handleError(error);
1827
- throw error;
1847
+ }
1848
+ if (linesToRemove.length > 0) {
1849
+ for (let i = linesToRemove.length - 1; i >= 0; i--) {
1850
+ lines.splice(linesToRemove[i], 1);
1851
+ }
1852
+ await fsPromises.writeFile(ROOT_README_PATH, lines.join("\n"), "utf-8");
1853
+ logger.info(
1854
+ `\u4ECE README.md \u4E2D\u5220\u9664\u7B14\u8BB0: ${noteIndex} (${linesToRemove.length} \u5904\u5F15\u7528)`
1855
+ );
1856
+ } else {
1857
+ logger.warn(`README.md \u4E2D\u672A\u627E\u5230\u7B14\u8BB0: ${noteIndex}`);
1828
1858
  }
1829
1859
  }
1830
1860
  /**
1831
- * 完整的推送流程:检查 -> 添加 -> 提交 -> 推送
1861
+ * 在首页 README 末尾添加新笔记
1862
+ * @param noteIndex - 笔记索引
1832
1863
  */
1833
- async pushWithCommit(commitMessage, options) {
1834
- await this.ensureValidRepo();
1835
- const status = await this.getStatus();
1836
- if (!status.hasChanges) {
1837
- this.logger.info("\u6CA1\u6709\u9700\u8981\u63D0\u4EA4\u7684\u66F4\u6539");
1864
+ async appendNoteToReadme(noteIndex) {
1865
+ const item = this.noteIndexCache.getByNoteIndex(noteIndex);
1866
+ if (!item) {
1867
+ logger.warn(`\u5C1D\u8BD5\u6DFB\u52A0\u4E0D\u5B58\u5728\u7684\u7B14\u8BB0: ${noteIndex}`);
1838
1868
  return;
1839
1869
  }
1840
- try {
1841
- this.logger.info(`\u6B63\u5728\u63A8\u9001 ${status.changedFiles} \u4E2A\u6587\u4EF6...`);
1842
- status.files.forEach((file, index) => {
1843
- console.log(` ${index + 1}. ${file.path}`);
1844
- });
1845
- await runCommand("git add .", this.dir);
1846
- const message = commitMessage || `update: ${status.changedFiles} files modified`;
1847
- await runCommand(`git commit -m "${message}"`, this.dir);
1848
- let cmd = "git push";
1849
- if (options?.force) cmd += " --force";
1850
- await runCommand(cmd, this.dir);
1851
- const remoteInfo = await this.getRemoteInfo();
1852
- if (remoteInfo) {
1853
- this.logger.success(
1854
- `\u63A8\u9001\u6210\u529F: ${status.changedFiles} \u4E2A\u6587\u4EF6 \u2192 https://github.com/${remoteInfo.owner}/${remoteInfo.repo}`
1855
- );
1856
- } else {
1857
- this.logger.success(`\u63A8\u9001\u6210\u529F: ${status.changedFiles} \u4E2A\u6587\u4EF6`);
1858
- }
1859
- } catch (error) {
1860
- this.logger.error(`\u63A8\u9001\u5931\u8D25`);
1861
- handleError(error);
1862
- throw error;
1863
- }
1870
+ const content = await fsPromises.readFile(ROOT_README_PATH, "utf-8");
1871
+ const lines = content.split("\n");
1872
+ const repoOwner = this.configManager.get("author");
1873
+ const repoName2 = this.configManager.get("repoName");
1874
+ const tempNoteInfo = {
1875
+ index: noteIndex,
1876
+ dirName: item.folderName,
1877
+ path: "",
1878
+ readmePath: "",
1879
+ configPath: "",
1880
+ config: item.noteConfig
1881
+ };
1882
+ const noteLine = buildNoteLineMarkdown(tempNoteInfo, repoOwner, repoName2);
1883
+ lines.push(noteLine);
1884
+ await fsPromises.writeFile(ROOT_README_PATH, lines.join("\n"), "utf-8");
1885
+ logger.info(`\u5728 README.md \u672B\u5C3E\u6DFB\u52A0\u7B14\u8BB0: ${noteIndex}`);
1864
1886
  }
1865
1887
  /**
1866
- * 完整的同步流程:拉取 -> 推送
1888
+ * 重命名根 README 中的目录标题
1867
1889
  */
1868
- async sync(options) {
1869
- const { commitMessage, rebase = true } = options || {};
1870
- try {
1871
- await this.pull({ rebase, autostash: true });
1872
- await this.pushWithCommit(commitMessage);
1873
- } catch (error) {
1874
- this.logger.error("Sync failed");
1875
- handleError(error);
1876
- throw error;
1890
+ async renameGroupInReadme(groupPath, newTitle) {
1891
+ this.assertGroupPath(groupPath);
1892
+ const cleanTitle = this.cleanHeadingTitle(newTitle);
1893
+ const lines = await this.readHomeReadmeLines();
1894
+ const block = this.findGroupBlock(lines, groupPath);
1895
+ const headingMark = "#".repeat(block.level);
1896
+ lines[block.headingIndex] = `${headingMark} ${cleanTitle}`;
1897
+ await this.writeHomeReadmeLines(lines);
1898
+ logger.info(`\u91CD\u547D\u540D\u76EE\u5F55: ${groupPath.join(" / ")} -> ${cleanTitle}`);
1899
+ }
1900
+ /**
1901
+ * 删除根 README 中的目录块,返回块内所有笔记编号
1902
+ */
1903
+ async deleteGroupFromReadme(groupPath) {
1904
+ this.assertGroupPath(groupPath);
1905
+ const lines = await this.readHomeReadmeLines();
1906
+ const block = this.findGroupBlock(lines, groupPath);
1907
+ const noteIndexes = this.collectNoteIndexes(
1908
+ lines,
1909
+ block.headingIndex,
1910
+ block.endIndex
1911
+ );
1912
+ lines.splice(block.headingIndex, block.endIndex - block.headingIndex);
1913
+ await this.writeHomeReadmeLines(lines);
1914
+ logger.info(
1915
+ `\u5220\u9664\u76EE\u5F55: ${groupPath.join(" / ")} (${noteIndexes.length} \u7BC7\u7B14\u8BB0)`
1916
+ );
1917
+ return noteIndexes;
1918
+ }
1919
+ /**
1920
+ * 在目录开头插入笔记链接
1921
+ */
1922
+ async insertNotesAtGroupStart(groupPath, notes) {
1923
+ this.assertGroupPath(groupPath);
1924
+ if (notes.length === 0) return;
1925
+ const lines = await this.readHomeReadmeLines();
1926
+ const block = this.findGroupBlock(lines, groupPath);
1927
+ const noteLines = this.buildNoteLines(notes);
1928
+ let insertIndex = block.headingIndex + 1;
1929
+ while (insertIndex < block.endIndex && lines[insertIndex].trim() === "") {
1930
+ insertIndex++;
1877
1931
  }
1932
+ lines.splice(insertIndex, 0, ...noteLines);
1933
+ await this.writeHomeReadmeLines(lines);
1934
+ logger.info(
1935
+ `\u5728\u76EE\u5F55\u5F00\u5934\u63D2\u5165\u7B14\u8BB0: ${groupPath.join(" / ")} (${notes.length} \u7BC7)`
1936
+ );
1878
1937
  }
1879
1938
  /**
1880
- * 显示状态摘要
1939
+ * 在指定笔记上方或下方插入笔记链接
1881
1940
  */
1882
- async showStatus(options) {
1883
- const { showFiles = true } = options || {};
1884
- const status = await this.getStatus();
1885
- const remoteInfo = await this.getRemoteInfo();
1886
- console.log("\n\u{1F4CA} Git \u72B6\u6001:");
1887
- console.log(` \u5206\u652F: ${status.branch}`);
1888
- if (remoteInfo) {
1889
- console.log(
1890
- ` \u8FDC\u7A0B: ${remoteInfo.owner}/${remoteInfo.repo} (${remoteInfo.type})`
1891
- );
1892
- }
1893
- if (status.hasChanges) {
1894
- console.log(
1895
- ` \u53D8\u66F4: ${status.changedFiles} \u4E2A\u6587\u4EF6 (\u5DF2\u6682\u5B58 ${status.staged}, \u672A\u6682\u5B58 ${status.unstaged}, \u672A\u8DDF\u8E2A ${status.untracked})`
1896
- );
1897
- if (showFiles && status.files.length > 0) {
1898
- console.log(" \u53D8\u66F4\u6587\u4EF6\u5217\u8868:");
1899
- const stagedFiles = status.files.filter((f) => f.status === "staged");
1900
- const unstagedFiles = status.files.filter(
1901
- (f) => f.status === "unstaged"
1902
- );
1903
- const untrackedFiles = status.files.filter(
1904
- (f) => f.status === "untracked"
1905
- );
1906
- if (stagedFiles.length > 0) {
1907
- console.log(" \u5DF2\u6682\u5B58:");
1908
- stagedFiles.forEach((f) => console.log(` \u2713 ${f.path}`));
1909
- }
1910
- if (unstagedFiles.length > 0) {
1911
- console.log(" \u672A\u6682\u5B58:");
1912
- unstagedFiles.forEach((f) => console.log(` \u2022 ${f.path}`));
1913
- }
1914
- if (untrackedFiles.length > 0) {
1915
- console.log(" \u672A\u8DDF\u8E2A:");
1916
- untrackedFiles.forEach((f) => console.log(` ? ${f.path}`));
1917
- }
1918
- }
1919
- } else {
1920
- console.log(" \u72B6\u6001: \u5DE5\u4F5C\u533A\u5E72\u51C0\uFF0C\u6CA1\u6709\u53D8\u66F4");
1921
- }
1922
- if (status.ahead > 0 || status.behind > 0) {
1923
- const syncInfo = [];
1924
- if (status.ahead > 0) syncInfo.push(`\u9886\u5148 ${status.ahead} \u4E2A\u63D0\u4EA4`);
1925
- if (status.behind > 0) syncInfo.push(`\u843D\u540E ${status.behind} \u4E2A\u63D0\u4EA4`);
1926
- console.log(` \u540C\u6B65: ${syncInfo.join(", ")}`);
1927
- }
1928
- console.log();
1929
- }
1930
- };
1931
-
1932
- // core/ProcessManager.ts
1933
- import { spawn } from "child_process";
1934
- var ProcessManager = class {
1935
- processes = /* @__PURE__ */ new Map();
1936
- logger;
1937
- constructor() {
1938
- this.logger = new Logger({ prefix: "process" });
1939
- process.on("exit", () => {
1940
- this.killAll();
1941
- });
1942
- process.on("SIGINT", () => {
1943
- this.killAll();
1944
- process.exit(0);
1945
- });
1946
- process.on("SIGTERM", () => {
1947
- this.killAll();
1948
- process.exit(0);
1949
- });
1941
+ async insertNotesAroundNote(targetNoteIndex, notes, placement) {
1942
+ if (notes.length === 0) return;
1943
+ const lines = await this.readHomeReadmeLines();
1944
+ const noteLineIndex = this.findNoteLineIndex(lines, targetNoteIndex);
1945
+ const insertIndex = placement === "before" ? noteLineIndex : noteLineIndex + 1;
1946
+ const noteLines = this.buildNoteLines(notes);
1947
+ lines.splice(insertIndex, 0, ...noteLines);
1948
+ await this.writeHomeReadmeLines(lines);
1949
+ logger.info(
1950
+ `\u5728\u7B14\u8BB0 ${targetNoteIndex} ${placement === "before" ? "\u4E0A\u65B9" : "\u4E0B\u65B9"}\u63D2\u5165 ${notes.length} \u7BC7\u7B14\u8BB0`
1951
+ );
1950
1952
  }
1951
1953
  /**
1952
- * 启动进程
1953
- * @param id - 进程ID
1954
- * @param command - 命令
1955
- * @param args - 参数列表
1956
- * @param options - spawn 选项
1957
- * @returns ProcessInfo
1954
+ * 移动笔记引用到目录开头,或移动到另一篇笔记前后
1958
1955
  */
1959
- spawn(id, command, args = [], options) {
1960
- if (this.processes.has(id)) {
1961
- this.logger.warn(`\u8FDB\u7A0B ${id} \u5DF2\u5B58\u5728\uFF0C\u5148\u505C\u6B62\u65E7\u8FDB\u7A0B`);
1962
- this.kill(id);
1956
+ async moveNoteInReadme(noteIndex, options) {
1957
+ const lines = await this.readHomeReadmeLines();
1958
+ const sourceIndex = this.findNoteLineIndex(lines, noteIndex);
1959
+ const [noteLine] = lines.splice(sourceIndex, 1);
1960
+ if (options.targetGroupPath) {
1961
+ const block = this.findGroupBlock(lines, options.targetGroupPath);
1962
+ let insertIndex2 = block.headingIndex + 1;
1963
+ while (insertIndex2 < block.endIndex && lines[insertIndex2].trim() === "") {
1964
+ insertIndex2++;
1965
+ }
1966
+ lines.splice(insertIndex2, 0, noteLine);
1967
+ await this.writeHomeReadmeLines(lines);
1968
+ return;
1963
1969
  }
1964
- const proc = spawn(command, args, {
1965
- stdio: "inherit",
1966
- shell: true,
1967
- ...options
1968
- });
1969
- const processInfo = {
1970
- id,
1971
- pid: proc.pid,
1972
- command,
1973
- args,
1974
- startTime: Date.now(),
1975
- process: proc
1976
- };
1977
- this.processes.set(id, processInfo);
1978
- proc.on("exit", (code, signal) => {
1979
- this.logger.info(`\u8FDB\u7A0B ${id} \u5DF2\u9000\u51FA (code: ${code}, signal: ${signal})`);
1980
- this.processes.delete(id);
1981
- });
1982
- proc.on("error", (err) => {
1983
- this.logger.error(`\u8FDB\u7A0B ${id} \u51FA\u9519: ${err.message}`);
1984
- this.processes.delete(id);
1985
- });
1986
- return processInfo;
1970
+ if (!options.targetNoteIndex) {
1971
+ throw new Error("\u7F3A\u5C11\u76EE\u6807\u7B14\u8BB0\u6216\u76EE\u6807\u76EE\u5F55");
1972
+ }
1973
+ if (options.targetNoteIndex === noteIndex) {
1974
+ throw new Error("\u4E0D\u80FD\u628A\u7B14\u8BB0\u79FB\u52A8\u5230\u81EA\u8EAB\u9644\u8FD1");
1975
+ }
1976
+ const targetIndex = this.findNoteLineIndex(lines, options.targetNoteIndex);
1977
+ const insertIndex = options.placement === "after" ? targetIndex + 1 : targetIndex;
1978
+ lines.splice(insertIndex, 0, noteLine);
1979
+ await this.writeHomeReadmeLines(lines);
1987
1980
  }
1988
1981
  /**
1989
- * 停止进程
1990
- * @param id - 进程ID
1991
- * @param signal - 信号(默认为 SIGTERM)
1992
- * @returns 是否成功停止
1982
+ * 移动目录块到同级目录前后
1993
1983
  */
1994
- kill(id, signal = "SIGTERM") {
1995
- const processInfo = this.processes.get(id);
1996
- if (!processInfo) {
1997
- this.logger.warn(`\u8FDB\u7A0B ${id} \u4E0D\u5B58\u5728`);
1998
- return false;
1984
+ async moveGroupInReadme(groupPath, targetGroupPath, placement = "before") {
1985
+ this.assertGroupPath(groupPath);
1986
+ this.assertGroupPath(targetGroupPath);
1987
+ if (this.isSamePath(groupPath, targetGroupPath)) {
1988
+ throw new Error("\u4E0D\u80FD\u628A\u76EE\u5F55\u79FB\u52A8\u5230\u81EA\u8EAB\u9644\u8FD1");
1999
1989
  }
2000
- this.logger.info(`\u505C\u6B62\u8FDB\u7A0B: ${id} (PID: ${processInfo.pid})`);
2001
- try {
2002
- const killed = processInfo.process.kill(signal);
2003
- if (killed) {
2004
- this.processes.delete(id);
2005
- return true;
2006
- }
2007
- return false;
2008
- } catch (error) {
2009
- this.logger.error(`\u505C\u6B62\u8FDB\u7A0B ${id} \u5931\u8D25: ${error}`);
2010
- return false;
1990
+ if (groupPath.length !== targetGroupPath.length) {
1991
+ throw new Error("\u7B2C\u4E00\u7248\u62D6\u62FD\u4EC5\u652F\u6301\u540C\u7EA7\u76EE\u5F55\u6392\u5E8F");
2011
1992
  }
1993
+ const lines = await this.readHomeReadmeLines();
1994
+ const sourceBlock = this.findGroupBlock(lines, groupPath);
1995
+ const movingLines = lines.splice(
1996
+ sourceBlock.headingIndex,
1997
+ sourceBlock.endIndex - sourceBlock.headingIndex
1998
+ );
1999
+ const targetBlock = this.findGroupBlock(lines, targetGroupPath);
2000
+ const insertIndex = placement === "after" ? targetBlock.endIndex : targetBlock.headingIndex;
2001
+ lines.splice(insertIndex, 0, ...movingLines);
2002
+ await this.writeHomeReadmeLines(lines);
2012
2003
  }
2013
2004
  /**
2014
- * 检查进程是否存在
2015
- * @param id - 进程ID
2016
- * @returns 是否存在
2005
+ * 更新根 README 和 sidebar,不重写每篇笔记 README
2017
2006
  */
2018
- has(id) {
2019
- return this.processes.has(id);
2007
+ async refreshHomeReadmeAndSidebar(notes) {
2008
+ const allNotes = notes ?? (this.noteIndexCache.isInitialized() ? this.noteIndexCache.toNoteInfoList() : this.noteManager.scanNotes());
2009
+ await this.updateHomeReadme(allNotes);
2010
+ await this.updateSidebar(allNotes);
2020
2011
  }
2021
2012
  /**
2022
- * 检查进程是否在运行
2023
- * @param id - 进程 ID
2024
- * @returns 是否在运行
2013
+ * 重新生成 sidebar.json(基于当前 README.md)
2014
+ * @param notes - 可选的笔记列表,不传则内部扫描
2025
2015
  */
2026
- isRunning(id) {
2027
- const processInfo = this.processes.get(id);
2028
- if (!processInfo) return false;
2029
- try {
2030
- return process.kill(processInfo.pid, 0);
2031
- } catch {
2032
- return false;
2016
+ async regenerateSidebar(notes) {
2017
+ const allNotes = notes ?? (this.noteIndexCache.isInitialized() ? this.noteIndexCache.toNoteInfoList() : this.noteManager.scanNotes());
2018
+ await this.updateSidebar(allNotes);
2019
+ logger.info("\u91CD\u65B0\u751F\u6210 sidebar.json");
2020
+ }
2021
+ async readHomeReadmeLines() {
2022
+ const content = await fsPromises.readFile(ROOT_README_PATH, "utf-8");
2023
+ return content.split("\n");
2024
+ }
2025
+ async writeHomeReadmeLines(lines) {
2026
+ const content = processEmptyLines(lines).join("\n");
2027
+ await fsPromises.writeFile(ROOT_README_PATH, content, "utf-8");
2028
+ }
2029
+ assertGroupPath(groupPath) {
2030
+ if (!Array.isArray(groupPath) || groupPath.length === 0) {
2031
+ throw new Error("\u76EE\u5F55\u8DEF\u5F84\u4E0D\u80FD\u4E3A\u7A7A");
2033
2032
  }
2034
2033
  }
2035
- /**
2036
- * 停止所有进程
2037
- * @param signal - 信号(默认为 SIGTERM)
2038
- */
2039
- killAll(signal = "SIGTERM") {
2040
- if (this.processes.size === 0) {
2041
- return;
2034
+ cleanHeadingTitle(title) {
2035
+ const cleanTitle = title.trim().replace(/^#+\s*/, "");
2036
+ if (!cleanTitle) {
2037
+ throw new Error("\u76EE\u5F55\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A");
2042
2038
  }
2043
- this.logger.info(`\u505C\u6B62\u6240\u6709\u8FDB\u7A0B (${this.processes.size} \u4E2A)`);
2044
- for (const [id, processInfo] of this.processes) {
2045
- try {
2046
- processInfo.process.kill(signal);
2047
- this.logger.info(`\u5DF2\u505C\u6B62\u8FDB\u7A0B: ${id}`);
2048
- } catch (error) {
2049
- this.logger.error(`\u505C\u6B62\u8FDB\u7A0B ${id} \u5931\u8D25: ${error}`);
2050
- }
2039
+ if (/\r|\n/.test(cleanTitle)) {
2040
+ throw new Error("\u76EE\u5F55\u540D\u79F0\u4E0D\u80FD\u5305\u542B\u6362\u884C");
2051
2041
  }
2052
- this.processes.clear();
2042
+ return cleanTitle;
2053
2043
  }
2054
- };
2055
-
2056
- // services/file-watcher/service.ts
2057
- import { request as httpRequest } from "http";
2058
-
2059
- // services/file-watcher/configChangeHandler.ts
2060
- var ConfigChangeHandler = class {
2061
- constructor(config2) {
2062
- this.config = config2;
2044
+ getHeadingInfo(line) {
2045
+ const match = line.match(/^(#{2,})\s+(.+)$/);
2046
+ if (!match) return null;
2047
+ return {
2048
+ level: match[1].length,
2049
+ text: match[2].trim()
2050
+ };
2063
2051
  }
2064
- async handle(events) {
2065
- if (events.length === 0) return [];
2066
- const changedIndexes = [];
2067
- const { state, noteService, noteIndexCache, logger: logger2 } = this.config;
2068
- for (const change of events) {
2069
- if (noteService.shouldIgnoreConfigChange(change.path)) {
2070
- logger2.debug(`\u5FFD\u7565 API \u5199\u5165\u7684\u914D\u7F6E\u6587\u4EF6: ${change.path}`);
2071
- continue;
2052
+ findGroupBlock(lines, groupPath) {
2053
+ const stack = [];
2054
+ for (let i = 0; i < lines.length; i++) {
2055
+ const heading = this.getHeadingInfo(lines[i]);
2056
+ if (!heading) continue;
2057
+ while (stack.length > 0 && stack[stack.length - 1].level >= heading.level) {
2058
+ stack.pop();
2072
2059
  }
2073
- const snapshot = state.readConfigSnapshot(change.path);
2074
- if (!snapshot) continue;
2075
- const cached = state.getConfigSnapshot(change.path);
2076
- state.setConfigSnapshot(change.path, snapshot);
2077
- noteIndexCache.updateConfig(change.noteIndex, snapshot);
2078
- if (!cached) continue;
2079
- const statusChanged = cached.done !== snapshot.done;
2080
- const otherChanged = cached.enableDiscussions !== snapshot.enableDiscussions || cached.description !== snapshot.description;
2081
- if (statusChanged) {
2082
- changedIndexes.push(change.noteIndex);
2083
- logger2.info(`\u68C0\u6D4B\u5230\u914D\u7F6E\u72B6\u6001\u53D8\u5316: done(${cached.done}\u2192${snapshot.done})`);
2084
- } else if (otherChanged) {
2085
- 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");
2060
+ stack.push(heading);
2061
+ const currentPath = stack.map((item) => item.text);
2062
+ if (this.isSamePath(currentPath, groupPath)) {
2063
+ return {
2064
+ headingIndex: i,
2065
+ endIndex: this.findGroupEndIndex(lines, i, heading.level),
2066
+ level: heading.level
2067
+ };
2086
2068
  }
2087
2069
  }
2088
- return changedIndexes;
2070
+ throw new Error(`\u672A\u627E\u5230\u76EE\u5F55: ${groupPath.join(" / ")}`);
2089
2071
  }
2090
- };
2091
-
2092
- // services/file-watcher/eventScheduler.ts
2093
- var DEFAULT_DEBOUNCE_MS = 1e3;
2094
- var DEFAULT_BATCH_WINDOW_MS = 1e3;
2095
- var DEFAULT_BATCH_THRESHOLD = 3;
2096
- var DEFAULT_BATCH_BUFFER_MS = 2e3;
2097
- var EventScheduler = class {
2098
- constructor(config2) {
2099
- this.config = config2;
2100
- this.debounceMs = config2.debounceMs ?? DEFAULT_DEBOUNCE_MS;
2101
- this.batchWindowMs = config2.batchWindowMs ?? DEFAULT_BATCH_WINDOW_MS;
2102
- this.batchThreshold = config2.batchThreshold ?? DEFAULT_BATCH_THRESHOLD;
2103
- this.batchBufferMs = config2.batchBufferMs ?? DEFAULT_BATCH_BUFFER_MS;
2072
+ findGroupEndIndex(lines, headingIndex, level) {
2073
+ for (let i = headingIndex + 1; i < lines.length; i++) {
2074
+ const heading = this.getHeadingInfo(lines[i]);
2075
+ if (heading && heading.level <= level) {
2076
+ return i;
2077
+ }
2078
+ }
2079
+ return lines.length;
2104
2080
  }
2105
- /** 待处理的文件变更事件队列 */
2106
- pendingEvents = /* @__PURE__ */ new Map();
2107
- /** 防抖定时器 */
2108
- updateTimer = null;
2109
- /** 批量更新恢复定时器 */
2110
- batchResumeTimer = null;
2111
- /** 记录最近的变更时间戳 */
2112
- recentChanges = [];
2113
- /** 标记是否正在更新,避免循环触发 - 类似一把更新行为锁 */
2114
- isUpdating = false;
2115
- debounceMs;
2116
- batchWindowMs;
2117
- batchThreshold;
2118
- batchBufferMs;
2119
- /**
2120
- * 设置更新状态锁,用于防止在执行耗时更新操作时被新的文件变更事件打断
2121
- *
2122
- * @param flag - true 表示正在更新(锁定),false 表示更新完成(解锁)
2123
- */
2124
- setUpdating(flag) {
2125
- this.isUpdating = flag;
2081
+ isSamePath(left, right) {
2082
+ if (left.length !== right.length) return false;
2083
+ return left.every((item, index) => item === right[index]);
2126
2084
  }
2127
- /**
2128
- * 获取当前是否处于更新锁定状态
2129
- *
2130
- * @returns true 表示正在执行更新操作(事件处理被暂停),false 表示空闲可处理新事件
2131
- */
2132
- getUpdating() {
2133
- return this.isUpdating;
2085
+ collectNoteIndexes(lines, startIndex, endIndex) {
2086
+ const noteIndexes = [];
2087
+ for (let i = startIndex; i < endIndex; i++) {
2088
+ const parsed = parseNoteLine(lines[i]);
2089
+ if (parsed.noteIndex) {
2090
+ noteIndexes.push(parsed.noteIndex);
2091
+ }
2092
+ }
2093
+ return [...new Set(noteIndexes)];
2134
2094
  }
2135
- /**
2136
- * 将文件变更事件加入待处理队列,并启动防抖定时器
2137
- *
2138
- * - 若同一文件路径的事件已存在,则忽略重复事件(去重)
2139
- * - 每次新事件都会重置防抖计时器,确保在变更停止后才触发处理
2140
- *
2141
- * @param event 文件变更事件
2142
- */
2143
- enqueue(event) {
2144
- if (this.pendingEvents.has(event.path)) return;
2145
- this.pendingEvents.set(event.path, event);
2146
- if (this.updateTimer) clearTimeout(this.updateTimer);
2147
- this.updateTimer = setTimeout(() => this.flush(), this.debounceMs);
2148
- }
2149
- /**
2150
- * 立即触发事件队列的处理(防抖到期或手动调用)
2151
- *
2152
- * - 若当前正在更新(isUpdating 为 true),则跳过以避免重复处理
2153
- * - 清空待处理事件队列,并通过 onFlush 回调交由上层服务处理
2154
- * - 处理开始后会锁定更新状态,防止处理过程中被新事件打断
2155
- */
2156
- flush() {
2157
- if (this.isUpdating) return;
2158
- if (this.pendingEvents.size === 0) return;
2159
- const events = Array.from(this.pendingEvents.values());
2160
- this.pendingEvents.clear();
2161
- this.isUpdating = true;
2162
- this.config.onFlush(events);
2163
- }
2164
- /**
2165
- * 记录当前变更时间并检测是否触发批量更新模式
2166
- *
2167
- * - 维护一个滑动时间窗口(BATCH_UPDATE_WINDOW_MS)内的变更记录
2168
- * - 若短时间内(1秒内)变更次数达到阈值(BATCH_UPDATE_THRESHOLD = 3),则判定为批量操作
2169
- * - 触发批量模式后:
2170
- * 1. 清空当前待处理事件队列,避免重复处理
2171
- * 2. 锁定更新状态(isUpdating = true)
2172
- * 3. 暂停监听服务,并在延迟(窗口 + 缓冲时间)后自动恢复
2173
- *
2174
- * @param now 当前时间戳(默认使用 Date.now())
2175
- * @returns true 表示已触发批量更新模式,false 表示仍处于普通监听模式
2176
- */
2177
- recordChangeAndDetectBatch(now = Date.now()) {
2178
- this.recentChanges.push(now);
2179
- this.recentChanges = this.recentChanges.filter(
2180
- (t) => now - t < this.batchWindowMs
2181
- );
2182
- if (this.recentChanges.length < this.batchThreshold) return false;
2183
- this.pendingEvents.clear();
2184
- this.recentChanges = [];
2185
- this.isUpdating = true;
2186
- this.config.onPauseForBatch();
2187
- this.batchResumeTimer = setTimeout(() => {
2188
- this.batchResumeTimer = null;
2189
- this.isUpdating = false;
2190
- this.config.reinit();
2191
- this.config.onResumeAfterBatch();
2192
- }, this.batchWindowMs + this.batchBufferMs);
2193
- return true;
2194
- }
2195
- /**
2196
- * 清理所有定时器,释放资源
2197
- *
2198
- * 在服务停止时调用,防止定时器在服务销毁后仍然触发回调
2199
- */
2200
- clearTimers() {
2201
- if (this.updateTimer) {
2202
- clearTimeout(this.updateTimer);
2203
- this.updateTimer = null;
2204
- }
2205
- if (this.batchResumeTimer) {
2206
- clearTimeout(this.batchResumeTimer);
2207
- this.batchResumeTimer = null;
2095
+ findNoteLineIndex(lines, noteIndex) {
2096
+ for (let i = 0; i < lines.length; i++) {
2097
+ const parsed = parseNoteLine(lines[i]);
2098
+ if (parsed.noteIndex === noteIndex) {
2099
+ return i;
2100
+ }
2208
2101
  }
2209
- this.pendingEvents.clear();
2210
- this.recentChanges = [];
2102
+ throw new Error(`README.md \u4E2D\u672A\u627E\u5230\u7B14\u8BB0: ${noteIndex}`);
2103
+ }
2104
+ buildNoteLines(notes) {
2105
+ const repoOwner = this.configManager.get("author");
2106
+ const repoName2 = this.configManager.get("repoName");
2107
+ return notes.map((note) => buildNoteLineMarkdown(note, repoOwner, repoName2));
2211
2108
  }
2212
2109
  };
2213
2110
 
2214
- // services/file-watcher/folderChangeHandler.ts
2215
- import { existsSync as existsSync3, promises as fsPromises } from "fs";
2111
+ // services/note/service.ts
2112
+ import { writeFileSync as writeFileSync4, readFileSync as readFileSync4, promises as fsPromises2 } from "fs";
2216
2113
  import { join as join3 } from "path";
2217
-
2218
- // services/file-watcher/internal.ts
2219
- var WATCH_EVENT_TYPES = {
2220
- README: "readme",
2221
- CONFIG: "config"
2222
- };
2223
- async function safeExecute(label, fn, logger2) {
2224
- try {
2225
- await fn();
2226
- return true;
2227
- } catch (error) {
2228
- logger2.error(`[${label}] ${error}`);
2229
- return false;
2230
- }
2231
- }
2114
+ import { v4 as uuidv4 } from "uuid";
2232
2115
 
2233
2116
  // config/templates.ts
2234
2117
  function generateNoteTitle(noteIndex, title, repoUrl) {
@@ -2237,1377 +2120,1730 @@ function generateNoteTitle(noteIndex, title, repoUrl) {
2237
2120
  return `# [${dirName}](${repoUrl}/${encodedDirName})`;
2238
2121
  }
2239
2122
 
2240
- // services/file-watcher/folderChangeHandler.ts
2241
- var RENAME_REVERT_DELAY_MS = 2e3;
2242
- var DELETE_REINIT_DELAY_MS = 1e3;
2243
- var UPDATE_UNLOCK_DELAY_MS = 500;
2244
- var FolderChangeHandler = class {
2245
- constructor(config2) {
2246
- this.config = config2;
2123
+ // services/note/service.ts
2124
+ var NEW_NOTES_README_MD_TEMPLATE = `
2125
+ <!-- region:toc -->
2126
+
2127
+ - [1. \u{1F3AF} \u672C\u8282\u5185\u5BB9](#1--\u672C\u8282\u5185\u5BB9)
2128
+ - [2. \u{1FAE7} \u8BC4\u4EF7](#2--\u8BC4\u4EF7)
2129
+
2130
+ <!-- endregion:toc -->
2131
+
2132
+ ## 1. \u{1F3AF} \u672C\u8282\u5185\u5BB9
2133
+
2134
+ - todo
2135
+
2136
+ ## 2. \u{1FAE7} \u8BC4\u4EF7
2137
+
2138
+ - todo
2139
+ `;
2140
+ var NoteService = class _NoteService {
2141
+ static instance;
2142
+ noteManager;
2143
+ noteIndexCache;
2144
+ ignoredConfigPaths = /* @__PURE__ */ new Set();
2145
+ constructor() {
2146
+ this.noteManager = NoteManager.getInstance();
2147
+ this.noteIndexCache = NoteIndexCache.getInstance();
2247
2148
  }
2248
- /** 活跃的定时器 ID 集合,用于统一清理 */
2249
- activeTimers = /* @__PURE__ */ new Set();
2250
- /**
2251
- * 清理所有活跃定时器,释放资源
2252
- *
2253
- * 在服务停止时调用,防止定时器在服务销毁后仍然触发回调
2254
- */
2255
- clearTimers() {
2256
- for (const timer of this.activeTimers) {
2257
- clearTimeout(timer);
2149
+ static getInstance() {
2150
+ if (!_NoteService.instance) {
2151
+ _NoteService.instance = new _NoteService();
2258
2152
  }
2259
- this.activeTimers.clear();
2260
- }
2261
- scheduleTimer(fn, delay) {
2262
- const timer = setTimeout(() => {
2263
- this.activeTimers.delete(timer);
2264
- fn();
2265
- }, delay);
2266
- this.activeTimers.add(timer);
2153
+ return _NoteService.instance;
2267
2154
  }
2268
- async handleFolderDeletion(deletedFolderName) {
2269
- const { scheduler, watchState, noteIndexCache, readmeService, logger: logger2 } = this.config;
2270
- if (scheduler.getUpdating()) return;
2271
- scheduler.setUpdating(true);
2272
- try {
2273
- const noteIndex = this.extractNoteIndexOrWarn(deletedFolderName);
2274
- if (!noteIndex) return;
2275
- logger2.info(`\u6B63\u5728\u5904\u7406\u7B14\u8BB0\u5220\u9664: ${noteIndex} (${deletedFolderName})`);
2276
- watchState.deleteNoteDir(deletedFolderName);
2277
- watchState.clearNoteCaches(deletedFolderName);
2278
- noteIndexCache.delete(noteIndex);
2279
- await safeExecute(
2280
- `\u5220\u9664\u7B14\u8BB0 ${noteIndex}`,
2281
- async () => {
2282
- await readmeService.deleteNoteFromReadme(noteIndex);
2283
- await readmeService.regenerateSidebar();
2284
- },
2285
- logger2
2286
- );
2287
- } finally {
2288
- this.scheduleTimer(() => {
2289
- scheduler.setUpdating(false);
2290
- watchState.initializeFromDisk();
2291
- }, DELETE_REINIT_DELAY_MS);
2292
- }
2155
+ /**
2156
+ * 标记配置文件在下次变更时被忽略(防止 API 写入触发文件监听循环)
2157
+ * @param configPath - 配置文件路径
2158
+ */
2159
+ ignoreNextConfigChange(configPath) {
2160
+ this.ignoredConfigPaths.add(configPath);
2293
2161
  }
2294
- async handleFolderRenameUpdate(oldName, newName) {
2295
- const { scheduler, watchState, logger: logger2 } = this.config;
2296
- if (scheduler.getUpdating()) return;
2297
- scheduler.setUpdating(true);
2298
- try {
2299
- const { oldNoteIndex, newNoteIndex } = this.validateRenameIndexes(
2300
- oldName,
2301
- newName
2302
- );
2303
- if (!oldNoteIndex || !newNoteIndex) return;
2304
- logger2.info(`\u6B63\u5728\u5904\u7406\u6587\u4EF6\u5939\u91CD\u547D\u540D: ${oldName} \u2192 ${newName}`);
2305
- if (oldNoteIndex === newNoteIndex) {
2306
- await safeExecute(
2307
- `\u6807\u9898\u91CD\u547D\u540D ${newNoteIndex}`,
2308
- () => this.handleTitleOnlyRename(newNoteIndex, newName),
2309
- logger2
2310
- );
2311
- } else {
2312
- await safeExecute(
2313
- `\u7D22\u5F15\u53D8\u66F4 ${oldNoteIndex}\u2192${newNoteIndex}`,
2314
- () => this.handleIndexChangedRename(oldNoteIndex, newNoteIndex),
2315
- logger2
2316
- );
2317
- }
2318
- this.config.onRenameSuccess?.({
2319
- oldFolder: oldName,
2320
- newFolder: newName,
2321
- noteIndex: newNoteIndex
2322
- });
2323
- } finally {
2324
- this.scheduleTimer(() => {
2325
- scheduler.setUpdating(false);
2326
- watchState.initializeFromDisk();
2327
- }, UPDATE_UNLOCK_DELAY_MS);
2162
+ /**
2163
+ * 检查配置文件是否应该被忽略
2164
+ * @param configPath - 配置文件路径
2165
+ * @returns 是否应该忽略
2166
+ */
2167
+ shouldIgnoreConfigChange(configPath) {
2168
+ if (this.ignoredConfigPaths.has(configPath)) {
2169
+ this.ignoredConfigPaths.delete(configPath);
2170
+ return true;
2328
2171
  }
2172
+ return false;
2329
2173
  }
2330
- // #region - 私有实现
2331
- validateRenameIndexes(oldName, newName) {
2332
- const { noteIndexCache, logger: logger2 } = this.config;
2333
- const oldNoteIndex = this.extractNoteIndexOrWarn(oldName);
2334
- const newNoteIndex = this.extractNoteIndexOrWarn(newName);
2335
- if (!oldNoteIndex || !newNoteIndex) {
2336
- return { oldNoteIndex: null, newNoteIndex: null };
2337
- }
2338
- if (!/^\d{4}$/.test(newNoteIndex)) {
2339
- logger2.error(`\u65B0\u7B14\u8BB0\u7D22\u5F15\u683C\u5F0F\u975E\u6CD5: ${newNoteIndex}\uFF0C\u81EA\u52A8\u56DE\u9000`);
2340
- this.revertFolderRename(oldName, newName);
2341
- return { oldNoteIndex: null, newNoteIndex: null };
2342
- }
2343
- if (oldNoteIndex !== newNoteIndex && noteIndexCache.has(newNoteIndex)) {
2344
- logger2.error(`\u65B0\u7B14\u8BB0\u7D22\u5F15 ${newNoteIndex} \u5DF2\u5B58\u5728\uFF0C\u81EA\u52A8\u56DE\u9000`);
2345
- this.revertFolderRename(oldName, newName);
2346
- return { oldNoteIndex: null, newNoteIndex: null };
2174
+ /**
2175
+ * 获取所有笔记
2176
+ * dev 模式下(缓存已初始化)从内存读取,其他模式回退到文件扫描
2177
+ * @returns 笔记信息数组
2178
+ */
2179
+ getAllNotes() {
2180
+ if (this.noteIndexCache.isInitialized()) {
2181
+ return this.noteIndexCache.toNoteInfoList();
2347
2182
  }
2348
- return { oldNoteIndex, newNoteIndex };
2183
+ return this.noteManager.scanNotes();
2349
2184
  }
2350
- async handleTitleOnlyRename(noteIndex, newName) {
2351
- const { noteIndexCache, readmeService, logger: logger2, notesDir } = this.config;
2352
- logger2.info(`\u7B14\u8BB0\u7D22\u5F15\u672A\u53D8 (${noteIndex})\uFF0C\u53EA\u66F4\u65B0\u6807\u9898`);
2353
- noteIndexCache.updateFolderName(noteIndex, newName);
2354
- await safeExecute(
2355
- `\u66F4\u65B0 README \u4E00\u7EA7\u6807\u9898 ${noteIndex}`,
2356
- () => this.rewriteNoteReadmeH1(notesDir, newName, noteIndex),
2357
- logger2
2358
- );
2359
- const item = noteIndexCache.getByNoteIndex(noteIndex);
2360
- if (item) {
2361
- await readmeService.updateNoteInReadme(noteIndex, item.noteConfig);
2185
+ /**
2186
+ * 获取笔记(通过索引)
2187
+ * @param noteIndex - 笔记索引(文件夹前 4 位数字)
2188
+ * @returns 笔记信息,未找到时返回 undefined
2189
+ */
2190
+ getNoteByIndex(noteIndex) {
2191
+ return this.noteManager.getNoteByIndex(noteIndex);
2192
+ }
2193
+ /**
2194
+ * 创建新笔记
2195
+ * @param options - 创建选项
2196
+ * @returns 新创建的笔记信息
2197
+ */
2198
+ async createNote(options = {}) {
2199
+ const {
2200
+ title = "new",
2201
+ category,
2202
+ enableDiscussions = false,
2203
+ configId,
2204
+ usedIndexes
2205
+ } = options;
2206
+ const noteIndex = this.generateNextNoteIndex(usedIndexes);
2207
+ const dirName = `${noteIndex}. ${title}`;
2208
+ const notePath = join3(NOTES_PATH, dirName);
2209
+ await ensureDirectory(notePath);
2210
+ const readmePath = join3(notePath, "README.md");
2211
+ const noteTitle = generateNoteTitle(noteIndex, title, REPO_NOTES_URL);
2212
+ const readmeContent = noteTitle + "\n" + NEW_NOTES_README_MD_TEMPLATE;
2213
+ writeFileSync4(readmePath, readmeContent, "utf-8");
2214
+ const configPath = join3(notePath, ".tnotes.json");
2215
+ const config2 = {
2216
+ id: configId || uuidv4(),
2217
+ // 配置 ID 使用 UUID(跨知识库唯一)
2218
+ bilibili: [],
2219
+ tnotes: [],
2220
+ yuque: [],
2221
+ done: false,
2222
+ category,
2223
+ enableDiscussions
2224
+ // created_at / updated_at 由 tn:push 时 fix-timestamps 自动写入
2225
+ };
2226
+ this.noteManager.writeNoteConfig(configPath, config2);
2227
+ const noteInfo = {
2228
+ index: noteIndex,
2229
+ // 返回的 id 是笔记索引(目录前缀)
2230
+ path: notePath,
2231
+ dirName,
2232
+ readmePath,
2233
+ configPath,
2234
+ config: config2
2235
+ };
2236
+ if (this.noteIndexCache.isInitialized()) {
2237
+ this.noteIndexCache.add(noteInfo);
2362
2238
  }
2363
- await readmeService.regenerateSidebar();
2364
- logger2.success(`\u6807\u9898\u66F4\u65B0\u5B8C\u6210`);
2239
+ logger.info(`Created new note: ${dirName}`);
2240
+ return noteInfo;
2365
2241
  }
2366
2242
  /**
2367
- * 重写指定笔记目录下 README.md 的一级标题。
2368
- *
2369
- * 输入的 `folderName` 形如 `0003. 评论功能的技术实现`,
2370
- * 据此推导出新标题文本(去掉 `${noteIndex}. ` 前缀)。
2243
+ * 删除笔记文件夹
2244
+ * @param noteIndex - 笔记索引
2245
+ * @returns 被删除的笔记信息
2371
2246
  */
2372
- async rewriteNoteReadmeH1(notesDir, folderName, noteIndex) {
2373
- const readmePath = join3(notesDir, folderName, "README.md");
2374
- if (!existsSync3(readmePath)) return;
2375
- const newTitle = folderName.replace(/^\d{4}\.\s*/, "").trim();
2376
- if (!newTitle) return;
2377
- const content = await fsPromises.readFile(readmePath, "utf-8");
2378
- const lines = content.split("\n");
2379
- let h1Index = -1;
2380
- for (let i = 0; i < lines.length; i++) {
2381
- if (lines[i].trim().startsWith("# ")) {
2382
- h1Index = i;
2383
- break;
2247
+ async deleteNote(noteIndex) {
2248
+ const note = this.getNoteByIndex(noteIndex);
2249
+ if (!note) {
2250
+ throw new Error(`\u7B14\u8BB0\u672A\u627E\u5230: ${noteIndex}`);
2251
+ }
2252
+ await fsPromises2.rm(note.path, { recursive: true, force: true });
2253
+ if (this.noteIndexCache.isInitialized()) {
2254
+ this.noteIndexCache.delete(noteIndex);
2255
+ }
2256
+ logger.info(`Deleted note: ${note.dirName}`);
2257
+ return note;
2258
+ }
2259
+ /**
2260
+ * 生成下一个笔记索引(填充空缺)
2261
+ * @param usedIndexes - 可选的已使用编号集合,不传则内部扫描
2262
+ * @returns 新的笔记索引(4位数字字符串,从 0001 到 9999)
2263
+ */
2264
+ generateNextNoteIndex(usedIndexes) {
2265
+ if (!usedIndexes) {
2266
+ const notes = this.getAllNotes();
2267
+ usedIndexes = /* @__PURE__ */ new Set();
2268
+ for (const note of notes) {
2269
+ const id = parseInt(note.index, 10);
2270
+ if (!isNaN(id) && id >= 1 && id <= 9999) {
2271
+ usedIndexes.add(id);
2272
+ }
2384
2273
  }
2385
2274
  }
2386
- if (h1Index === -1) {
2387
- this.config.logger.warn(
2388
- `\u672A\u5728 ${readmePath} \u627E\u5230\u4E00\u7EA7\u6807\u9898\uFF0C\u8DF3\u8FC7 README H1 \u66F4\u65B0`
2389
- );
2390
- return;
2275
+ if (usedIndexes.size === 0) {
2276
+ return "0001";
2391
2277
  }
2392
- const newH1 = generateNoteTitle(noteIndex, newTitle, REPO_NOTES_URL);
2393
- if (lines[h1Index] === newH1) return;
2394
- lines[h1Index] = newH1;
2395
- await fsPromises.writeFile(readmePath, lines.join("\n"), "utf-8");
2278
+ for (let i = 1; i <= 9999; i++) {
2279
+ if (!usedIndexes.has(i)) {
2280
+ return i.toString().padStart(CONSTANTS.NOTE_INDEX_LENGTH, "0");
2281
+ }
2282
+ }
2283
+ throw new Error("\u6240\u6709\u7B14\u8BB0\u7F16\u53F7 (0001-9999) \u5DF2\u88AB\u5360\u7528\uFF0C\u65E0\u6CD5\u521B\u5EFA\u65B0\u7B14\u8BB0");
2396
2284
  }
2397
- async handleIndexChangedRename(oldNoteIndex, newNoteIndex) {
2398
- const { noteService, noteIndexCache, readmeService, logger: logger2 } = this.config;
2399
- logger2.info(`\u7B14\u8BB0\u7D22\u5F15\u53D8\u66F4: ${oldNoteIndex} \u2192 ${newNoteIndex}`);
2400
- await readmeService.deleteNoteFromReadme(oldNoteIndex);
2401
- const newNote = noteService.getNoteByIndex(newNoteIndex);
2402
- if (newNote) {
2403
- noteIndexCache.delete(oldNoteIndex);
2404
- noteIndexCache.add(newNote);
2405
- await readmeService.appendNoteToReadme(newNoteIndex);
2285
+ /**
2286
+ * 更新笔记配置
2287
+ * @param noteIndex - 笔记索引
2288
+ * @param updates - 配置更新
2289
+ */
2290
+ async updateNoteConfig(noteIndex, updates) {
2291
+ const note = this.getNoteByIndex(noteIndex);
2292
+ if (!note || !note.config) {
2293
+ throw new Error(`Note not found or no config: ${noteIndex}`);
2294
+ }
2295
+ const oldConfig = { ...note.config };
2296
+ const updatedConfig = {
2297
+ ...note.config,
2298
+ ...updates
2299
+ };
2300
+ this.ignoreNextConfigChange(note.configPath);
2301
+ this.noteManager.updateNoteConfig(note, updatedConfig);
2302
+ this.noteIndexCache.updateConfig(noteIndex, updatedConfig);
2303
+ const needsGlobalUpdate = this.checkNeedsGlobalUpdate(
2304
+ oldConfig,
2305
+ updatedConfig
2306
+ );
2307
+ if (needsGlobalUpdate) {
2308
+ logger.info(`\u68C0\u6D4B\u5230\u5168\u5C40\u5B57\u6BB5\u53D8\u66F4 (${noteIndex})\uFF0C\u6B63\u5728\u589E\u91CF\u66F4\u65B0\u5168\u5C40\u6587\u4EF6...`);
2309
+ const readmeService = ReadmeService.getInstance();
2310
+ await readmeService.updateNoteInReadme(noteIndex, updates);
2406
2311
  await readmeService.regenerateSidebar();
2407
- logger2.success(`\u7B14\u8BB0\u7D22\u5F15\u53D8\u66F4\u5904\u7406\u5B8C\u6210`);
2312
+ logger.info(`\u5168\u5C40\u6587\u4EF6\u589E\u91CF\u66F4\u65B0\u5B8C\u6210 (${noteIndex})`);
2408
2313
  } else {
2409
- logger2.error(`\u672A\u627E\u5230\u65B0\u7B14\u8BB0: ${newNoteIndex}`);
2314
+ logger.debug(`\u914D\u7F6E\u66F4\u65B0\u4E0D\u5F71\u54CD\u5168\u5C40\u6587\u4EF6 (${noteIndex})`);
2410
2315
  }
2411
2316
  }
2412
- async revertFolderRename(oldName, newName) {
2413
- const { notesDir, scheduler, watchState, logger: logger2 } = this.config;
2317
+ /**
2318
+ * 检查配置更新是否需要触发全局更新
2319
+ * @param oldConfig - 旧配置
2320
+ * @param newConfig - 新配置
2321
+ * @returns 是否需要全局更新
2322
+ */
2323
+ checkNeedsGlobalUpdate(oldConfig, newConfig) {
2324
+ const globalFields = ["done"];
2325
+ for (const field of globalFields) {
2326
+ if (oldConfig[field] !== newConfig[field]) {
2327
+ return true;
2328
+ }
2329
+ }
2330
+ return false;
2331
+ }
2332
+ /**
2333
+ * 修正笔记标题
2334
+ * @param noteInfo - 笔记信息
2335
+ * @returns 是否进行了修正
2336
+ */
2337
+ async fixNoteTitle(noteInfo) {
2414
2338
  try {
2415
- const oldPath = join3(notesDir, oldName);
2416
- const newPath = join3(notesDir, newName);
2417
- if (existsSync3(newPath)) {
2418
- scheduler.setUpdating(true);
2419
- await fsPromises.rename(newPath, oldPath);
2420
- logger2.warn(`\u6587\u4EF6\u5939\u5DF2\u56DE\u9000: ${newName} \u2192 ${oldName}`);
2421
- this.scheduleTimer(() => {
2422
- scheduler.setUpdating(false);
2423
- watchState.initializeFromDisk();
2424
- }, RENAME_REVERT_DELAY_MS);
2339
+ const readmeContent = readFileSync4(noteInfo.readmePath, "utf-8");
2340
+ if (readmeContent.length === 0) return false;
2341
+ const lines = readmeContent.split("\n");
2342
+ const match = noteInfo.dirName.match(/^\d{4}\.\s+(.+)$/);
2343
+ if (!match) {
2344
+ logger.warn(`\u68C0\u6D4B\u5230\u9519\u8BEF\u7684\u7B14\u8BB0\u76EE\u5F55\u540D\u79F0\uFF1A${noteInfo.dirName}`);
2345
+ return false;
2346
+ }
2347
+ const expectedTitle = match[1];
2348
+ const expectedH1 = generateNoteTitle(
2349
+ noteInfo.index,
2350
+ expectedTitle,
2351
+ REPO_NOTES_URL
2352
+ );
2353
+ const firstLine = lines[0].trim();
2354
+ if (!firstLine.startsWith("# ")) {
2355
+ lines.unshift(expectedH1);
2356
+ writeFileSync4(noteInfo.readmePath, lines.join("\n"), "utf-8");
2357
+ logger.info(`Added title to: ${noteInfo.dirName}`);
2358
+ return true;
2359
+ }
2360
+ if (firstLine !== expectedH1) {
2361
+ lines[0] = expectedH1;
2362
+ writeFileSync4(noteInfo.readmePath, lines.join("\n"), "utf-8");
2363
+ logger.info(`Fixed title for: ${noteInfo.dirName}`);
2364
+ return true;
2425
2365
  }
2366
+ return false;
2426
2367
  } catch (error) {
2427
- logger2.error(`\u56DE\u9000\u6587\u4EF6\u5939\u91CD\u547D\u540D\u5931\u8D25: ${error}`);
2368
+ logger.error(`Failed to fix title for: ${noteInfo.dirName}`, error);
2369
+ return false;
2428
2370
  }
2429
2371
  }
2430
- extractNoteIndexOrWarn(name) {
2431
- const noteIndex = name.match(/^(\d{4})/)?.[1] || null;
2432
- if (!noteIndex) {
2433
- this.config.logger.warn(`\u65E0\u6CD5\u4ECE\u6587\u4EF6\u5939\u540D\u79F0\u63D0\u53D6\u7B14\u8BB0\u7D22\u5F15: ${name}`);
2372
+ /**
2373
+ * 修正所有笔记的标题
2374
+ * @param providedNotes - 可选的笔记列表,不传则内部扫描
2375
+ * @returns 修正的笔记数量
2376
+ */
2377
+ async fixAllNoteTitles(providedNotes) {
2378
+ const notes = providedNotes ?? this.getAllNotes();
2379
+ let fixedCount = 0;
2380
+ for (const note of notes) {
2381
+ const fixed = await this.fixNoteTitle(note);
2382
+ if (fixed) {
2383
+ fixedCount++;
2384
+ }
2434
2385
  }
2435
- return noteIndex;
2386
+ if (fixedCount > 0) {
2387
+ logger.info(`Fixed ${fixedCount} note titles`);
2388
+ }
2389
+ return fixedCount;
2436
2390
  }
2437
- // #endregion - 私有实现
2438
2391
  };
2439
2392
 
2440
- // services/file-watcher/fsWatcherAdapter.ts
2441
- import { watch } from "fs";
2442
- import { basename, dirname, join as join4, sep } from "path";
2443
- var FsWatcherAdapter = class {
2444
- constructor(config2) {
2445
- this.config = config2;
2446
- }
2447
- /** 文件系统监听器实例 */
2448
- watcher = null;
2449
- start() {
2450
- const { logger: logger2 } = this.config;
2451
- if (this.watcher) {
2452
- logger2.warn("\u6587\u4EF6\u76D1\u542C\u670D\u52A1\u5DF2\u542F\u52A8");
2453
- return;
2454
- }
2455
- this.watcher = watch(
2456
- this.config.notesDir,
2457
- { recursive: true },
2458
- (eventType, filename) => this.handleFsEvent(eventType, filename)
2459
- );
2460
- logger2.debug(`\u6587\u4EF6\u76D1\u542C\u5DF2\u542F\u52A8\uFF0C\u76D1\u542C\u76EE\u5F55\uFF1A${this.config.notesDir}`);
2461
- }
2462
- stop() {
2463
- if (!this.watcher) return;
2464
- this.watcher.close();
2465
- this.watcher = null;
2466
- }
2467
- isWatching() {
2468
- return this.watcher !== null;
2469
- }
2470
- handleFsEvent(eventType, filename) {
2471
- const { isUpdating, onRename, onNoteEvent } = this.config;
2472
- if (!filename || // 忽略无文件变更
2473
- isUpdating()) {
2474
- return;
2475
- }
2476
- if (eventType === "rename" && // 检测文件夹 rename 事件
2477
- !filename.includes(sep)) {
2478
- onRename(filename);
2479
- return;
2480
- }
2481
- const baseFilename = basename(filename);
2482
- if (baseFilename !== "README.md" && baseFilename !== ".tnotes.json") {
2483
- return;
2484
- }
2485
- const fullPath = join4(this.config.notesDir, filename);
2486
- const event = this.buildWatchEvent(fullPath, filename);
2487
- if (!event) {
2488
- return;
2489
- }
2490
- onNoteEvent(event);
2491
- }
2492
- buildWatchEvent(fullPath, filename) {
2493
- const noteDirName = basename(dirname(fullPath));
2494
- const noteIndex = NoteManager.extractNoteIndex(noteDirName);
2495
- if (!noteIndex) {
2496
- NoteManager.warnInvalidNoteIndex(noteDirName);
2497
- return null;
2498
- }
2499
- const fileType = filename.endsWith("README.md") ? WATCH_EVENT_TYPES.README : WATCH_EVENT_TYPES.CONFIG;
2500
- return {
2501
- path: fullPath,
2502
- type: fileType,
2503
- noteIndex,
2504
- noteDirName,
2505
- noteDirPath: dirname(fullPath)
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/readmeChangeHandler.ts
2543
- var ReadmeChangeHandler = class {
2393
+ // services/file-watcher/service.ts
2394
+ import { request as httpRequest } from "http";
2395
+
2396
+ // services/file-watcher/configChangeHandler.ts
2397
+ var ConfigChangeHandler = class {
2544
2398
  constructor(config2) {
2545
2399
  this.config = config2;
2546
2400
  }
2547
2401
  async handle(events) {
2548
- if (events.length === 0) return;
2549
- const indexes = [...new Set(events.map((c) => c.noteIndex))];
2550
- for (const noteIndex of indexes) {
2551
- const noteInfo = this.config.noteService.getNoteByIndex(noteIndex);
2552
- if (noteInfo) {
2553
- await this.config.noteService.fixNoteTitle(noteInfo);
2554
- }
2555
- }
2556
- }
2557
- };
2558
-
2559
- // services/file-watcher/renameDetector.ts
2560
- import { existsSync as existsSync4 } from "fs";
2561
- import { join as join5 } from "path";
2562
- var FOLDER_RENAME_DETECT_WINDOW_MS = 500;
2563
- var RenameDetector = class {
2564
- constructor(config2) {
2565
- this.config = config2;
2566
- }
2567
- /** 待处理的文件夹重命名 */
2568
- pendingFolderRename = null;
2569
- /** 文件夹重命名检测定时器 */
2570
- folderRenameTimer = null;
2571
- handleFsRename(folderName) {
2572
- const { notesDir, dirCache, logger: logger2, onDelete, onRename } = this.config;
2573
- const folderPath = join5(notesDir, folderName);
2574
- const folderExists = existsSync4(folderPath);
2575
- const noteIndex = NoteManager.extractNoteIndex(folderName);
2576
- if (!noteIndex) {
2577
- logger2.warn(`\u65E0\u6CD5\u4ECE\u6587\u4EF6\u5939\u540D\u79F0\u63D0\u53D6\u7B14\u8BB0\u7D22\u5F15: ${folderName}`);
2578
- return;
2579
- }
2580
- if (!folderExists) {
2581
- if (dirCache.has(folderName)) {
2582
- logger2.info(`\u68C0\u6D4B\u5230\u6587\u4EF6\u5939\u5220\u9664/\u91CD\u547D\u540D: ${folderName}`);
2583
- this.pendingFolderRename = { oldName: folderName, time: Date.now() };
2584
- if (this.folderRenameTimer) clearTimeout(this.folderRenameTimer);
2585
- this.folderRenameTimer = setTimeout(() => {
2586
- if (this.pendingFolderRename) {
2587
- logger2.warn(`\u68C0\u6D4B\u5230\u7B14\u8BB0\u5220\u9664: ${this.pendingFolderRename.oldName}`);
2588
- onDelete(this.pendingFolderRename.oldName);
2589
- }
2590
- this.pendingFolderRename = null;
2591
- this.folderRenameTimer = null;
2592
- }, FOLDER_RENAME_DETECT_WINDOW_MS);
2593
- }
2594
- return;
2595
- }
2596
- if (!dirCache.has(folderName)) {
2597
- logger2.info(`\u68C0\u6D4B\u5230\u6587\u4EF6\u5939\u521B\u5EFA/\u91CD\u547D\u540D: ${folderName}`);
2598
- if (this.pendingFolderRename && Date.now() - this.pendingFolderRename.time < FOLDER_RENAME_DETECT_WINDOW_MS) {
2599
- const oldName = this.pendingFolderRename.oldName;
2600
- const oldNoteIndex = NoteManager.extractNoteIndex(oldName);
2601
- if (oldNoteIndex && oldNoteIndex === noteIndex) {
2602
- logger2.info(`\u68C0\u6D4B\u5230\u6587\u4EF6\u5939\u91CD\u547D\u540D: ${oldName} \u2192 ${folderName}`);
2603
- if (this.folderRenameTimer) {
2604
- clearTimeout(this.folderRenameTimer);
2605
- this.folderRenameTimer = null;
2606
- }
2607
- onRename(oldName, folderName);
2608
- this.pendingFolderRename = null;
2609
- } else if (oldNoteIndex && oldNoteIndex !== noteIndex) {
2610
- logger2.warn(`\u7D22\u5F15\u51B2\u7A81\uFF0C\u56DE\u9000: ${oldName} -> ${folderName}`);
2611
- }
2402
+ if (events.length === 0) return [];
2403
+ const changedIndexes = [];
2404
+ const { state, noteService, noteIndexCache, logger: logger2 } = this.config;
2405
+ for (const change of events) {
2406
+ if (noteService.shouldIgnoreConfigChange(change.path)) {
2407
+ logger2.debug(`\u5FFD\u7565 API \u5199\u5165\u7684\u914D\u7F6E\u6587\u4EF6: ${change.path}`);
2408
+ continue;
2612
2409
  }
2613
- dirCache.add(folderName);
2614
- if (this.pendingFolderRename) {
2615
- dirCache.delete(this.pendingFolderRename.oldName);
2410
+ const snapshot = state.readConfigSnapshot(change.path);
2411
+ if (!snapshot) continue;
2412
+ const cached = state.getConfigSnapshot(change.path);
2413
+ state.setConfigSnapshot(change.path, snapshot);
2414
+ noteIndexCache.updateConfig(change.noteIndex, snapshot);
2415
+ if (!cached) continue;
2416
+ const statusChanged = cached.done !== snapshot.done;
2417
+ const otherChanged = cached.enableDiscussions !== snapshot.enableDiscussions || cached.description !== snapshot.description;
2418
+ if (statusChanged) {
2419
+ changedIndexes.push(change.noteIndex);
2420
+ logger2.info(`\u68C0\u6D4B\u5230\u914D\u7F6E\u72B6\u6001\u53D8\u5316: done(${cached.done}\u2192${snapshot.done})`);
2421
+ } else if (otherChanged) {
2422
+ 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");
2616
2423
  }
2617
2424
  }
2618
- }
2619
- clearTimers() {
2620
- if (this.folderRenameTimer) {
2621
- clearTimeout(this.folderRenameTimer);
2622
- this.folderRenameTimer = null;
2623
- }
2624
- this.pendingFolderRename = null;
2425
+ return changedIndexes;
2625
2426
  }
2626
2427
  };
2627
2428
 
2628
- // services/file-watcher/watchState.ts
2629
- import { createHash } from "crypto";
2630
- import { existsSync as existsSync5, readFileSync as readFileSync3, readdirSync as readdirSync2, statSync } from "fs";
2631
- import { join as join6 } from "path";
2632
- var WatchState = class {
2429
+ // services/file-watcher/eventScheduler.ts
2430
+ var DEFAULT_DEBOUNCE_MS = 1e3;
2431
+ var DEFAULT_BATCH_WINDOW_MS = 1e3;
2432
+ var DEFAULT_BATCH_THRESHOLD = 3;
2433
+ var DEFAULT_BATCH_BUFFER_MS = 2e3;
2434
+ var EventScheduler = class {
2633
2435
  constructor(config2) {
2634
2436
  this.config = config2;
2437
+ this.debounceMs = config2.debounceMs ?? DEFAULT_DEBOUNCE_MS;
2438
+ this.batchWindowMs = config2.batchWindowMs ?? DEFAULT_BATCH_WINDOW_MS;
2439
+ this.batchThreshold = config2.batchThreshold ?? DEFAULT_BATCH_THRESHOLD;
2440
+ this.batchBufferMs = config2.batchBufferMs ?? DEFAULT_BATCH_BUFFER_MS;
2635
2441
  }
2636
- /** 文件哈希缓存 */
2637
- fileHashes = /* @__PURE__ */ new Map();
2638
- /** 笔记目录缓存 */
2639
- noteDirCache = /* @__PURE__ */ new Set();
2640
- /** 笔记配置缓存 */
2641
- configCache = /* @__PURE__ */ new Map();
2442
+ /** 待处理的文件变更事件队列 */
2443
+ pendingEvents = /* @__PURE__ */ new Map();
2444
+ /** 防抖定时器 */
2445
+ updateTimer = null;
2446
+ /** 批量更新恢复定时器 */
2447
+ batchResumeTimer = null;
2448
+ /** 记录最近的变更时间戳 */
2449
+ recentChanges = [];
2450
+ /** 标记是否正在更新,避免循环触发 - 类似一把更新行为锁 */
2451
+ isUpdating = false;
2452
+ debounceMs;
2453
+ batchWindowMs;
2454
+ batchThreshold;
2455
+ batchBufferMs;
2642
2456
  /**
2643
- * 获取指定文件的 MD5 哈希值,若文件不存在或读取失败返回 null
2457
+ * 设置更新状态锁,用于防止在执行耗时更新操作时被新的文件变更事件打断
2644
2458
  *
2645
- * @param filePath 文件路径
2646
- * @returns 文件哈希
2459
+ * @param flag - true 表示正在更新(锁定),false 表示更新完成(解锁)
2647
2460
  */
2648
- getFileHash(filePath) {
2649
- try {
2650
- if (!existsSync5(filePath)) return null;
2651
- const content = readFileSync3(filePath, "utf-8");
2652
- if (content.length === 0) return null;
2653
- return createHash("md5").update(content).digest("hex");
2654
- } catch {
2655
- return null;
2656
- }
2461
+ setUpdating(flag) {
2462
+ this.isUpdating = flag;
2657
2463
  }
2658
2464
  /**
2659
- * 更新文件哈希缓存,只有当文件内容发生变化时才更新并返回 true
2465
+ * 获取当前是否处于更新锁定状态
2660
2466
  *
2661
- * @param filePath 文件路径
2662
- * @returns 是否发生变化
2467
+ * @returns true 表示正在执行更新操作(事件处理被暂停),false 表示空闲可处理新事件
2663
2468
  */
2664
- updateFileHash(filePath) {
2665
- const current = this.getFileHash(filePath);
2666
- if (!current) return false;
2667
- const prev = this.fileHashes.get(filePath);
2668
- if (prev === current) return false;
2669
- this.fileHashes.set(filePath, current);
2670
- return true;
2469
+ getUpdating() {
2470
+ return this.isUpdating;
2671
2471
  }
2672
2472
  /**
2673
- * 检查指定名称的笔记目录是否已存在于缓存中
2473
+ * 将文件变更事件加入待处理队列,并启动防抖定时器
2674
2474
  *
2675
- * @param name 笔记目录名称
2676
- * @returns 若存在则返回 true,否则返回 false
2475
+ * - 若同一文件路径的事件已存在,则忽略重复事件(去重)
2476
+ * - 每次新事件都会重置防抖计时器,确保在变更停止后才触发处理
2477
+ *
2478
+ * @param event 文件变更事件
2677
2479
  */
2678
- hasNoteDir(name) {
2679
- return this.noteDirCache.has(name);
2480
+ enqueue(event) {
2481
+ if (this.pendingEvents.has(event.path)) return;
2482
+ this.pendingEvents.set(event.path, event);
2483
+ if (this.updateTimer) clearTimeout(this.updateTimer);
2484
+ this.updateTimer = setTimeout(() => this.flush(), this.debounceMs);
2680
2485
  }
2681
2486
  /**
2682
- * 将指定名称的笔记目录添加到缓存中
2487
+ * 立即触发事件队列的处理(防抖到期或手动调用)
2683
2488
  *
2684
- * @param name 笔记目录名称
2489
+ * - 若当前正在更新(isUpdating 为 true),则跳过以避免重复处理
2490
+ * - 清空待处理事件队列,并通过 onFlush 回调交由上层服务处理
2491
+ * - 处理开始后会锁定更新状态,防止处理过程中被新事件打断
2685
2492
  */
2686
- addNoteDir(name) {
2687
- this.noteDirCache.add(name);
2688
- }
2689
- /**
2690
- * 从缓存中移除指定名称的笔记目录
2691
- *
2692
- * @param name 笔记目录名称
2693
- */
2694
- deleteNoteDir(name) {
2695
- this.noteDirCache.delete(name);
2696
- }
2697
- /**
2698
- * 清空所有缓存数据,包括文件哈希、笔记目录和配置快照
2699
- */
2700
- clearAll() {
2701
- this.fileHashes.clear();
2702
- this.noteDirCache.clear();
2703
- this.configCache.clear();
2704
- }
2705
- /**
2706
- * 清除指定笔记目录相关的缓存数据,包括 README.md 和 .tnotes.json 的文件哈希及配置快照
2707
- *
2708
- * @param noteDirName 笔记目录名称
2709
- */
2710
- clearNoteCaches(noteDirName) {
2711
- const readmePath = join6(this.config.notesDir, noteDirName, "README.md");
2712
- const configPath = join6(this.config.notesDir, noteDirName, ".tnotes.json");
2713
- this.fileHashes.delete(readmePath);
2714
- this.fileHashes.delete(configPath);
2715
- this.configCache.delete(configPath);
2493
+ flush() {
2494
+ if (this.isUpdating) return;
2495
+ if (this.pendingEvents.size === 0) return;
2496
+ const events = Array.from(this.pendingEvents.values());
2497
+ this.pendingEvents.clear();
2498
+ this.isUpdating = true;
2499
+ this.config.onFlush(events);
2716
2500
  }
2717
2501
  /**
2718
- * 获取指定配置文件路径对应的配置快照
2502
+ * 记录当前变更时间并检测是否触发批量更新模式
2719
2503
  *
2720
- * @param configPath 配置文件路径(通常为 .tnotes.json 的绝对路径)
2721
- * @returns 配置快照,若不存在则返回 undefined
2722
- */
2723
- getConfigSnapshot(configPath) {
2724
- return this.configCache.get(configPath);
2725
- }
2726
- /**
2727
- * 设置指定配置文件路径的配置快照到缓存中
2504
+ * - 维护一个滑动时间窗口(BATCH_UPDATE_WINDOW_MS)内的变更记录
2505
+ * - 若短时间内(1秒内)变更次数达到阈值(BATCH_UPDATE_THRESHOLD = 3),则判定为批量操作
2506
+ * - 触发批量模式后:
2507
+ * 1. 清空当前待处理事件队列,避免重复处理
2508
+ * 2. 锁定更新状态(isUpdating = true)
2509
+ * 3. 暂停监听服务,并在延迟(窗口 + 缓冲时间)后自动恢复
2728
2510
  *
2729
- * @param configPath 配置文件路径(通常为 .tnotes.json 的绝对路径)
2730
- * @param snapshot 配置快照对象
2511
+ * @param now 当前时间戳(默认使用 Date.now())
2512
+ * @returns true 表示已触发批量更新模式,false 表示仍处于普通监听模式
2731
2513
  */
2732
- setConfigSnapshot(configPath, snapshot) {
2733
- this.configCache.set(configPath, snapshot);
2514
+ recordChangeAndDetectBatch(now = Date.now()) {
2515
+ this.recentChanges.push(now);
2516
+ this.recentChanges = this.recentChanges.filter(
2517
+ (t) => now - t < this.batchWindowMs
2518
+ );
2519
+ if (this.recentChanges.length < this.batchThreshold) return false;
2520
+ this.pendingEvents.clear();
2521
+ this.recentChanges = [];
2522
+ this.isUpdating = true;
2523
+ this.config.onPauseForBatch();
2524
+ this.batchResumeTimer = setTimeout(() => {
2525
+ this.batchResumeTimer = null;
2526
+ this.isUpdating = false;
2527
+ this.config.reinit();
2528
+ this.config.onResumeAfterBatch();
2529
+ }, this.batchWindowMs + this.batchBufferMs);
2530
+ return true;
2734
2531
  }
2735
2532
  /**
2736
- * 读取指定配置文件的快照
2737
- *
2738
- * 解析 .tnotes.json 配置文件,提取 done、enableDiscussions、description 字段。
2533
+ * 清理所有定时器,释放资源
2739
2534
  *
2740
- * @param configPath 配置文件路径(通常为 .tnotes.json 的绝对路径)
2741
- * @returns 配置快照,若文件不存在或解析失败则返回 null
2535
+ * 在服务停止时调用,防止定时器在服务销毁后仍然触发回调
2742
2536
  */
2743
- readConfigSnapshot(configPath) {
2744
- try {
2745
- if (!existsSync5(configPath)) return null;
2746
- const content = readFileSync3(configPath, "utf-8");
2747
- const config2 = JSON.parse(content);
2748
- return {
2749
- done: Boolean(config2.done),
2750
- enableDiscussions: Boolean(config2.enableDiscussions),
2751
- description: config2.description || ""
2752
- };
2753
- } catch (error) {
2754
- this.config.logger.error(`[\u8BFB\u53D6\u914D\u7F6E\u5FEB\u7167] ${error}`);
2755
- return null;
2537
+ clearTimers() {
2538
+ if (this.updateTimer) {
2539
+ clearTimeout(this.updateTimer);
2540
+ this.updateTimer = null;
2756
2541
  }
2757
- }
2758
- /**
2759
- * 从磁盘初始化监听状态缓存:
2760
- * 遍历笔记根目录下的所有子目录,将每个笔记目录的 README.md 和 .tnotes.json
2761
- * 的哈希值及配置快照加载到缓存中。
2762
- */
2763
- initializeFromDisk() {
2764
- try {
2765
- const noteDirs = readdirSync2(this.config.notesDir);
2766
- this.clearAll();
2767
- for (const noteDir of noteDirs) {
2768
- const noteDirPath = join6(this.config.notesDir, noteDir);
2769
- if (!statSync(noteDirPath).isDirectory()) continue;
2770
- this.noteDirCache.add(noteDir);
2771
- const readmePath = join6(noteDirPath, "README.md");
2772
- const readmeHash = this.getFileHash(readmePath);
2773
- if (readmeHash) this.fileHashes.set(readmePath, readmeHash);
2774
- const configPath = join6(noteDirPath, ".tnotes.json");
2775
- const configHash = this.getFileHash(configPath);
2776
- if (configHash) {
2777
- this.fileHashes.set(configPath, configHash);
2778
- const snapshot = this.readConfigSnapshot(configPath);
2779
- if (snapshot) this.configCache.set(configPath, snapshot);
2780
- }
2781
- }
2782
- } catch (error) {
2783
- this.config.logger.warn(
2784
- `[initializeFromDisk] \u521D\u59CB\u5316\u76D1\u542C\u72B6\u6001\u5931\u8D25: ${error}`
2785
- );
2542
+ if (this.batchResumeTimer) {
2543
+ clearTimeout(this.batchResumeTimer);
2544
+ this.batchResumeTimer = null;
2786
2545
  }
2546
+ this.pendingEvents.clear();
2547
+ this.recentChanges = [];
2787
2548
  }
2788
2549
  };
2789
2550
 
2790
- // services/note/service.ts
2791
- import { writeFileSync as writeFileSync4, readFileSync as readFileSync5 } from "fs";
2792
- import { join as join7 } from "path";
2793
- import { v4 as uuidv4 } from "uuid";
2551
+ // services/file-watcher/folderChangeHandler.ts
2552
+ import { existsSync as existsSync4, promises as fsPromises3 } from "fs";
2553
+ import { join as join4 } from "path";
2794
2554
 
2795
- // services/readme/service.ts
2796
- import {
2797
- existsSync as existsSync6,
2798
- readFileSync as readFileSync4,
2799
- writeFileSync as writeFileSync3,
2800
- promises as fsPromises2
2801
- } from "fs";
2802
- var ReadmeService = class _ReadmeService {
2803
- static instance;
2804
- noteManager;
2805
- readmeGenerator;
2806
- configManager;
2807
- noteIndexCache;
2808
- constructor() {
2809
- this.noteManager = NoteManager.getInstance();
2810
- this.readmeGenerator = new ReadmeGenerator();
2811
- this.configManager = ConfigManager.getInstance();
2812
- this.noteIndexCache = NoteIndexCache.getInstance();
2555
+ // services/file-watcher/internal.ts
2556
+ var WATCH_EVENT_TYPES = {
2557
+ README: "readme",
2558
+ CONFIG: "config"
2559
+ };
2560
+ async function safeExecute(label, fn, logger2) {
2561
+ try {
2562
+ await fn();
2563
+ return true;
2564
+ } catch (error) {
2565
+ logger2.error(`[${label}] ${error}`);
2566
+ return false;
2813
2567
  }
2814
- static getInstance() {
2815
- if (!_ReadmeService.instance) {
2816
- _ReadmeService.instance = new _ReadmeService();
2817
- }
2818
- return _ReadmeService.instance;
2568
+ }
2569
+
2570
+ // services/file-watcher/folderChangeHandler.ts
2571
+ var RENAME_REVERT_DELAY_MS = 2e3;
2572
+ var DELETE_REINIT_DELAY_MS = 1e3;
2573
+ var UPDATE_UNLOCK_DELAY_MS = 500;
2574
+ var FolderChangeHandler = class {
2575
+ constructor(config2) {
2576
+ this.config = config2;
2819
2577
  }
2578
+ /** 活跃的定时器 ID 集合,用于统一清理 */
2579
+ activeTimers = /* @__PURE__ */ new Set();
2820
2580
  /**
2821
- * 更新所有笔记的 README
2822
- * @param options - 更新选项
2581
+ * 清理所有活跃定时器,释放资源
2582
+ *
2583
+ * 在服务停止时调用,防止定时器在服务销毁后仍然触发回调
2823
2584
  */
2824
- async updateAllReadmes(options = {}) {
2825
- const {
2826
- updateSidebar = true,
2827
- updateHome = true,
2828
- notes: providedNotes
2829
- } = options;
2830
- logger.info("\u5F00\u59CB\u66F4\u65B0\u77E5\u8BC6\u5E93...");
2831
- const notes = providedNotes ?? this.noteManager.scanNotes();
2832
- logger.info(`\u626B\u63CF\u5230 ${notes.length} \u7BC7\u7B14\u8BB0`);
2833
- const changedIndexes = await this.getChangedNoteIndexes();
2834
- const shouldIncrementalUpdate = changedIndexes.size > 0 && changedIndexes.size < notes.length * 0.3;
2835
- let notesToUpdate = notes;
2836
- if (shouldIncrementalUpdate) {
2837
- notesToUpdate = notes.filter((note) => changedIndexes.has(note.index));
2838
- logger.info(
2839
- `\u68C0\u6D4B\u5230 ${changedIndexes.size} \u7BC7\u7B14\u8BB0\u6709\u53D8\u66F4\uFF0C\u4F7F\u7528\u589E\u91CF\u66F4\u65B0\u6A21\u5F0F`
2840
- );
2841
- } else {
2842
- logger.info("\u4F7F\u7528\u5168\u91CF\u66F4\u65B0\u6A21\u5F0F");
2843
- }
2844
- const startTime = Date.now();
2845
- await this.updateNoteReadmesInParallel(notesToUpdate);
2846
- const updateTime = Date.now() - startTime;
2847
- logger.info(`\u66F4\u65B0\u4E86 ${notesToUpdate.length} \u7BC7\u7B14\u8BB0 (\u8017\u65F6 ${updateTime}ms)`);
2848
- if (updateHome) {
2849
- await this.updateHomeReadme(notes);
2850
- }
2851
- if (updateSidebar) {
2852
- await this.updateSidebar(notes);
2585
+ clearTimers() {
2586
+ for (const timer of this.activeTimers) {
2587
+ clearTimeout(timer);
2853
2588
  }
2854
- logger.info("\u77E5\u8BC6\u5E93\u66F4\u65B0\u5B8C\u6210\uFF01");
2589
+ this.activeTimers.clear();
2855
2590
  }
2856
- /**
2857
- * 只更新指定笔记的 README(不更新 sidebar、home)
2858
- * @param noteIndexes - 笔记索引数组,例如 ['0001', '0002']
2859
- */
2860
- async updateNoteReadmesOnly(noteIndexes) {
2861
- if (noteIndexes.length === 0) return;
2862
- const notesToUpdate = [];
2863
- for (const noteIndex of noteIndexes) {
2864
- const note = this.noteManager.getNoteByIndex(noteIndex);
2865
- if (note) {
2866
- notesToUpdate.push(note);
2867
- } else {
2868
- logger.warn(`\u7B14\u8BB0\u672A\u627E\u5230: ${noteIndex}`);
2869
- }
2870
- }
2871
- if (notesToUpdate.length === 0) {
2872
- logger.warn("\u6CA1\u6709\u627E\u5230\u9700\u8981\u66F4\u65B0\u7684\u7B14\u8BB0");
2873
- return;
2874
- }
2875
- for (const note of notesToUpdate) {
2876
- try {
2877
- this.readmeGenerator.updateNoteReadme(note);
2878
- } catch (error) {
2879
- logger.error(`\u66F4\u65B0\u7B14\u8BB0 ${note.dirName} \u5931\u8D25`, error);
2880
- }
2881
- }
2591
+ scheduleTimer(fn, delay) {
2592
+ const timer = setTimeout(() => {
2593
+ this.activeTimers.delete(timer);
2594
+ fn();
2595
+ }, delay);
2596
+ this.activeTimers.add(timer);
2882
2597
  }
2883
- /**
2884
- * 获取变更的笔记索引集合
2885
- * @returns 变更的笔记索引集合
2886
- */
2887
- async getChangedNoteIndexes() {
2598
+ async handleFolderDeletion(deletedFolderName) {
2599
+ const { scheduler, watchState, noteIndexCache, readmeService, logger: logger2 } = this.config;
2600
+ if (scheduler.getUpdating()) return;
2601
+ scheduler.setUpdating(true);
2888
2602
  try {
2889
- return getChangedIds();
2890
- } catch (error) {
2891
- return /* @__PURE__ */ new Set();
2603
+ const noteIndex = this.extractNoteIndexOrWarn(deletedFolderName);
2604
+ if (!noteIndex) return;
2605
+ logger2.info(`\u6B63\u5728\u5904\u7406\u7B14\u8BB0\u5220\u9664: ${noteIndex} (${deletedFolderName})`);
2606
+ watchState.deleteNoteDir(deletedFolderName);
2607
+ watchState.clearNoteCaches(deletedFolderName);
2608
+ noteIndexCache.delete(noteIndex);
2609
+ await safeExecute(
2610
+ `\u5220\u9664\u7B14\u8BB0 ${noteIndex}`,
2611
+ async () => {
2612
+ await readmeService.deleteNoteFromReadme(noteIndex);
2613
+ await readmeService.regenerateSidebar();
2614
+ },
2615
+ logger2
2616
+ );
2617
+ } finally {
2618
+ this.scheduleTimer(() => {
2619
+ scheduler.setUpdating(false);
2620
+ watchState.initializeFromDisk();
2621
+ }, DELETE_REINIT_DELAY_MS);
2892
2622
  }
2893
2623
  }
2894
- /**
2895
- * 并行更新多个笔记的 README
2896
- * @param notes - 笔记信息数组
2897
- */
2898
- async updateNoteReadmesInParallel(notes) {
2899
- const batchSize = 10;
2900
- const batches = [];
2901
- for (let i = 0; i < notes.length; i += batchSize) {
2902
- batches.push(notes.slice(i, i + batchSize));
2903
- }
2904
- let successCount = 0;
2905
- let failCount = 0;
2906
- for (const batch of batches) {
2907
- const results = await Promise.allSettled(
2908
- batch.map(
2909
- (note) => Promise.resolve().then(() => {
2910
- this.readmeGenerator.updateNoteReadme(note);
2911
- })
2912
- )
2624
+ async handleFolderRenameUpdate(oldName, newName) {
2625
+ const { scheduler, watchState, logger: logger2 } = this.config;
2626
+ if (scheduler.getUpdating()) return;
2627
+ scheduler.setUpdating(true);
2628
+ try {
2629
+ const { oldNoteIndex, newNoteIndex } = this.validateRenameIndexes(
2630
+ oldName,
2631
+ newName
2913
2632
  );
2914
- for (const result of results) {
2915
- if (result.status === "fulfilled") {
2916
- successCount++;
2917
- } else {
2918
- failCount++;
2919
- logger.error("\u66F4\u65B0\u7B14\u8BB0\u5931\u8D25", result.reason);
2920
- }
2633
+ if (!oldNoteIndex || !newNoteIndex) return;
2634
+ logger2.info(`\u6B63\u5728\u5904\u7406\u6587\u4EF6\u5939\u91CD\u547D\u540D: ${oldName} \u2192 ${newName}`);
2635
+ if (oldNoteIndex === newNoteIndex) {
2636
+ await safeExecute(
2637
+ `\u6807\u9898\u91CD\u547D\u540D ${newNoteIndex}`,
2638
+ () => this.handleTitleOnlyRename(newNoteIndex, newName),
2639
+ logger2
2640
+ );
2641
+ } else {
2642
+ await safeExecute(
2643
+ `\u7D22\u5F15\u53D8\u66F4 ${oldNoteIndex}\u2192${newNoteIndex}`,
2644
+ () => this.handleIndexChangedRename(oldNoteIndex, newNoteIndex),
2645
+ logger2
2646
+ );
2921
2647
  }
2922
- }
2923
- if (failCount > 0) {
2924
- logger.warn(`\u66F4\u65B0\u5B8C\u6210\uFF1A\u6210\u529F ${successCount} \u7BC7\uFF0C\u5931\u8D25 ${failCount} \u7BC7`);
2648
+ this.config.onRenameSuccess?.({
2649
+ oldFolder: oldName,
2650
+ newFolder: newName,
2651
+ noteIndex: newNoteIndex
2652
+ });
2653
+ } finally {
2654
+ this.scheduleTimer(() => {
2655
+ scheduler.setUpdating(false);
2656
+ watchState.initializeFromDisk();
2657
+ }, UPDATE_UNLOCK_DELAY_MS);
2925
2658
  }
2926
2659
  }
2927
- /**
2928
- * 更新侧边栏配置
2929
- * @param notes - 笔记信息数组
2930
- */
2931
- async updateSidebar(notes) {
2932
- if (!existsSync6(ROOT_README_PATH)) {
2933
- logger.error("\u672A\u627E\u5230\u9996\u9875 README\uFF0C\u65E0\u6CD5\u751F\u6210\u4FA7\u8FB9\u680F");
2934
- return;
2660
+ // #region - 私有实现
2661
+ validateRenameIndexes(oldName, newName) {
2662
+ const { noteIndexCache, logger: logger2 } = this.config;
2663
+ const oldNoteIndex = this.extractNoteIndexOrWarn(oldName);
2664
+ const newNoteIndex = this.extractNoteIndexOrWarn(newName);
2665
+ if (!oldNoteIndex || !newNoteIndex) {
2666
+ return { oldNoteIndex: null, newNoteIndex: null };
2935
2667
  }
2936
- const content = readFileSync4(ROOT_README_PATH, "utf-8");
2937
- const lines = content.split("\n");
2938
- const itemList = [];
2939
- const titles = [];
2940
- const titlesNotesCount = [];
2941
- let currentNoteCount = 0;
2942
- let inTocRegion = false;
2943
- for (const line of lines) {
2944
- if (line.includes("<!-- region:toc -->")) {
2945
- inTocRegion = true;
2946
- continue;
2947
- }
2948
- if (line.includes("<!-- endregion:toc -->")) {
2949
- inTocRegion = false;
2950
- continue;
2951
- }
2952
- if (inTocRegion) {
2953
- continue;
2954
- }
2955
- const parsed = parseNoteLine(line);
2956
- if (parsed.isMatch && parsed.noteIndex) {
2957
- const note = notes.find((n) => n.index === parsed.noteIndex);
2958
- if (!note) {
2959
- logger.warn(`\u672A\u627E\u5230\u7B14\u8BB0\u7D22\u5F15: ${parsed.noteIndex}`);
2960
- continue;
2961
- }
2962
- let statusEmoji = "\u23F0 ";
2963
- if (note?.config) {
2964
- if (note.config.done) {
2965
- statusEmoji = "\u2705 ";
2966
- }
2967
- }
2968
- const sidebarShowNoteId2 = this.configManager.get("sidebarShowNoteId");
2969
- let displayText = note.dirName;
2970
- if (!sidebarShowNoteId2) {
2971
- displayText = note.dirName.replace(/^\d{4}\.\s/, "");
2972
- }
2973
- itemList.push({
2974
- text: statusEmoji + displayText,
2975
- link: `/notes/${note.dirName}/README`
2976
- });
2977
- currentNoteCount++;
2978
- continue;
2979
- }
2980
- const titleMatch = line.match(/^(#{2,})\s+(.+)$/);
2981
- if (titleMatch) {
2982
- if (titles.length > 0) {
2983
- titlesNotesCount.push(currentNoteCount);
2984
- }
2985
- titles.push(line);
2986
- currentNoteCount = 0;
2987
- }
2668
+ if (!/^\d{4}$/.test(newNoteIndex)) {
2669
+ logger2.error(`\u65B0\u7B14\u8BB0\u7D22\u5F15\u683C\u5F0F\u975E\u6CD5: ${newNoteIndex}\uFF0C\u81EA\u52A8\u56DE\u9000`);
2670
+ this.revertFolderRename(oldName, newName);
2671
+ return { oldNoteIndex: null, newNoteIndex: null };
2988
2672
  }
2989
- if (titles.length > 0) {
2990
- titlesNotesCount.push(currentNoteCount);
2673
+ if (oldNoteIndex !== newNoteIndex && noteIndexCache.has(newNoteIndex)) {
2674
+ logger2.error(`\u65B0\u7B14\u8BB0\u7D22\u5F15 ${newNoteIndex} \u5DF2\u5B58\u5728\uFF0C\u81EA\u52A8\u56DE\u9000`);
2675
+ this.revertFolderRename(oldName, newName);
2676
+ return { oldNoteIndex: null, newNoteIndex: null };
2991
2677
  }
2992
- const sidebarIsCollapsed = true;
2993
- const hierarchicalSidebar = genHierarchicalSidebar(
2994
- itemList,
2995
- titles,
2996
- titlesNotesCount,
2997
- sidebarIsCollapsed
2998
- );
2999
- writeFileSync3(
3000
- VP_SIDEBAR_PATH,
3001
- JSON.stringify(hierarchicalSidebar, null, 2),
3002
- "utf-8"
3003
- );
3004
- logger.info("\u5DF2\u66F4\u65B0\u4FA7\u8FB9\u680F\u914D\u7F6E");
3005
- }
3006
- /**
3007
- * 更新首页 README
3008
- * @param notes - 笔记信息数组
3009
- */
3010
- async updateHomeReadme(notes) {
3011
- this.readmeGenerator.updateHomeReadme(notes, ROOT_README_PATH);
2678
+ return { oldNoteIndex, newNoteIndex };
3012
2679
  }
3013
- /**
3014
- * 增量更新首页 README 中的单个笔记
3015
- * @param noteIndex - 笔记索引
3016
- * @param updates - 需要更新的配置字段
3017
- */
3018
- async updateNoteInReadme(noteIndex, updates) {
3019
- const item = this.noteIndexCache.getByNoteIndex(noteIndex);
3020
- if (!item) {
3021
- logger.warn(`\u5C1D\u8BD5\u66F4\u65B0\u4E0D\u5B58\u5728\u7684\u7B14\u8BB0: ${noteIndex}`);
3022
- return;
3023
- }
3024
- const content = await fsPromises2.readFile(ROOT_README_PATH, "utf-8");
3025
- const lines = content.split("\n");
3026
- const repoOwner = this.configManager.get("author");
3027
- const repoName2 = this.configManager.get("repoName");
3028
- const mergedConfig = { ...item.noteConfig, ...updates };
3029
- const tempNoteInfo = {
3030
- index: noteIndex,
3031
- dirName: item.folderName,
3032
- path: "",
3033
- readmePath: "",
3034
- configPath: "",
3035
- config: mergedConfig
3036
- };
3037
- let updated = false;
3038
- for (let i = 0; i < lines.length; i++) {
3039
- const parsed = parseNoteLine(lines[i]);
3040
- if (parsed.noteIndex === noteIndex) {
3041
- lines[i] = buildNoteLineMarkdown(tempNoteInfo, repoOwner, repoName2);
3042
- updated = true;
3043
- }
3044
- }
3045
- if (updated) {
3046
- await fsPromises2.writeFile(ROOT_README_PATH, lines.join("\n"), "utf-8");
3047
- logger.info(`\u589E\u91CF\u66F4\u65B0 README.md \u4E2D\u7684\u7B14\u8BB0: ${noteIndex}`);
3048
- } else {
3049
- logger.warn(`README.md \u4E2D\u672A\u627E\u5230\u7B14\u8BB0: ${noteIndex}`);
2680
+ async handleTitleOnlyRename(noteIndex, newName) {
2681
+ const { noteIndexCache, readmeService, logger: logger2, notesDir } = this.config;
2682
+ logger2.info(`\u7B14\u8BB0\u7D22\u5F15\u672A\u53D8 (${noteIndex})\uFF0C\u53EA\u66F4\u65B0\u6807\u9898`);
2683
+ noteIndexCache.updateFolderName(noteIndex, newName);
2684
+ await safeExecute(
2685
+ `\u66F4\u65B0 README \u4E00\u7EA7\u6807\u9898 ${noteIndex}`,
2686
+ () => this.rewriteNoteReadmeH1(notesDir, newName, noteIndex),
2687
+ logger2
2688
+ );
2689
+ const item = noteIndexCache.getByNoteIndex(noteIndex);
2690
+ if (item) {
2691
+ await readmeService.updateNoteInReadme(noteIndex, item.noteConfig);
3050
2692
  }
2693
+ await readmeService.regenerateSidebar();
2694
+ logger2.success(`\u6807\u9898\u66F4\u65B0\u5B8C\u6210`);
3051
2695
  }
3052
2696
  /**
3053
- * 从首页 README 中删除笔记
3054
- * @param noteIndex - 笔记索引
2697
+ * 重写指定笔记目录下 README.md 的一级标题。
2698
+ *
2699
+ * 输入的 `folderName` 形如 `0003. 评论功能的技术实现`,
2700
+ * 据此推导出新标题文本(去掉 `${noteIndex}. ` 前缀)。
3055
2701
  */
3056
- async deleteNoteFromReadme(noteIndex) {
3057
- const content = await fsPromises2.readFile(ROOT_README_PATH, "utf-8");
2702
+ async rewriteNoteReadmeH1(notesDir, folderName, noteIndex) {
2703
+ const readmePath = join4(notesDir, folderName, "README.md");
2704
+ if (!existsSync4(readmePath)) return;
2705
+ const newTitle = folderName.replace(/^\d{4}\.\s*/, "").trim();
2706
+ if (!newTitle) return;
2707
+ const content = await fsPromises3.readFile(readmePath, "utf-8");
3058
2708
  const lines = content.split("\n");
3059
- const linesToRemove = [];
2709
+ let h1Index = -1;
3060
2710
  for (let i = 0; i < lines.length; i++) {
3061
- const parsed = parseNoteLine(lines[i]);
3062
- if (parsed.noteIndex === noteIndex) {
3063
- linesToRemove.push(i);
2711
+ if (lines[i].trim().startsWith("# ")) {
2712
+ h1Index = i;
2713
+ break;
3064
2714
  }
3065
2715
  }
3066
- if (linesToRemove.length > 0) {
3067
- for (let i = linesToRemove.length - 1; i >= 0; i--) {
3068
- lines.splice(linesToRemove[i], 1);
3069
- }
3070
- await fsPromises2.writeFile(ROOT_README_PATH, lines.join("\n"), "utf-8");
3071
- logger.info(
3072
- `\u4ECE README.md \u4E2D\u5220\u9664\u7B14\u8BB0: ${noteIndex} (${linesToRemove.length} \u5904\u5F15\u7528)`
2716
+ if (h1Index === -1) {
2717
+ this.config.logger.warn(
2718
+ `\u672A\u5728 ${readmePath} \u627E\u5230\u4E00\u7EA7\u6807\u9898\uFF0C\u8DF3\u8FC7 README H1 \u66F4\u65B0`
3073
2719
  );
3074
- } else {
3075
- logger.warn(`README.md \u4E2D\u672A\u627E\u5230\u7B14\u8BB0: ${noteIndex}`);
2720
+ return;
3076
2721
  }
3077
- }
2722
+ const newH1 = generateNoteTitle(noteIndex, newTitle, REPO_NOTES_URL);
2723
+ if (lines[h1Index] === newH1) return;
2724
+ lines[h1Index] = newH1;
2725
+ await fsPromises3.writeFile(readmePath, lines.join("\n"), "utf-8");
2726
+ }
2727
+ async handleIndexChangedRename(oldNoteIndex, newNoteIndex) {
2728
+ const { noteService, noteIndexCache, readmeService, logger: logger2 } = this.config;
2729
+ logger2.info(`\u7B14\u8BB0\u7D22\u5F15\u53D8\u66F4: ${oldNoteIndex} \u2192 ${newNoteIndex}`);
2730
+ await readmeService.deleteNoteFromReadme(oldNoteIndex);
2731
+ const newNote = noteService.getNoteByIndex(newNoteIndex);
2732
+ if (newNote) {
2733
+ noteIndexCache.delete(oldNoteIndex);
2734
+ noteIndexCache.add(newNote);
2735
+ await readmeService.appendNoteToReadme(newNoteIndex);
2736
+ await readmeService.regenerateSidebar();
2737
+ logger2.success(`\u7B14\u8BB0\u7D22\u5F15\u53D8\u66F4\u5904\u7406\u5B8C\u6210`);
2738
+ } else {
2739
+ logger2.error(`\u672A\u627E\u5230\u65B0\u7B14\u8BB0: ${newNoteIndex}`);
2740
+ }
2741
+ }
2742
+ async revertFolderRename(oldName, newName) {
2743
+ const { notesDir, scheduler, watchState, logger: logger2 } = this.config;
2744
+ try {
2745
+ const oldPath = join4(notesDir, oldName);
2746
+ const newPath = join4(notesDir, newName);
2747
+ if (existsSync4(newPath)) {
2748
+ scheduler.setUpdating(true);
2749
+ await fsPromises3.rename(newPath, oldPath);
2750
+ logger2.warn(`\u6587\u4EF6\u5939\u5DF2\u56DE\u9000: ${newName} \u2192 ${oldName}`);
2751
+ this.scheduleTimer(() => {
2752
+ scheduler.setUpdating(false);
2753
+ watchState.initializeFromDisk();
2754
+ }, RENAME_REVERT_DELAY_MS);
2755
+ }
2756
+ } catch (error) {
2757
+ logger2.error(`\u56DE\u9000\u6587\u4EF6\u5939\u91CD\u547D\u540D\u5931\u8D25: ${error}`);
2758
+ }
2759
+ }
2760
+ extractNoteIndexOrWarn(name) {
2761
+ const noteIndex = name.match(/^(\d{4})/)?.[1] || null;
2762
+ if (!noteIndex) {
2763
+ this.config.logger.warn(`\u65E0\u6CD5\u4ECE\u6587\u4EF6\u5939\u540D\u79F0\u63D0\u53D6\u7B14\u8BB0\u7D22\u5F15: ${name}`);
2764
+ }
2765
+ return noteIndex;
2766
+ }
2767
+ // #endregion - 私有实现
2768
+ };
2769
+
2770
+ // services/file-watcher/fsWatcherAdapter.ts
2771
+ import { watch } from "fs";
2772
+ import { basename, dirname, join as join5, sep } from "path";
2773
+ var FsWatcherAdapter = class {
2774
+ constructor(config2) {
2775
+ this.config = config2;
2776
+ }
2777
+ /** 文件系统监听器实例 */
2778
+ watcher = null;
2779
+ start() {
2780
+ const { logger: logger2 } = this.config;
2781
+ if (this.watcher) {
2782
+ logger2.warn("\u6587\u4EF6\u76D1\u542C\u670D\u52A1\u5DF2\u542F\u52A8");
2783
+ return;
2784
+ }
2785
+ this.watcher = watch(
2786
+ this.config.notesDir,
2787
+ { recursive: true },
2788
+ (eventType, filename) => this.handleFsEvent(eventType, filename)
2789
+ );
2790
+ logger2.debug(`\u6587\u4EF6\u76D1\u542C\u5DF2\u542F\u52A8\uFF0C\u76D1\u542C\u76EE\u5F55\uFF1A${this.config.notesDir}`);
2791
+ }
2792
+ stop() {
2793
+ if (!this.watcher) return;
2794
+ this.watcher.close();
2795
+ this.watcher = null;
2796
+ }
2797
+ isWatching() {
2798
+ return this.watcher !== null;
2799
+ }
2800
+ handleFsEvent(eventType, filename) {
2801
+ const { isUpdating, onRename, onNoteEvent } = this.config;
2802
+ if (!filename || // 忽略无文件变更
2803
+ isUpdating()) {
2804
+ return;
2805
+ }
2806
+ if (eventType === "rename" && // 检测文件夹 rename 事件
2807
+ !filename.includes(sep)) {
2808
+ onRename(filename);
2809
+ return;
2810
+ }
2811
+ const baseFilename = basename(filename);
2812
+ if (baseFilename !== "README.md" && baseFilename !== ".tnotes.json") {
2813
+ return;
2814
+ }
2815
+ const fullPath = join5(this.config.notesDir, filename);
2816
+ const event = this.buildWatchEvent(fullPath, filename);
2817
+ if (!event) {
2818
+ return;
2819
+ }
2820
+ onNoteEvent(event);
2821
+ }
2822
+ buildWatchEvent(fullPath, filename) {
2823
+ const noteDirName = basename(dirname(fullPath));
2824
+ const noteIndex = NoteManager.extractNoteIndex(noteDirName);
2825
+ if (!noteIndex) {
2826
+ NoteManager.warnInvalidNoteIndex(noteDirName);
2827
+ return null;
2828
+ }
2829
+ const fileType = filename.endsWith("README.md") ? WATCH_EVENT_TYPES.README : WATCH_EVENT_TYPES.CONFIG;
2830
+ return {
2831
+ path: fullPath,
2832
+ type: fileType,
2833
+ noteIndex,
2834
+ noteDirName,
2835
+ noteDirPath: dirname(fullPath)
2836
+ };
2837
+ }
2838
+ };
2839
+
2840
+ // services/file-watcher/globalUpdateCoordinator.ts
2841
+ var GlobalUpdateCoordinator = class {
2842
+ constructor(config2) {
2843
+ this.config = config2;
2844
+ }
2845
+ async applyConfigUpdates(changedNoteIndexes) {
2846
+ if (changedNoteIndexes.length === 0) return;
2847
+ const { readmeService, noteIndexCache, logger: logger2 } = this.config;
2848
+ logger2.info("\u68C0\u6D4B\u5230\u7B14\u8BB0\u72B6\u6001\u53D8\u5316\uFF0C\u589E\u91CF\u66F4\u65B0\u5168\u5C40\u6587\u4EF6...");
2849
+ for (const noteIndex of changedNoteIndexes) {
2850
+ await safeExecute(
2851
+ `\u589E\u91CF\u66F4\u65B0 ${noteIndex}`,
2852
+ async () => {
2853
+ const item = noteIndexCache.getByNoteIndex(noteIndex);
2854
+ await readmeService.updateNoteInReadme(
2855
+ noteIndex,
2856
+ item?.noteConfig || {}
2857
+ );
2858
+ logger2.info(`\u589E\u91CF\u66F4\u65B0 README \u4E2D\u7684\u7B14\u8BB0: ${noteIndex}`);
2859
+ },
2860
+ logger2
2861
+ );
2862
+ }
2863
+ await readmeService.regenerateSidebar();
2864
+ }
2865
+ async updateNoteReadmesOnly(events) {
2866
+ const noteIndexesToUpdate = [...new Set(events.map((c) => c.noteIndex))];
2867
+ if (noteIndexesToUpdate.length === 0) return;
2868
+ await this.config.readmeService.updateNoteReadmesOnly(noteIndexesToUpdate);
2869
+ }
2870
+ };
2871
+
2872
+ // services/file-watcher/readmeChangeHandler.ts
2873
+ var ReadmeChangeHandler = class {
2874
+ constructor(config2) {
2875
+ this.config = config2;
2876
+ }
2877
+ async handle(events) {
2878
+ if (events.length === 0) return;
2879
+ const indexes = [...new Set(events.map((c) => c.noteIndex))];
2880
+ for (const noteIndex of indexes) {
2881
+ const noteInfo = this.config.noteService.getNoteByIndex(noteIndex);
2882
+ if (noteInfo) {
2883
+ await this.config.noteService.fixNoteTitle(noteInfo);
2884
+ }
2885
+ }
2886
+ }
2887
+ };
2888
+
2889
+ // services/file-watcher/renameDetector.ts
2890
+ import { existsSync as existsSync5 } from "fs";
2891
+ import { join as join6 } from "path";
2892
+ var FOLDER_RENAME_DETECT_WINDOW_MS = 500;
2893
+ var RenameDetector = class {
2894
+ constructor(config2) {
2895
+ this.config = config2;
2896
+ }
2897
+ /** 待处理的文件夹重命名 */
2898
+ pendingFolderRename = null;
2899
+ /** 文件夹重命名检测定时器 */
2900
+ folderRenameTimer = null;
2901
+ handleFsRename(folderName) {
2902
+ const { notesDir, dirCache, logger: logger2, onDelete, onRename } = this.config;
2903
+ const folderPath = join6(notesDir, folderName);
2904
+ const folderExists = existsSync5(folderPath);
2905
+ const noteIndex = NoteManager.extractNoteIndex(folderName);
2906
+ if (!noteIndex) {
2907
+ logger2.warn(`\u65E0\u6CD5\u4ECE\u6587\u4EF6\u5939\u540D\u79F0\u63D0\u53D6\u7B14\u8BB0\u7D22\u5F15: ${folderName}`);
2908
+ return;
2909
+ }
2910
+ if (!folderExists) {
2911
+ if (dirCache.has(folderName)) {
2912
+ logger2.info(`\u68C0\u6D4B\u5230\u6587\u4EF6\u5939\u5220\u9664/\u91CD\u547D\u540D: ${folderName}`);
2913
+ this.pendingFolderRename = { oldName: folderName, time: Date.now() };
2914
+ if (this.folderRenameTimer) clearTimeout(this.folderRenameTimer);
2915
+ this.folderRenameTimer = setTimeout(() => {
2916
+ if (this.pendingFolderRename) {
2917
+ logger2.warn(`\u68C0\u6D4B\u5230\u7B14\u8BB0\u5220\u9664: ${this.pendingFolderRename.oldName}`);
2918
+ onDelete(this.pendingFolderRename.oldName);
2919
+ }
2920
+ this.pendingFolderRename = null;
2921
+ this.folderRenameTimer = null;
2922
+ }, FOLDER_RENAME_DETECT_WINDOW_MS);
2923
+ }
2924
+ return;
2925
+ }
2926
+ if (!dirCache.has(folderName)) {
2927
+ logger2.info(`\u68C0\u6D4B\u5230\u6587\u4EF6\u5939\u521B\u5EFA/\u91CD\u547D\u540D: ${folderName}`);
2928
+ if (this.pendingFolderRename && Date.now() - this.pendingFolderRename.time < FOLDER_RENAME_DETECT_WINDOW_MS) {
2929
+ const oldName = this.pendingFolderRename.oldName;
2930
+ const oldNoteIndex = NoteManager.extractNoteIndex(oldName);
2931
+ if (oldNoteIndex && oldNoteIndex === noteIndex) {
2932
+ logger2.info(`\u68C0\u6D4B\u5230\u6587\u4EF6\u5939\u91CD\u547D\u540D: ${oldName} \u2192 ${folderName}`);
2933
+ if (this.folderRenameTimer) {
2934
+ clearTimeout(this.folderRenameTimer);
2935
+ this.folderRenameTimer = null;
2936
+ }
2937
+ onRename(oldName, folderName);
2938
+ this.pendingFolderRename = null;
2939
+ } else if (oldNoteIndex && oldNoteIndex !== noteIndex) {
2940
+ logger2.warn(`\u7D22\u5F15\u51B2\u7A81\uFF0C\u56DE\u9000: ${oldName} -> ${folderName}`);
2941
+ }
2942
+ }
2943
+ dirCache.add(folderName);
2944
+ if (this.pendingFolderRename) {
2945
+ dirCache.delete(this.pendingFolderRename.oldName);
2946
+ }
2947
+ }
2948
+ }
2949
+ clearTimers() {
2950
+ if (this.folderRenameTimer) {
2951
+ clearTimeout(this.folderRenameTimer);
2952
+ this.folderRenameTimer = null;
2953
+ }
2954
+ this.pendingFolderRename = null;
2955
+ }
2956
+ };
2957
+
2958
+ // services/file-watcher/watchState.ts
2959
+ import { createHash } from "crypto";
2960
+ import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync as readdirSync2, statSync } from "fs";
2961
+ import { join as join7 } from "path";
2962
+ var WatchState = class {
2963
+ constructor(config2) {
2964
+ this.config = config2;
2965
+ }
2966
+ /** 文件哈希缓存 */
2967
+ fileHashes = /* @__PURE__ */ new Map();
2968
+ /** 笔记目录缓存 */
2969
+ noteDirCache = /* @__PURE__ */ new Set();
2970
+ /** 笔记配置缓存 */
2971
+ configCache = /* @__PURE__ */ new Map();
2972
+ /**
2973
+ * 获取指定文件的 MD5 哈希值,若文件不存在或读取失败返回 null
2974
+ *
2975
+ * @param filePath 文件路径
2976
+ * @returns 文件哈希
2977
+ */
2978
+ getFileHash(filePath) {
2979
+ try {
2980
+ if (!existsSync6(filePath)) return null;
2981
+ const content = readFileSync5(filePath, "utf-8");
2982
+ if (content.length === 0) return null;
2983
+ return createHash("md5").update(content).digest("hex");
2984
+ } catch {
2985
+ return null;
2986
+ }
2987
+ }
2988
+ /**
2989
+ * 更新文件哈希缓存,只有当文件内容发生变化时才更新并返回 true
2990
+ *
2991
+ * @param filePath 文件路径
2992
+ * @returns 是否发生变化
2993
+ */
2994
+ updateFileHash(filePath) {
2995
+ const current = this.getFileHash(filePath);
2996
+ if (!current) return false;
2997
+ const prev = this.fileHashes.get(filePath);
2998
+ if (prev === current) return false;
2999
+ this.fileHashes.set(filePath, current);
3000
+ return true;
3001
+ }
3002
+ /**
3003
+ * 检查指定名称的笔记目录是否已存在于缓存中
3004
+ *
3005
+ * @param name 笔记目录名称
3006
+ * @returns 若存在则返回 true,否则返回 false
3007
+ */
3008
+ hasNoteDir(name) {
3009
+ return this.noteDirCache.has(name);
3010
+ }
3011
+ /**
3012
+ * 将指定名称的笔记目录添加到缓存中
3013
+ *
3014
+ * @param name 笔记目录名称
3015
+ */
3016
+ addNoteDir(name) {
3017
+ this.noteDirCache.add(name);
3018
+ }
3019
+ /**
3020
+ * 从缓存中移除指定名称的笔记目录
3021
+ *
3022
+ * @param name 笔记目录名称
3023
+ */
3024
+ deleteNoteDir(name) {
3025
+ this.noteDirCache.delete(name);
3026
+ }
3027
+ /**
3028
+ * 清空所有缓存数据,包括文件哈希、笔记目录和配置快照
3029
+ */
3030
+ clearAll() {
3031
+ this.fileHashes.clear();
3032
+ this.noteDirCache.clear();
3033
+ this.configCache.clear();
3034
+ }
3035
+ /**
3036
+ * 清除指定笔记目录相关的缓存数据,包括 README.md 和 .tnotes.json 的文件哈希及配置快照
3037
+ *
3038
+ * @param noteDirName 笔记目录名称
3039
+ */
3040
+ clearNoteCaches(noteDirName) {
3041
+ const readmePath = join7(this.config.notesDir, noteDirName, "README.md");
3042
+ const configPath = join7(this.config.notesDir, noteDirName, ".tnotes.json");
3043
+ this.fileHashes.delete(readmePath);
3044
+ this.fileHashes.delete(configPath);
3045
+ this.configCache.delete(configPath);
3046
+ }
3047
+ /**
3048
+ * 获取指定配置文件路径对应的配置快照
3049
+ *
3050
+ * @param configPath 配置文件路径(通常为 .tnotes.json 的绝对路径)
3051
+ * @returns 配置快照,若不存在则返回 undefined
3052
+ */
3053
+ getConfigSnapshot(configPath) {
3054
+ return this.configCache.get(configPath);
3055
+ }
3056
+ /**
3057
+ * 设置指定配置文件路径的配置快照到缓存中
3058
+ *
3059
+ * @param configPath 配置文件路径(通常为 .tnotes.json 的绝对路径)
3060
+ * @param snapshot 配置快照对象
3061
+ */
3062
+ setConfigSnapshot(configPath, snapshot) {
3063
+ this.configCache.set(configPath, snapshot);
3064
+ }
3065
+ /**
3066
+ * 读取指定配置文件的快照
3067
+ *
3068
+ * 解析 .tnotes.json 配置文件,提取 done、enableDiscussions、description 字段。
3069
+ *
3070
+ * @param configPath 配置文件路径(通常为 .tnotes.json 的绝对路径)
3071
+ * @returns 配置快照,若文件不存在或解析失败则返回 null
3072
+ */
3073
+ readConfigSnapshot(configPath) {
3074
+ try {
3075
+ if (!existsSync6(configPath)) return null;
3076
+ const content = readFileSync5(configPath, "utf-8");
3077
+ const config2 = JSON.parse(content);
3078
+ return {
3079
+ done: Boolean(config2.done),
3080
+ enableDiscussions: Boolean(config2.enableDiscussions),
3081
+ description: config2.description || ""
3082
+ };
3083
+ } catch (error) {
3084
+ this.config.logger.error(`[\u8BFB\u53D6\u914D\u7F6E\u5FEB\u7167] ${error}`);
3085
+ return null;
3086
+ }
3087
+ }
3088
+ /**
3089
+ * 从磁盘初始化监听状态缓存:
3090
+ * 遍历笔记根目录下的所有子目录,将每个笔记目录的 README.md 和 .tnotes.json
3091
+ * 的哈希值及配置快照加载到缓存中。
3092
+ */
3093
+ initializeFromDisk() {
3094
+ try {
3095
+ const noteDirs = readdirSync2(this.config.notesDir);
3096
+ this.clearAll();
3097
+ for (const noteDir of noteDirs) {
3098
+ const noteDirPath = join7(this.config.notesDir, noteDir);
3099
+ if (!statSync(noteDirPath).isDirectory()) continue;
3100
+ this.noteDirCache.add(noteDir);
3101
+ const readmePath = join7(noteDirPath, "README.md");
3102
+ const readmeHash = this.getFileHash(readmePath);
3103
+ if (readmeHash) this.fileHashes.set(readmePath, readmeHash);
3104
+ const configPath = join7(noteDirPath, ".tnotes.json");
3105
+ const configHash = this.getFileHash(configPath);
3106
+ if (configHash) {
3107
+ this.fileHashes.set(configPath, configHash);
3108
+ const snapshot = this.readConfigSnapshot(configPath);
3109
+ if (snapshot) this.configCache.set(configPath, snapshot);
3110
+ }
3111
+ }
3112
+ } catch (error) {
3113
+ this.config.logger.warn(
3114
+ `[initializeFromDisk] \u521D\u59CB\u5316\u76D1\u542C\u72B6\u6001\u5931\u8D25: ${error}`
3115
+ );
3116
+ }
3117
+ }
3118
+ };
3119
+
3120
+ // services/file-watcher/service.ts
3121
+ var NOTES_DIR_NOT_SET_ERROR = "NOTES_DIR_PATH \u672A\u8BBE\u7F6E\uFF0C\u65E0\u6CD5\u542F\u52A8\u6587\u4EF6\u76D1\u542C";
3122
+ var UPDATE_UNLOCK_DELAY_MS2 = 500;
3123
+ var RENAME_BROADCAST_PATH = "/__tnotes_broadcast_rename";
3124
+ var FileWatcherService = class _FileWatcherService {
3125
+ constructor(notesDir = NOTES_DIR_PATH) {
3126
+ this.notesDir = notesDir;
3127
+ if (!this.notesDir) {
3128
+ throw new Error(NOTES_DIR_NOT_SET_ERROR);
3129
+ }
3130
+ this.init();
3131
+ _FileWatcherService.instance = this;
3132
+ }
3133
+ static instance = null;
3134
+ watchState;
3135
+ scheduler;
3136
+ renameDetector;
3137
+ configHandler;
3138
+ readmeHandler;
3139
+ coordinator;
3140
+ folderHandler;
3141
+ adapter;
3142
+ noteService;
3143
+ readmeService;
3144
+ noteIndexCache;
3145
+ unlockTimer = null;
3146
+ static getInstance() {
3147
+ return _FileWatcherService.instance;
3148
+ }
3149
+ init() {
3150
+ this.noteService = NoteService.getInstance();
3151
+ this.readmeService = ReadmeService.getInstance();
3152
+ this.noteIndexCache = NoteIndexCache.getInstance();
3153
+ this.watchState = this.initWatchState();
3154
+ this.scheduler = this.initScheduler();
3155
+ this.folderHandler = this.initFolderHandler();
3156
+ this.renameDetector = this.initRenameDetector();
3157
+ this.configHandler = this.initConfigHandler();
3158
+ this.readmeHandler = this.initReadmeHandler();
3159
+ this.coordinator = this.initCoordinator();
3160
+ this.adapter = this.initAdapter();
3161
+ }
3162
+ initWatchState() {
3163
+ const watchState = new WatchState({ notesDir: this.notesDir, logger });
3164
+ watchState.initializeFromDisk();
3165
+ return watchState;
3166
+ }
3167
+ initScheduler() {
3168
+ return new EventScheduler({
3169
+ onFlush: (events) => this.handleFileChange(events),
3170
+ onPauseForBatch: () => logger.warn("\u76D1\u542C\u670D\u52A1\u6682\u505C 3s \u7B49\u5F85\u6279\u91CF\u66F4\u65B0\u5B8C\u6210..."),
3171
+ onResumeAfterBatch: () => logger.info("\u6062\u590D\u81EA\u52A8\u76D1\u542C"),
3172
+ reinit: () => this.watchState.initializeFromDisk()
3173
+ });
3174
+ }
3175
+ initFolderHandler() {
3176
+ return new FolderChangeHandler({
3177
+ notesDir: this.notesDir,
3178
+ watchState: this.watchState,
3179
+ scheduler: this.scheduler,
3180
+ noteService: this.noteService,
3181
+ readmeService: this.readmeService,
3182
+ noteIndexCache: this.noteIndexCache,
3183
+ logger,
3184
+ onRenameSuccess: (payload) => {
3185
+ void this.broadcastRename(payload);
3186
+ }
3187
+ });
3188
+ }
3189
+ async broadcastRename(payload) {
3190
+ const path2 = `/${repoName}${RENAME_BROADCAST_PATH}`;
3191
+ const body = JSON.stringify(payload);
3192
+ const hosts = ["127.0.0.1", "::1"];
3193
+ let lastError = null;
3194
+ for (const host of hosts) {
3195
+ try {
3196
+ await this.postOnce(host, path2, body);
3197
+ return;
3198
+ } catch (error) {
3199
+ lastError = error;
3200
+ }
3201
+ }
3202
+ logger.warn(
3203
+ `\u5E7F\u64AD\u91CD\u547D\u540D\u4E8B\u4EF6\u5931\u8D25 (POST http://[127.0.0.1|::1]:${port}${path2}): ${String(lastError)}`
3204
+ );
3205
+ }
3206
+ postOnce(host, path2, body) {
3207
+ return new Promise((resolve2, reject) => {
3208
+ const req = httpRequest(
3209
+ {
3210
+ host,
3211
+ port,
3212
+ path: path2,
3213
+ method: "POST",
3214
+ family: host.includes(":") ? 6 : 4,
3215
+ headers: {
3216
+ "Content-Type": "application/json",
3217
+ "Content-Length": Buffer.byteLength(body)
3218
+ }
3219
+ },
3220
+ (res) => {
3221
+ res.on("data", () => {
3222
+ });
3223
+ res.on("end", () => {
3224
+ if (res.statusCode && res.statusCode >= 400) {
3225
+ reject(new Error(`HTTP ${res.statusCode}`));
3226
+ return;
3227
+ }
3228
+ resolve2();
3229
+ });
3230
+ }
3231
+ );
3232
+ req.on("error", reject);
3233
+ req.write(body);
3234
+ req.end();
3235
+ });
3236
+ }
3237
+ initRenameDetector() {
3238
+ return new RenameDetector({
3239
+ notesDir: this.notesDir,
3240
+ dirCache: {
3241
+ has: (name) => this.watchState.hasNoteDir(name),
3242
+ add: (name) => this.watchState.addNoteDir(name),
3243
+ delete: (name) => this.watchState.deleteNoteDir(name)
3244
+ },
3245
+ logger,
3246
+ onDelete: (oldName) => this.folderHandler.handleFolderDeletion(oldName),
3247
+ onRename: (oldName, newName) => this.folderHandler.handleFolderRenameUpdate(oldName, newName)
3248
+ });
3249
+ }
3250
+ initConfigHandler() {
3251
+ return new ConfigChangeHandler({
3252
+ state: this.watchState,
3253
+ noteService: this.noteService,
3254
+ noteIndexCache: this.noteIndexCache,
3255
+ logger
3256
+ });
3257
+ }
3258
+ initReadmeHandler() {
3259
+ return new ReadmeChangeHandler({ noteService: this.noteService });
3260
+ }
3261
+ initCoordinator() {
3262
+ return new GlobalUpdateCoordinator({
3263
+ readmeService: this.readmeService,
3264
+ noteIndexCache: this.noteIndexCache,
3265
+ logger
3266
+ });
3267
+ }
3268
+ initAdapter() {
3269
+ return new FsWatcherAdapter({
3270
+ notesDir: this.notesDir,
3271
+ isUpdating: () => this.scheduler.getUpdating(),
3272
+ onRename: (folderName) => this.renameDetector.handleFsRename(folderName),
3273
+ onNoteEvent: (event) => this.onNoteEvent(event),
3274
+ logger
3275
+ });
3276
+ }
3277
+ start() {
3278
+ this.watchState.initializeFromDisk();
3279
+ this.adapter.start();
3280
+ }
3281
+ stop() {
3282
+ this.adapter.stop();
3283
+ this.scheduler.clearTimers();
3284
+ this.renameDetector.clearTimers();
3285
+ this.folderHandler.clearTimers();
3286
+ if (this.unlockTimer) {
3287
+ clearTimeout(this.unlockTimer);
3288
+ this.unlockTimer = null;
3289
+ }
3290
+ logger.info("\u6587\u4EF6\u76D1\u542C\u670D\u52A1\u5DF2\u505C\u6B62");
3291
+ }
3292
+ pause() {
3293
+ this.scheduler.setUpdating(true);
3294
+ logger.info("\u6587\u4EF6\u76D1\u542C\u5DF2\u6682\u505C");
3295
+ }
3296
+ resume() {
3297
+ this.watchState.initializeFromDisk();
3298
+ this.scheduler.setUpdating(false);
3299
+ logger.info("\u6587\u4EF6\u76D1\u542C\u5DF2\u6062\u590D");
3300
+ }
3301
+ isWatching() {
3302
+ return this.adapter.isWatching();
3303
+ }
3078
3304
  /**
3079
- * 在首页 README 末尾添加新笔记
3080
- * @param noteIndex - 笔记索引
3305
+ * 挂起文件监听(关闭 fs.watch 句柄),用于需要操作文件夹的场景(如重命名)
3306
+ * Windows fs.watch 会锁住文件夹句柄,必须先关闭才能重命名
3081
3307
  */
3082
- async appendNoteToReadme(noteIndex) {
3083
- const item = this.noteIndexCache.getByNoteIndex(noteIndex);
3084
- if (!item) {
3085
- logger.warn(`\u5C1D\u8BD5\u6DFB\u52A0\u4E0D\u5B58\u5728\u7684\u7B14\u8BB0: ${noteIndex}`);
3086
- return;
3308
+ suspend() {
3309
+ this.adapter.stop();
3310
+ this.scheduler.setUpdating(true);
3311
+ logger.info("\u6587\u4EF6\u76D1\u542C\u5DF2\u6302\u8D77\uFF08fs.watch \u5DF2\u5173\u95ED\uFF09");
3312
+ }
3313
+ /**
3314
+ * 恢复文件监听(重新启动 fs.watch)
3315
+ */
3316
+ unsuspend() {
3317
+ this.watchState.initializeFromDisk();
3318
+ this.scheduler.setUpdating(false);
3319
+ this.adapter.start();
3320
+ logger.info("\u6587\u4EF6\u76D1\u542C\u5DF2\u6062\u590D\uFF08fs.watch \u5DF2\u91CD\u542F\uFF09");
3321
+ }
3322
+ // #region - 私有实现
3323
+ onNoteEvent(event) {
3324
+ if (!this.isNoteFile(event.path)) return;
3325
+ if (!this.watchState.updateFileHash(event.path)) return;
3326
+ if (this.scheduler.recordChangeAndDetectBatch()) return;
3327
+ this.scheduler.enqueue(event);
3328
+ }
3329
+ async handleFileChange(events) {
3330
+ try {
3331
+ const configChanges = events.filter(
3332
+ (e) => e.type === WATCH_EVENT_TYPES.CONFIG
3333
+ );
3334
+ const readmeChanges = events.filter(
3335
+ (e) => e.type === WATCH_EVENT_TYPES.README
3336
+ );
3337
+ const changedNoteIndexes = await this.configHandler.handle(configChanges);
3338
+ if (changedNoteIndexes.length > 0) {
3339
+ await safeExecute(
3340
+ "\u914D\u7F6E\u53D8\u66F4\u66F4\u65B0",
3341
+ () => this.coordinator.applyConfigUpdates(changedNoteIndexes),
3342
+ logger
3343
+ );
3344
+ return;
3345
+ }
3346
+ await safeExecute(
3347
+ "README \u53D8\u66F4\u66F4\u65B0",
3348
+ async () => {
3349
+ await this.readmeHandler.handle(readmeChanges);
3350
+ await this.coordinator.updateNoteReadmesOnly(events);
3351
+ },
3352
+ logger
3353
+ );
3354
+ } finally {
3355
+ if (this.unlockTimer) clearTimeout(this.unlockTimer);
3356
+ this.unlockTimer = setTimeout(() => {
3357
+ this.unlockTimer = null;
3358
+ this.scheduler.setUpdating(false);
3359
+ }, UPDATE_UNLOCK_DELAY_MS2);
3087
3360
  }
3088
- const content = await fsPromises2.readFile(ROOT_README_PATH, "utf-8");
3089
- const lines = content.split("\n");
3090
- const repoOwner = this.configManager.get("author");
3091
- const repoName2 = this.configManager.get("repoName");
3092
- const tempNoteInfo = {
3093
- index: noteIndex,
3094
- dirName: item.folderName,
3095
- path: "",
3096
- readmePath: "",
3097
- configPath: "",
3098
- config: item.noteConfig
3099
- };
3100
- const noteLine = buildNoteLineMarkdown(tempNoteInfo, repoOwner, repoName2);
3101
- lines.push(noteLine);
3102
- await fsPromises2.writeFile(ROOT_README_PATH, lines.join("\n"), "utf-8");
3103
- logger.info(`\u5728 README.md \u672B\u5C3E\u6DFB\u52A0\u7B14\u8BB0: ${noteIndex}`);
3361
+ }
3362
+ isNoteFile(filePath) {
3363
+ return filePath.endsWith("README.md") || filePath.endsWith(".tnotes.json");
3364
+ }
3365
+ // #endregion - 私有实现
3366
+ };
3367
+
3368
+ // core/GitManager.ts
3369
+ var GitManager = class {
3370
+ logger;
3371
+ dir;
3372
+ constructor(dir, logger2) {
3373
+ this.dir = dir;
3374
+ this.logger = logger2?.child("git") || new Logger({ prefix: "git" });
3375
+ }
3376
+ /**
3377
+ * 检查是否为有效的 Git 仓库
3378
+ */
3379
+ async isValidRepo() {
3380
+ try {
3381
+ const result = await runCommand(
3382
+ "git rev-parse --is-inside-work-tree",
3383
+ this.dir
3384
+ );
3385
+ return result.trim() === "true";
3386
+ } catch {
3387
+ return false;
3388
+ }
3389
+ }
3390
+ /**
3391
+ * 确保是有效的 Git 仓库,否则抛出错误
3392
+ */
3393
+ async ensureValidRepo() {
3394
+ if (!await this.isValidRepo()) {
3395
+ throw createError.gitNotRepo(this.dir);
3396
+ }
3397
+ }
3398
+ /**
3399
+ * 获取 Git 状态
3400
+ */
3401
+ async getStatus() {
3402
+ await this.ensureValidRepo();
3403
+ const statusOutput = await runCommand(
3404
+ "git -c core.quotePath=false status --porcelain",
3405
+ this.dir
3406
+ );
3407
+ const lines = statusOutput.trim().split("\n").filter((line) => line);
3408
+ const files = lines.map((line) => {
3409
+ const statusCode = line.substring(0, 2);
3410
+ let path2 = line.substring(3);
3411
+ path2 = path2.replace(/^"(.*)"$/, "$1");
3412
+ let status = "modified";
3413
+ if (line.startsWith("??")) {
3414
+ status = "untracked";
3415
+ } else if (/^[MADRC]/.test(statusCode)) {
3416
+ status = "staged";
3417
+ } else if (/^.[MD]/.test(statusCode)) {
3418
+ status = "unstaged";
3419
+ }
3420
+ return { path: path2, status, statusCode };
3421
+ });
3422
+ const staged = files.filter((f) => f.status === "staged").length;
3423
+ const unstaged = files.filter((f) => f.status === "unstaged").length;
3424
+ const untracked = files.filter((f) => f.status === "untracked").length;
3425
+ const branch = await runCommand("git branch --show-current", this.dir);
3426
+ let ahead = 0;
3427
+ let behind = 0;
3428
+ try {
3429
+ const aheadBehind = await runCommand(
3430
+ "git rev-list --left-right --count @{upstream}...HEAD",
3431
+ this.dir
3432
+ );
3433
+ const [behindStr, aheadStr] = aheadBehind.trim().split(" ");
3434
+ behind = parseInt(behindStr) || 0;
3435
+ ahead = parseInt(aheadStr) || 0;
3436
+ } catch {
3437
+ }
3438
+ return {
3439
+ hasChanges: lines.length > 0,
3440
+ changedFiles: lines.length,
3441
+ staged,
3442
+ unstaged,
3443
+ untracked,
3444
+ branch: branch.trim(),
3445
+ ahead,
3446
+ behind,
3447
+ files
3448
+ };
3104
3449
  }
3105
3450
  /**
3106
- * 重新生成 sidebar.json(基于当前 README.md)
3107
- * @param notes - 可选的笔记列表,不传则内部扫描
3451
+ * 获取远程仓库信息
3108
3452
  */
3109
- async regenerateSidebar(notes) {
3110
- const allNotes = notes ?? (this.noteIndexCache.isInitialized() ? this.noteIndexCache.toNoteInfoList() : this.noteManager.scanNotes());
3111
- await this.updateSidebar(allNotes);
3112
- logger.info("\u91CD\u65B0\u751F\u6210 sidebar.json");
3113
- }
3114
- };
3115
-
3116
- // services/note/service.ts
3117
- var NEW_NOTES_README_MD_TEMPLATE = `
3118
- <!-- region:toc -->
3119
-
3120
- - [1. \u{1F3AF} \u672C\u8282\u5185\u5BB9](#1--\u672C\u8282\u5185\u5BB9)
3121
- - [2. \u{1FAE7} \u8BC4\u4EF7](#2--\u8BC4\u4EF7)
3122
-
3123
- <!-- endregion:toc -->
3124
-
3125
- ## 1. \u{1F3AF} \u672C\u8282\u5185\u5BB9
3126
-
3127
- - todo
3128
-
3129
- ## 2. \u{1FAE7} \u8BC4\u4EF7
3130
-
3131
- - todo
3132
- `;
3133
- var NoteService = class _NoteService {
3134
- static instance;
3135
- noteManager;
3136
- noteIndexCache;
3137
- ignoredConfigPaths = /* @__PURE__ */ new Set();
3138
- constructor() {
3139
- this.noteManager = NoteManager.getInstance();
3140
- this.noteIndexCache = NoteIndexCache.getInstance();
3141
- }
3142
- static getInstance() {
3143
- if (!_NoteService.instance) {
3144
- _NoteService.instance = new _NoteService();
3453
+ async getRemoteInfo() {
3454
+ try {
3455
+ await this.ensureValidRepo();
3456
+ const remoteUrl = await runCommand(
3457
+ "git config --get remote.origin.url",
3458
+ this.dir
3459
+ );
3460
+ const url = remoteUrl.trim();
3461
+ if (!url) return null;
3462
+ const httpsMatch = url.match(
3463
+ /https:\/\/(?:www\.)?github\.com\/([^/]+)\/(.+?)(?:\.git)?$/
3464
+ );
3465
+ if (httpsMatch) {
3466
+ return {
3467
+ url,
3468
+ type: "https",
3469
+ owner: httpsMatch[1],
3470
+ repo: httpsMatch[2]
3471
+ };
3472
+ }
3473
+ const sshMatch = url.match(/git@github\.com:([^/]+)\/(.+?)(?:\.git)?$/);
3474
+ if (sshMatch) {
3475
+ return {
3476
+ url,
3477
+ type: "ssh",
3478
+ owner: sshMatch[1],
3479
+ repo: sshMatch[2]
3480
+ };
3481
+ }
3482
+ return { url, type: "unknown" };
3483
+ } catch {
3484
+ return null;
3145
3485
  }
3146
- return _NoteService.instance;
3147
3486
  }
3148
3487
  /**
3149
- * 标记配置文件在下次变更时被忽略(防止 API 写入触发文件监听循环)
3150
- * @param configPath - 配置文件路径
3488
+ * 检查是否有未提交的更改
3151
3489
  */
3152
- ignoreNextConfigChange(configPath) {
3153
- this.ignoredConfigPaths.add(configPath);
3490
+ async hasUncommittedChanges() {
3491
+ const status = await this.getStatus();
3492
+ return status.hasChanges;
3154
3493
  }
3155
3494
  /**
3156
- * 检查配置文件是否应该被忽略
3157
- * @param configPath - 配置文件路径
3158
- * @returns 是否应该忽略
3495
+ * Stash 当前更改
3159
3496
  */
3160
- shouldIgnoreConfigChange(configPath) {
3161
- if (this.ignoredConfigPaths.has(configPath)) {
3162
- this.ignoredConfigPaths.delete(configPath);
3497
+ async stash(message) {
3498
+ try {
3499
+ await this.ensureValidRepo();
3500
+ const cmd = message ? `git stash push -m "${message}"` : "git stash push";
3501
+ await runCommand(cmd, this.dir);
3502
+ this.logger.info("Stashed uncommitted changes");
3163
3503
  return true;
3504
+ } catch (error) {
3505
+ this.logger.warn("Failed to stash changes");
3506
+ return false;
3164
3507
  }
3165
- return false;
3166
3508
  }
3167
3509
  /**
3168
- * 获取所有笔记
3169
- * dev 模式下(缓存已初始化)从内存读取,其他模式回退到文件扫描
3170
- * @returns 笔记信息数组
3510
+ * Pop stash
3171
3511
  */
3172
- getAllNotes() {
3173
- if (this.noteIndexCache.isInitialized()) {
3174
- return this.noteIndexCache.toNoteInfoList();
3512
+ async stashPop() {
3513
+ try {
3514
+ await this.ensureValidRepo();
3515
+ await runCommand("git stash pop", this.dir);
3516
+ this.logger.info("Restored stashed changes");
3517
+ return true;
3518
+ } catch (error) {
3519
+ this.logger.warn("Failed to restore stashed changes");
3520
+ return false;
3175
3521
  }
3176
- return this.noteManager.scanNotes();
3177
- }
3178
- /**
3179
- * 获取笔记(通过索引)
3180
- * @param noteIndex - 笔记索引(文件夹前 4 位数字)
3181
- * @returns 笔记信息,未找到时返回 undefined
3182
- */
3183
- getNoteByIndex(noteIndex) {
3184
- return this.noteManager.getNoteByIndex(noteIndex);
3185
- }
3186
- /**
3187
- * 创建新笔记
3188
- * @param options - 创建选项
3189
- * @returns 新创建的笔记信息
3190
- */
3191
- async createNote(options = {}) {
3192
- const {
3193
- title = "new",
3194
- category,
3195
- enableDiscussions = false,
3196
- configId,
3197
- usedIndexes
3198
- } = options;
3199
- const noteIndex = this.generateNextNoteIndex(usedIndexes);
3200
- const dirName = `${noteIndex}. ${title}`;
3201
- const notePath = join7(NOTES_PATH, dirName);
3202
- await ensureDirectory(notePath);
3203
- const readmePath = join7(notePath, "README.md");
3204
- const noteTitle = generateNoteTitle(noteIndex, title, REPO_NOTES_URL);
3205
- const readmeContent = noteTitle + "\n" + NEW_NOTES_README_MD_TEMPLATE;
3206
- writeFileSync4(readmePath, readmeContent, "utf-8");
3207
- const configPath = join7(notePath, ".tnotes.json");
3208
- const config2 = {
3209
- id: configId || uuidv4(),
3210
- // 配置 ID 使用 UUID(跨知识库唯一)
3211
- bilibili: [],
3212
- tnotes: [],
3213
- yuque: [],
3214
- done: false,
3215
- category,
3216
- enableDiscussions
3217
- // created_at / updated_at 由 tn:push 时 fix-timestamps 自动写入
3218
- };
3219
- this.noteManager.writeNoteConfig(configPath, config2);
3220
- logger.info(`Created new note: ${dirName}`);
3221
- return {
3222
- index: noteIndex,
3223
- // 返回的 id 是笔记索引(目录前缀)
3224
- path: notePath,
3225
- dirName,
3226
- readmePath,
3227
- configPath,
3228
- config: config2
3229
- };
3230
3522
  }
3231
3523
  /**
3232
- * 生成下一个笔记索引(填充空缺)
3233
- * @param usedIndexes - 可选的已使用编号集合,不传则内部扫描
3234
- * @returns 新的笔记索引(4位数字字符串,从 0001 到 9999)
3524
+ * 拉取远程更新
3235
3525
  */
3236
- generateNextNoteIndex(usedIndexes) {
3237
- if (!usedIndexes) {
3238
- const notes = this.getAllNotes();
3239
- usedIndexes = /* @__PURE__ */ new Set();
3240
- for (const note of notes) {
3241
- const id = parseInt(note.index, 10);
3242
- if (!isNaN(id) && id >= 1 && id <= 9999) {
3243
- usedIndexes.add(id);
3526
+ async pull(options) {
3527
+ await this.ensureValidRepo();
3528
+ const { rebase = true, autostash = true } = options || {};
3529
+ const hasChanges = await this.hasUncommittedChanges();
3530
+ let didStash = false;
3531
+ if (hasChanges && !autostash) {
3532
+ this.logger.warn("Repository has uncommitted changes");
3533
+ didStash = await this.stash("Auto-stash before pull");
3534
+ }
3535
+ try {
3536
+ const beforeCommit = await runCommand("git rev-parse HEAD", this.dir);
3537
+ this.logger.info("\u6B63\u5728\u62C9\u53D6\u8FDC\u7A0B\u66F4\u65B0...");
3538
+ const cmd = `git pull ${rebase ? "--rebase" : ""} ${autostash ? "--autostash" : ""}`.trim();
3539
+ await runCommand(cmd, this.dir);
3540
+ const afterCommit = await runCommand("git rev-parse HEAD", this.dir);
3541
+ if (beforeCommit.trim() !== afterCommit.trim()) {
3542
+ try {
3543
+ const diffOutput = await runCommand(
3544
+ `git diff --name-only ${beforeCommit.trim()}..${afterCommit.trim()}`,
3545
+ this.dir
3546
+ );
3547
+ const changedFiles = diffOutput.trim().split("\n").filter((f) => f);
3548
+ if (changedFiles.length > 0) {
3549
+ console.log(` \u66F4\u65B0\u4E86 ${changedFiles.length} \u4E2A\u6587\u4EF6:`);
3550
+ changedFiles.forEach((file, index) => {
3551
+ console.log(` ${index + 1}. ${file}`);
3552
+ });
3553
+ }
3554
+ this.logger.success(`\u62C9\u53D6\u6210\u529F: ${changedFiles.length} \u4E2A\u6587\u4EF6\u5DF2\u66F4\u65B0`);
3555
+ } catch {
3556
+ this.logger.success("\u62C9\u53D6\u6210\u529F");
3244
3557
  }
3558
+ } else {
3559
+ this.logger.info("\u5DF2\u662F\u6700\u65B0\uFF0C\u6CA1\u6709\u9700\u8981\u62C9\u53D6\u7684\u66F4\u65B0");
3245
3560
  }
3246
- }
3247
- if (usedIndexes.size === 0) {
3248
- return "0001";
3249
- }
3250
- for (let i = 1; i <= 9999; i++) {
3251
- if (!usedIndexes.has(i)) {
3252
- return i.toString().padStart(CONSTANTS.NOTE_INDEX_LENGTH, "0");
3561
+ } catch (error) {
3562
+ this.logger.error("\u62C9\u53D6\u5931\u8D25");
3563
+ handleError(error);
3564
+ throw error;
3565
+ } finally {
3566
+ if (didStash) {
3567
+ await this.stashPop();
3253
3568
  }
3254
3569
  }
3255
- throw new Error("\u6240\u6709\u7B14\u8BB0\u7F16\u53F7 (0001-9999) \u5DF2\u88AB\u5360\u7528\uFF0C\u65E0\u6CD5\u521B\u5EFA\u65B0\u7B14\u8BB0");
3256
3570
  }
3257
3571
  /**
3258
- * 更新笔记配置
3259
- * @param noteIndex - 笔记索引
3260
- * @param updates - 配置更新
3572
+ * 提交更改
3261
3573
  */
3262
- async updateNoteConfig(noteIndex, updates) {
3263
- const note = this.getNoteByIndex(noteIndex);
3264
- if (!note || !note.config) {
3265
- throw new Error(`Note not found or no config: ${noteIndex}`);
3266
- }
3267
- const oldConfig = { ...note.config };
3268
- const updatedConfig = {
3269
- ...note.config,
3270
- ...updates
3271
- };
3272
- this.ignoreNextConfigChange(note.configPath);
3273
- this.noteManager.updateNoteConfig(note, updatedConfig);
3274
- this.noteIndexCache.updateConfig(noteIndex, updatedConfig);
3275
- const needsGlobalUpdate = this.checkNeedsGlobalUpdate(
3276
- oldConfig,
3277
- updatedConfig
3278
- );
3279
- if (needsGlobalUpdate) {
3280
- logger.info(`\u68C0\u6D4B\u5230\u5168\u5C40\u5B57\u6BB5\u53D8\u66F4 (${noteIndex})\uFF0C\u6B63\u5728\u589E\u91CF\u66F4\u65B0\u5168\u5C40\u6587\u4EF6...`);
3281
- const readmeService = ReadmeService.getInstance();
3282
- await readmeService.updateNoteInReadme(noteIndex, updates);
3283
- await readmeService.regenerateSidebar();
3284
- logger.info(`\u5168\u5C40\u6587\u4EF6\u589E\u91CF\u66F4\u65B0\u5B8C\u6210 (${noteIndex})`);
3285
- } else {
3286
- logger.debug(`\u914D\u7F6E\u66F4\u65B0\u4E0D\u5F71\u54CD\u5168\u5C40\u6587\u4EF6 (${noteIndex})`);
3574
+ async commit(message) {
3575
+ await this.ensureValidRepo();
3576
+ try {
3577
+ await runCommand(`git commit -m "${message}"`, this.dir);
3578
+ this.logger.success(`Committed: ${message}`);
3579
+ } catch (error) {
3580
+ handleError(error);
3581
+ throw error;
3287
3582
  }
3288
3583
  }
3289
3584
  /**
3290
- * 检查配置更新是否需要触发全局更新
3291
- * @param oldConfig - 旧配置
3292
- * @param newConfig - 新配置
3293
- * @returns 是否需要全局更新
3585
+ * 添加文件到暂存区
3294
3586
  */
3295
- checkNeedsGlobalUpdate(oldConfig, newConfig) {
3296
- const globalFields = ["done"];
3297
- for (const field of globalFields) {
3298
- if (oldConfig[field] !== newConfig[field]) {
3299
- return true;
3300
- }
3587
+ async add(files = ".") {
3588
+ await this.ensureValidRepo();
3589
+ const fileList = Array.isArray(files) ? files.join(" ") : files;
3590
+ try {
3591
+ await runCommand(`git add ${fileList}`, this.dir);
3592
+ this.logger.info(`Staged changes: ${fileList}`);
3593
+ } catch (error) {
3594
+ handleError(error);
3595
+ throw error;
3301
3596
  }
3302
- return false;
3303
3597
  }
3304
3598
  /**
3305
- * 修正笔记标题
3306
- * @param noteInfo - 笔记信息
3307
- * @returns 是否进行了修正
3599
+ * 推送到远程仓库
3308
3600
  */
3309
- async fixNoteTitle(noteInfo) {
3601
+ async push(options) {
3602
+ await this.ensureValidRepo();
3603
+ const { force = false, setUpstream = false } = options || {};
3310
3604
  try {
3311
- const readmeContent = readFileSync5(noteInfo.readmePath, "utf-8");
3312
- if (readmeContent.length === 0) return false;
3313
- const lines = readmeContent.split("\n");
3314
- const match = noteInfo.dirName.match(/^\d{4}\.\s+(.+)$/);
3315
- if (!match) {
3316
- logger.warn(`\u68C0\u6D4B\u5230\u9519\u8BEF\u7684\u7B14\u8BB0\u76EE\u5F55\u540D\u79F0\uFF1A${noteInfo.dirName}`);
3317
- return false;
3318
- }
3319
- const expectedTitle = match[1];
3320
- const expectedH1 = generateNoteTitle(
3321
- noteInfo.index,
3322
- expectedTitle,
3323
- REPO_NOTES_URL
3324
- );
3325
- const firstLine = lines[0].trim();
3326
- if (!firstLine.startsWith("# ")) {
3327
- lines.unshift(expectedH1);
3328
- writeFileSync4(noteInfo.readmePath, lines.join("\n"), "utf-8");
3329
- logger.info(`Added title to: ${noteInfo.dirName}`);
3330
- return true;
3331
- }
3332
- if (firstLine !== expectedH1) {
3333
- lines[0] = expectedH1;
3334
- writeFileSync4(noteInfo.readmePath, lines.join("\n"), "utf-8");
3335
- logger.info(`Fixed title for: ${noteInfo.dirName}`);
3336
- return true;
3605
+ const status = await this.getStatus();
3606
+ this.logger.progress(`\u6B63\u5728\u63A8\u9001\u5230\u8FDC\u7A0B (${status.branch})...`);
3607
+ let cmd = "git push";
3608
+ if (force) cmd += " --force";
3609
+ if (setUpstream) cmd += ` --set-upstream origin ${status.branch}`;
3610
+ await runCommand(cmd, this.dir);
3611
+ const remoteInfo = await this.getRemoteInfo();
3612
+ if (remoteInfo) {
3613
+ this.logger.success(`\u63A8\u9001\u6210\u529F \u2192 ${remoteInfo.owner}/${remoteInfo.repo}`);
3614
+ } else {
3615
+ this.logger.success("\u63A8\u9001\u6210\u529F");
3337
3616
  }
3338
- return false;
3339
3617
  } catch (error) {
3340
- logger.error(`Failed to fix title for: ${noteInfo.dirName}`, error);
3341
- return false;
3618
+ this.logger.error("\u63A8\u9001\u5931\u8D25");
3619
+ handleError(error);
3620
+ throw error;
3342
3621
  }
3343
3622
  }
3344
3623
  /**
3345
- * 修正所有笔记的标题
3346
- * @param providedNotes - 可选的笔记列表,不传则内部扫描
3347
- * @returns 修正的笔记数量
3624
+ * 完整的推送流程:检查 -> 添加 -> 提交 -> 推送
3348
3625
  */
3349
- async fixAllNoteTitles(providedNotes) {
3350
- const notes = providedNotes ?? this.getAllNotes();
3351
- let fixedCount = 0;
3352
- for (const note of notes) {
3353
- const fixed = await this.fixNoteTitle(note);
3354
- if (fixed) {
3355
- fixedCount++;
3356
- }
3626
+ async pushWithCommit(commitMessage, options) {
3627
+ await this.ensureValidRepo();
3628
+ const status = await this.getStatus();
3629
+ if (!status.hasChanges) {
3630
+ this.logger.info("\u6CA1\u6709\u9700\u8981\u63D0\u4EA4\u7684\u66F4\u6539");
3631
+ return;
3357
3632
  }
3358
- if (fixedCount > 0) {
3359
- logger.info(`Fixed ${fixedCount} note titles`);
3633
+ try {
3634
+ this.logger.info(`\u6B63\u5728\u63A8\u9001 ${status.changedFiles} \u4E2A\u6587\u4EF6...`);
3635
+ status.files.forEach((file, index) => {
3636
+ console.log(` ${index + 1}. ${file.path}`);
3637
+ });
3638
+ await runCommand("git add .", this.dir);
3639
+ const message = commitMessage || `update: ${status.changedFiles} files modified`;
3640
+ await runCommand(`git commit -m "${message}"`, this.dir);
3641
+ let cmd = "git push";
3642
+ if (options?.force) cmd += " --force";
3643
+ await runCommand(cmd, this.dir);
3644
+ const remoteInfo = await this.getRemoteInfo();
3645
+ if (remoteInfo) {
3646
+ this.logger.success(
3647
+ `\u63A8\u9001\u6210\u529F: ${status.changedFiles} \u4E2A\u6587\u4EF6 \u2192 https://github.com/${remoteInfo.owner}/${remoteInfo.repo}`
3648
+ );
3649
+ } else {
3650
+ this.logger.success(`\u63A8\u9001\u6210\u529F: ${status.changedFiles} \u4E2A\u6587\u4EF6`);
3651
+ }
3652
+ } catch (error) {
3653
+ this.logger.error(`\u63A8\u9001\u5931\u8D25`);
3654
+ handleError(error);
3655
+ throw error;
3360
3656
  }
3361
- return fixedCount;
3362
3657
  }
3363
- };
3364
-
3365
- // services/file-watcher/service.ts
3366
- var NOTES_DIR_NOT_SET_ERROR = "NOTES_DIR_PATH \u672A\u8BBE\u7F6E\uFF0C\u65E0\u6CD5\u542F\u52A8\u6587\u4EF6\u76D1\u542C";
3367
- var UPDATE_UNLOCK_DELAY_MS2 = 500;
3368
- var RENAME_BROADCAST_PATH = "/__tnotes_broadcast_rename";
3369
- var FileWatcherService = class _FileWatcherService {
3370
- constructor(notesDir = NOTES_DIR_PATH) {
3371
- this.notesDir = notesDir;
3372
- if (!this.notesDir) {
3373
- throw new Error(NOTES_DIR_NOT_SET_ERROR);
3658
+ /**
3659
+ * 完整的同步流程:拉取 -> 推送
3660
+ */
3661
+ async sync(options) {
3662
+ const { commitMessage, rebase = true } = options || {};
3663
+ try {
3664
+ await this.pull({ rebase, autostash: true });
3665
+ await this.pushWithCommit(commitMessage);
3666
+ } catch (error) {
3667
+ this.logger.error("Sync failed");
3668
+ handleError(error);
3669
+ throw error;
3374
3670
  }
3375
- this.init();
3376
- _FileWatcherService.instance = this;
3377
- }
3378
- static instance = null;
3379
- watchState;
3380
- scheduler;
3381
- renameDetector;
3382
- configHandler;
3383
- readmeHandler;
3384
- coordinator;
3385
- folderHandler;
3386
- adapter;
3387
- noteService;
3388
- readmeService;
3389
- noteIndexCache;
3390
- unlockTimer = null;
3391
- static getInstance() {
3392
- return _FileWatcherService.instance;
3393
- }
3394
- init() {
3395
- this.noteService = NoteService.getInstance();
3396
- this.readmeService = ReadmeService.getInstance();
3397
- this.noteIndexCache = NoteIndexCache.getInstance();
3398
- this.watchState = this.initWatchState();
3399
- this.scheduler = this.initScheduler();
3400
- this.folderHandler = this.initFolderHandler();
3401
- this.renameDetector = this.initRenameDetector();
3402
- this.configHandler = this.initConfigHandler();
3403
- this.readmeHandler = this.initReadmeHandler();
3404
- this.coordinator = this.initCoordinator();
3405
- this.adapter = this.initAdapter();
3406
- }
3407
- initWatchState() {
3408
- const watchState = new WatchState({ notesDir: this.notesDir, logger });
3409
- watchState.initializeFromDisk();
3410
- return watchState;
3411
- }
3412
- initScheduler() {
3413
- return new EventScheduler({
3414
- onFlush: (events) => this.handleFileChange(events),
3415
- onPauseForBatch: () => logger.warn("\u76D1\u542C\u670D\u52A1\u6682\u505C 3s \u7B49\u5F85\u6279\u91CF\u66F4\u65B0\u5B8C\u6210..."),
3416
- onResumeAfterBatch: () => logger.info("\u6062\u590D\u81EA\u52A8\u76D1\u542C"),
3417
- reinit: () => this.watchState.initializeFromDisk()
3418
- });
3419
- }
3420
- initFolderHandler() {
3421
- return new FolderChangeHandler({
3422
- notesDir: this.notesDir,
3423
- watchState: this.watchState,
3424
- scheduler: this.scheduler,
3425
- noteService: this.noteService,
3426
- readmeService: this.readmeService,
3427
- noteIndexCache: this.noteIndexCache,
3428
- logger,
3429
- onRenameSuccess: (payload) => {
3430
- void this.broadcastRename(payload);
3431
- }
3432
- });
3433
3671
  }
3434
- async broadcastRename(payload) {
3435
- const path2 = `/${repoName}${RENAME_BROADCAST_PATH}`;
3436
- const body = JSON.stringify(payload);
3437
- const hosts = ["127.0.0.1", "::1"];
3438
- let lastError = null;
3439
- for (const host of hosts) {
3440
- try {
3441
- await this.postOnce(host, path2, body);
3442
- return;
3443
- } catch (error) {
3444
- lastError = error;
3672
+ /**
3673
+ * 显示状态摘要
3674
+ */
3675
+ async showStatus(options) {
3676
+ const { showFiles = true } = options || {};
3677
+ const status = await this.getStatus();
3678
+ const remoteInfo = await this.getRemoteInfo();
3679
+ console.log("\n\u{1F4CA} Git \u72B6\u6001:");
3680
+ console.log(` \u5206\u652F: ${status.branch}`);
3681
+ if (remoteInfo) {
3682
+ console.log(
3683
+ ` \u8FDC\u7A0B: ${remoteInfo.owner}/${remoteInfo.repo} (${remoteInfo.type})`
3684
+ );
3685
+ }
3686
+ if (status.hasChanges) {
3687
+ console.log(
3688
+ ` \u53D8\u66F4: ${status.changedFiles} \u4E2A\u6587\u4EF6 (\u5DF2\u6682\u5B58 ${status.staged}, \u672A\u6682\u5B58 ${status.unstaged}, \u672A\u8DDF\u8E2A ${status.untracked})`
3689
+ );
3690
+ if (showFiles && status.files.length > 0) {
3691
+ console.log(" \u53D8\u66F4\u6587\u4EF6\u5217\u8868:");
3692
+ const stagedFiles = status.files.filter((f) => f.status === "staged");
3693
+ const unstagedFiles = status.files.filter(
3694
+ (f) => f.status === "unstaged"
3695
+ );
3696
+ const untrackedFiles = status.files.filter(
3697
+ (f) => f.status === "untracked"
3698
+ );
3699
+ if (stagedFiles.length > 0) {
3700
+ console.log(" \u5DF2\u6682\u5B58:");
3701
+ stagedFiles.forEach((f) => console.log(` \u2713 ${f.path}`));
3702
+ }
3703
+ if (unstagedFiles.length > 0) {
3704
+ console.log(" \u672A\u6682\u5B58:");
3705
+ unstagedFiles.forEach((f) => console.log(` \u2022 ${f.path}`));
3706
+ }
3707
+ if (untrackedFiles.length > 0) {
3708
+ console.log(" \u672A\u8DDF\u8E2A:");
3709
+ untrackedFiles.forEach((f) => console.log(` ? ${f.path}`));
3710
+ }
3445
3711
  }
3712
+ } else {
3713
+ console.log(" \u72B6\u6001: \u5DE5\u4F5C\u533A\u5E72\u51C0\uFF0C\u6CA1\u6709\u53D8\u66F4");
3714
+ }
3715
+ if (status.ahead > 0 || status.behind > 0) {
3716
+ const syncInfo = [];
3717
+ if (status.ahead > 0) syncInfo.push(`\u9886\u5148 ${status.ahead} \u4E2A\u63D0\u4EA4`);
3718
+ if (status.behind > 0) syncInfo.push(`\u843D\u540E ${status.behind} \u4E2A\u63D0\u4EA4`);
3719
+ console.log(` \u540C\u6B65: ${syncInfo.join(", ")}`);
3446
3720
  }
3447
- logger.warn(
3448
- `\u5E7F\u64AD\u91CD\u547D\u540D\u4E8B\u4EF6\u5931\u8D25 (POST http://[127.0.0.1|::1]:${port}${path2}): ${String(lastError)}`
3449
- );
3721
+ console.log();
3450
3722
  }
3451
- postOnce(host, path2, body) {
3452
- return new Promise((resolve2, reject) => {
3453
- const req = httpRequest(
3454
- {
3455
- host,
3456
- port,
3457
- path: path2,
3458
- method: "POST",
3459
- family: host.includes(":") ? 6 : 4,
3460
- headers: {
3461
- "Content-Type": "application/json",
3462
- "Content-Length": Buffer.byteLength(body)
3463
- }
3464
- },
3465
- (res) => {
3466
- res.on("data", () => {
3467
- });
3468
- res.on("end", () => {
3469
- if (res.statusCode && res.statusCode >= 400) {
3470
- reject(new Error(`HTTP ${res.statusCode}`));
3471
- return;
3472
- }
3473
- resolve2();
3474
- });
3475
- }
3476
- );
3477
- req.on("error", reject);
3478
- req.write(body);
3479
- req.end();
3723
+ };
3724
+
3725
+ // core/ProcessManager.ts
3726
+ import { spawn } from "child_process";
3727
+ var ProcessManager = class {
3728
+ processes = /* @__PURE__ */ new Map();
3729
+ logger;
3730
+ constructor() {
3731
+ this.logger = new Logger({ prefix: "process" });
3732
+ process.on("exit", () => {
3733
+ this.killAll();
3480
3734
  });
3481
- }
3482
- initRenameDetector() {
3483
- return new RenameDetector({
3484
- notesDir: this.notesDir,
3485
- dirCache: {
3486
- has: (name) => this.watchState.hasNoteDir(name),
3487
- add: (name) => this.watchState.addNoteDir(name),
3488
- delete: (name) => this.watchState.deleteNoteDir(name)
3489
- },
3490
- logger,
3491
- onDelete: (oldName) => this.folderHandler.handleFolderDeletion(oldName),
3492
- onRename: (oldName, newName) => this.folderHandler.handleFolderRenameUpdate(oldName, newName)
3735
+ process.on("SIGINT", () => {
3736
+ this.killAll();
3737
+ process.exit(0);
3493
3738
  });
3494
- }
3495
- initConfigHandler() {
3496
- return new ConfigChangeHandler({
3497
- state: this.watchState,
3498
- noteService: this.noteService,
3499
- noteIndexCache: this.noteIndexCache,
3500
- logger
3739
+ process.on("SIGTERM", () => {
3740
+ this.killAll();
3741
+ process.exit(0);
3501
3742
  });
3502
3743
  }
3503
- initReadmeHandler() {
3504
- return new ReadmeChangeHandler({ noteService: this.noteService });
3505
- }
3506
- initCoordinator() {
3507
- return new GlobalUpdateCoordinator({
3508
- readmeService: this.readmeService,
3509
- noteIndexCache: this.noteIndexCache,
3510
- logger
3744
+ /**
3745
+ * 启动进程
3746
+ * @param id - 进程ID
3747
+ * @param command - 命令
3748
+ * @param args - 参数列表
3749
+ * @param options - spawn 选项
3750
+ * @returns ProcessInfo
3751
+ */
3752
+ spawn(id, command, args = [], options) {
3753
+ if (this.processes.has(id)) {
3754
+ this.logger.warn(`\u8FDB\u7A0B ${id} \u5DF2\u5B58\u5728\uFF0C\u5148\u505C\u6B62\u65E7\u8FDB\u7A0B`);
3755
+ this.kill(id);
3756
+ }
3757
+ const proc = spawn(command, args, {
3758
+ stdio: "inherit",
3759
+ shell: true,
3760
+ ...options
3511
3761
  });
3512
- }
3513
- initAdapter() {
3514
- return new FsWatcherAdapter({
3515
- notesDir: this.notesDir,
3516
- isUpdating: () => this.scheduler.getUpdating(),
3517
- onRename: (folderName) => this.renameDetector.handleFsRename(folderName),
3518
- onNoteEvent: (event) => this.onNoteEvent(event),
3519
- logger
3762
+ const processInfo = {
3763
+ id,
3764
+ pid: proc.pid,
3765
+ command,
3766
+ args,
3767
+ startTime: Date.now(),
3768
+ process: proc
3769
+ };
3770
+ this.processes.set(id, processInfo);
3771
+ proc.on("exit", (code, signal) => {
3772
+ this.logger.info(`\u8FDB\u7A0B ${id} \u5DF2\u9000\u51FA (code: ${code}, signal: ${signal})`);
3773
+ this.processes.delete(id);
3520
3774
  });
3775
+ proc.on("error", (err) => {
3776
+ this.logger.error(`\u8FDB\u7A0B ${id} \u51FA\u9519: ${err.message}`);
3777
+ this.processes.delete(id);
3778
+ });
3779
+ return processInfo;
3521
3780
  }
3522
- start() {
3523
- this.watchState.initializeFromDisk();
3524
- this.adapter.start();
3525
- }
3526
- stop() {
3527
- this.adapter.stop();
3528
- this.scheduler.clearTimers();
3529
- this.renameDetector.clearTimers();
3530
- this.folderHandler.clearTimers();
3531
- if (this.unlockTimer) {
3532
- clearTimeout(this.unlockTimer);
3533
- this.unlockTimer = null;
3781
+ /**
3782
+ * 停止进程
3783
+ * @param id - 进程ID
3784
+ * @param signal - 信号(默认为 SIGTERM)
3785
+ * @returns 是否成功停止
3786
+ */
3787
+ kill(id, signal = "SIGTERM") {
3788
+ const processInfo = this.processes.get(id);
3789
+ if (!processInfo) {
3790
+ this.logger.warn(`\u8FDB\u7A0B ${id} \u4E0D\u5B58\u5728`);
3791
+ return false;
3792
+ }
3793
+ this.logger.info(`\u505C\u6B62\u8FDB\u7A0B: ${id} (PID: ${processInfo.pid})`);
3794
+ try {
3795
+ const killed = processInfo.process.kill(signal);
3796
+ if (killed) {
3797
+ this.processes.delete(id);
3798
+ return true;
3799
+ }
3800
+ return false;
3801
+ } catch (error) {
3802
+ this.logger.error(`\u505C\u6B62\u8FDB\u7A0B ${id} \u5931\u8D25: ${error}`);
3803
+ return false;
3534
3804
  }
3535
- logger.info("\u6587\u4EF6\u76D1\u542C\u670D\u52A1\u5DF2\u505C\u6B62");
3536
- }
3537
- pause() {
3538
- this.scheduler.setUpdating(true);
3539
- logger.info("\u6587\u4EF6\u76D1\u542C\u5DF2\u6682\u505C");
3540
- }
3541
- resume() {
3542
- this.watchState.initializeFromDisk();
3543
- this.scheduler.setUpdating(false);
3544
- logger.info("\u6587\u4EF6\u76D1\u542C\u5DF2\u6062\u590D");
3545
- }
3546
- isWatching() {
3547
- return this.adapter.isWatching();
3548
3805
  }
3549
3806
  /**
3550
- * 挂起文件监听(关闭 fs.watch 句柄),用于需要操作文件夹的场景(如重命名)
3551
- * Windows fs.watch 会锁住文件夹句柄,必须先关闭才能重命名
3807
+ * 检查进程是否存在
3808
+ * @param id - 进程ID
3809
+ * @returns 是否存在
3552
3810
  */
3553
- suspend() {
3554
- this.adapter.stop();
3555
- this.scheduler.setUpdating(true);
3556
- logger.info("\u6587\u4EF6\u76D1\u542C\u5DF2\u6302\u8D77\uFF08fs.watch \u5DF2\u5173\u95ED\uFF09");
3811
+ has(id) {
3812
+ return this.processes.has(id);
3557
3813
  }
3558
3814
  /**
3559
- * 恢复文件监听(重新启动 fs.watch)
3815
+ * 检查进程是否在运行
3816
+ * @param id - 进程 ID
3817
+ * @returns 是否在运行
3560
3818
  */
3561
- unsuspend() {
3562
- this.watchState.initializeFromDisk();
3563
- this.scheduler.setUpdating(false);
3564
- this.adapter.start();
3565
- logger.info("\u6587\u4EF6\u76D1\u542C\u5DF2\u6062\u590D\uFF08fs.watch \u5DF2\u91CD\u542F\uFF09");
3566
- }
3567
- // #region - 私有实现
3568
- onNoteEvent(event) {
3569
- if (!this.isNoteFile(event.path)) return;
3570
- if (!this.watchState.updateFileHash(event.path)) return;
3571
- if (this.scheduler.recordChangeAndDetectBatch()) return;
3572
- this.scheduler.enqueue(event);
3573
- }
3574
- async handleFileChange(events) {
3819
+ isRunning(id) {
3820
+ const processInfo = this.processes.get(id);
3821
+ if (!processInfo) return false;
3575
3822
  try {
3576
- const configChanges = events.filter(
3577
- (e) => e.type === WATCH_EVENT_TYPES.CONFIG
3578
- );
3579
- const readmeChanges = events.filter(
3580
- (e) => e.type === WATCH_EVENT_TYPES.README
3581
- );
3582
- const changedNoteIndexes = await this.configHandler.handle(configChanges);
3583
- if (changedNoteIndexes.length > 0) {
3584
- await safeExecute(
3585
- "\u914D\u7F6E\u53D8\u66F4\u66F4\u65B0",
3586
- () => this.coordinator.applyConfigUpdates(changedNoteIndexes),
3587
- logger
3588
- );
3589
- return;
3590
- }
3591
- await safeExecute(
3592
- "README \u53D8\u66F4\u66F4\u65B0",
3593
- async () => {
3594
- await this.readmeHandler.handle(readmeChanges);
3595
- await this.coordinator.updateNoteReadmesOnly(events);
3596
- },
3597
- logger
3598
- );
3599
- } finally {
3600
- if (this.unlockTimer) clearTimeout(this.unlockTimer);
3601
- this.unlockTimer = setTimeout(() => {
3602
- this.unlockTimer = null;
3603
- this.scheduler.setUpdating(false);
3604
- }, UPDATE_UNLOCK_DELAY_MS2);
3823
+ return process.kill(processInfo.pid, 0);
3824
+ } catch {
3825
+ return false;
3605
3826
  }
3606
3827
  }
3607
- isNoteFile(filePath) {
3608
- return filePath.endsWith("README.md") || filePath.endsWith(".tnotes.json");
3828
+ /**
3829
+ * 停止所有进程
3830
+ * @param signal - 信号(默认为 SIGTERM)
3831
+ */
3832
+ killAll(signal = "SIGTERM") {
3833
+ if (this.processes.size === 0) {
3834
+ return;
3835
+ }
3836
+ this.logger.info(`\u505C\u6B62\u6240\u6709\u8FDB\u7A0B (${this.processes.size} \u4E2A)`);
3837
+ for (const [id, processInfo] of this.processes) {
3838
+ try {
3839
+ processInfo.process.kill(signal);
3840
+ this.logger.info(`\u5DF2\u505C\u6B62\u8FDB\u7A0B: ${id}`);
3841
+ } catch (error) {
3842
+ this.logger.error(`\u505C\u6B62\u8FDB\u7A0B ${id} \u5931\u8D25: ${error}`);
3843
+ }
3844
+ }
3845
+ this.processes.clear();
3609
3846
  }
3610
- // #endregion - 私有实现
3611
3847
  };
3612
3848
 
3613
3849
  // services/git/service.ts