@tnotesjs/core 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/config/ConfigManager.ts +137 -0
  2. package/config/constants.ts +121 -0
  3. package/config/index.ts +25 -0
  4. package/config/templates.ts +49 -0
  5. package/core/GitManager.ts +513 -0
  6. package/core/NoteIndexCache.ts +194 -0
  7. package/core/NoteManager.ts +407 -0
  8. package/core/ProcessManager.ts +180 -0
  9. package/core/ReadmeGenerator.ts +215 -0
  10. package/core/TocGenerator.ts +212 -0
  11. package/core/index.ts +11 -0
  12. package/package.json +5 -2
  13. package/services/file-watcher/configChangeHandler.ts +64 -0
  14. package/services/file-watcher/eventScheduler.ts +179 -0
  15. package/services/file-watcher/folderChangeHandler.ts +325 -0
  16. package/services/file-watcher/fsWatcherAdapter.ts +128 -0
  17. package/services/file-watcher/globalUpdateCoordinator.ts +60 -0
  18. package/services/file-watcher/index.ts +7 -0
  19. package/services/file-watcher/internal.ts +79 -0
  20. package/services/file-watcher/readmeChangeHandler.ts +28 -0
  21. package/services/file-watcher/renameDetector.ts +120 -0
  22. package/services/file-watcher/service.ts +352 -0
  23. package/services/file-watcher/watchState.ts +194 -0
  24. package/services/git/index.ts +7 -0
  25. package/services/git/service.ts +114 -0
  26. package/services/index.ts +15 -0
  27. package/services/init-sub-repo/index.ts +2 -0
  28. package/services/init-sub-repo/initSubRepoLogic.test.ts +162 -0
  29. package/services/init-sub-repo/initSubRepoLogic.ts +304 -0
  30. package/services/init-sub-repo/service.ts +101 -0
  31. package/services/note/index.ts +7 -0
  32. package/services/note/service.ts +362 -0
  33. package/services/readme/index.ts +7 -0
  34. package/services/readme/service.ts +761 -0
  35. package/services/timestamp/index.ts +7 -0
  36. package/services/timestamp/service.ts +465 -0
  37. package/services/toc/index.ts +5 -0
  38. package/services/toc/moveTocInside.test.ts +73 -0
  39. package/services/toc/service.ts +759 -0
  40. package/services/vitepress/index.ts +7 -0
  41. package/services/vitepress/service.ts +339 -0
  42. package/utils/errorHandler.ts +174 -0
  43. package/utils/file.ts +17 -0
  44. package/utils/genHierarchicalSidebar.ts +69 -0
  45. package/utils/generateAnchor.ts +24 -0
  46. package/utils/getChangedIds.ts +35 -0
  47. package/utils/index.ts +71 -0
  48. package/utils/logger.ts +231 -0
  49. package/utils/markdown.ts +75 -0
  50. package/utils/migrateReadmeToToc.test.ts +111 -0
  51. package/utils/migrateReadmeToToc.ts +135 -0
  52. package/utils/parseArgs.ts +90 -0
  53. package/utils/parseReadmeCompletedNotes.test.ts +90 -0
  54. package/utils/parseReadmeCompletedNotes.ts +108 -0
  55. package/utils/portUtils.ts +113 -0
  56. package/utils/readmeHelpers.ts +190 -0
  57. package/utils/runCommand.ts +29 -0
  58. package/utils/tocHelpers.test.ts +278 -0
  59. package/utils/tocHelpers.ts +855 -0
  60. package/utils/tocNodeId.test.ts +60 -0
  61. package/utils/tocNodeId.ts +97 -0
  62. package/utils/validators.ts +102 -0
