@tnotesjs/core 0.2.0 → 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/dist/chunk-5QN44ES5.js +6050 -0
- package/dist/chunk-5Y5IYS6C.js +200 -0
- package/dist/cli/index.js +986 -0
- package/dist/index.js +8 -0
- package/dist/vitepress/config/index.js +1686 -0
- package/package.json +6 -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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tnotesjs/core",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "TNotes 知识库核心框架 —— 基于 VitePress 的笔记管理系统",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@10.17.1",
|
|
@@ -22,7 +22,10 @@
|
|
|
22
22
|
"./vitepress/*": "./vitepress/*"
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
|
-
"config/
|
|
25
|
+
"config/",
|
|
26
|
+
"core/",
|
|
27
|
+
"services/",
|
|
28
|
+
"utils/",
|
|
26
29
|
"dist/",
|
|
27
30
|
"templates/",
|
|
28
31
|
"vitepress/",
|
|
@@ -30,6 +33,7 @@
|
|
|
30
33
|
],
|
|
31
34
|
"scripts": {
|
|
32
35
|
"build": "tsup",
|
|
36
|
+
"prepublishOnly": "pnpm build",
|
|
33
37
|
"build:check": "tsc --noEmit -p tsconfig.build.json",
|
|
34
38
|
"typecheck": "vue-tsc --noEmit -p tsconfig.build.json",
|
|
35
39
|
"dev": "tsup --watch",
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* services/file-watcher/configChangeHandler.ts
|
|
3
|
+
*
|
|
4
|
+
* 配置变更处理
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { WatchEvent } from './internal'
|
|
8
|
+
import type { WatchState } from './watchState'
|
|
9
|
+
import type { NoteIndexCache } from '../../core/NoteIndexCache'
|
|
10
|
+
import type { Logger } from '../../utils'
|
|
11
|
+
import type { NoteService } from '../note/service'
|
|
12
|
+
|
|
13
|
+
interface ConfigChangeHandlerConfig {
|
|
14
|
+
/** 监听状态管理器 */
|
|
15
|
+
state: WatchState
|
|
16
|
+
/** 笔记服务实例 */
|
|
17
|
+
noteService: NoteService
|
|
18
|
+
/** 笔记索引缓存实例 */
|
|
19
|
+
noteIndexCache: NoteIndexCache
|
|
20
|
+
/** 日志记录器 */
|
|
21
|
+
logger: Logger
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class ConfigChangeHandler {
|
|
25
|
+
constructor(private config: ConfigChangeHandlerConfig) {}
|
|
26
|
+
|
|
27
|
+
async handle(events: WatchEvent[]): Promise<string[]> {
|
|
28
|
+
if (events.length === 0) return []
|
|
29
|
+
const changedIndexes: string[] = []
|
|
30
|
+
|
|
31
|
+
const { state, noteService, noteIndexCache, logger } = this.config
|
|
32
|
+
|
|
33
|
+
for (const change of events) {
|
|
34
|
+
// 忽略由 API 主动写入的更新,避免重复触发
|
|
35
|
+
if (noteService.shouldIgnoreConfigChange(change.path)) {
|
|
36
|
+
logger.debug(`忽略 API 写入的配置文件: ${change.path}`)
|
|
37
|
+
continue
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const snapshot = state.readConfigSnapshot(change.path)
|
|
41
|
+
if (!snapshot) continue
|
|
42
|
+
|
|
43
|
+
const cached = state.getConfigSnapshot(change.path)
|
|
44
|
+
state.setConfigSnapshot(change.path, snapshot)
|
|
45
|
+
noteIndexCache.updateConfig(change.noteIndex, snapshot)
|
|
46
|
+
|
|
47
|
+
if (!cached) continue
|
|
48
|
+
|
|
49
|
+
const statusChanged = cached.done !== snapshot.done
|
|
50
|
+
const otherChanged =
|
|
51
|
+
cached.enableDiscussions !== snapshot.enableDiscussions ||
|
|
52
|
+
cached.description !== snapshot.description
|
|
53
|
+
|
|
54
|
+
if (statusChanged) {
|
|
55
|
+
changedIndexes.push(change.noteIndex)
|
|
56
|
+
logger.info(`检测到配置状态变化: done(${cached.done}→${snapshot.done})`)
|
|
57
|
+
} else if (otherChanged) {
|
|
58
|
+
logger.info('检测到配置非状态字段变化,已刷新缓存(无需全局更新)')
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return changedIndexes
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* services/file-watcher/eventScheduler.ts
|
|
3
|
+
*
|
|
4
|
+
* 事件调度:防抖 + 批量检测 + 队列
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { WatchEvent } from './internal'
|
|
8
|
+
|
|
9
|
+
/** 默认防抖延迟(毫秒) */
|
|
10
|
+
const DEFAULT_DEBOUNCE_MS = 1000
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 默认批量更新检测窗口(毫秒)
|
|
14
|
+
*
|
|
15
|
+
* - 如果在窗口时间内检测到超过阈值个文件变更,则判定为批量更新
|
|
16
|
+
* - 暂定是 1s 内 3 个文件变更的阈值,正常编写笔记的情况下,1s 内不会超过 3 个文件同时变更,通常不会误判
|
|
17
|
+
* - 当批量更新的行为被检测到之后,会暂停监听服务(窗口 + 缓冲时间)后再恢复
|
|
18
|
+
*/
|
|
19
|
+
const DEFAULT_BATCH_WINDOW_MS = 1000
|
|
20
|
+
|
|
21
|
+
/** 默认批量更新阈值(文件数) */
|
|
22
|
+
const DEFAULT_BATCH_THRESHOLD = 3
|
|
23
|
+
|
|
24
|
+
/** 默认批量更新安全缓冲(毫秒) */
|
|
25
|
+
const DEFAULT_BATCH_BUFFER_MS = 2000
|
|
26
|
+
|
|
27
|
+
interface EventSchedulerConfig {
|
|
28
|
+
/** 防抖延迟(毫秒),默认 1000 */
|
|
29
|
+
debounceMs?: number
|
|
30
|
+
/** 批量更新检测窗口(毫秒),默认 1000 */
|
|
31
|
+
batchWindowMs?: number
|
|
32
|
+
/** 批量更新阈值(文件数),默认 3 */
|
|
33
|
+
batchThreshold?: number
|
|
34
|
+
/** 批量更新安全缓冲(毫秒),默认 2000 */
|
|
35
|
+
batchBufferMs?: number
|
|
36
|
+
/** 当事件队列需要刷新处理时的回调函数 */
|
|
37
|
+
onFlush: (events: WatchEvent[]) => void
|
|
38
|
+
/** 检测到批量更新时暂停监听服务的回调函数 */
|
|
39
|
+
onPauseForBatch: () => void
|
|
40
|
+
/** 批量更新结束后恢复监听服务的回调函数 */
|
|
41
|
+
onResumeAfterBatch: () => void
|
|
42
|
+
/** 重新初始化调度器的回调函数 */
|
|
43
|
+
reinit: () => void
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class EventScheduler {
|
|
47
|
+
/** 待处理的文件变更事件队列 */
|
|
48
|
+
private pendingEvents: Map<string, WatchEvent> = new Map()
|
|
49
|
+
|
|
50
|
+
/** 防抖定时器 */
|
|
51
|
+
private updateTimer: NodeJS.Timeout | null = null
|
|
52
|
+
|
|
53
|
+
/** 批量更新恢复定时器 */
|
|
54
|
+
private batchResumeTimer: NodeJS.Timeout | null = null
|
|
55
|
+
|
|
56
|
+
/** 记录最近的变更时间戳 */
|
|
57
|
+
private recentChanges: number[] = []
|
|
58
|
+
|
|
59
|
+
/** 标记是否正在更新,避免循环触发 - 类似一把更新行为锁 */
|
|
60
|
+
private isUpdating = false
|
|
61
|
+
|
|
62
|
+
private readonly debounceMs: number
|
|
63
|
+
private readonly batchWindowMs: number
|
|
64
|
+
private readonly batchThreshold: number
|
|
65
|
+
private readonly batchBufferMs: number
|
|
66
|
+
|
|
67
|
+
constructor(private config: EventSchedulerConfig) {
|
|
68
|
+
this.debounceMs = config.debounceMs ?? DEFAULT_DEBOUNCE_MS
|
|
69
|
+
this.batchWindowMs = config.batchWindowMs ?? DEFAULT_BATCH_WINDOW_MS
|
|
70
|
+
this.batchThreshold = config.batchThreshold ?? DEFAULT_BATCH_THRESHOLD
|
|
71
|
+
this.batchBufferMs = config.batchBufferMs ?? DEFAULT_BATCH_BUFFER_MS
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 设置更新状态锁,用于防止在执行耗时更新操作时被新的文件变更事件打断
|
|
76
|
+
*
|
|
77
|
+
* @param flag - true 表示正在更新(锁定),false 表示更新完成(解锁)
|
|
78
|
+
*/
|
|
79
|
+
setUpdating(flag: boolean) {
|
|
80
|
+
this.isUpdating = flag
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 获取当前是否处于更新锁定状态
|
|
85
|
+
*
|
|
86
|
+
* @returns true 表示正在执行更新操作(事件处理被暂停),false 表示空闲可处理新事件
|
|
87
|
+
*/
|
|
88
|
+
getUpdating() {
|
|
89
|
+
return this.isUpdating
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 将文件变更事件加入待处理队列,并启动防抖定时器
|
|
94
|
+
*
|
|
95
|
+
* - 若同一文件路径的事件已存在,则忽略重复事件(去重)
|
|
96
|
+
* - 每次新事件都会重置防抖计时器,确保在变更停止后才触发处理
|
|
97
|
+
*
|
|
98
|
+
* @param event 文件变更事件
|
|
99
|
+
*/
|
|
100
|
+
enqueue(event: WatchEvent) {
|
|
101
|
+
// 事件去重:同一路径的变更只保留一次,降低抖动
|
|
102
|
+
if (this.pendingEvents.has(event.path)) return
|
|
103
|
+
this.pendingEvents.set(event.path, event)
|
|
104
|
+
if (this.updateTimer) clearTimeout(this.updateTimer)
|
|
105
|
+
this.updateTimer = setTimeout(() => this.flush(), this.debounceMs)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 立即触发事件队列的处理(防抖到期或手动调用)
|
|
110
|
+
*
|
|
111
|
+
* - 若当前正在更新(isUpdating 为 true),则跳过以避免重复处理
|
|
112
|
+
* - 清空待处理事件队列,并通过 onFlush 回调交由上层服务处理
|
|
113
|
+
* - 处理开始后会锁定更新状态,防止处理过程中被新事件打断
|
|
114
|
+
*/
|
|
115
|
+
flush() {
|
|
116
|
+
if (this.isUpdating) return
|
|
117
|
+
if (this.pendingEvents.size === 0) return
|
|
118
|
+
const events = Array.from(this.pendingEvents.values())
|
|
119
|
+
this.pendingEvents.clear()
|
|
120
|
+
this.isUpdating = true
|
|
121
|
+
this.config.onFlush(events)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 记录当前变更时间并检测是否触发批量更新模式
|
|
126
|
+
*
|
|
127
|
+
* - 维护一个滑动时间窗口(BATCH_UPDATE_WINDOW_MS)内的变更记录
|
|
128
|
+
* - 若短时间内(1秒内)变更次数达到阈值(BATCH_UPDATE_THRESHOLD = 3),则判定为批量操作
|
|
129
|
+
* - 触发批量模式后:
|
|
130
|
+
* 1. 清空当前待处理事件队列,避免重复处理
|
|
131
|
+
* 2. 锁定更新状态(isUpdating = true)
|
|
132
|
+
* 3. 暂停监听服务,并在延迟(窗口 + 缓冲时间)后自动恢复
|
|
133
|
+
*
|
|
134
|
+
* @param now 当前时间戳(默认使用 Date.now())
|
|
135
|
+
* @returns true 表示已触发批量更新模式,false 表示仍处于普通监听模式
|
|
136
|
+
*/
|
|
137
|
+
recordChangeAndDetectBatch(now: number = Date.now()): boolean {
|
|
138
|
+
// 记录近期变更时间戳,用于检测“短时间高频”场景并切换到批量模式
|
|
139
|
+
this.recentChanges.push(now)
|
|
140
|
+
this.recentChanges = this.recentChanges.filter(
|
|
141
|
+
(t) => now - t < this.batchWindowMs,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
if (this.recentChanges.length < this.batchThreshold) return false
|
|
145
|
+
|
|
146
|
+
this.pendingEvents.clear()
|
|
147
|
+
this.recentChanges = []
|
|
148
|
+
this.isUpdating = true
|
|
149
|
+
this.config.onPauseForBatch()
|
|
150
|
+
|
|
151
|
+
this.batchResumeTimer = setTimeout(() => {
|
|
152
|
+
// 批量结束后重建状态并恢复监听
|
|
153
|
+
this.batchResumeTimer = null
|
|
154
|
+
this.isUpdating = false
|
|
155
|
+
this.config.reinit()
|
|
156
|
+
this.config.onResumeAfterBatch()
|
|
157
|
+
}, this.batchWindowMs + this.batchBufferMs)
|
|
158
|
+
|
|
159
|
+
return true
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 清理所有定时器,释放资源
|
|
164
|
+
*
|
|
165
|
+
* 在服务停止时调用,防止定时器在服务销毁后仍然触发回调
|
|
166
|
+
*/
|
|
167
|
+
clearTimers(): void {
|
|
168
|
+
if (this.updateTimer) {
|
|
169
|
+
clearTimeout(this.updateTimer)
|
|
170
|
+
this.updateTimer = null
|
|
171
|
+
}
|
|
172
|
+
if (this.batchResumeTimer) {
|
|
173
|
+
clearTimeout(this.batchResumeTimer)
|
|
174
|
+
this.batchResumeTimer = null
|
|
175
|
+
}
|
|
176
|
+
this.pendingEvents.clear()
|
|
177
|
+
this.recentChanges = []
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -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
|
+
}
|