@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.
- package/config/ConfigManager.ts +137 -0
- package/config/constants.ts +121 -0
- package/config/index.ts +25 -0
- package/config/templates.ts +49 -0
- package/core/GitManager.ts +513 -0
- package/core/NoteIndexCache.ts +194 -0
- package/core/NoteManager.ts +407 -0
- package/core/ProcessManager.ts +180 -0
- package/core/ReadmeGenerator.ts +215 -0
- package/core/TocGenerator.ts +212 -0
- package/core/index.ts +11 -0
- package/package.json +5 -2
- package/services/file-watcher/configChangeHandler.ts +64 -0
- package/services/file-watcher/eventScheduler.ts +179 -0
- package/services/file-watcher/folderChangeHandler.ts +325 -0
- package/services/file-watcher/fsWatcherAdapter.ts +128 -0
- package/services/file-watcher/globalUpdateCoordinator.ts +60 -0
- package/services/file-watcher/index.ts +7 -0
- package/services/file-watcher/internal.ts +79 -0
- package/services/file-watcher/readmeChangeHandler.ts +28 -0
- package/services/file-watcher/renameDetector.ts +120 -0
- package/services/file-watcher/service.ts +352 -0
- package/services/file-watcher/watchState.ts +194 -0
- package/services/git/index.ts +7 -0
- package/services/git/service.ts +114 -0
- package/services/index.ts +15 -0
- package/services/init-sub-repo/index.ts +2 -0
- package/services/init-sub-repo/initSubRepoLogic.test.ts +162 -0
- package/services/init-sub-repo/initSubRepoLogic.ts +304 -0
- package/services/init-sub-repo/service.ts +101 -0
- package/services/note/index.ts +7 -0
- package/services/note/service.ts +362 -0
- package/services/readme/index.ts +7 -0
- package/services/readme/service.ts +761 -0
- package/services/timestamp/index.ts +7 -0
- package/services/timestamp/service.ts +465 -0
- package/services/toc/index.ts +5 -0
- package/services/toc/moveTocInside.test.ts +73 -0
- package/services/toc/service.ts +759 -0
- package/services/vitepress/index.ts +7 -0
- package/services/vitepress/service.ts +339 -0
- package/utils/errorHandler.ts +174 -0
- package/utils/file.ts +17 -0
- package/utils/genHierarchicalSidebar.ts +69 -0
- package/utils/generateAnchor.ts +24 -0
- package/utils/getChangedIds.ts +35 -0
- package/utils/index.ts +71 -0
- package/utils/logger.ts +231 -0
- package/utils/markdown.ts +75 -0
- package/utils/migrateReadmeToToc.test.ts +111 -0
- package/utils/migrateReadmeToToc.ts +135 -0
- package/utils/parseArgs.ts +90 -0
- package/utils/parseReadmeCompletedNotes.test.ts +90 -0
- package/utils/parseReadmeCompletedNotes.ts +108 -0
- package/utils/portUtils.ts +113 -0
- package/utils/readmeHelpers.ts +190 -0
- package/utils/runCommand.ts +29 -0
- package/utils/tocHelpers.test.ts +278 -0
- package/utils/tocHelpers.ts +855 -0
- package/utils/tocNodeId.test.ts +60 -0
- package/utils/tocNodeId.ts +97 -0
- 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,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
|
+
}
|