@@ -0,0 +1,325 @@
1
+ /**
2
+ * services/file-watcher/folderChangeHandler.ts
3
+ *
4
+ * 文件夹变更处理:删除、重命名、回退
5
+ */
6
+
7
+ import { existsSync, promises as fsPromises } from 'fs'
8
+ import { join } from 'path'
9
+
10
+ import { safeExecute } from './internal'
11
+ import { REPO_NOTES_URL } from '../../config/constants'
12
+ import { generateNoteTitle } from '../../config/templates'
13
+
14
+ import type { EventScheduler } from './eventScheduler'
15
+ import type { WatchState } from './watchState'
16
+ import type { NoteIndexCache } from '../../core/NoteIndexCache'
17
+ import type { Logger } from '../../utils'
18
+ import type { NoteService } from '../note/service'
19
+ import type { ReadmeService } from '../readme/service'
20
+ import type { TocService } from '../toc/service'
21
+
22
+ const RENAME_REVERT_DELAY_MS = 2000
23
+
24
+ /**
25
+ * 检测到笔记目录名称变更或者被删除的事件时,需要更新根目录下的 README.md 和 sidebar.json
26
+ *
27
+ * 暂定 1s 作为缓冲,1s 过后再恢复监听
28
+ */
29
+ const DELETE_REINIT_DELAY_MS = 1000
30
+
31
+ const UPDATE_UNLOCK_DELAY_MS = 500
32
+
33
+ interface FolderChangeHandlerConfig {
34
+ /** 笔记目录路径 */
35
+ notesDir: string
36
+ /** 监听状态管理器 */
37
+ watchState: WatchState
38
+ /** 事件调度器 */
39
+ scheduler: EventScheduler
40
+ /** 笔记服务实例 */
41
+ noteService: NoteService
42
+ /** README 服务实例 */
43
+ readmeService: ReadmeService
44
+ /** TOC 服务实例 */
45
+ tocService: TocService
46
+ /** 笔记索引缓存实例 */
47
+ noteIndexCache: NoteIndexCache
48
+ /** 日志记录器 */
49
+ logger: Logger
50
+ /**
51
+ * 文件夹重命名成功后回调(可选)。
52
+ *
53
+ * 用于在 dev server 中通过 Vite WS 通知前端跳转到新 URL,
54
+ * 让「文件系统直接改名」与「关于面板保存」体验一致。
55
+ */
56
+ onRenameSuccess?: (payload: {
57
+ oldFolder: string
58
+ newFolder: string
59
+ noteIndex: string
60
+ }) => void
61
+ /** 笔记结构变更(增删改目录)后触发 dev 搜索索引重建 */
62
+ onNoteStructureChanged?: (reason: string) => void
63
+ }
64
+
65
+ export class FolderChangeHandler {
66
+ /** 活跃的定时器 ID 集合,用于统一清理 */
67
+ private activeTimers: Set<NodeJS.Timeout> = new Set()
68
+
69
+ constructor(private config: FolderChangeHandlerConfig) {}
70
+
71
+ /**
72
+ * 清理所有活跃定时器,释放资源
73
+ *
74
+ * 在服务停止时调用,防止定时器在服务销毁后仍然触发回调
75
+ */
76
+ clearTimers(): void {
77
+ for (const timer of this.activeTimers) {
78
+ clearTimeout(timer)
79
+ }
80
+ this.activeTimers.clear()
81
+ }
82
+
83
+ private scheduleTimer(fn: () => void, delay: number): void {
84
+ const timer = setTimeout(() => {
85
+ this.activeTimers.delete(timer)
86
+ fn()
87
+ }, delay)
88
+ this.activeTimers.add(timer)
89
+ }
90
+
91
+ async handleFolderDeletion(deletedFolderName: string): Promise<void> {
92
+ const { scheduler, watchState, noteIndexCache, tocService, logger } =
93
+ this.config
94
+
95
+ if (scheduler.getUpdating()) return
96
+ scheduler.setUpdating(true)
97
+
98
+ try {
99
+ const noteIndex = this.extractNoteIndexOrWarn(deletedFolderName)
100
+ if (!noteIndex) return
101
+
102
+ logger.info(`正在处理笔记删除: ${noteIndex} (${deletedFolderName})`)
103
+
104
+ // 清理缓存
105
+ watchState.deleteNoteDir(deletedFolderName)
106
+ watchState.clearNoteCaches(deletedFolderName)
107
+ noteIndexCache.delete(noteIndex)
108
+
109
+ // 更新根 README.md、sidebar.json
110
+ await safeExecute(
111
+ `删除笔记 ${noteIndex}`,
112
+ async () => {
113
+ await tocService.deleteNoteFromToc(noteIndex)
114
+ await tocService.regenerateSidebar()
115
+ this.config.onNoteStructureChanged?.(`watcher:delete:${noteIndex}`)
116
+ },
117
+ logger,
118
+ )
119
+ } finally {
120
+ this.scheduleTimer(() => {
121
+ scheduler.setUpdating(false)
122
+ watchState.initializeFromDisk()
123
+ }, DELETE_REINIT_DELAY_MS)
124
+ }
125
+ }
126
+
127
+ async handleFolderRenameUpdate(
128
+ oldName: string,
129
+ newName: string,
130
+ ): Promise<void> {
131
+ const { scheduler, watchState, logger } = this.config
132
+
133
+ if (scheduler.getUpdating()) return
134
+ scheduler.setUpdating(true)
135
+
136
+ try {
137
+ const { oldNoteIndex, newNoteIndex } = this.validateRenameIndexes(
138
+ oldName,
139
+ newName,
140
+ )
141
+ if (!oldNoteIndex || !newNoteIndex) return
142
+
143
+ logger.info(`正在处理文件夹重命名: ${oldName} → ${newName}`)
144
+
145
+ if (oldNoteIndex === newNoteIndex) {
146
+ await safeExecute(
147
+ `标题重命名 ${newNoteIndex}`,
148
+ () => this.handleTitleOnlyRename(newNoteIndex, newName),
149
+ logger,
150
+ )
151
+ } else {
152
+ await safeExecute(
153
+ `索引变更 ${oldNoteIndex}→${newNoteIndex}`,
154
+ () => this.handleIndexChangedRename(oldNoteIndex, newNoteIndex),
155
+ logger,
156
+ )
157
+ }
158
+
159
+ this.config.onRenameSuccess?.({
160
+ oldFolder: oldName,
161
+ newFolder: newName,
162
+ noteIndex: newNoteIndex,
163
+ })
164
+ } finally {
165
+ this.scheduleTimer(() => {
166
+ scheduler.setUpdating(false)
167
+ watchState.initializeFromDisk()
168
+ }, UPDATE_UNLOCK_DELAY_MS)
169
+ }
170
+ }
171
+
172
+ // #region - 私有实现
173
+
174
+ private validateRenameIndexes(
175
+ oldName: string,
176
+ newName: string,
177
+ ): { oldNoteIndex: string | null; newNoteIndex: string | null } {
178
+ const { noteIndexCache, logger } = this.config
179
+ const oldNoteIndex = this.extractNoteIndexOrWarn(oldName)
180
+ const newNoteIndex = this.extractNoteIndexOrWarn(newName)
181
+
182
+ if (!oldNoteIndex || !newNoteIndex) {
183
+ return { oldNoteIndex: null, newNoteIndex: null }
184
+ }
185
+
186
+ if (!/^\d{4}$/.test(newNoteIndex)) {
187
+ logger.error(`新笔记索引格式非法: ${newNoteIndex},自动回退`)
188
+ this.revertFolderRename(oldName, newName)
189
+ return { oldNoteIndex: null, newNoteIndex: null }
190
+ }
191
+
192
+ if (oldNoteIndex !== newNoteIndex && noteIndexCache.has(newNoteIndex)) {
193
+ logger.error(`新笔记索引 ${newNoteIndex} 已存在,自动回退`)
194
+ this.revertFolderRename(oldName, newName)
195
+ return { oldNoteIndex: null, newNoteIndex: null }
196
+ }
197
+
198
+ return { oldNoteIndex, newNoteIndex }
199
+ }
200
+
201
+ private async handleTitleOnlyRename(
202
+ noteIndex: string,
203
+ newName: string,
204
+ ): Promise<void> {
205
+ const { noteIndexCache, tocService, logger, notesDir } = this.config
206
+
207
+ logger.info(`笔记索引未变 (${noteIndex}),只更新标题`)
208
+ noteIndexCache.updateFolderName(noteIndex, newName)
209
+
210
+ // 同步重写笔记目录内 README.md 的一级标题,与 RenameNoteCommand 行为一致
211
+ await safeExecute(
212
+ `更新 README 一级标题 ${noteIndex}`,
213
+ () => this.rewriteNoteReadmeH1(notesDir, newName, noteIndex),
214
+ logger,
215
+ )
216
+
217
+ await tocService.renameNoteInToc(noteIndex, newName)
218
+ await tocService.regenerateSidebar()
219
+ this.config.onNoteStructureChanged?.(`watcher:rename-title:${noteIndex}`)
220
+ logger.success(`标题更新完成`)
221
+ }
222
+
223
+ /**
224
+ * 重写指定笔记目录下 README.md 的一级标题。
225
+ *
226
+ * 输入的 `folderName` 形如 `0003. 评论功能的技术实现`,
227
+ * 据此推导出新标题文本(去掉 `${noteIndex}. ` 前缀)。
228
+ */
229
+ private async rewriteNoteReadmeH1(
230
+ notesDir: string,
231
+ folderName: string,
232
+ noteIndex: string,
233
+ ): Promise<void> {
234
+ const readmePath = join(notesDir, folderName, 'README.md')
235
+ if (!existsSync(readmePath)) return
236
+
237
+ const newTitle = folderName.replace(/^\d{4}\.\s*/, '').trim()
238
+ if (!newTitle) return
239
+
240
+ const content = await fsPromises.readFile(readmePath, 'utf-8')
241
+ const lines = content.split('\n')
242
+
243
+ let h1Index = -1
244
+ for (let i = 0; i < lines.length; i++) {
245
+ if (lines[i].trim().startsWith('# ')) {
246
+ h1Index = i
247
+ break
248
+ }
249
+ }
250
+
251
+ if (h1Index === -1) {
252
+ this.config.logger.warn(
253
+ `未在 ${readmePath} 找到一级标题,跳过 README H1 更新`,
254
+ )
255
+ return
256
+ }
257
+
258
+ const newH1 = generateNoteTitle(noteIndex, newTitle, REPO_NOTES_URL)
259
+ if (lines[h1Index] === newH1) return
260
+
261
+ lines[h1Index] = newH1
262
+ await fsPromises.writeFile(readmePath, lines.join('\n'), 'utf-8')
263
+ }
264
+
265
+ private async handleIndexChangedRename(
266
+ oldNoteIndex: string,
267
+ newNoteIndex: string,
268
+ ): Promise<void> {
269
+ const { noteService, noteIndexCache, tocService, logger } = this.config
270
+
271
+ logger.info(`笔记索引变更: ${oldNoteIndex} → ${newNoteIndex}`)
272
+
273
+ await tocService.deleteNoteFromToc(oldNoteIndex)
274
+
275
+ const newNote = noteService.getNoteByIndex(newNoteIndex)
276
+
277
+ if (newNote) {
278
+ noteIndexCache.delete(oldNoteIndex)
279
+ noteIndexCache.add(newNote)
280
+
281
+ await tocService.appendNoteToToc(newNoteIndex)
282
+ await tocService.regenerateSidebar()
283
+ this.config.onNoteStructureChanged?.(
284
+ `watcher:rename-index:${oldNoteIndex}->${newNoteIndex}`,
285
+ )
286
+ logger.success(`笔记索引变更处理完成`)
287
+ } else {
288
+ logger.error(`未找到新笔记: ${newNoteIndex}`)
289
+ }
290
+ }
291
+
292
+ private async revertFolderRename(
293
+ oldName: string,
294
+ newName: string,
295
+ ): Promise<void> {
296
+ const { notesDir, scheduler, watchState, logger } = this.config
297
+
298
+ try {
299
+ const oldPath = join(notesDir, oldName)
300
+ const newPath = join(notesDir, newName)
301
+
302
+ if (existsSync(newPath)) {
303
+ scheduler.setUpdating(true)
304
+ await fsPromises.rename(newPath, oldPath)
305
+ logger.warn(`文件夹已回退: ${newName} → ${oldName}`)
306
+ this.scheduleTimer(() => {
307
+ scheduler.setUpdating(false)
308
+ watchState.initializeFromDisk()
309
+ }, RENAME_REVERT_DELAY_MS)
310
+ }
311
+ } catch (error) {
312
+ logger.error(`回退文件夹重命名失败: ${error}`)
313
+ }
314
+ }
315
+
316
+ private extractNoteIndexOrWarn(name: string): string | null {
317
+ const noteIndex = name.match(/^(\d{4})/)?.[1] || null
318
+ if (!noteIndex) {
319
+ this.config.logger.warn(`无法从文件夹名称提取笔记索引: ${name}`)
320
+ }
321
+ return noteIndex
322
+ }
323
+
324
+ // #endregion - 私有实现
325
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * services/file-watcher/fsWatcherAdapter.ts
3
+ *
4
+ * fs.watch 适配器:仅负责监听和事件分发
5
+ */
6
+
7
+ import { watch } from 'fs'
8
+ import { basename, dirname, join, sep } from 'path'
9
+
10
+ import { WATCH_EVENT_TYPES } from './internal'
11
+ import { NoteManager } from '../../core/NoteManager'
12
+
13
+ import type { WatchEvent, WatchEventType } from './internal'
14
+ import type { Logger } from '../../utils'
15
+ import type { FSWatcher } from 'fs'
16
+
17
+ interface FsWatcherAdapterConfig {
18
+ /** 笔记目录路径 */
19
+ notesDir: string
20
+ /** 检查是否正在更新的方法 */
21
+ isUpdating: () => boolean
22
+ /** 文件夹重命名事件回调 */
23
+ onRename: (folderName: string) => void
24
+ /** 笔记事件处理回调 */
25
+ onNoteEvent: (event: WatchEvent) => void
26
+ /** 日志记录器 */
27
+ logger: Logger
28
+ }
29
+
30
+ export class FsWatcherAdapter {
31
+ /** 文件系统监听器实例 */
32
+ private watcher: FSWatcher | null = null
33
+
34
+ constructor(private config: FsWatcherAdapterConfig) {}
35
+
36
+ start(): void {
37
+ const { logger } = this.config
38
+
39
+ if (this.watcher) {
40
+ logger.warn('文件监听服务已启动')
41
+ return
42
+ }
43
+
44
+ this.watcher = watch(
45
+ this.config.notesDir,
46
+ { recursive: true },
47
+ (eventType, filename) => this.handleFsEvent(eventType, filename),
48
+ )
49
+
50
+ logger.debug(`文件监听已启动,监听目录:${this.config.notesDir}`)
51
+ }
52
+
53
+ stop(): void {
54
+ if (!this.watcher) return
55
+ this.watcher.close()
56
+ this.watcher = null
57
+ }
58
+
59
+ isWatching(): boolean {
60
+ return this.watcher !== null
61
+ }
62
+
63
+ private handleFsEvent(eventType: string, filename: string | undefined): void {
64
+ const { isUpdating, onRename, onNoteEvent } = this.config
65
+
66
+ // 过滤无效事件
67
+ // 处理需要跳过监听的场景
68
+ if (
69
+ !filename || // 忽略无文件变更
70
+ isUpdating() // 如果正在更新,忽略所有变更
71
+ ) {
72
+ return
73
+ }
74
+
75
+ // 文件夹级事件
76
+ // 处理笔记名称(笔记所属的直接父级文件夹名称)发生变化的场景
77
+ // 根层 rename:文件夹创建/删除/重命名,交给 RenameDetector
78
+ if (
79
+ eventType === 'rename' && // 检测文件夹 rename 事件
80
+ !filename.includes(sep) // 顶层文件夹名称发生变更
81
+ ) {
82
+ onRename(filename)
83
+ return
84
+ }
85
+
86
+ // 文件级事件
87
+ // 处理笔记文件内容(笔记 README.md 文件、笔记配置 .tnotes.json 文件)发生变化的场景
88
+ // 只处理 README.md 和 .tnotes.json 文件的变更
89
+ const baseFilename = basename(filename)
90
+ if (baseFilename !== 'README.md' && baseFilename !== '.tnotes.json') {
91
+ // 忽略非目标文件的变更事件(如 assets 文件夹、其他文件等)
92
+ return
93
+ }
94
+
95
+ const fullPath = join(this.config.notesDir, filename)
96
+ const event = this.buildWatchEvent(fullPath, filename)
97
+ if (!event) {
98
+ // 无法构建变更事件 - 通常是笔记格式错误导致,比如笔记名的索引不是 0001-9999
99
+ return
100
+ }
101
+
102
+ onNoteEvent(event)
103
+ }
104
+
105
+ private buildWatchEvent(
106
+ fullPath: string,
107
+ filename: string,
108
+ ): WatchEvent | null {
109
+ const noteDirName = basename(dirname(fullPath))
110
+ const noteIndex = NoteManager.extractNoteIndex(noteDirName)
111
+ if (!noteIndex) {
112
+ NoteManager.warnInvalidNoteIndex(noteDirName)
113
+ return null
114
+ }
115
+
116
+ const fileType: WatchEventType = filename.endsWith('README.md')
117
+ ? WATCH_EVENT_TYPES.README
118
+ : WATCH_EVENT_TYPES.CONFIG
119
+
120
+ return {
121
+ path: fullPath,
122
+ type: fileType,
123
+ noteIndex,
124
+ noteDirName,
125
+ noteDirPath: dirname(fullPath),
126
+ }
127
+ }
128
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * services/file-watcher/globalUpdateCoordinator.ts
3
+ *
4
+ * 全局更新协调:应用配置更新、更新 README 列表
5
+ */
6
+
7
+ import { safeExecute } from './internal'
8
+
9
+ import type { WatchEvent } from './internal'
10
+ import type { NoteIndexCache } from '../../core/NoteIndexCache'
11
+ import type { Logger } from '../../utils'
12
+ import type { ReadmeService } from '../readme/service'
13
+ import type { TocService } from '../toc/service'
14
+
15
+
16
+ interface GlobalUpdateCoordinatorConfig {
17
+ /** README 服务实例,用于更新笔记 README */
18
+ readmeService: ReadmeService
19
+ /** TOC 服务实例,用于更新 TOC.md 与 sidebar */
20
+ tocService: TocService
21
+ /** 笔记索引缓存实例 */
22
+ noteIndexCache: NoteIndexCache
23
+ /** 日志记录器 */
24
+ logger: Logger
25
+ }
26
+
27
+ export class GlobalUpdateCoordinator {
28
+ constructor(private config: GlobalUpdateCoordinatorConfig) {}
29
+
30
+ async applyConfigUpdates(changedNoteIndexes: string[]): Promise<void> {
31
+ if (changedNoteIndexes.length === 0) return
32
+
33
+ const { tocService, noteIndexCache, logger } = this.config
34
+
35
+ logger.info('检测到笔记状态变化,增量更新全局文件...')
36
+
37
+ for (const noteIndex of changedNoteIndexes) {
38
+ await safeExecute(
39
+ `增量更新 ${noteIndex}`,
40
+ async () => {
41
+ const item = noteIndexCache.getByNoteIndex(noteIndex)
42
+ await tocService.updateNoteInToc(
43
+ noteIndex,
44
+ item?.noteConfig || {},
45
+ )
46
+ logger.info(`增量更新 TOC.md 中的笔记: ${noteIndex}`)
47
+ },
48
+ logger,
49
+ )
50
+ }
51
+
52
+ await tocService.regenerateSidebar()
53
+ }
54
+
55
+ async updateNoteReadmesOnly(events: WatchEvent[]): Promise<void> {
56
+ const noteIndexesToUpdate = [...new Set(events.map((c) => c.noteIndex))]
57
+ if (noteIndexesToUpdate.length === 0) return
58
+ await this.config.readmeService.updateNoteReadmesOnly(noteIndexesToUpdate)
59
+ }
60
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * services/file-watcher/index.ts
3
+ *
4
+ * 文件监听层入口
5
+ */
6
+
7
+ export { FileWatcherService } from './service'
@@ -0,0 +1,79 @@
1
+ /**
2
+ * services/file-watcher/internal.ts
3
+ *
4
+ * 文件监听层内部模型:仅供 file-watcher 层使用
5
+ */
6
+
7
+ import type { Logger } from '../../utils'
8
+
9
+ /**
10
+ * 监听事件类型常量
11
+ */
12
+ export const WATCH_EVENT_TYPES = {
13
+ README: 'readme',
14
+ CONFIG: 'config',
15
+ } as const
16
+
17
+ export type WatchEventType =
18
+ (typeof WATCH_EVENT_TYPES)[keyof typeof WATCH_EVENT_TYPES]
19
+
20
+ /**
21
+ * 监听到的文件变更事件
22
+ *
23
+ * 示例:
24
+ *
25
+ * ```js
26
+ * {
27
+ * path: 'C:\\tnotesjs\\TNotes.introduction\\notes\\0001. TNotes 简介\\README.md',
28
+ * type: 'readme',
29
+ * noteIndex: '0001',
30
+ * noteDirName: '0001. TNotes 简介',
31
+ * noteDirPath: 'C:\\tnotesjs\\TNotes.introduction\\notes\\0001. TNotes 简介'
32
+ * }
33
+ * ```
34
+ */
35
+ export interface WatchEvent {
36
+ /**
37
+ * 笔记文件(README.md、.tnotes.json)的绝对路径
38
+ */
39
+ path: string
40
+ type: WatchEventType
41
+ noteIndex: string
42
+ noteDirName: string
43
+ noteDirPath: string
44
+ /**
45
+ * 文件夹重命名时的旧名称
46
+ */
47
+ oldNoteDirName?: string
48
+ }
49
+
50
+ export type ConfigSnapshot = {
51
+ done: boolean
52
+ enableDiscussions: boolean
53
+ description: string
54
+ }
55
+
56
+ /**
57
+ * 统一的异步错误处理辅助函数
58
+ *
59
+ * 用于包装可能抛出异常的异步操作,将错误统一通过 logger.error 输出,
60
+ * 避免各模块错误处理方式不一致(静默吞掉、格式各异等)。
61
+ *
62
+ * @param label 操作标签,用于错误日志前缀
63
+ * @param fn 需要执行的异步函数
64
+ * @param logger 日志记录器
65
+ * @returns true 表示执行成功,false 表示捕获到异常
66
+ */
67
+ export async function safeExecute(
68
+ label: string,
69
+ fn: () => Promise<void>,
70
+ logger: Logger,
71
+ ): Promise<boolean> {
72
+ try {
73
+ await fn()
74
+ return true
75
+ } catch (error) {
76
+ logger.error(`[${label}] ${error}`)
77
+ return false
78
+ }
79
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * services/file-watcher/readmeChangeHandler.ts
3
+ *
4
+ * README 变更处理
5
+ */
6
+
7
+ import type { WatchEvent } from './internal'
8
+ import type { NoteService } from '../note/service'
9
+
10
+ interface ReadmeChangeHandlerConfig {
11
+ /** 笔记服务实例 */
12
+ noteService: NoteService
13
+ }
14
+
15
+ export class ReadmeChangeHandler {
16
+ constructor(private config: ReadmeChangeHandlerConfig) {}
17
+
18
+ async handle(events: WatchEvent[]): Promise<void> {
19
+ if (events.length === 0) return
20
+ const indexes = [...new Set(events.map((c) => c.noteIndex))]
21
+ for (const noteIndex of indexes) {
22
+ const noteInfo = this.config.noteService.getNoteByIndex(noteIndex)
23
+ if (noteInfo) {
24
+ await this.config.noteService.fixNoteTitle(noteInfo)
25
+ }
26
+ }
27
+ }
28
+ }