@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,362 @@
1
+ /**
2
+ * services/NoteService.ts
3
+ *
4
+ * 笔记服务 - 封装笔记相关的业务逻辑
5
+ */
6
+
7
+ import { writeFileSync, readFileSync, promises as fsPromises } from 'fs'
8
+ import { join } from 'path'
9
+ import { v4 as uuidv4 } from 'uuid'
10
+
11
+ import { NOTES_PATH, CONSTANTS, REPO_NOTES_URL } from '../../config/constants'
12
+ import {
13
+ generateNoteTitle,
14
+ getNewNoteReadmeBody,
15
+ } from '../../config/templates'
16
+ import { NoteIndexCache } from '../../core/NoteIndexCache'
17
+ import { NoteManager } from '../../core/NoteManager'
18
+ import { ensureDirectory, logger } from '../../utils'
19
+ import { TocService } from '../toc/service'
20
+
21
+ import type { NoteInfo, NoteConfig } from '../../types'
22
+
23
+ /**
24
+ * 创建新笔记的选项
25
+ */
26
+ interface CreateNoteOptions {
27
+ title?: string
28
+ category?: string
29
+ enableDiscussions?: boolean
30
+ configId?: string // 配置文件中的 UUID(跨所有知识库唯一)
31
+ usedIndexes?: Set<number> // 可选的已使用编号集合,用于批量创建时避免重复扫描
32
+ }
33
+
34
+ /**
35
+ * 笔记服务类
36
+ */
37
+ export class NoteService {
38
+ private static instance: NoteService
39
+
40
+ private noteManager: NoteManager
41
+ private noteIndexCache: NoteIndexCache
42
+ private ignoredConfigPaths: Set<string> = new Set()
43
+
44
+ private constructor() {
45
+ this.noteManager = NoteManager.getInstance()
46
+ this.noteIndexCache = NoteIndexCache.getInstance()
47
+ }
48
+
49
+ static getInstance(): NoteService {
50
+ if (!NoteService.instance) {
51
+ NoteService.instance = new NoteService()
52
+ }
53
+ return NoteService.instance
54
+ }
55
+
56
+ /**
57
+ * 标记配置文件在下次变更时被忽略(防止 API 写入触发文件监听循环)
58
+ * @param configPath - 配置文件路径
59
+ */
60
+ ignoreNextConfigChange(configPath: string): void {
61
+ this.ignoredConfigPaths.add(configPath)
62
+ }
63
+
64
+ /**
65
+ * 检查配置文件是否应该被忽略
66
+ * @param configPath - 配置文件路径
67
+ * @returns 是否应该忽略
68
+ */
69
+ shouldIgnoreConfigChange(configPath: string): boolean {
70
+ if (this.ignoredConfigPaths.has(configPath)) {
71
+ this.ignoredConfigPaths.delete(configPath)
72
+ return true
73
+ }
74
+ return false
75
+ }
76
+
77
+ /**
78
+ * 获取所有笔记
79
+ * dev 模式下(缓存已初始化)从内存读取,其他模式回退到文件扫描
80
+ * @returns 笔记信息数组
81
+ */
82
+ getAllNotes(): NoteInfo[] {
83
+ if (this.noteIndexCache.isInitialized()) {
84
+ return this.noteIndexCache.toNoteInfoList()
85
+ }
86
+ return this.noteManager.scanNotes()
87
+ }
88
+
89
+ /**
90
+ * 获取笔记(通过索引)
91
+ * @param noteIndex - 笔记索引(文件夹前 4 位数字)
92
+ * @returns 笔记信息,未找到时返回 undefined
93
+ */
94
+ getNoteByIndex(noteIndex: string): NoteInfo | undefined {
95
+ return this.noteManager.getNoteByIndex(noteIndex)
96
+ }
97
+
98
+ /**
99
+ * 创建新笔记
100
+ * @param options - 创建选项
101
+ * @returns 新创建的笔记信息
102
+ */
103
+ async createNote(options: CreateNoteOptions = {}): Promise<NoteInfo> {
104
+ const {
105
+ title = 'new',
106
+ category,
107
+ enableDiscussions = false,
108
+ configId,
109
+ usedIndexes,
110
+ } = options
111
+
112
+ // 生成笔记索引(填充空缺)
113
+ const noteIndex = this.generateNextNoteIndex(usedIndexes)
114
+ const dirName = `${noteIndex}. ${title}`
115
+ const notePath = join(NOTES_PATH, dirName)
116
+
117
+ // 确保目录存在
118
+ await ensureDirectory(notePath)
119
+
120
+ // 创建 README.md(包含一级标题)
121
+ const readmePath = join(notePath, 'README.md')
122
+ const noteTitle = generateNoteTitle(noteIndex, title, REPO_NOTES_URL)
123
+ const readmeContent = noteTitle + '\n' + getNewNoteReadmeBody()
124
+ writeFileSync(readmePath, readmeContent, 'utf-8')
125
+
126
+ // 创建 .tnotes.json(使用 UUID 作为配置 ID)
127
+ const configPath = join(notePath, '.tnotes.json')
128
+ const config: NoteConfig = {
129
+ id: configId || uuidv4(), // 配置 ID 使用 UUID(跨知识库唯一)
130
+ bilibili: [],
131
+ tnotes: [],
132
+ yuque: [],
133
+ done: false,
134
+ category,
135
+ enableDiscussions,
136
+ // created_at / updated_at 由 tn:push 时 fix-timestamps 自动写入
137
+ }
138
+ this.noteManager.writeNoteConfig(configPath, config)
139
+
140
+ const noteInfo: NoteInfo = {
141
+ index: noteIndex, // 返回的 id 是笔记索引(目录前缀)
142
+ path: notePath,
143
+ dirName,
144
+ readmePath,
145
+ configPath,
146
+ config,
147
+ }
148
+
149
+ if (this.noteIndexCache.isInitialized()) {
150
+ this.noteIndexCache.add(noteInfo)
151
+ }
152
+
153
+ logger.info(`Created new note: ${dirName}`)
154
+
155
+ return noteInfo
156
+ }
157
+
158
+ /**
159
+ * 删除笔记文件夹
160
+ * @param noteIndex - 笔记索引
161
+ * @returns 被删除的笔记信息
162
+ */
163
+ async deleteNote(noteIndex: string): Promise<NoteInfo> {
164
+ const note = this.getNoteByIndex(noteIndex)
165
+ if (!note) {
166
+ throw new Error(`笔记未找到: ${noteIndex}`)
167
+ }
168
+
169
+ await fsPromises.rm(note.path, { recursive: true, force: true })
170
+
171
+ if (this.noteIndexCache.isInitialized()) {
172
+ this.noteIndexCache.delete(noteIndex)
173
+ }
174
+
175
+ logger.info(`Deleted note: ${note.dirName}`)
176
+
177
+ return note
178
+ }
179
+
180
+ /**
181
+ * 生成下一个笔记索引(填充空缺)
182
+ * @param usedIndexes - 可选的已使用编号集合,不传则内部扫描
183
+ * @returns 新的笔记索引(4位数字字符串,从 0001 到 9999)
184
+ */
185
+ private generateNextNoteIndex(usedIndexes?: Set<number>): string {
186
+ if (!usedIndexes) {
187
+ const notes = this.getAllNotes()
188
+ usedIndexes = new Set<number>()
189
+ for (const note of notes) {
190
+ const id = parseInt(note.index, 10)
191
+ if (!isNaN(id) && id >= 1 && id <= 9999) {
192
+ usedIndexes.add(id)
193
+ }
194
+ }
195
+ }
196
+
197
+ if (usedIndexes.size === 0) {
198
+ return '0001'
199
+ }
200
+
201
+ // 从 1 开始查找第一个未使用的编号
202
+ for (let i = 1; i <= 9999; i++) {
203
+ if (!usedIndexes.has(i)) {
204
+ return i.toString().padStart(CONSTANTS.NOTE_INDEX_LENGTH, '0')
205
+ }
206
+ }
207
+
208
+ // 如果所有编号都被占用(极端情况)
209
+ throw new Error('所有笔记编号 (0001-9999) 已被占用,无法创建新笔记')
210
+ }
211
+
212
+ /**
213
+ * 更新笔记配置
214
+ * @param noteIndex - 笔记索引
215
+ * @param updates - 配置更新
216
+ */
217
+ async updateNoteConfig(
218
+ noteIndex: string,
219
+ updates: Partial<NoteConfig>,
220
+ ): Promise<void> {
221
+ const note = this.getNoteByIndex(noteIndex)
222
+ if (!note || !note.config) {
223
+ throw new Error(`Note not found or no config: ${noteIndex}`)
224
+ }
225
+
226
+ const oldConfig = { ...note.config }
227
+ const updatedConfig: NoteConfig = {
228
+ ...note.config,
229
+ ...updates,
230
+ }
231
+
232
+ // 标记配置文件为忽略(防止文件监听触发循环更新)
233
+ this.ignoreNextConfigChange(note.configPath)
234
+
235
+ // 更新笔记配置文件
236
+ this.noteManager.updateNoteConfig(note, updatedConfig)
237
+
238
+ // 更新内存索引
239
+ this.noteIndexCache.updateConfig(noteIndex, updatedConfig)
240
+
241
+ // 检查是否需要更新全局文件
242
+ const needsGlobalUpdate = this.checkNeedsGlobalUpdate(
243
+ oldConfig,
244
+ updatedConfig,
245
+ )
246
+
247
+ if (needsGlobalUpdate) {
248
+ logger.info(`检测到全局字段变更 (${noteIndex}),正在增量更新全局文件...`)
249
+
250
+ // 使用增量更新
251
+ const tocService = TocService.getInstance()
252
+
253
+ await tocService.updateNoteInToc(noteIndex, updates)
254
+ await tocService.regenerateSidebar()
255
+
256
+ logger.info(`全局文件增量更新完成 (${noteIndex})`)
257
+ } else {
258
+ logger.debug(`配置更新不影响全局文件 (${noteIndex})`)
259
+ }
260
+ }
261
+
262
+ /**
263
+ * 检查配置更新是否需要触发全局更新
264
+ * @param oldConfig - 旧配置
265
+ * @param newConfig - 新配置
266
+ * @returns 是否需要全局更新
267
+ */
268
+ private checkNeedsGlobalUpdate(
269
+ oldConfig: NoteConfig,
270
+ newConfig: NoteConfig,
271
+ ): boolean {
272
+ // 影响全局的字段:done
273
+ const globalFields: (keyof NoteConfig)[] = ['done']
274
+
275
+ for (const field of globalFields) {
276
+ if (oldConfig[field] !== newConfig[field]) {
277
+ return true
278
+ }
279
+ }
280
+
281
+ return false
282
+ }
283
+
284
+ /**
285
+ * 修正笔记标题
286
+ * @param noteInfo - 笔记信息
287
+ * @returns 是否进行了修正
288
+ */
289
+ async fixNoteTitle(noteInfo: NoteInfo): Promise<boolean> {
290
+ try {
291
+ const readmeContent = readFileSync(noteInfo.readmePath, 'utf-8')
292
+
293
+ // 跳过空内容(可能是其他进程写入时的 truncate 中间状态)
294
+ if (readmeContent.length === 0) return false
295
+
296
+ const lines = readmeContent.split('\n')
297
+
298
+ // 提取目录名中的标题(去掉编号)
299
+ const match = noteInfo.dirName.match(/^\d{4}\.\s+(.+)$/)
300
+ if (!match) {
301
+ logger.warn(`检测到错误的笔记目录名称:${noteInfo.dirName}`)
302
+ return false
303
+ }
304
+
305
+ const expectedTitle = match[1]
306
+ const expectedH1 = generateNoteTitle(
307
+ noteInfo.index,
308
+ expectedTitle,
309
+ REPO_NOTES_URL,
310
+ )
311
+
312
+ // 检查第一行是否为一级标题
313
+ const firstLine = lines[0].trim()
314
+
315
+ if (!firstLine.startsWith('# ')) {
316
+ // 缺少一级标题,在第一行插入
317
+ lines.unshift(expectedH1)
318
+ writeFileSync(noteInfo.readmePath, lines.join('\n'), 'utf-8')
319
+ logger.info(`Added title to: ${noteInfo.dirName}`)
320
+ return true
321
+ }
322
+
323
+ // 检查标题是否正确
324
+ if (firstLine !== expectedH1) {
325
+ // 标题不正确,替换第一行
326
+ lines[0] = expectedH1
327
+ writeFileSync(noteInfo.readmePath, lines.join('\n'), 'utf-8')
328
+ logger.info(`Fixed title for: ${noteInfo.dirName}`)
329
+ return true
330
+ }
331
+
332
+ return false
333
+ } catch (error) {
334
+ logger.error(`Failed to fix title for: ${noteInfo.dirName}`, error)
335
+ return false
336
+ }
337
+ }
338
+
339
+ /**
340
+ * 修正所有笔记的标题
341
+ * @param providedNotes - 可选的笔记列表,不传则内部扫描
342
+ * @returns 修正的笔记数量
343
+ */
344
+ async fixAllNoteTitles(providedNotes?: NoteInfo[]): Promise<number> {
345
+ const notes = providedNotes ?? this.getAllNotes()
346
+ // logger.debug('打印前 3 篇笔记信息:', notes.slice(0, 3))
347
+ let fixedCount = 0
348
+
349
+ for (const note of notes) {
350
+ const fixed = await this.fixNoteTitle(note)
351
+ if (fixed) {
352
+ fixedCount++
353
+ }
354
+ }
355
+
356
+ if (fixedCount > 0) {
357
+ logger.info(`Fixed ${fixedCount} note titles`)
358
+ }
359
+
360
+ return fixedCount
361
+ }
362
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * services/readme/index.ts
3
+ *
4
+ * README 服务入口
5
+ */
6
+
7
+ export { ReadmeService } from './service'