@tnotesjs/core 0.2.1 → 0.3.0

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 (86) hide show
  1. package/README.md +0 -1
  2. package/config/ConfigManager.ts +137 -0
  3. package/config/constants.ts +121 -0
  4. package/config/defaultConfig.ts +33 -4
  5. package/config/index.ts +31 -0
  6. package/config/templates.ts +49 -0
  7. package/core/GitManager.ts +513 -0
  8. package/core/NoteIndexCache.ts +200 -0
  9. package/core/NoteManager.ts +395 -0
  10. package/core/ProcessManager.ts +180 -0
  11. package/core/ReadmeGenerator.ts +215 -0
  12. package/core/TocGenerator.ts +212 -0
  13. package/core/index.ts +11 -0
  14. package/dist/{chunk-5Y5IYS6C.js → chunk-UJG3UU5X.js} +21 -5
  15. package/dist/{chunk-5QN44ES5.js → chunk-XCWFZLER.js} +48 -368
  16. package/dist/cli/index.js +26 -78
  17. package/dist/index.js +1 -1
  18. package/dist/vitepress/config/index.js +2 -2
  19. package/package.json +5 -2
  20. package/services/file-watcher/configChangeHandler.ts +64 -0
  21. package/services/file-watcher/eventScheduler.ts +179 -0
  22. package/services/file-watcher/folderChangeHandler.ts +325 -0
  23. package/services/file-watcher/fsWatcherAdapter.ts +128 -0
  24. package/services/file-watcher/globalUpdateCoordinator.ts +60 -0
  25. package/services/file-watcher/index.ts +7 -0
  26. package/services/file-watcher/internal.ts +79 -0
  27. package/services/file-watcher/readmeChangeHandler.ts +28 -0
  28. package/services/file-watcher/renameDetector.ts +120 -0
  29. package/services/file-watcher/service.ts +352 -0
  30. package/services/file-watcher/watchState.ts +194 -0
  31. package/services/git/index.ts +7 -0
  32. package/services/git/service.ts +114 -0
  33. package/services/index.ts +14 -0
  34. package/services/init-sub-repo/index.ts +2 -0
  35. package/services/init-sub-repo/initSubRepoLogic.test.ts +162 -0
  36. package/services/init-sub-repo/initSubRepoLogic.ts +303 -0
  37. package/services/init-sub-repo/service.ts +101 -0
  38. package/services/note/index.ts +7 -0
  39. package/services/note/service.ts +361 -0
  40. package/services/readme/index.ts +7 -0
  41. package/services/readme/service.ts +761 -0
  42. package/services/toc/index.ts +5 -0
  43. package/services/toc/moveTocInside.test.ts +73 -0
  44. package/services/toc/service.ts +759 -0
  45. package/services/vitepress/index.ts +7 -0
  46. package/services/vitepress/service.ts +339 -0
  47. package/types/config.ts +0 -3
  48. package/types/note.ts +0 -2
  49. package/utils/errorHandler.ts +174 -0
  50. package/utils/file.ts +17 -0
  51. package/utils/genHierarchicalSidebar.ts +69 -0
  52. package/utils/generateAnchor.ts +24 -0
  53. package/utils/getChangedIds.ts +35 -0
  54. package/utils/index.ts +71 -0
  55. package/utils/logger.ts +231 -0
  56. package/utils/markdown.ts +75 -0
  57. package/utils/migrateReadmeToToc.test.ts +111 -0
  58. package/utils/migrateReadmeToToc.ts +135 -0
  59. package/utils/parseArgs.ts +90 -0
  60. package/utils/parseReadmeCompletedNotes.test.ts +90 -0
  61. package/utils/parseReadmeCompletedNotes.ts +108 -0
  62. package/utils/portUtils.ts +113 -0
  63. package/utils/readmeHelpers.ts +190 -0
  64. package/utils/runCommand.ts +29 -0
  65. package/utils/tocHelpers.test.ts +278 -0
  66. package/utils/tocHelpers.ts +855 -0
  67. package/utils/tocNodeId.test.ts +60 -0
  68. package/utils/tocNodeId.ts +97 -0
  69. package/utils/validators.ts +102 -0
  70. package/vitepress/assets/icons/icon__cursor.svg +4 -0
  71. package/vitepress/assets/icons/index.ts +1 -0
  72. package/vitepress/components/Layout/AboutPanel.vue +0 -24
  73. package/vitepress/components/Layout/DocBeforeControls.vue +6 -14
  74. package/vitepress/components/Layout/DocFooter.vue +3 -3
  75. package/vitepress/components/Layout/Layout.vue +0 -30
  76. package/vitepress/components/Layout/composables/useVSCodeIntegration.ts +20 -11
  77. package/vitepress/components/Layout/homeReadme.data.ts +0 -48
  78. package/vitepress/components/MarkMap/MarkMap.vue +6 -13
  79. package/vitepress/components/Settings/Settings.vue +76 -73
  80. package/vitepress/components/SidebarCard/FolderTreeItems.vue +16 -6
  81. package/vitepress/components/SidebarCard/SidebarCard.vue +21 -27
  82. package/vitepress/components/composables/useLocalIde.ts +83 -0
  83. package/vitepress/components/constants.ts +5 -6
  84. package/vitepress/components/utils/vscodePaths.test.ts +22 -0
  85. package/vitepress/components/utils/vscodePaths.ts +24 -2
  86. package/vitepress/theme/styles/doc-layout.scss +4 -4
