@tnotesjs/core 0.6.1 → 0.8.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 (92) hide show
  1. package/commands/BaseCommand.ts +58 -0
  2. package/commands/build/BuildCommand.ts +25 -0
  3. package/commands/build/PreviewCommand.ts +29 -0
  4. package/commands/build/index.ts +8 -0
  5. package/commands/dev/DevCommand.ts +75 -0
  6. package/commands/dev/index.ts +7 -0
  7. package/commands/git/PullCommand.ts +25 -0
  8. package/commands/git/PushCommand.ts +64 -0
  9. package/commands/git/index.ts +8 -0
  10. package/commands/index.ts +11 -0
  11. package/commands/init-sub-repo/InitSubRepoCommand.ts +206 -0
  12. package/commands/init-sub-repo/index.ts +1 -0
  13. package/commands/misc/HelpCommand.ts +104 -0
  14. package/commands/misc/index.ts +7 -0
  15. package/commands/models.ts +87 -0
  16. package/commands/note/CreateNoteCommand.ts +160 -0
  17. package/commands/note/RenameNoteCommand.ts +147 -0
  18. package/commands/note/UpdateNoteConfigCommand.ts +78 -0
  19. package/commands/note/index.ts +9 -0
  20. package/commands/registry.ts +47 -0
  21. package/commands/update/UpdateCommand.ts +219 -0
  22. package/commands/update/index.ts +7 -0
  23. package/commands/update-completed-count/UpdateCompletedCountCommand.ts +208 -0
  24. package/commands/update-completed-count/index.ts +5 -0
  25. package/config/templates.ts +0 -5
  26. package/dist/{chunk-5F5RJ2HV.cjs → chunk-7FEANCT6.cjs} +16 -7
  27. package/dist/chunk-AGVER5MX.cjs +1746 -0
  28. package/dist/{chunk-TJ4527KA.cjs → chunk-BL22KD4V.cjs} +324 -950
  29. package/dist/{chunk-56PKXCOX.js → chunk-GWG75A5M.js} +13 -4
  30. package/dist/{chunk-DUTS75WQ.js → chunk-XNO3PCEH.js} +374 -1000
  31. package/dist/chunk-ZEQS64PE.js +1746 -0
  32. package/dist/cli/index.cjs +76 -155
  33. package/dist/cli/index.js +10 -89
  34. package/dist/markdown/index.cjs +8 -12
  35. package/dist/markdown/index.d.cts +1 -1
  36. package/dist/markdown/index.d.ts +1 -1
  37. package/dist/markdown/index.js +7 -11
  38. package/dist/{types-CwBWwNVv.d.ts → types-C_d0fFeD.d.ts} +8 -1
  39. package/dist/{types-CpsEy8lA.d.cts → types-MRBGgJTX.d.cts} +8 -1
  40. package/dist/vitepress/config/index.cjs +402 -178
  41. package/dist/vitepress/config/index.js +392 -168
  42. package/dist/workspace/index.cjs +4 -1202
  43. package/dist/workspace/index.d.cts +2 -2
  44. package/dist/workspace/index.d.ts +2 -2
  45. package/dist/workspace/index.js +4 -1202
  46. package/markdown/components.ts +86 -0
  47. package/markdown/index.ts +17 -0
  48. package/markdown/noteFormatter.test.ts +44 -0
  49. package/markdown/noteFormatter.ts +237 -0
  50. package/package.json +7 -3
  51. package/services/file-watcher/folderChangeHandler.ts +19 -15
  52. package/services/file-watcher/globalUpdateCoordinator.ts +21 -27
  53. package/services/file-watcher/service.ts +1 -6
  54. package/services/index.ts +0 -1
  55. package/services/note/service.ts +4 -6
  56. package/services/readme/service.ts +11 -18
  57. package/services/reconcileToc.ts +20 -0
  58. package/utils/index.ts +0 -9
  59. package/{services/toc → utils}/moveTocInside.test.ts +1 -1
  60. package/vitepress/components/BilibiliOutsidePlayer/BilibiliOutsidePlayer.vue +9 -18
  61. package/vitepress/components/EnWordList/EnWordList.vue +16 -662
  62. package/vitepress/components/Footprints/Footprints.vue +15 -537
  63. package/vitepress/components/Mermaid/Mermaid.vue +13 -588
  64. package/vitepress/components/MindmapPreview/MindmapPreview.vue +12 -434
  65. package/vitepress/components/MindmapPreview/markdown.ts +1 -1
  66. package/vitepress/components/NotesTable/NotesTable.vue +11 -130
  67. package/vitepress/components/constants.ts +2 -2
  68. package/vitepress/components/sidebar.data.ts +8 -5
  69. package/vitepress/configs/markdown.config.ts +170 -26
  70. package/vitepress/plugins/sidebarStructurePlugin.ts +242 -145
  71. package/vitepress/theme/index.ts +9 -13
  72. package/vitepress/theme/styles/base.scss +15 -0
  73. package/workspace/atomic.ts +113 -0
  74. package/workspace/errors.ts +27 -0
  75. package/workspace/index.ts +40 -0
  76. package/workspace/mutationQueue.ts +28 -0
  77. package/workspace/paths.ts +64 -0
  78. package/workspace/reconcile.test.ts +300 -0
  79. package/workspace/reconcile.ts +95 -0
  80. package/workspace/scanner.ts +292 -0
  81. package/workspace/types.ts +224 -0
  82. package/workspace/workspace.test.ts +333 -0
  83. package/workspace/workspace.ts +1020 -0
  84. package/dist/chunk-3R7QSABB.cjs +0 -502
  85. package/dist/chunk-CZXRQPFJ.js +0 -11
  86. package/dist/chunk-HXED7KVT.cjs +0 -11
  87. package/dist/chunk-U6IBLREL.js +0 -502
  88. package/services/toc/index.ts +0 -5
  89. package/services/toc/service.ts +0 -759
  90. package/utils/migrateReadmeToToc.test.ts +0 -111
  91. package/utils/migrateReadmeToToc.ts +0 -135
  92. package/vitepress/components/EnWordList/RightClickMenu.vue +0 -93
