@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.
- package/README.md +87 -87
- package/dist/{chunk-FVKQPBPP.js → chunk-4MO7F7Q4.js} +2005 -1769
- package/dist/cli/index.js +1 -1
- package/dist/vitepress/config/index.js +212 -4
- package/package.json +1 -1
- package/types/config.ts +61 -61
- package/types/index.ts +11 -11
- package/types/note.ts +33 -33
- package/vitepress/assets/icons/icon__check.svg +3 -3
- package/vitepress/assets/icons/icon__clipboard.svg +7 -7
- package/vitepress/assets/icons/icon__close.svg +1 -1
- package/vitepress/assets/icons/icon__collapse.svg +1 -1
- package/vitepress/assets/icons/icon__confirm.svg +1 -1
- package/vitepress/assets/icons/icon__copy.svg +4 -4
- package/vitepress/assets/icons/icon__focus.svg +1 -1
- package/vitepress/assets/icons/icon__fold.svg +3 -3
- package/vitepress/assets/icons/icon__folder.svg +1 -1
- package/vitepress/assets/icons/icon__fullscreen.svg +1 -1
- package/vitepress/assets/icons/icon__fullscreen_exit.svg +1 -1
- package/vitepress/assets/icons/icon__github.svg +3 -3
- package/vitepress/assets/icons/icon__mindmap.svg +1 -1
- package/vitepress/assets/icons/icon__next.svg +1 -1
- package/vitepress/assets/icons/icon__number_gray.svg +1 -1
- package/vitepress/assets/icons/icon__number_purple.svg +1 -1
- package/vitepress/assets/icons/icon__prev.svg +1 -1
- package/vitepress/assets/icons/icon__restore.svg +1 -1
- package/vitepress/assets/icons/icon__rotate.svg +4 -4
- package/vitepress/assets/icons/icon__search.svg +1 -1
- package/vitepress/assets/icons/icon__setting.svg +1 -0
- package/vitepress/assets/icons/icon__sidebar_collapsed.svg +1 -1
- package/vitepress/assets/icons/icon__sidebar_opened.svg +1 -1
- package/vitepress/assets/icons/icon__totop.svg +5 -5
- package/vitepress/assets/icons/icon__vscode.svg +5 -5
- package/vitepress/assets/icons/icon__zoom_fit.svg +1 -1
- package/vitepress/assets/icons/icon__zoom_in.svg +1 -1
- package/vitepress/assets/icons/icon__zoom_out.svg +1 -1
- package/vitepress/assets/icons/icon__zoom_reset.svg +1 -1
- package/vitepress/assets/icons/index.ts +39 -38
- package/vitepress/components/BilibiliOutsidePlayer/BilibiliOutsidePlayer.vue +20 -20
- package/vitepress/components/CodeBlockFullscreen/CodeBlockFullscreen.vue +373 -373
- package/vitepress/components/CodeBlockFullscreen/index.ts +115 -115
- package/vitepress/components/CodeBlockFullscreen/styles.css +64 -64
- package/vitepress/components/Discussions/Discussions.module.scss +32 -32
- package/vitepress/components/Discussions/Discussions.vue +211 -211
- package/vitepress/components/EnWordList/EnWordList.module.scss +124 -124
- package/vitepress/components/EnWordList/EnWordList.vue +543 -543
- package/vitepress/components/EnWordList/RightClickMenu.module.scss +22 -22
- package/vitepress/components/EnWordList/RightClickMenu.vue +66 -66
- package/vitepress/components/Footprints/Footprints.module.scss +93 -93
- package/vitepress/components/Footprints/Footprints.vue +377 -377
- package/vitepress/components/Layout/AboutModal.module.scss +233 -233
- package/vitepress/components/Layout/AboutModal.vue +105 -105
- package/vitepress/components/Layout/AboutPanel.vue +270 -270
- package/vitepress/components/Layout/ContentCollapse.vue +603 -603
- package/vitepress/components/Layout/CustomSidebar.vue +878 -608
- package/vitepress/components/Layout/DocBeforeControls.vue +149 -149
- package/vitepress/components/Layout/DocFooter.vue +233 -233
- package/vitepress/components/Layout/ImagePreview.module.scss +201 -201
- package/vitepress/components/Layout/ImagePreview.vue +281 -281
- package/vitepress/components/Layout/Layout.module.scss +661 -661
- package/vitepress/components/Layout/Layout.vue +618 -560
- package/vitepress/components/Layout/NavBarSettingsTrigger.vue +59 -0
- package/vitepress/components/Layout/NoteStatus.vue +140 -140
- package/vitepress/components/Layout/SidebarItems.vue +1083 -268
- package/vitepress/components/Layout/SidebarNavBefore.vue +171 -114
- package/vitepress/components/Layout/Swiper.vue +167 -167
- package/vitepress/components/Layout/ToggleFullContent.module.scss +11 -11
- package/vitepress/components/Layout/ToggleFullContent.vue +35 -35
- package/vitepress/components/Layout/ToggleSidebar.module.scss +11 -11
- package/vitepress/components/Layout/ToggleSidebar.vue +36 -36
- package/vitepress/components/Layout/composables/useCollapseControl.ts +88 -88
- package/vitepress/components/Layout/composables/useNoteConfig.ts +119 -119
- package/vitepress/components/Layout/composables/useNoteSave.ts +178 -178
- package/vitepress/components/Layout/composables/useNoteValidation.ts +85 -85
- package/vitepress/components/Layout/composables/useRedirect.ts +113 -113
- package/vitepress/components/Layout/composables/useVSCodeIntegration.ts +85 -85
- package/vitepress/components/Layout/homeReadme.data.ts +124 -124
- package/vitepress/components/MarkMap/MarkMap.module.scss +159 -159
- package/vitepress/components/MarkMap/MarkMap.vue +405 -405
- package/vitepress/components/Mermaid/Mermaid.module.scss +275 -275
- package/vitepress/components/Mermaid/Mermaid.vue +365 -365
- package/vitepress/components/NotesTable/NotesTable.module.scss +77 -77
- package/vitepress/components/NotesTable/NotesTable.vue +98 -98
- package/vitepress/components/NotesTable/README.md +67 -67
- package/vitepress/components/Settings/Settings.module.scss +375 -433
- package/vitepress/components/Settings/Settings.vue +376 -306
- package/vitepress/components/SidebarCard/MindMapView.vue +484 -484
- package/vitepress/components/SidebarCard/NotesTrendChart.vue +108 -108
- package/vitepress/components/SidebarCard/SidebarCard.vue +946 -946
- package/vitepress/components/Tooltip/Tooltip.vue +70 -70
- package/vitepress/components/constants.ts +109 -91
- package/vitepress/components/notesConfig.data.ts +73 -73
- package/vitepress/components/sidebar.data.ts +59 -59
- package/vitepress/components/tnotes-config.data.ts +21 -21
- package/vitepress/components/utils.ts +26 -26
- package/vitepress/config/index.ts +169 -167
- package/vitepress/configs/constants.ts +26 -26
- package/vitepress/configs/head.config.ts +25 -25
- package/vitepress/configs/index.ts +9 -9
- package/vitepress/configs/markdown-it.d.ts +23 -23
- package/vitepress/configs/markdown.config.ts +366 -366
- package/vitepress/configs/theme.config.ts +108 -108
- package/vitepress/plugins/buildProgressPlugin.ts +434 -434
- package/vitepress/plugins/getNoteByConfigIdPlugin.ts +107 -107
- package/vitepress/plugins/renameNotePlugin.ts +68 -68
- package/vitepress/plugins/sidebarStructurePlugin.ts +260 -0
- package/vitepress/plugins/updateConfigPlugin.ts +63 -63
- package/vitepress/theme/index.ts +137 -137
- package/vitepress/theme/styles/base.scss +50 -50
- package/vitepress/theme/styles/components/404.scss +31 -31
- package/vitepress/theme/styles/components/collapse.scss +175 -175
- package/vitepress/theme/styles/components/markmap.scss +101 -101
- package/vitepress/theme/styles/components/swiper.scss +255 -255
- package/vitepress/theme/styles/index.scss +25 -25
- package/vitepress/theme/styles/layout.scss +70 -62
- package/vitepress/theme/styles/utilities.scss +39 -39
- 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
|
-
//
|
|
1576
|
-
var
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
1603
|
+
* 更新所有笔记的 README
|
|
1604
|
+
* @param options - 更新选项
|
|
1607
1605
|
*/
|
|
1608
|
-
async
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
const
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
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
|
-
|
|
1641
|
-
|
|
1642
|
-
ahead = parseInt(aheadStr) || 0;
|
|
1643
|
-
} catch {
|
|
1623
|
+
} else {
|
|
1624
|
+
logger.info("\u4F7F\u7528\u5168\u91CF\u66F4\u65B0\u6A21\u5F0F");
|
|
1644
1625
|
}
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
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
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
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
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
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
|
|
1669
|
+
async getChangedNoteIndexes() {
|
|
1705
1670
|
try {
|
|
1706
|
-
|
|
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
|
-
|
|
1713
|
-
return false;
|
|
1673
|
+
return /* @__PURE__ */ new Set();
|
|
1714
1674
|
}
|
|
1715
1675
|
}
|
|
1716
1676
|
/**
|
|
1717
|
-
*
|
|
1677
|
+
* 并行更新多个笔记的 README
|
|
1678
|
+
* @param notes - 笔记信息数组
|
|
1718
1679
|
*/
|
|
1719
|
-
async
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
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
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
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
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
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
|
-
|
|
1766
|
-
|
|
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
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
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
|
|
1782
|
-
|
|
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
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
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
|
|
1809
|
-
await
|
|
1810
|
-
const
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
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
|
-
}
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
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
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
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
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
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
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
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
|
|
1883
|
-
|
|
1884
|
-
const
|
|
1885
|
-
const
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
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
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
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
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
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
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
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
|
-
|
|
2001
|
-
|
|
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
|
-
|
|
2019
|
-
|
|
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
|
|
2024
|
-
* @returns 是否在运行
|
|
2013
|
+
* 重新生成 sidebar.json(基于当前 README.md)
|
|
2014
|
+
* @param notes - 可选的笔记列表,不传则内部扫描
|
|
2025
2015
|
*/
|
|
2026
|
-
|
|
2027
|
-
const
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2044
|
-
|
|
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
|
-
|
|
2042
|
+
return cleanTitle;
|
|
2053
2043
|
}
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
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
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
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
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
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
|
-
|
|
2070
|
+
throw new Error(`\u672A\u627E\u5230\u76EE\u5F55: ${groupPath.join(" / ")}`);
|
|
2089
2071
|
}
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
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
|
-
|
|
2210
|
-
|
|
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/
|
|
2215
|
-
import {
|
|
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/
|
|
2241
|
-
var
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
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
|
-
|
|
2249
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
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
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
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
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
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
|
|
2183
|
+
return this.noteManager.scanNotes();
|
|
2349
2184
|
}
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
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
|
-
|
|
2364
|
-
|
|
2239
|
+
logger.info(`Created new note: ${dirName}`);
|
|
2240
|
+
return noteInfo;
|
|
2365
2241
|
}
|
|
2366
2242
|
/**
|
|
2367
|
-
*
|
|
2368
|
-
*
|
|
2369
|
-
*
|
|
2370
|
-
* 据此推导出新标题文本(去掉 `${noteIndex}. ` 前缀)。
|
|
2243
|
+
* 删除笔记文件夹
|
|
2244
|
+
* @param noteIndex - 笔记索引
|
|
2245
|
+
* @returns 被删除的笔记信息
|
|
2371
2246
|
*/
|
|
2372
|
-
async
|
|
2373
|
-
const
|
|
2374
|
-
if (!
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
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 (
|
|
2387
|
-
|
|
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
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
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
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
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
|
-
|
|
2312
|
+
logger.info(`\u5168\u5C40\u6587\u4EF6\u589E\u91CF\u66F4\u65B0\u5B8C\u6210 (${noteIndex})`);
|
|
2408
2313
|
} else {
|
|
2409
|
-
|
|
2314
|
+
logger.debug(`\u914D\u7F6E\u66F4\u65B0\u4E0D\u5F71\u54CD\u5168\u5C40\u6587\u4EF6 (${noteIndex})`);
|
|
2410
2315
|
}
|
|
2411
2316
|
}
|
|
2412
|
-
|
|
2413
|
-
|
|
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
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
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
|
-
|
|
2368
|
+
logger.error(`Failed to fix title for: ${noteInfo.dirName}`, error);
|
|
2369
|
+
return false;
|
|
2428
2370
|
}
|
|
2429
2371
|
}
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
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
|
-
|
|
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/
|
|
2441
|
-
import {
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
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
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
if (
|
|
2553
|
-
|
|
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
|
-
|
|
2614
|
-
if (
|
|
2615
|
-
|
|
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/
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
var
|
|
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
|
-
|
|
2638
|
-
/**
|
|
2639
|
-
|
|
2640
|
-
/**
|
|
2641
|
-
|
|
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
|
-
*
|
|
2457
|
+
* 设置更新状态锁,用于防止在执行耗时更新操作时被新的文件变更事件打断
|
|
2644
2458
|
*
|
|
2645
|
-
* @param
|
|
2646
|
-
* @returns 文件哈希
|
|
2459
|
+
* @param flag - true 表示正在更新(锁定),false 表示更新完成(解锁)
|
|
2647
2460
|
*/
|
|
2648
|
-
|
|
2649
|
-
|
|
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
|
-
*
|
|
2465
|
+
* 获取当前是否处于更新锁定状态
|
|
2660
2466
|
*
|
|
2661
|
-
* @
|
|
2662
|
-
* @returns 是否发生变化
|
|
2467
|
+
* @returns true 表示正在执行更新操作(事件处理被暂停),false 表示空闲可处理新事件
|
|
2663
2468
|
*/
|
|
2664
|
-
|
|
2665
|
-
|
|
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
|
-
*
|
|
2676
|
-
*
|
|
2475
|
+
* - 若同一文件路径的事件已存在,则忽略重复事件(去重)
|
|
2476
|
+
* - 每次新事件都会重置防抖计时器,确保在变更停止后才触发处理
|
|
2477
|
+
*
|
|
2478
|
+
* @param event 文件变更事件
|
|
2677
2479
|
*/
|
|
2678
|
-
|
|
2679
|
-
|
|
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
|
-
*
|
|
2489
|
+
* - 若当前正在更新(isUpdating 为 true),则跳过以避免重复处理
|
|
2490
|
+
* - 清空待处理事件队列,并通过 onFlush 回调交由上层服务处理
|
|
2491
|
+
* - 处理开始后会锁定更新状态,防止处理过程中被新事件打断
|
|
2685
2492
|
*/
|
|
2686
|
-
|
|
2687
|
-
this.
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
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
|
-
*
|
|
2721
|
-
*
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
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
|
|
2730
|
-
* @
|
|
2511
|
+
* @param now 当前时间戳(默认使用 Date.now())
|
|
2512
|
+
* @returns true 表示已触发批量更新模式,false 表示仍处于普通监听模式
|
|
2731
2513
|
*/
|
|
2732
|
-
|
|
2733
|
-
this.
|
|
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
|
-
*
|
|
2741
|
-
* @returns 配置快照,若文件不存在或解析失败则返回 null
|
|
2535
|
+
* 在服务停止时调用,防止定时器在服务销毁后仍然触发回调
|
|
2742
2536
|
*/
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
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/
|
|
2791
|
-
import {
|
|
2792
|
-
import { join as
|
|
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/
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
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
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
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
|
-
*
|
|
2822
|
-
*
|
|
2581
|
+
* 清理所有活跃定时器,释放资源
|
|
2582
|
+
*
|
|
2583
|
+
* 在服务停止时调用,防止定时器在服务销毁后仍然触发回调
|
|
2823
2584
|
*/
|
|
2824
|
-
|
|
2825
|
-
const {
|
|
2826
|
-
|
|
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
|
-
|
|
2589
|
+
this.activeTimers.clear();
|
|
2855
2590
|
}
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
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
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
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
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
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
|
-
|
|
2924
|
-
|
|
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
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
if (!
|
|
2933
|
-
|
|
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
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
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 (
|
|
2990
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
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
|
-
*
|
|
3054
|
-
*
|
|
2697
|
+
* 重写指定笔记目录下 README.md 的一级标题。
|
|
2698
|
+
*
|
|
2699
|
+
* 输入的 `folderName` 形如 `0003. 评论功能的技术实现`,
|
|
2700
|
+
* 据此推导出新标题文本(去掉 `${noteIndex}. ` 前缀)。
|
|
3055
2701
|
*/
|
|
3056
|
-
async
|
|
3057
|
-
const
|
|
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
|
-
|
|
2709
|
+
let h1Index = -1;
|
|
3060
2710
|
for (let i = 0; i < lines.length; i++) {
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
2711
|
+
if (lines[i].trim().startsWith("# ")) {
|
|
2712
|
+
h1Index = i;
|
|
2713
|
+
break;
|
|
3064
2714
|
}
|
|
3065
2715
|
}
|
|
3066
|
-
if (
|
|
3067
|
-
|
|
3068
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
3080
|
-
*
|
|
3305
|
+
* 挂起文件监听(关闭 fs.watch 句柄),用于需要操作文件夹的场景(如重命名)
|
|
3306
|
+
* Windows 上 fs.watch 会锁住文件夹句柄,必须先关闭才能重命名
|
|
3081
3307
|
*/
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
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
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
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
|
-
*
|
|
3107
|
-
* @param notes - 可选的笔记列表,不传则内部扫描
|
|
3451
|
+
* 获取远程仓库信息
|
|
3108
3452
|
*/
|
|
3109
|
-
async
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
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
|
-
*
|
|
3150
|
-
* @param configPath - 配置文件路径
|
|
3488
|
+
* 检查是否有未提交的更改
|
|
3151
3489
|
*/
|
|
3152
|
-
|
|
3153
|
-
this.
|
|
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
|
-
|
|
3161
|
-
|
|
3162
|
-
this.
|
|
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
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
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
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
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
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
if (
|
|
3252
|
-
|
|
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
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
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
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
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
|
|
3601
|
+
async push(options) {
|
|
3602
|
+
await this.ensureValidRepo();
|
|
3603
|
+
const { force = false, setUpstream = false } = options || {};
|
|
3310
3604
|
try {
|
|
3311
|
-
const
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
if (
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
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(
|
|
3341
|
-
|
|
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
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
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
|
-
|
|
3359
|
-
logger.info(
|
|
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
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
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
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
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
|
-
|
|
3483
|
-
|
|
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
|
-
|
|
3496
|
-
|
|
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
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
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
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
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
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
this.
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
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
|
-
*
|
|
3551
|
-
*
|
|
3807
|
+
* 检查进程是否存在
|
|
3808
|
+
* @param id - 进程ID
|
|
3809
|
+
* @returns 是否存在
|
|
3552
3810
|
*/
|
|
3553
|
-
|
|
3554
|
-
this.
|
|
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
|
-
*
|
|
3815
|
+
* 检查进程是否在运行
|
|
3816
|
+
* @param id - 进程 ID
|
|
3817
|
+
* @returns 是否在运行
|
|
3560
3818
|
*/
|
|
3561
|
-
|
|
3562
|
-
this.
|
|
3563
|
-
|
|
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
|
-
|
|
3577
|
-
|
|
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
|
-
|
|
3608
|
-
|
|
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
|