@@ -0,0 +1,513 @@
1
+ /**
2
+ * core/GitManager.ts
3
+ *
4
+ * Git 仓库管理器 - 提供统一的 Git 操作接口
5
+ */
6
+
7
+ import { Logger, runCommand, createError, handleError } from '../utils'
8
+
9
+ /**
10
+ * Git 文件状态接口
11
+ */
12
+ interface GitFileStatus {
13
+ path: string
14
+ status: 'staged' | 'unstaged' | 'untracked' | 'modified'
15
+ statusCode: string
16
+ }
17
+
18
+ /**
19
+ * Git 状态信息接口
20
+ */
21
+ interface GitStatus {
22
+ hasChanges: boolean
23
+ changedFiles: number
24
+ staged: number
25
+ unstaged: number
26
+ untracked: number
27
+ branch: string
28
+ ahead: number
29
+ behind: number
30
+ files: GitFileStatus[]
31
+ }
32
+
33
+ /**
34
+ * Git 远程信息接口
35
+ */
36
+ interface GitRemoteInfo {
37
+ url: string
38
+ type: 'https' | 'ssh' | 'unknown'
39
+ owner?: string
40
+ repo?: string
41
+ }
42
+
43
+ /**
44
+ * Git 管理器类
45
+ */
46
+ export class GitManager {
47
+ private logger: Logger
48
+ private dir: string
49
+
50
+ constructor(dir: string, logger?: Logger) {
51
+ this.dir = dir
52
+ this.logger = logger?.child('git') || new Logger({ prefix: 'git' })
53
+ }
54
+
55
+ /**
56
+ * 检查是否为有效的 Git 仓库
57
+ */
58
+ async isValidRepo(): Promise<boolean> {
59
+ try {
60
+ const result = await runCommand(
61
+ 'git rev-parse --is-inside-work-tree',
62
+ this.dir,
63
+ )
64
+ return result.trim() === 'true'
65
+ } catch {
66
+ return false
67
+ }
68
+ }
69
+
70
+ /**
71
+ * 确保是有效的 Git 仓库,否则抛出错误
72
+ */
73
+ async ensureValidRepo(): Promise<void> {
74
+ if (!(await this.isValidRepo())) {
75
+ throw createError.gitNotRepo(this.dir)
76
+ }
77
+ }
78
+
79
+ /**
80
+ * 获取 Git 状态
81
+ */
82
+ async getStatus(): Promise<GitStatus> {
83
+ await this.ensureValidRepo()
84
+
85
+ // 使用 -c core.quotePath=false 禁用路径转义,正确显示中文文件名
86
+ const statusOutput = await runCommand(
87
+ 'git -c core.quotePath=false status --porcelain',
88
+ this.dir,
89
+ )
90
+ const lines = statusOutput
91
+ .trim()
92
+ .split('\n')
93
+ .filter((line) => line)
94
+
95
+ // 解析文件状态
96
+ const files: GitFileStatus[] = lines.map((line) => {
97
+ const statusCode = line.substring(0, 2)
98
+ let path = line.substring(3)
99
+
100
+ // 移除 git 添加的引号(即使设置了 core.quotePath=false,某些情况下仍会加引号)
101
+ path = path.replace(/^"(.*)"$/, '$1')
102
+
103
+ let status: GitFileStatus['status'] = 'modified'
104
+ if (line.startsWith('??')) {
105
+ status = 'untracked'
106
+ } else if (/^[MADRC]/.test(statusCode)) {
107
+ status = 'staged'
108
+ } else if (/^.[MD]/.test(statusCode)) {
109
+ status = 'unstaged'
110
+ }
111
+
112
+ return { path, status, statusCode }
113
+ })
114
+
115
+ const staged = files.filter((f) => f.status === 'staged').length
116
+ const unstaged = files.filter((f) => f.status === 'unstaged').length
117
+ const untracked = files.filter((f) => f.status === 'untracked').length
118
+
119
+ // 获取当前分支
120
+ const branch = await runCommand('git branch --show-current', this.dir)
121
+
122
+ // 获取远程同步状态
123
+ let ahead = 0
124
+ let behind = 0
125
+ try {
126
+ const aheadBehind = await runCommand(
127
+ 'git rev-list --left-right --count @{upstream}...HEAD',
128
+ this.dir,
129
+ )
130
+ const [behindStr, aheadStr] = aheadBehind.trim().split('\t')
131
+ behind = parseInt(behindStr) || 0
132
+ ahead = parseInt(aheadStr) || 0
133
+ } catch {
134
+ // 可能没有上游分支
135
+ }
136
+
137
+ return {
138
+ hasChanges: lines.length > 0,
139
+ changedFiles: lines.length,
140
+ staged,
141
+ unstaged,
142
+ untracked,
143
+ branch: branch.trim(),
144
+ ahead,
145
+ behind,
146
+ files,
147
+ }
148
+ }
149
+
150
+ /**
151
+ * 获取远程仓库信息
152
+ */
153
+ async getRemoteInfo(): Promise<GitRemoteInfo | null> {
154
+ try {
155
+ await this.ensureValidRepo()
156
+ const remoteUrl = await runCommand(
157
+ 'git config --get remote.origin.url',
158
+ this.dir,
159
+ )
160
+ const url = remoteUrl.trim()
161
+
162
+ if (!url) return null
163
+
164
+ // 解析 HTTPS URL
165
+ const httpsMatch = url.match(
166
+ /https:\/\/(?:www\.)?github\.com\/([^/]+)\/(.+?)(?:\.git)?$/,
167
+ )
168
+ if (httpsMatch) {
169
+ return {
170
+ url,
171
+ type: 'https',
172
+ owner: httpsMatch[1],
173
+ repo: httpsMatch[2],
174
+ }
175
+ }
176
+
177
+ // 解析 SSH URL
178
+ const sshMatch = url.match(/git@github\.com:([^/]+)\/(.+?)(?:\.git)?$/)
179
+ if (sshMatch) {
180
+ return {
181
+ url,
182
+ type: 'ssh',
183
+ owner: sshMatch[1],
184
+ repo: sshMatch[2],
185
+ }
186
+ }
187
+
188
+ return { url, type: 'unknown' }
189
+ } catch {
190
+ return null
191
+ }
192
+ }
193
+
194
+ /**
195
+ * 检查是否有未提交的更改
196
+ */
197
+ async hasUncommittedChanges(): Promise<boolean> {
198
+ const status = await this.getStatus()
199
+ return status.hasChanges
200
+ }
201
+
202
+ /**
203
+ * Stash 当前更改
204
+ */
205
+ async stash(message?: string): Promise<boolean> {
206
+ try {
207
+ await this.ensureValidRepo()
208
+ const cmd = message ? `git stash push -m "${message}"` : 'git stash push'
209
+ await runCommand(cmd, this.dir)
210
+ this.logger.info('Stashed uncommitted changes')
211
+ return true
212
+ } catch (error) {
213
+ this.logger.warn('Failed to stash changes')
214
+ return false
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Pop stash
220
+ */
221
+ async stashPop(): Promise<boolean> {
222
+ try {
223
+ await this.ensureValidRepo()
224
+ await runCommand('git stash pop', this.dir)
225
+ this.logger.info('Restored stashed changes')
226
+ return true
227
+ } catch (error) {
228
+ this.logger.warn('Failed to restore stashed changes')
229
+ return false
230
+ }
231
+ }
232
+
233
+ /**
234
+ * 拉取远程更新
235
+ */
236
+ async pull(options?: {
237
+ rebase?: boolean
238
+ autostash?: boolean
239
+ }): Promise<void> {
240
+ await this.ensureValidRepo()
241
+
242
+ const { rebase = true, autostash = true } = options || {}
243
+
244
+ // 检查是否有未提交的更改
245
+ const hasChanges = await this.hasUncommittedChanges()
246
+ let didStash = false
247
+
248
+ if (hasChanges && !autostash) {
249
+ this.logger.warn('Repository has uncommitted changes')
250
+ didStash = await this.stash('Auto-stash before pull')
251
+ }
252
+
253
+ try {
254
+ // 获取远程更新前记录当前提交
255
+ const beforeCommit = await runCommand('git rev-parse HEAD', this.dir)
256
+
257
+ this.logger.info('正在拉取远程更新...')
258
+
259
+ const cmd = `git pull ${rebase ? '--rebase' : ''} ${
260
+ autostash ? '--autostash' : ''
261
+ }`.trim()
262
+ await runCommand(cmd, this.dir)
263
+
264
+ // 获取拉取后的提交
265
+ const afterCommit = await runCommand('git rev-parse HEAD', this.dir)
266
+
267
+ // 如果有更新,显示更新的文件列表
268
+ if (beforeCommit.trim() !== afterCommit.trim()) {
269
+ try {
270
+ const diffOutput = await runCommand(
271
+ `git diff --name-only ${beforeCommit.trim()}..${afterCommit.trim()}`,
272
+ this.dir,
273
+ )
274
+ const changedFiles = diffOutput
275
+ .trim()
276
+ .split('\n')
277
+ .filter((f) => f)
278
+
279
+ if (changedFiles.length > 0) {
280
+ console.log(` 更新了 ${changedFiles.length} 个文件:`)
281
+ changedFiles.forEach((file, index) => {
282
+ console.log(` ${index + 1}. ${file}`)
283
+ })
284
+ }
285
+
286
+ this.logger.success(`拉取成功: ${changedFiles.length} 个文件已更新`)
287
+ } catch {
288
+ this.logger.success('拉取成功')
289
+ }
290
+ } else {
291
+ this.logger.info('已是最新,没有需要拉取的更新')
292
+ }
293
+ } catch (error) {
294
+ this.logger.error('拉取失败')
295
+ handleError(error)
296
+ throw error
297
+ } finally {
298
+ // 如果之前手动 stash 了,尝试 pop
299
+ if (didStash) {
300
+ await this.stashPop()
301
+ }
302
+ }
303
+ }
304
+
305
+ /**
306
+ * 提交更改
307
+ */
308
+ async commit(message: string): Promise<void> {
309
+ await this.ensureValidRepo()
310
+
311
+ try {
312
+ await runCommand(`git commit -m "${message}"`, this.dir)
313
+ this.logger.success(`Committed: ${message}`)
314
+ } catch (error) {
315
+ handleError(error)
316
+ throw error
317
+ }
318
+ }
319
+
320
+ /**
321
+ * 添加文件到暂存区
322
+ */
323
+ async add(files: string | string[] = '.'): Promise<void> {
324
+ await this.ensureValidRepo()
325
+
326
+ const fileList = Array.isArray(files) ? files.join(' ') : files
327
+ try {
328
+ await runCommand(`git add ${fileList}`, this.dir)
329
+ this.logger.info(`Staged changes: ${fileList}`)
330
+ } catch (error) {
331
+ handleError(error)
332
+ throw error
333
+ }
334
+ }
335
+
336
+ /**
337
+ * 推送到远程仓库
338
+ */
339
+ async push(options?: {
340
+ force?: boolean
341
+ setUpstream?: boolean
342
+ }): Promise<void> {
343
+ await this.ensureValidRepo()
344
+
345
+ const { force = false, setUpstream = false } = options || {}
346
+
347
+ try {
348
+ const status = await this.getStatus()
349
+ this.logger.progress(`正在推送到远程 (${status.branch})...`)
350
+
351
+ let cmd = 'git push'
352
+ if (force) cmd += ' --force'
353
+ if (setUpstream) cmd += ` --set-upstream origin ${status.branch}`
354
+
355
+ await runCommand(cmd, this.dir)
356
+
357
+ const remoteInfo = await this.getRemoteInfo()
358
+ if (remoteInfo) {
359
+ this.logger.success(`推送成功 → ${remoteInfo.owner}/${remoteInfo.repo}`)
360
+ } else {
361
+ this.logger.success('推送成功')
362
+ }
363
+ } catch (error) {
364
+ this.logger.error('推送失败')
365
+ handleError(error)
366
+ throw error
367
+ }
368
+ }
369
+
370
+ /**
371
+ * 完整的推送流程:检查 -> 添加 -> 提交 -> 推送
372
+ */
373
+ async pushWithCommit(
374
+ commitMessage?: string,
375
+ options?: { force?: boolean; showFiles?: boolean },
376
+ ): Promise<void> {
377
+ await this.ensureValidRepo()
378
+
379
+ const status = await this.getStatus()
380
+
381
+ // 检查是否有更改
382
+ if (!status.hasChanges) {
383
+ this.logger.info('没有需要提交的更改')
384
+ return
385
+ }
386
+
387
+ try {
388
+ // 显示开始信息和文件列表
389
+ this.logger.info(`正在推送 ${status.changedFiles} 个文件...`)
390
+
391
+ // 显示文件列表
392
+ status.files.forEach((file, index) => {
393
+ console.log(` ${index + 1}. ${file.path}`)
394
+ })
395
+
396
+ // 添加所有更改(静默执行)
397
+ await runCommand('git add .', this.dir)
398
+
399
+ // 生成提交信息
400
+ const message =
401
+ commitMessage || `update: ${status.changedFiles} files modified`
402
+
403
+ // 提交(静默执行)
404
+ await runCommand(`git commit -m "${message}"`, this.dir)
405
+
406
+ // 推送(静默执行)
407
+ let cmd = 'git push'
408
+ if (options?.force) cmd += ' --force'
409
+
410
+ await runCommand(cmd, this.dir)
411
+
412
+ // 只在成功时显示结果
413
+ const remoteInfo = await this.getRemoteInfo()
414
+ if (remoteInfo) {
415
+ this.logger.success(
416
+ `推送成功: ${status.changedFiles} 个文件 → https://github.com/${remoteInfo.owner}/${remoteInfo.repo}`,
417
+ )
418
+ } else {
419
+ this.logger.success(`推送成功: ${status.changedFiles} 个文件`)
420
+ }
421
+ } catch (error) {
422
+ // 失败时显示完整错误信息
423
+ this.logger.error(`推送失败`)
424
+ handleError(error)
425
+ throw error
426
+ }
427
+ }
428
+
429
+ /**
430
+ * 完整的同步流程:拉取 -> 推送
431
+ */
432
+ async sync(options?: {
433
+ commitMessage?: string
434
+ rebase?: boolean
435
+ }): Promise<void> {
436
+ const { commitMessage, rebase = true } = options || {}
437
+
438
+ try {
439
+ // 先拉取
440
+ await this.pull({ rebase, autostash: true })
441
+
442
+ // 再推送
443
+ await this.pushWithCommit(commitMessage)
444
+ } catch (error) {
445
+ this.logger.error('Sync failed')
446
+ handleError(error)
447
+ throw error
448
+ }
449
+ }
450
+
451
+ /**
452
+ * 显示状态摘要
453
+ */
454
+ async showStatus(options?: { showFiles?: boolean }): Promise<void> {
455
+ const { showFiles = true } = options || {}
456
+ const status = await this.getStatus()
457
+ const remoteInfo = await this.getRemoteInfo()
458
+
459
+ console.log('\n📊 Git 状态:')
460
+ console.log(` 分支: ${status.branch}`)
461
+ if (remoteInfo) {
462
+ console.log(
463
+ ` 远程: ${remoteInfo.owner}/${remoteInfo.repo} (${remoteInfo.type})`,
464
+ )
465
+ }
466
+
467
+ if (status.hasChanges) {
468
+ console.log(
469
+ ` 变更: ${status.changedFiles} 个文件 (已暂存 ${status.staged}, 未暂存 ${status.unstaged}, 未跟踪 ${status.untracked})`,
470
+ )
471
+
472
+ // 显示文件列表
473
+ if (showFiles && status.files.length > 0) {
474
+ console.log(' 变更文件列表:')
475
+
476
+ // 按状态分组显示
477
+ const stagedFiles = status.files.filter((f) => f.status === 'staged')
478
+ const unstagedFiles = status.files.filter(
479
+ (f) => f.status === 'unstaged',
480
+ )
481
+ const untrackedFiles = status.files.filter(
482
+ (f) => f.status === 'untracked',
483
+ )
484
+
485
+ if (stagedFiles.length > 0) {
486
+ console.log(' 已暂存:')
487
+ stagedFiles.forEach((f) => console.log(` ✓ ${f.path}`))
488
+ }
489
+
490
+ if (unstagedFiles.length > 0) {
491
+ console.log(' 未暂存:')
492
+ unstagedFiles.forEach((f) => console.log(` • ${f.path}`))
493
+ }
494
+
495
+ if (untrackedFiles.length > 0) {
496
+ console.log(' 未跟踪:')
497
+ untrackedFiles.forEach((f) => console.log(` ? ${f.path}`))
498
+ }
499
+ }
500
+ } else {
501
+ console.log(' 状态: 工作区干净,没有变更')
502
+ }
503
+
504
+ if (status.ahead > 0 || status.behind > 0) {
505
+ const syncInfo = []
506
+ if (status.ahead > 0) syncInfo.push(`领先 ${status.ahead} 个提交`)
507
+ if (status.behind > 0) syncInfo.push(`落后 ${status.behind} 个提交`)
508
+ console.log(` 同步: ${syncInfo.join(', ')}`)
509
+ }
510
+
511
+ console.log()
512
+ }
513
+ }
@@ -0,0 +1,200 @@
1
+ /**
2
+ * core/NoteIndexCache.ts
3
+ *
4
+ * 笔记索引缓存 - 维护笔记的内存索引,避免重复扫描文件系统
5
+ */
6
+
7
+ import { join } from 'path'
8
+
9
+ import { NOTES_PATH } from '../config/constants'
10
+ import { logger } from '../utils'
11
+
12
+ import type { NoteInfo, NoteConfig } from '../types'
13
+
14
+ /**
15
+ * 索引项结构
16
+ */
17
+ interface NoteIndexItem {
18
+ /** 笔记索引(文件夹名前 4 位数字,如 "0001") */
19
+ noteIndex: string
20
+ /** 完整文件夹名称(如 "0001. TNotes 简介") */
21
+ folderName: string
22
+ /** 笔记配置(与 .tnotes.json 结构一致) */
23
+ noteConfig: NoteConfig
24
+ }
25
+
26
+ /**
27
+ * 笔记索引缓存类
28
+ * 提供快速的笔记查询和更新能力
29
+ */
30
+ export class NoteIndexCache {
31
+ private static instance: NoteIndexCache | null = null
32
+
33
+ /** noteIndex -> NoteIndexItem 的映射 */
34
+ private byNoteIndex: Map<string, NoteIndexItem> = new Map()
35
+
36
+ /** configId (UUID) -> noteIndex 的映射,用于快速反向查询 */
37
+ private byConfigId: Map<string, string> = new Map()
38
+
39
+ /** 是否已完成初始化 */
40
+ private _initialized = false
41
+
42
+ private constructor() {}
43
+
44
+ /**
45
+ * 获取单例实例
46
+ */
47
+ static getInstance(): NoteIndexCache {
48
+ if (!NoteIndexCache.instance) {
49
+ NoteIndexCache.instance = new NoteIndexCache()
50
+ }
51
+ return NoteIndexCache.instance
52
+ }
53
+
54
+ /**
55
+ * 初始化索引缓存
56
+ * @param notes - 扫描得到的笔记列表(已由 NoteManager.scanNotes 完成重复检测)
57
+ */
58
+ initialize(notes: NoteInfo[]): void {
59
+ this.byNoteIndex.clear()
60
+ this.byConfigId.clear()
61
+
62
+ // 构建索引
63
+ for (const note of notes) {
64
+ const item: NoteIndexItem = {
65
+ noteIndex: note.index,
66
+ folderName: note.dirName,
67
+ noteConfig: note.config,
68
+ }
69
+
70
+ this.byNoteIndex.set(note.index, item)
71
+ this.byConfigId.set(note.config.id, note.index)
72
+ }
73
+
74
+ this._initialized = true
75
+ }
76
+
77
+ /**
78
+ * 是否已完成初始化
79
+ */
80
+ isInitialized(): boolean {
81
+ return this._initialized
82
+ }
83
+
84
+ /**
85
+ * 从缓存构建 NoteInfo 列表(纯内存,零 I/O)
86
+ * @returns 笔记信息数组
87
+ */
88
+ toNoteInfoList(): NoteInfo[] {
89
+ const result: NoteInfo[] = []
90
+ for (const item of this.byNoteIndex.values()) {
91
+ const notePath = join(NOTES_PATH, item.folderName)
92
+ result.push({
93
+ index: item.noteIndex,
94
+ path: notePath,
95
+ dirName: item.folderName,
96
+ readmePath: join(notePath, 'README.md'),
97
+ configPath: join(notePath, '.tnotes.json'),
98
+ config: item.noteConfig,
99
+ })
100
+ }
101
+ return result
102
+ }
103
+
104
+ /**
105
+ * 根据 noteIndex 获取索引项
106
+ */
107
+ getByNoteIndex(noteIndex: string): NoteIndexItem | undefined {
108
+ return this.byNoteIndex.get(noteIndex)
109
+ }
110
+
111
+ /**
112
+ * 根据 configId (UUID) 获取索引项
113
+ */
114
+ getByConfigId(configId: string): NoteIndexItem | undefined {
115
+ const noteIndex = this.byConfigId.get(configId)
116
+ return noteIndex ? this.byNoteIndex.get(noteIndex) : undefined
117
+ }
118
+
119
+ /**
120
+ * 检查 noteIndex 是否存在
121
+ */
122
+ has(noteIndex: string): boolean {
123
+ return this.byNoteIndex.has(noteIndex)
124
+ }
125
+
126
+ /**
127
+ * 更新笔记配置
128
+ * @param noteIndex - 笔记索引
129
+ * @param configUpdates - 要更新的配置字段
130
+ */
131
+ updateConfig(noteIndex: string, configUpdates: Partial<NoteConfig>): void {
132
+ const item = this.byNoteIndex.get(noteIndex)
133
+ if (!item) {
134
+ logger.warn(`尝试更新不存在的笔记: ${noteIndex}`)
135
+ return
136
+ }
137
+
138
+ Object.assign(item.noteConfig, configUpdates)
139
+ // 剥离内存中残留的废弃时间戳字段,避免后续写回时恢复
140
+ const noteConfigRecord = item.noteConfig as unknown as Record<
141
+ string,
142
+ unknown
143
+ >
144
+ delete noteConfigRecord.created_at
145
+ delete noteConfigRecord.updated_at
146
+
147
+ logger.debug(`更新笔记配置: ${noteIndex}`, configUpdates)
148
+ }
149
+
150
+ /**
151
+ * 删除笔记
152
+ * @param noteIndex - 笔记索引
153
+ */
154
+ delete(noteIndex: string): void {
155
+ const item = this.byNoteIndex.get(noteIndex)
156
+ if (!item) {
157
+ logger.warn(`尝试删除不存在的笔记: ${noteIndex}`)
158
+ return
159
+ }
160
+
161
+ // 同时删除两个索引
162
+ this.byNoteIndex.delete(noteIndex)
163
+ this.byConfigId.delete(item.noteConfig.id)
164
+
165
+ logger.info(`删除笔记索引: ${noteIndex}`)
166
+ }
167
+
168
+ /**
169
+ * 添加新笔记
170
+ * @param note - 笔记信息
171
+ */
172
+ add(note: NoteInfo): void {
173
+ const item: NoteIndexItem = {
174
+ noteIndex: note.index,
175
+ folderName: note.dirName,
176
+ noteConfig: note.config,
177
+ }
178
+
179
+ this.byNoteIndex.set(note.index, item)
180
+ this.byConfigId.set(note.config.id, note.index)
181
+
182
+ logger.info(`添加笔记索引: ${note.index}`)
183
+ }
184
+
185
+ /**
186
+ * 更新笔记的文件夹名称(标题变更时)
187
+ * @param noteIndex - 笔记索引
188
+ * @param newFolderName - 新的文件夹名称
189
+ */
190
+ updateFolderName(noteIndex: string, newFolderName: string): void {
191
+ const item = this.byNoteIndex.get(noteIndex)
192
+ if (!item) {
193
+ logger.warn(`尝试更新不存在的笔记: ${noteIndex}`)
194
+ return
195
+ }
196
+
197
+ item.folderName = newFolderName
198
+ logger.debug(`更新笔记文件夹名称: ${noteIndex} -> ${newFolderName}`)
199
+ }
200
+ }