@@ -0,0 +1,86 @@
1
+ export type TNotesComponentKind =
2
+ | 'block-component'
3
+ | 'inline-component'
4
+ | 'container'
5
+ | 'fenced-language'
6
+
7
+ export interface TNotesComponentDescriptor {
8
+ name: string
9
+ aliases?: string[]
10
+ kind: TNotesComponentKind
11
+ editable: 'visual' | 'source-only' | 'placeholder'
12
+ }
13
+
14
+ /**
15
+ * Runtime-neutral registry shared by Desk and other TNotes clients. It does not
16
+ * import Vue or VitePress and is therefore safe in Node/Electron workers.
17
+ */
18
+ export const TNOTES_COMPONENTS: readonly TNotesComponentDescriptor[] = [
19
+ {
20
+ name: 'BilibiliVideo',
21
+ kind: 'block-component',
22
+ editable: 'visual',
23
+ },
24
+ {
25
+ name: 'WordList',
26
+ kind: 'block-component',
27
+ editable: 'visual',
28
+ },
29
+ {
30
+ name: 'Footprints',
31
+ kind: 'block-component',
32
+ editable: 'visual',
33
+ },
34
+ {
35
+ name: 'NotesTable',
36
+ kind: 'block-component',
37
+ editable: 'visual',
38
+ },
39
+ {
40
+ name: 'Tooltip',
41
+ kind: 'inline-component',
42
+ editable: 'visual',
43
+ },
44
+ {
45
+ name: 'Mindmap',
46
+ aliases: ['MindmapPreview'],
47
+ kind: 'block-component',
48
+ editable: 'visual',
49
+ },
50
+ {
51
+ name: 'Mermaid',
52
+ kind: 'block-component',
53
+ editable: 'visual',
54
+ },
55
+ {
56
+ name: 'Discussions',
57
+ kind: 'block-component',
58
+ editable: 'placeholder',
59
+ },
60
+ {
61
+ name: 'SidebarCard',
62
+ kind: 'block-component',
63
+ editable: 'placeholder',
64
+ },
65
+ { name: 'swiper', kind: 'container', editable: 'visual' },
66
+ { name: 'code-group', kind: 'container', editable: 'visual' },
67
+ { name: 'footprints', kind: 'container', editable: 'visual' },
68
+ { name: 'details', kind: 'container', editable: 'visual' },
69
+ { name: 'info', kind: 'container', editable: 'visual' },
70
+ { name: 'tip', kind: 'container', editable: 'visual' },
71
+ { name: 'warning', kind: 'container', editable: 'visual' },
72
+ { name: 'danger', kind: 'container', editable: 'visual' },
73
+ { name: 'mermaid', kind: 'fenced-language', editable: 'visual' },
74
+ { name: 'mindmap', kind: 'fenced-language', editable: 'visual' },
75
+ ] as const
76
+
77
+ export function findTNotesComponent(
78
+ name: string,
79
+ ): TNotesComponentDescriptor | undefined {
80
+ const normalized = name.toLowerCase()
81
+ return TNOTES_COMPONENTS.find(
82
+ (component) =>
83
+ component.name.toLowerCase() === normalized ||
84
+ component.aliases?.some((alias) => alias.toLowerCase() === normalized),
85
+ )
86
+ }
@@ -0,0 +1,17 @@
1
+ export {
2
+ NOTE_TOC_END_TAG,
3
+ NOTE_TOC_START_TAG,
4
+ formatTNotesNote,
5
+ } from './noteFormatter'
6
+ export type {
7
+ FormatTNotesNoteInput,
8
+ FormatTNotesNoteResult,
9
+ } from './noteFormatter'
10
+ export {
11
+ TNOTES_COMPONENTS,
12
+ findTNotesComponent,
13
+ } from './components'
14
+ export type {
15
+ TNotesComponentDescriptor,
16
+ TNotesComponentKind,
17
+ } from './components'
@@ -0,0 +1,44 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import {
4
+ findTNotesComponent,
5
+ formatTNotesNote,
6
+ TNOTES_COMPONENTS,
7
+ } from './index'
8
+
9
+ describe('TNotes Markdown contract', () => {
10
+ it('exposes runtime-neutral component descriptors', () => {
11
+ expect(findTNotesComponent('NotesTable')?.name).toBe('NotesTable')
12
+ expect(findTNotesComponent('WordList')?.name).toBe('WordList')
13
+ expect(findTNotesComponent('BilibiliVideo')?.name).toBe('BilibiliVideo')
14
+ expect(findTNotesComponent('N')).toBeUndefined()
15
+ expect(findTNotesComponent('SWIPER')?.kind).toBe('container')
16
+ expect(findTNotesComponent('Mermaid')?.editable).toBe('visual')
17
+ expect(TNOTES_COMPONENTS.some((item) => item.name === 'markmap')).toBe(false)
18
+ expect(TNOTES_COMPONENTS.some((item) => item.name === 'mindmap')).toBe(true)
19
+ })
20
+
21
+ it('restores missing generated markers without interpreting fenced headings', async () => {
22
+ const result = await formatTNotesNote({
23
+ content: '## Heading\n\n~~~md\n### source\n~~~\n',
24
+ noteIndex: '0042',
25
+ title: 'Answer',
26
+ repoOwner: 'tnotesjs',
27
+ repoName: 'TNotes.fixture',
28
+ noteConfig: {
29
+ id: 'answer',
30
+ bilibili: [],
31
+ tnotes: [],
32
+ yuque: [],
33
+ done: false,
34
+ enableDiscussions: false,
35
+ },
36
+ prettier: false,
37
+ })
38
+
39
+ expect(result.content).toContain('<!-- region:toc -->')
40
+ expect(result.content).toContain('<!-- endregion:toc -->')
41
+ expect(result.generatedToc).toEqual(['- [1. Heading](#1-heading)'])
42
+ expect(result.content).toContain('~~~md\n### source\n~~~')
43
+ })
44
+ })
@@ -0,0 +1,237 @@
1
+ import { generateAnchor } from '../utils/generateAnchor'
2
+
3
+ import type { WorkspaceNoteConfig } from '../workspace/types'
4
+
5
+ export const NOTE_TOC_START_TAG = '<!-- region:toc -->'
6
+ export const NOTE_TOC_END_TAG = '<!-- endregion:toc -->'
7
+
8
+ /**
9
+ * Lazy-loads prettier so consumers that never format (e.g. the read-only Nav
10
+ * VS Code extension) don't pay the top-level import cost / don't break their
11
+ * bundle (prettier's ESM entry crashes inside a CJS bundle at load time).
12
+ */
13
+ async function prettierFormat(
14
+ content: string,
15
+ options: { parser: string; proseWrap: string; endOfLine: string },
16
+ ): Promise<string> {
17
+ const module = await import('prettier')
18
+ const prettier = (module as { default?: typeof import('prettier') }).default ?? module
19
+ return prettier.format(content, options as Parameters<typeof prettier.format>[1])
20
+ }
21
+
22
+ export interface FormatTNotesNoteInput {
23
+ content: string
24
+ noteIndex: string
25
+ title: string
26
+ repoOwner: string
27
+ repoName: string
28
+ noteConfig: WorkspaceNoteConfig
29
+ prettier?: boolean
30
+ }
31
+
32
+ export interface FormatTNotesNoteResult {
33
+ content: string
34
+ generatedTitle: string
35
+ generatedToc: string[]
36
+ }
37
+
38
+ function generatedNoteTitle(input: FormatTNotesNoteInput): string {
39
+ const dirName = `${input.noteIndex}. ${input.title}`
40
+ const encodedDirName = encodeURIComponent(dirName)
41
+ const repositoryUrl = `https://github.com/${input.repoOwner}/${input.repoName}/tree/main/notes`
42
+ return `# [${dirName}](${repositoryUrl}/${encodedDirName})`
43
+ }
44
+
45
+ function ensureGeneratedTitle(lines: string[], generatedTitle: string): void {
46
+ if (lines.length === 0) {
47
+ lines.push(generatedTitle)
48
+ return
49
+ }
50
+
51
+ lines[0] = lines[0].replace(/^\uFEFF/, '')
52
+ if (lines[0].trimStart().startsWith('# ')) {
53
+ lines[0] = generatedTitle
54
+ return
55
+ }
56
+
57
+ lines.unshift(generatedTitle, '')
58
+ }
59
+
60
+ function ensureTocRegion(lines: string[]): { start: number; end: number } {
61
+ let start = lines.findIndex((line) => line.trim() === NOTE_TOC_START_TAG)
62
+ let end = lines.findIndex(
63
+ (line, index) => index > start && line.trim() === NOTE_TOC_END_TAG,
64
+ )
65
+
66
+ if (start >= 0 && end > start) return { start, end }
67
+
68
+ if (start >= 0) lines.splice(start, 1)
69
+ if (end >= 0) lines.splice(end > start ? end - 1 : end, 1)
70
+
71
+ let insertAt = 1
72
+ while (insertAt < lines.length && lines[insertAt].trim() === '') insertAt++
73
+ lines.splice(
74
+ insertAt,
75
+ 0,
76
+ '',
77
+ NOTE_TOC_START_TAG,
78
+ '',
79
+ NOTE_TOC_END_TAG,
80
+ '',
81
+ )
82
+ start = insertAt + 1
83
+ end = insertAt + 3
84
+ return { start, end }
85
+ }
86
+
87
+ interface Heading {
88
+ level: number
89
+ text: string
90
+ }
91
+
92
+ function normalizeHeadings(lines: string[]): Heading[] {
93
+ const headings: Heading[] = []
94
+ const counters = { h2: 0, h3: 0 }
95
+ let fence: { marker: '`' | '~'; length: number } | null = null
96
+ let inHtmlComment = false
97
+
98
+ for (let index = 0; index < lines.length; index++) {
99
+ const line = lines[index]
100
+ const fenceMatch = line.match(/^\s{0,3}(`{3,}|~{3,})/)
101
+ if (fenceMatch) {
102
+ const marker = fenceMatch[1][0] as '`' | '~'
103
+ if (!fence) {
104
+ fence = { marker, length: fenceMatch[1].length }
105
+ } else if (fence.marker === marker && fenceMatch[1].length >= fence.length) {
106
+ fence = null
107
+ }
108
+ continue
109
+ }
110
+ if (fence) continue
111
+
112
+ if (inHtmlComment) {
113
+ if (line.includes('-->')) inHtmlComment = false
114
+ continue
115
+ }
116
+ const commentStart = line.indexOf('<!--')
117
+ if (commentStart >= 0) {
118
+ const commentEnd = line.indexOf('-->', commentStart + 4)
119
+ if (commentEnd < 0) inHtmlComment = true
120
+ if (line.trimStart().startsWith('<!--')) continue
121
+ }
122
+
123
+ const match = line.match(/^(#{2,6})\s+(.+?)\s*#*\s*$/)
124
+ if (!match) continue
125
+
126
+ const level = match[1].length
127
+ const plainText = match[2]
128
+ .replace(/^\d+(?:\.\d+)+\.?\s+|^\d+\.\s+/, '')
129
+ .trim()
130
+ let text = plainText
131
+
132
+ if (level === 2) {
133
+ counters.h2 += 1
134
+ counters.h3 = 0
135
+ text = `${counters.h2}. ${plainText}`
136
+ } else if (level === 3) {
137
+ counters.h3 += 1
138
+ text = `${counters.h2}.${counters.h3}. ${plainText}`
139
+ }
140
+
141
+ lines[index] = `${'#'.repeat(level)} ${text}`
142
+ headings.push({ level, text })
143
+ }
144
+
145
+ return headings
146
+ }
147
+
148
+ function buildHeadingToc(headings: Heading[]): string[] {
149
+ return headings.map((heading) => {
150
+ const indent = ' '.repeat(Math.max(0, heading.level - 2) * 2)
151
+ return `${indent}- [${heading.text}](#${generateAnchor(heading.text)})`
152
+ })
153
+ }
154
+
155
+ function stringArray(value: unknown): string[] {
156
+ return Array.isArray(value)
157
+ ? value.filter((item): item is string => typeof item === 'string')
158
+ : []
159
+ }
160
+
161
+ function buildResourceToc(input: FormatTNotesNoteInput): string[] {
162
+ const bilibili = stringArray(input.noteConfig.bilibili)
163
+ const relatedTNotes = stringArray(input.noteConfig.tnotes)
164
+ const yuque = stringArray(input.noteConfig.yuque)
165
+ if (bilibili.length + relatedTNotes.length + yuque.length === 0) return []
166
+
167
+ const lines: string[] = ['::: details 📚 相关资源', '']
168
+ if (bilibili.length > 0) {
169
+ lines.push(
170
+ '- [📺 bilibili(笔记视频资源)](https://space.bilibili.com/407241004)',
171
+ ...bilibili.map(
172
+ (bvid, index) =>
173
+ ` - [bilibili.${input.repoName}.${input.noteIndex}.${index + 1}](https://www.bilibili.com/video/${bvid})`,
174
+ ),
175
+ )
176
+ }
177
+ if (relatedTNotes.length > 0) {
178
+ lines.push(
179
+ '- [📒 TNotes(相关知识库)](https://tnotesjs.github.io/TNotes/)',
180
+ ...relatedTNotes.map(
181
+ (repoName) =>
182
+ ` - [TNotes.${repoName}](https://tnotesjs.github.io/TNotes.${repoName}/)`,
183
+ ),
184
+ )
185
+ }
186
+ if (yuque.length > 0) {
187
+ const base = 'https://www.yuque.com/tdahuyou/tnotes.yuque/'
188
+ lines.push(
189
+ `- [📂 TNotes.yuque(笔记附件资源)](${base})`,
190
+ ...yuque.map(
191
+ (slug) =>
192
+ ` - [TNotes.yuque.${input.repoName.replace('TNotes.', '')}.${input.noteIndex}](${base}${slug})`,
193
+ ),
194
+ )
195
+ }
196
+ lines.push('', ':::', '')
197
+ return lines
198
+ }
199
+
200
+ export async function formatTNotesNote(
201
+ input: FormatTNotesNoteInput,
202
+ ): Promise<FormatTNotesNoteResult> {
203
+ let content = input.content.replace(/\r\n?/g, '\n')
204
+ if (input.prettier !== false) {
205
+ content = await prettierFormat(content, {
206
+ parser: 'markdown',
207
+ proseWrap: 'never',
208
+ endOfLine: 'lf',
209
+ })
210
+ }
211
+
212
+ const lines = content.replace(/\n$/, '').split('\n')
213
+ const title = generatedNoteTitle(input)
214
+ ensureGeneratedTitle(lines, title)
215
+ let region = ensureTocRegion(lines)
216
+ const headings = normalizeHeadings(lines)
217
+
218
+ // Heading normalization does not add/remove lines, but resolving the region
219
+ // again makes the invariant explicit and protects future format extensions.
220
+ region = ensureTocRegion(lines)
221
+ const toc = buildHeadingToc(headings)
222
+ const resources = buildResourceToc(input)
223
+ lines.splice(
224
+ region.start + 1,
225
+ region.end - region.start - 1,
226
+ '',
227
+ ...resources,
228
+ ...toc,
229
+ '',
230
+ )
231
+
232
+ return {
233
+ content: `${lines.join('\n').replace(/\n{3,}$/g, '\n\n')}\n`,
234
+ generatedTitle: title,
235
+ generatedToc: toc,
236
+ }
237
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tnotesjs/core",
3
- "version": "0.6.1",
3
+ "version": "0.8.0",
4
4
  "description": "TNotes 知识库核心框架 —— 基于 VitePress 的笔记管理系统",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.17.1",
@@ -34,14 +34,17 @@
34
34
  "./vitepress/*": "./vitepress/*"
35
35
  },
36
36
  "files": [
37
+ "commands/",
37
38
  "config/",
38
39
  "core/",
40
+ "markdown/",
39
41
  "services/",
40
42
  "utils/",
41
43
  "dist/",
42
44
  "templates/",
43
45
  "vitepress/",
44
- "types/"
46
+ "types/",
47
+ "workspace/"
45
48
  ],
46
49
  "scripts": {
47
50
  "build": "tsup",
@@ -61,7 +64,8 @@
61
64
  "vue": "^3.5.0"
62
65
  },
63
66
  "dependencies": {
64
- "@tnotesjs/mindmap-core": "^0.2.1",
67
+ "@tnotesjs/mindmap-core": "^0.2.2",
68
+ "@tnotesjs/ui": "^0.1.1",
65
69
  "echarts": "^6.0.0",
66
70
  "github-slugger": "^2.0.0",
67
71
  "markdown-it-container": "^4.0.0",
@@ -5,11 +5,12 @@
5
5
  */
6
6
 
7
7
  import { existsSync, promises as fsPromises } from 'fs'
8
- import { join } from 'path'
8
+ import { dirname, join } from 'path'
9
9
 
10
10
  import { safeExecute } from './internal'
11
11
  import { REPO_NOTES_URL } from '../../config/constants'
12
12
  import { generateNoteTitle } from '../../config/templates'
13
+ import { reconcileTocFromFiles } from '../reconcileToc'
13
14
 
14
15
  import type { EventScheduler } from './eventScheduler'
15
16
  import type { WatchState } from './watchState'
@@ -17,7 +18,6 @@ import type { NoteIndexCache } from '../../core/NoteIndexCache'
17
18
  import type { Logger } from '../../utils'
18
19
  import type { NoteService } from '../note/service'
19
20
  import type { ReadmeService } from '../readme/service'
20
- import type { TocService } from '../toc/service'
21
21
 
22
22
  const RENAME_REVERT_DELAY_MS = 2000
23
23
 
@@ -41,8 +41,6 @@ interface FolderChangeHandlerConfig {
41
41
  noteService: NoteService
42
42
  /** README 服务实例 */
43
43
  readmeService: ReadmeService
44
- /** TOC 服务实例 */
45
- tocService: TocService
46
44
  /** 笔记索引缓存实例 */
47
45
  noteIndexCache: NoteIndexCache
48
46
  /** 日志记录器 */
@@ -89,7 +87,7 @@ export class FolderChangeHandler {
89
87
  }
90
88
 
91
89
  async handleFolderDeletion(deletedFolderName: string): Promise<void> {
92
- const { scheduler, watchState, noteIndexCache, tocService, logger } =
90
+ const { scheduler, watchState, noteIndexCache, logger, notesDir } =
93
91
  this.config
94
92
 
95
93
  if (scheduler.getUpdating()) return
@@ -106,12 +104,11 @@ export class FolderChangeHandler {
106
104
  watchState.clearNoteCaches(deletedFolderName)
107
105
  noteIndexCache.delete(noteIndex)
108
106
 
109
- // 更新根 README.md、sidebar.json
107
+ // files→TOC 对齐(Workspace):TOC 行随目录消失而移除
110
108
  await safeExecute(
111
109
  `删除笔记 ${noteIndex}`,
112
110
  async () => {
113
- await tocService.deleteNoteFromToc(noteIndex)
114
- await tocService.regenerateSidebar()
111
+ await reconcileTocFromFiles(dirname(notesDir))
115
112
  this.config.onNoteStructureChanged?.(`watcher:delete:${noteIndex}`)
116
113
  },
117
114
  logger,
@@ -202,7 +199,7 @@ export class FolderChangeHandler {
202
199
  noteIndex: string,
203
200
  newName: string,
204
201
  ): Promise<void> {
205
- const { noteIndexCache, tocService, logger, notesDir } = this.config
202
+ const { noteIndexCache, logger, notesDir } = this.config
206
203
 
207
204
  logger.info(`笔记索引未变 (${noteIndex}),只更新标题`)
208
205
  noteIndexCache.updateFolderName(noteIndex, newName)
@@ -214,8 +211,12 @@ export class FolderChangeHandler {
214
211
  logger,
215
212
  )
216
213
 
217
- await tocService.renameNoteInToc(noteIndex, newName)
218
- await tocService.regenerateSidebar()
214
+ // files→TOC 对齐(Workspace):标题以目录名为准,行位置/完成标记保留
215
+ await safeExecute(
216
+ `标题重命名同步目录 ${noteIndex}`,
217
+ () => reconcileTocFromFiles(dirname(notesDir)),
218
+ logger,
219
+ )
219
220
  this.config.onNoteStructureChanged?.(`watcher:rename-title:${noteIndex}`)
220
221
  logger.success(`标题更新完成`)
221
222
  }
@@ -266,11 +267,16 @@ export class FolderChangeHandler {
266
267
  oldNoteIndex: string,
267
268
  newNoteIndex: string,
268
269
  ): Promise<void> {
269
- const { noteService, noteIndexCache, tocService, logger } = this.config
270
+ const { noteService, noteIndexCache, logger, notesDir } = this.config
270
271
 
271
272
  logger.info(`笔记索引变更: ${oldNoteIndex} → ${newNoteIndex}`)
272
273
 
273
- await tocService.deleteNoteFromToc(oldNoteIndex)
274
+ // files→TOC 对齐(Workspace):旧索引行随目录消失移除,新索引行按编号追加
275
+ await safeExecute(
276
+ `索引变更同步目录 ${oldNoteIndex}→${newNoteIndex}`,
277
+ () => reconcileTocFromFiles(dirname(notesDir)),
278
+ logger,
279
+ )
274
280
 
275
281
  const newNote = noteService.getNoteByIndex(newNoteIndex)
276
282
 
@@ -278,8 +284,6 @@ export class FolderChangeHandler {
278
284
  noteIndexCache.delete(oldNoteIndex)
279
285
  noteIndexCache.add(newNote)
280
286
 
281
- await tocService.appendNoteToToc(newNoteIndex)
282
- await tocService.regenerateSidebar()
283
287
  this.config.onNoteStructureChanged?.(
284
288
  `watcher:rename-index:${oldNoteIndex}->${newNoteIndex}`,
285
289
  )
@@ -1,25 +1,26 @@
1
1
  /**
2
2
  * services/file-watcher/globalUpdateCoordinator.ts
3
3
  *
4
- * 全局更新协调:应用配置更新、更新 README 列表
4
+ * 全局更新协调:应用配置更新、更新 README 列表。
5
+ * TOC/sidebar 的写入统一走 Workspace 的 `toc.reconcileFromFiles()`
6
+ * (0004 方案 B),不再直接操作 TocService。
5
7
  */
6
8
 
9
+ import { dirname } from 'path'
10
+
7
11
  import { safeExecute } from './internal'
12
+ import { reconcileTocFromFiles } from '../reconcileToc'
8
13
 
9
14
  import type { WatchEvent } from './internal'
10
- import type { NoteIndexCache } from '../../core/NoteIndexCache'
11
15
  import type { Logger } from '../../utils'
12
16
  import type { ReadmeService } from '../readme/service'
13
- import type { TocService } from '../toc/service'
14
17
 
15
18
 
16
19
  interface GlobalUpdateCoordinatorConfig {
20
+ /** 笔记目录路径 */
21
+ notesDir: string
17
22
  /** README 服务实例,用于更新笔记 README */
18
23
  readmeService: ReadmeService
19
- /** TOC 服务实例,用于更新 TOC.md 与 sidebar */
20
- tocService: TocService
21
- /** 笔记索引缓存实例 */
22
- noteIndexCache: NoteIndexCache
23
24
  /** 日志记录器 */
24
25
  logger: Logger
25
26
  }
@@ -30,26 +31,19 @@ export class GlobalUpdateCoordinator {
30
31
  async applyConfigUpdates(changedNoteIndexes: string[]): Promise<void> {
31
32
  if (changedNoteIndexes.length === 0) return
32
33
 
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()
34
+ const { logger, notesDir } = this.config
35
+
36
+ logger.info('检测到笔记状态变化,同步全局文件...')
37
+
38
+ // files→TOC 对齐(Workspace):config.done 等以文件为真值,
39
+ // 同步到 TOC 行标记与 sidebar。
40
+ await safeExecute(
41
+ '配置变更同步目录',
42
+ () => reconcileTocFromFiles(dirname(notesDir)),
43
+ logger,
44
+ )
45
+
46
+ logger.info(`已同步配置变更的笔记: ${changedNoteIndexes.join(', ')}`)
53
47
  }
54
48
 
55
49
  async updateNoteReadmesOnly(events: WatchEvent[]): Promise<void> {
@@ -24,7 +24,6 @@ import { NoteIndexCache } from '../../core/NoteIndexCache'
24
24
  import { logger } from '../../utils'
25
25
  import { NoteService } from '../note/service'
26
26
  import { ReadmeService } from '../readme/service'
27
- import { TocService } from '../toc/service'
28
27
 
29
28
  import type { WatchEvent } from './internal'
30
29
 
@@ -49,7 +48,6 @@ export class FileWatcherService {
49
48
  private adapter!: FsWatcherAdapter
50
49
  private noteService!: NoteService
51
50
  private readmeService!: ReadmeService
52
- private tocService!: TocService
53
51
  private noteIndexCache!: NoteIndexCache
54
52
  private unlockTimer: NodeJS.Timeout | null = null
55
53
 
@@ -68,7 +66,6 @@ export class FileWatcherService {
68
66
  private init(): void {
69
67
  this.noteService = NoteService.getInstance()
70
68
  this.readmeService = ReadmeService.getInstance()
71
- this.tocService = TocService.getInstance()
72
69
  this.noteIndexCache = NoteIndexCache.getInstance()
73
70
 
74
71
  this.watchState = this.initWatchState()
@@ -103,7 +100,6 @@ export class FileWatcherService {
103
100
  scheduler: this.scheduler,
104
101
  noteService: this.noteService,
105
102
  readmeService: this.readmeService,
106
- tocService: this.tocService,
107
103
  noteIndexCache: this.noteIndexCache,
108
104
  logger,
109
105
  onRenameSuccess: (payload) => {
@@ -228,9 +224,8 @@ export class FileWatcherService {
228
224
 
229
225
  private initCoordinator(): GlobalUpdateCoordinator {
230
226
  return new GlobalUpdateCoordinator({
227
+ notesDir: this.notesDir,
231
228
  readmeService: this.readmeService,
232
- tocService: this.tocService,
233
- noteIndexCache: this.noteIndexCache,
234
229
  logger,
235
230
  })
236
231
  }
package/services/index.ts CHANGED
@@ -8,7 +8,6 @@ export { FileWatcherService } from './file-watcher'
8
8
  export { GitService } from './git'
9
9
  export { NoteService } from './note'
10
10
  export { ReadmeService } from './readme'
11
- export { TocService } from './toc'
12
11
  export { VitepressService } from './vitepress'
13
12
  export { InitSubRepoService } from './init-sub-repo'
14
13
  export * from './init-sub-repo/initSubRepoLogic'
@@ -8,7 +8,7 @@ import { writeFileSync, readFileSync, promises as fsPromises } from 'fs'
8
8
  import { join } from 'path'
9
9
  import { v4 as uuidv4 } from 'uuid'
10
10
 
11
- import { NOTES_PATH, CONSTANTS, REPO_NOTES_URL } from '../../config/constants'
11
+ import { NOTES_PATH, CONSTANTS, REPO_NOTES_URL, ROOT_DIR_PATH } from '../../config/constants'
12
12
  import {
13
13
  generateNoteTitle,
14
14
  getNewNoteReadmeBody,
@@ -16,7 +16,7 @@ import {
16
16
  import { NoteIndexCache } from '../../core/NoteIndexCache'
17
17
  import { NoteManager } from '../../core/NoteManager'
18
18
  import { ensureDirectory, logger } from '../../utils'
19
- import { TocService } from '../toc/service'
19
+ import { reconcileTocFromFiles } from '../reconcileToc'
20
20
 
21
21
  import type { NoteInfo, NoteConfig } from '../../types'
22
22
 
@@ -247,10 +247,8 @@ export class NoteService {
247
247
  logger.info(`检测到全局字段变更 (${noteIndex}),正在增量更新全局文件...`)
248
248
 
249
249
  // 使用增量更新
250
- const tocService = TocService.getInstance()
251
-
252
- await tocService.updateNoteInToc(noteIndex, updates)
253
- await tocService.regenerateSidebar()
250
+ // files→TOC 对齐(Workspace):config 已写盘,同步 TOC 行标记与 sidebar
251
+ await reconcileTocFromFiles(ROOT_DIR_PATH)
254
252
 
255
253
  logger.info(`全局文件增量更新完成 (${noteIndex})`)
256
254
  } else {