@tnotesjs/core 0.1.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.
Potentially problematic release.
This version of @tnotesjs/core might be problematic. Click here for more details.
- package/README.md +105 -0
- package/dist/chunk-K3X5OP3N.js +1532 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +4199 -0
- package/dist/index.d.ts +138 -0
- package/dist/index.js +9 -0
- package/package.json +74 -0
- package/types/config.ts +61 -0
- package/types/index.ts +11 -0
- package/types/note.ts +33 -0
- package/vitepress/assets/icons/icon__check.svg +3 -0
- package/vitepress/assets/icons/icon__clipboard.svg +8 -0
- package/vitepress/assets/icons/icon__close.svg +1 -0
- package/vitepress/assets/icons/icon__collapse.svg +1 -0
- package/vitepress/assets/icons/icon__confirm.svg +1 -0
- package/vitepress/assets/icons/icon__copy.svg +4 -0
- package/vitepress/assets/icons/icon__focus.svg +1 -0
- package/vitepress/assets/icons/icon__fold.svg +3 -0
- package/vitepress/assets/icons/icon__folder.svg +1 -0
- package/vitepress/assets/icons/icon__fullscreen.svg +1 -0
- package/vitepress/assets/icons/icon__fullscreen_exit.svg +1 -0
- package/vitepress/assets/icons/icon__github.svg +4 -0
- package/vitepress/assets/icons/icon__mindmap.svg +1 -0
- package/vitepress/assets/icons/icon__next.svg +1 -0
- package/vitepress/assets/icons/icon__number_gray.svg +1 -0
- package/vitepress/assets/icons/icon__number_purple.svg +1 -0
- package/vitepress/assets/icons/icon__prev.svg +1 -0
- package/vitepress/assets/icons/icon__restore.svg +1 -0
- package/vitepress/assets/icons/icon__rotate.svg +4 -0
- package/vitepress/assets/icons/icon__search.svg +1 -0
- package/vitepress/assets/icons/icon__sidebar_collapsed.svg +1 -0
- package/vitepress/assets/icons/icon__sidebar_opened.svg +1 -0
- package/vitepress/assets/icons/icon__totop.svg +6 -0
- package/vitepress/assets/icons/icon__vscode.svg +6 -0
- package/vitepress/assets/icons/icon__zoom_fit.svg +1 -0
- package/vitepress/assets/icons/icon__zoom_in.svg +1 -0
- package/vitepress/assets/icons/icon__zoom_out.svg +1 -0
- package/vitepress/assets/icons/icon__zoom_reset.svg +1 -0
- package/vitepress/assets/icons/index.ts +38 -0
- package/vitepress/components/BilibiliOutsidePlayer/BilibiliOutsidePlayer.vue +20 -0
- package/vitepress/components/CodeBlockFullscreen/CodeBlockFullscreen.vue +373 -0
- package/vitepress/components/CodeBlockFullscreen/index.ts +115 -0
- package/vitepress/components/CodeBlockFullscreen/styles.css +64 -0
- package/vitepress/components/Discussions/Discussions.module.scss +32 -0
- package/vitepress/components/Discussions/Discussions.vue +211 -0
- package/vitepress/components/EnWordList/EnWordList.module.scss +124 -0
- package/vitepress/components/EnWordList/EnWordList.vue +543 -0
- package/vitepress/components/EnWordList/RightClickMenu.module.scss +22 -0
- package/vitepress/components/EnWordList/RightClickMenu.vue +66 -0
- package/vitepress/components/Footprints/Footprints.module.scss +93 -0
- package/vitepress/components/Footprints/Footprints.vue +377 -0
- package/vitepress/components/Layout/AboutModal.module.scss +233 -0
- package/vitepress/components/Layout/AboutModal.vue +105 -0
- package/vitepress/components/Layout/AboutPanel.vue +266 -0
- package/vitepress/components/Layout/ContentCollapse.vue +603 -0
- package/vitepress/components/Layout/CustomSidebar.vue +605 -0
- package/vitepress/components/Layout/DocBeforeControls.vue +139 -0
- package/vitepress/components/Layout/DocFooter.vue +225 -0
- package/vitepress/components/Layout/ImagePreview.module.scss +201 -0
- package/vitepress/components/Layout/ImagePreview.vue +281 -0
- package/vitepress/components/Layout/Layout.module.scss +661 -0
- package/vitepress/components/Layout/Layout.vue +542 -0
- package/vitepress/components/Layout/NoteStatus.vue +140 -0
- package/vitepress/components/Layout/SidebarItems.vue +263 -0
- package/vitepress/components/Layout/SidebarNavBefore.vue +92 -0
- package/vitepress/components/Layout/Swiper.vue +167 -0
- package/vitepress/components/Layout/ToggleFullContent.module.scss +11 -0
- package/vitepress/components/Layout/ToggleFullContent.vue +34 -0
- package/vitepress/components/Layout/ToggleSidebar.module.scss +11 -0
- package/vitepress/components/Layout/ToggleSidebar.vue +35 -0
- package/vitepress/components/Layout/composables/useCollapseControl.ts +88 -0
- package/vitepress/components/Layout/composables/useNoteConfig.ts +121 -0
- package/vitepress/components/Layout/composables/useNoteSave.ts +173 -0
- package/vitepress/components/Layout/composables/useNoteValidation.ts +85 -0
- package/vitepress/components/Layout/composables/useRedirect.ts +110 -0
- package/vitepress/components/Layout/composables/useVSCodeIntegration.ts +85 -0
- package/vitepress/components/Layout/homeReadme.data.ts +124 -0
- package/vitepress/components/LoadingPage/LoadingPage.vue +192 -0
- package/vitepress/components/MarkMap/MarkMap.module.scss +159 -0
- package/vitepress/components/MarkMap/MarkMap.vue +404 -0
- package/vitepress/components/Mermaid/Mermaid.module.scss +275 -0
- package/vitepress/components/Mermaid/Mermaid.vue +364 -0
- package/vitepress/components/NotesTable/NotesTable.module.scss +77 -0
- package/vitepress/components/NotesTable/NotesTable.vue +98 -0
- package/vitepress/components/NotesTable/README.md +67 -0
- package/vitepress/components/Settings/Settings.module.scss +433 -0
- package/vitepress/components/Settings/Settings.vue +306 -0
- package/vitepress/components/SidebarCard/MindMapView.vue +483 -0
- package/vitepress/components/SidebarCard/NotesTrendChart.vue +108 -0
- package/vitepress/components/SidebarCard/SidebarCard.vue +948 -0
- package/vitepress/components/Tooltip/Tooltip.vue +70 -0
- package/vitepress/components/constants.ts +91 -0
- package/vitepress/components/notesConfig.data.ts +73 -0
- package/vitepress/components/sidebar.data.ts +59 -0
- package/vitepress/components/tnotes-config.data.ts +21 -0
- package/vitepress/components/utils.ts +26 -0
- package/vitepress/config/index.ts +126 -0
- package/vitepress/configs/constants.ts +26 -0
- package/vitepress/configs/head.config.ts +25 -0
- package/vitepress/configs/index.ts +9 -0
- package/vitepress/configs/markdown-it.d.ts +23 -0
- package/vitepress/configs/markdown.config.ts +366 -0
- package/vitepress/configs/theme.config.ts +108 -0
- package/vitepress/plugins/buildProgressPlugin.ts +390 -0
- package/vitepress/plugins/getNoteByConfigIdPlugin.ts +107 -0
- package/vitepress/plugins/renameNotePlugin.ts +60 -0
- package/vitepress/plugins/updateConfigPlugin.ts +63 -0
- package/vitepress/theme/index.ts +95 -0
- package/vitepress/theme/styles/base.scss +50 -0
- package/vitepress/theme/styles/components/404.scss +31 -0
- package/vitepress/theme/styles/components/collapse.scss +175 -0
- package/vitepress/theme/styles/components/markmap.scss +101 -0
- package/vitepress/theme/styles/components/swiper.scss +255 -0
- package/vitepress/theme/styles/index.scss +25 -0
- package/vitepress/theme/styles/layout.scss +62 -0
- package/vitepress/theme/styles/utilities.scss +39 -0
- package/vitepress/theme/styles/vitepress-override.scss +25 -0
|
@@ -0,0 +1,4199 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
CONSTANTS,
|
|
4
|
+
ConfigManager,
|
|
5
|
+
EN_WORDS_DIR,
|
|
6
|
+
EOL,
|
|
7
|
+
Logger,
|
|
8
|
+
NOTES_DIR_PATH,
|
|
9
|
+
NOTES_PATH,
|
|
10
|
+
NoteManager,
|
|
11
|
+
REPO_NOTES_URL,
|
|
12
|
+
ROOT_CONFIG_PATH,
|
|
13
|
+
ROOT_DIR_PATH,
|
|
14
|
+
ROOT_README_PATH,
|
|
15
|
+
TNOTES_BASE_DIR,
|
|
16
|
+
TNOTES_CORE_DIR,
|
|
17
|
+
VP_SIDEBAR_PATH,
|
|
18
|
+
buildNoteLineMarkdown,
|
|
19
|
+
createAddNumberToTitle,
|
|
20
|
+
createError,
|
|
21
|
+
createLogger,
|
|
22
|
+
ensureDirectory,
|
|
23
|
+
genHierarchicalSidebar,
|
|
24
|
+
generateToc,
|
|
25
|
+
getChangedIds,
|
|
26
|
+
getTargetDirs,
|
|
27
|
+
handleError,
|
|
28
|
+
isPortInUse,
|
|
29
|
+
killPortProcess,
|
|
30
|
+
logger,
|
|
31
|
+
parseArgs,
|
|
32
|
+
parseNoteLine,
|
|
33
|
+
parseReadmeCompletedNotes,
|
|
34
|
+
processEmptyLines,
|
|
35
|
+
pullAllRepos,
|
|
36
|
+
pushAllRepos,
|
|
37
|
+
runCommand,
|
|
38
|
+
syncAllRepos,
|
|
39
|
+
validateNoteTitle,
|
|
40
|
+
waitForPort
|
|
41
|
+
} from "../chunk-K3X5OP3N.js";
|
|
42
|
+
|
|
43
|
+
// commands/models.ts
|
|
44
|
+
var COMMAND_NAMES = {
|
|
45
|
+
BUILD: "build",
|
|
46
|
+
CREATE_NOTES: "create-notes",
|
|
47
|
+
DEV: "dev",
|
|
48
|
+
FIX_TIMESTAMPS: "fix-timestamps",
|
|
49
|
+
HELP: "help",
|
|
50
|
+
PREVIEW: "preview",
|
|
51
|
+
PULL: "pull",
|
|
52
|
+
PUSH: "push",
|
|
53
|
+
RENAME_NOTE: "rename-note",
|
|
54
|
+
SYNC_CORE: "sync-core",
|
|
55
|
+
SYNC: "sync",
|
|
56
|
+
UPDATE: "update",
|
|
57
|
+
UPDATE_COMPLETED_COUNT: "update-completed-count",
|
|
58
|
+
UPDATE_NOTE_CONFIG: "update-note-config"
|
|
59
|
+
};
|
|
60
|
+
var COMMAND_DESCRIPTIONS = {
|
|
61
|
+
[COMMAND_NAMES.DEV]: "\u542F\u52A8\u77E5\u8BC6\u5E93\u5F00\u53D1\u670D\u52A1",
|
|
62
|
+
[COMMAND_NAMES.BUILD]: "\u6784\u5EFA\u77E5\u8BC6\u5E93",
|
|
63
|
+
[COMMAND_NAMES.PREVIEW]: "\u9884\u89C8\u6784\u5EFA\u540E\u7684\u77E5\u8BC6\u5E93",
|
|
64
|
+
[COMMAND_NAMES.UPDATE]: "\u6839\u636E\u7B14\u8BB0\u5185\u5BB9\u66F4\u65B0\u77E5\u8BC6\u5E93",
|
|
65
|
+
[COMMAND_NAMES.UPDATE_COMPLETED_COUNT]: "\u66F4\u65B0\u5B8C\u6210\u7B14\u8BB0\u6570\u91CF\u5386\u53F2\u8BB0\u5F55\uFF08\u8FD1 1 \u5E74\uFF0C\u6700\u8FD1 12 \u4E2A\u6708\uFF09",
|
|
66
|
+
[COMMAND_NAMES.CREATE_NOTES]: "\u65B0\u5EFA\u7B14\u8BB0\uFF08\u652F\u6301\u6279\u91CF\u521B\u5EFA\uFF09",
|
|
67
|
+
[COMMAND_NAMES.PUSH]: "\u5C06\u77E5\u8BC6\u5E93\u63A8\u9001\u5230 GitHub (\u4F7F\u7528 --all \u63A8\u9001\u6240\u6709\u77E5\u8BC6\u5E93)",
|
|
68
|
+
[COMMAND_NAMES.PULL]: "\u5C06 GitHub \u7684\u77E5\u8BC6\u5E93\u62C9\u4E0B\u6765 (\u4F7F\u7528 --all \u62C9\u53D6\u6240\u6709\u77E5\u8BC6\u5E93)",
|
|
69
|
+
[COMMAND_NAMES.SYNC]: "\u540C\u6B65\u672C\u5730\u548C\u8FDC\u7A0B\u7684\u77E5\u8BC6\u5E93\u72B6\u6001 (\u4F7F\u7528 --all \u540C\u6B65\u6240\u6709\u77E5\u8BC6\u5E93)",
|
|
70
|
+
[COMMAND_NAMES.SYNC_CORE]: "\u540C\u6B65\u6240\u6709\u5144\u5F1F\u77E5\u8BC6\u5E93\u7684 TNotes.core \u5230\u6700\u65B0\u7248\u672C",
|
|
71
|
+
[COMMAND_NAMES.FIX_TIMESTAMPS]: "\u4FEE\u590D\u6240\u6709\u7B14\u8BB0\u7684\u65F6\u95F4\u6233\uFF08\u57FA\u4E8E git \u5386\u53F2\uFF09",
|
|
72
|
+
[COMMAND_NAMES.UPDATE_NOTE_CONFIG]: "\u66F4\u65B0\u7B14\u8BB0\u914D\u7F6E\u6587\u4EF6",
|
|
73
|
+
[COMMAND_NAMES.RENAME_NOTE]: "\u91CD\u547D\u540D\u7B14\u8BB0",
|
|
74
|
+
[COMMAND_NAMES.HELP]: "\u663E\u793A\u5E2E\u52A9\u4FE1\u606F"
|
|
75
|
+
};
|
|
76
|
+
var COMMAND_OPTIONS = {
|
|
77
|
+
ALL: "all",
|
|
78
|
+
QUIET: "quiet",
|
|
79
|
+
FORCE: "force"
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// commands/BaseCommand.ts
|
|
83
|
+
var BaseCommand = class {
|
|
84
|
+
constructor(name) {
|
|
85
|
+
this.name = name;
|
|
86
|
+
this.options = {};
|
|
87
|
+
this.logger = logger.child(name);
|
|
88
|
+
}
|
|
89
|
+
/** 命令描述(从静态配置读取) */
|
|
90
|
+
get description() {
|
|
91
|
+
return COMMAND_DESCRIPTIONS[this.name];
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* 设置命令选项
|
|
95
|
+
*/
|
|
96
|
+
setOptions(options) {
|
|
97
|
+
this.options = { ...this.options, ...options };
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* 执行命令(带错误处理)
|
|
101
|
+
*/
|
|
102
|
+
async execute() {
|
|
103
|
+
const startTime = Date.now();
|
|
104
|
+
try {
|
|
105
|
+
this.logger.start(this.description);
|
|
106
|
+
await this.run();
|
|
107
|
+
const duration = Date.now() - startTime;
|
|
108
|
+
this.logger.done(`\u547D\u4EE4\u6267\u884C\u8017\u65F6\uFF1A${duration} ms`);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
handleError(error);
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// services/file-watcher/internal.ts
|
|
117
|
+
var WATCH_EVENT_TYPES = {
|
|
118
|
+
README: "readme",
|
|
119
|
+
CONFIG: "config"
|
|
120
|
+
};
|
|
121
|
+
async function safeExecute(label, fn, logger2) {
|
|
122
|
+
try {
|
|
123
|
+
await fn();
|
|
124
|
+
return true;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
logger2.error(`[${label}] ${error}`);
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// services/file-watcher/watchState.ts
|
|
132
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
133
|
+
import { createHash } from "crypto";
|
|
134
|
+
import { join } from "path";
|
|
135
|
+
var WatchState = class {
|
|
136
|
+
constructor(config) {
|
|
137
|
+
this.config = config;
|
|
138
|
+
/** 文件哈希缓存 */
|
|
139
|
+
this.fileHashes = /* @__PURE__ */ new Map();
|
|
140
|
+
/** 笔记目录缓存 */
|
|
141
|
+
this.noteDirCache = /* @__PURE__ */ new Set();
|
|
142
|
+
/** 笔记配置缓存 */
|
|
143
|
+
this.configCache = /* @__PURE__ */ new Map();
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* 获取指定文件的 MD5 哈希值,若文件不存在或读取失败返回 null
|
|
147
|
+
*
|
|
148
|
+
* @param filePath 文件路径
|
|
149
|
+
* @returns 文件哈希
|
|
150
|
+
*/
|
|
151
|
+
getFileHash(filePath) {
|
|
152
|
+
try {
|
|
153
|
+
if (!existsSync(filePath)) return null;
|
|
154
|
+
const content = readFileSync(filePath, "utf-8");
|
|
155
|
+
if (content.length === 0) return null;
|
|
156
|
+
return createHash("md5").update(content).digest("hex");
|
|
157
|
+
} catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* 更新文件哈希缓存,只有当文件内容发生变化时才更新并返回 true
|
|
163
|
+
*
|
|
164
|
+
* @param filePath 文件路径
|
|
165
|
+
* @returns 是否发生变化
|
|
166
|
+
*/
|
|
167
|
+
updateFileHash(filePath) {
|
|
168
|
+
const current = this.getFileHash(filePath);
|
|
169
|
+
if (!current) return false;
|
|
170
|
+
const prev = this.fileHashes.get(filePath);
|
|
171
|
+
if (prev === current) return false;
|
|
172
|
+
this.fileHashes.set(filePath, current);
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* 检查指定名称的笔记目录是否已存在于缓存中
|
|
177
|
+
*
|
|
178
|
+
* @param name 笔记目录名称
|
|
179
|
+
* @returns 若存在则返回 true,否则返回 false
|
|
180
|
+
*/
|
|
181
|
+
hasNoteDir(name) {
|
|
182
|
+
return this.noteDirCache.has(name);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* 将指定名称的笔记目录添加到缓存中
|
|
186
|
+
*
|
|
187
|
+
* @param name 笔记目录名称
|
|
188
|
+
*/
|
|
189
|
+
addNoteDir(name) {
|
|
190
|
+
this.noteDirCache.add(name);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* 从缓存中移除指定名称的笔记目录
|
|
194
|
+
*
|
|
195
|
+
* @param name 笔记目录名称
|
|
196
|
+
*/
|
|
197
|
+
deleteNoteDir(name) {
|
|
198
|
+
this.noteDirCache.delete(name);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* 清空所有缓存数据,包括文件哈希、笔记目录和配置快照
|
|
202
|
+
*/
|
|
203
|
+
clearAll() {
|
|
204
|
+
this.fileHashes.clear();
|
|
205
|
+
this.noteDirCache.clear();
|
|
206
|
+
this.configCache.clear();
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* 清除指定笔记目录相关的缓存数据,包括 README.md 和 .tnotes.json 的文件哈希及配置快照
|
|
210
|
+
*
|
|
211
|
+
* @param noteDirName 笔记目录名称
|
|
212
|
+
*/
|
|
213
|
+
clearNoteCaches(noteDirName) {
|
|
214
|
+
const readmePath = join(this.config.notesDir, noteDirName, "README.md");
|
|
215
|
+
const configPath = join(this.config.notesDir, noteDirName, ".tnotes.json");
|
|
216
|
+
this.fileHashes.delete(readmePath);
|
|
217
|
+
this.fileHashes.delete(configPath);
|
|
218
|
+
this.configCache.delete(configPath);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* 获取指定配置文件路径对应的配置快照
|
|
222
|
+
*
|
|
223
|
+
* @param configPath 配置文件路径(通常为 .tnotes.json 的绝对路径)
|
|
224
|
+
* @returns 配置快照,若不存在则返回 undefined
|
|
225
|
+
*/
|
|
226
|
+
getConfigSnapshot(configPath) {
|
|
227
|
+
return this.configCache.get(configPath);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* 设置指定配置文件路径的配置快照到缓存中
|
|
231
|
+
*
|
|
232
|
+
* @param configPath 配置文件路径(通常为 .tnotes.json 的绝对路径)
|
|
233
|
+
* @param snapshot 配置快照对象
|
|
234
|
+
*/
|
|
235
|
+
setConfigSnapshot(configPath, snapshot) {
|
|
236
|
+
this.configCache.set(configPath, snapshot);
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* 读取指定配置文件的快照
|
|
240
|
+
*
|
|
241
|
+
* 解析 .tnotes.json 配置文件,提取 done、enableDiscussions、description 字段。
|
|
242
|
+
*
|
|
243
|
+
* @param configPath 配置文件路径(通常为 .tnotes.json 的绝对路径)
|
|
244
|
+
* @returns 配置快照,若文件不存在或解析失败则返回 null
|
|
245
|
+
*/
|
|
246
|
+
readConfigSnapshot(configPath) {
|
|
247
|
+
try {
|
|
248
|
+
if (!existsSync(configPath)) return null;
|
|
249
|
+
const content = readFileSync(configPath, "utf-8");
|
|
250
|
+
const config = JSON.parse(content);
|
|
251
|
+
return {
|
|
252
|
+
done: Boolean(config.done),
|
|
253
|
+
enableDiscussions: Boolean(config.enableDiscussions),
|
|
254
|
+
description: config.description || ""
|
|
255
|
+
};
|
|
256
|
+
} catch (error) {
|
|
257
|
+
this.config.logger.error(`[\u8BFB\u53D6\u914D\u7F6E\u5FEB\u7167] ${error}`);
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* 从磁盘初始化监听状态缓存:
|
|
263
|
+
* 遍历笔记根目录下的所有子目录,将每个笔记目录的 README.md 和 .tnotes.json
|
|
264
|
+
* 的哈希值及配置快照加载到缓存中。
|
|
265
|
+
*/
|
|
266
|
+
initializeFromDisk() {
|
|
267
|
+
try {
|
|
268
|
+
const noteDirs = readdirSync(this.config.notesDir);
|
|
269
|
+
this.clearAll();
|
|
270
|
+
for (const noteDir of noteDirs) {
|
|
271
|
+
const noteDirPath = join(this.config.notesDir, noteDir);
|
|
272
|
+
if (!statSync(noteDirPath).isDirectory()) continue;
|
|
273
|
+
this.noteDirCache.add(noteDir);
|
|
274
|
+
const readmePath = join(noteDirPath, "README.md");
|
|
275
|
+
const readmeHash = this.getFileHash(readmePath);
|
|
276
|
+
if (readmeHash) this.fileHashes.set(readmePath, readmeHash);
|
|
277
|
+
const configPath = join(noteDirPath, ".tnotes.json");
|
|
278
|
+
const configHash = this.getFileHash(configPath);
|
|
279
|
+
if (configHash) {
|
|
280
|
+
this.fileHashes.set(configPath, configHash);
|
|
281
|
+
const snapshot = this.readConfigSnapshot(configPath);
|
|
282
|
+
if (snapshot) this.configCache.set(configPath, snapshot);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
} catch (error) {
|
|
286
|
+
this.config.logger.warn(
|
|
287
|
+
`[initializeFromDisk] \u521D\u59CB\u5316\u76D1\u542C\u72B6\u6001\u5931\u8D25: ${error}`
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
// services/file-watcher/eventScheduler.ts
|
|
294
|
+
var DEFAULT_DEBOUNCE_MS = 1e3;
|
|
295
|
+
var DEFAULT_BATCH_WINDOW_MS = 1e3;
|
|
296
|
+
var DEFAULT_BATCH_THRESHOLD = 3;
|
|
297
|
+
var DEFAULT_BATCH_BUFFER_MS = 2e3;
|
|
298
|
+
var EventScheduler = class {
|
|
299
|
+
constructor(config) {
|
|
300
|
+
this.config = config;
|
|
301
|
+
/** 待处理的文件变更事件队列 */
|
|
302
|
+
this.pendingEvents = /* @__PURE__ */ new Map();
|
|
303
|
+
/** 防抖定时器 */
|
|
304
|
+
this.updateTimer = null;
|
|
305
|
+
/** 批量更新恢复定时器 */
|
|
306
|
+
this.batchResumeTimer = null;
|
|
307
|
+
/** 记录最近的变更时间戳 */
|
|
308
|
+
this.recentChanges = [];
|
|
309
|
+
/** 标记是否正在更新,避免循环触发 - 类似一把更新行为锁 */
|
|
310
|
+
this.isUpdating = false;
|
|
311
|
+
this.debounceMs = config.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
312
|
+
this.batchWindowMs = config.batchWindowMs ?? DEFAULT_BATCH_WINDOW_MS;
|
|
313
|
+
this.batchThreshold = config.batchThreshold ?? DEFAULT_BATCH_THRESHOLD;
|
|
314
|
+
this.batchBufferMs = config.batchBufferMs ?? DEFAULT_BATCH_BUFFER_MS;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* 设置更新状态锁,用于防止在执行耗时更新操作时被新的文件变更事件打断
|
|
318
|
+
*
|
|
319
|
+
* @param flag - true 表示正在更新(锁定),false 表示更新完成(解锁)
|
|
320
|
+
*/
|
|
321
|
+
setUpdating(flag) {
|
|
322
|
+
this.isUpdating = flag;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* 获取当前是否处于更新锁定状态
|
|
326
|
+
*
|
|
327
|
+
* @returns true 表示正在执行更新操作(事件处理被暂停),false 表示空闲可处理新事件
|
|
328
|
+
*/
|
|
329
|
+
getUpdating() {
|
|
330
|
+
return this.isUpdating;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* 将文件变更事件加入待处理队列,并启动防抖定时器
|
|
334
|
+
*
|
|
335
|
+
* - 若同一文件路径的事件已存在,则忽略重复事件(去重)
|
|
336
|
+
* - 每次新事件都会重置防抖计时器,确保在变更停止后才触发处理
|
|
337
|
+
*
|
|
338
|
+
* @param event 文件变更事件
|
|
339
|
+
*/
|
|
340
|
+
enqueue(event) {
|
|
341
|
+
if (this.pendingEvents.has(event.path)) return;
|
|
342
|
+
this.pendingEvents.set(event.path, event);
|
|
343
|
+
if (this.updateTimer) clearTimeout(this.updateTimer);
|
|
344
|
+
this.updateTimer = setTimeout(() => this.flush(), this.debounceMs);
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* 立即触发事件队列的处理(防抖到期或手动调用)
|
|
348
|
+
*
|
|
349
|
+
* - 若当前正在更新(isUpdating 为 true),则跳过以避免重复处理
|
|
350
|
+
* - 清空待处理事件队列,并通过 onFlush 回调交由上层服务处理
|
|
351
|
+
* - 处理开始后会锁定更新状态,防止处理过程中被新事件打断
|
|
352
|
+
*/
|
|
353
|
+
flush() {
|
|
354
|
+
if (this.isUpdating) return;
|
|
355
|
+
if (this.pendingEvents.size === 0) return;
|
|
356
|
+
const events = Array.from(this.pendingEvents.values());
|
|
357
|
+
this.pendingEvents.clear();
|
|
358
|
+
this.isUpdating = true;
|
|
359
|
+
this.config.onFlush(events);
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* 记录当前变更时间并检测是否触发批量更新模式
|
|
363
|
+
*
|
|
364
|
+
* - 维护一个滑动时间窗口(BATCH_UPDATE_WINDOW_MS)内的变更记录
|
|
365
|
+
* - 若短时间内(1秒内)变更次数达到阈值(BATCH_UPDATE_THRESHOLD = 3),则判定为批量操作
|
|
366
|
+
* - 触发批量模式后:
|
|
367
|
+
* 1. 清空当前待处理事件队列,避免重复处理
|
|
368
|
+
* 2. 锁定更新状态(isUpdating = true)
|
|
369
|
+
* 3. 暂停监听服务,并在延迟(窗口 + 缓冲时间)后自动恢复
|
|
370
|
+
*
|
|
371
|
+
* @param now 当前时间戳(默认使用 Date.now())
|
|
372
|
+
* @returns true 表示已触发批量更新模式,false 表示仍处于普通监听模式
|
|
373
|
+
*/
|
|
374
|
+
recordChangeAndDetectBatch(now = Date.now()) {
|
|
375
|
+
this.recentChanges.push(now);
|
|
376
|
+
this.recentChanges = this.recentChanges.filter(
|
|
377
|
+
(t) => now - t < this.batchWindowMs
|
|
378
|
+
);
|
|
379
|
+
if (this.recentChanges.length < this.batchThreshold) return false;
|
|
380
|
+
this.pendingEvents.clear();
|
|
381
|
+
this.recentChanges = [];
|
|
382
|
+
this.isUpdating = true;
|
|
383
|
+
this.config.onPauseForBatch();
|
|
384
|
+
this.batchResumeTimer = setTimeout(() => {
|
|
385
|
+
this.batchResumeTimer = null;
|
|
386
|
+
this.isUpdating = false;
|
|
387
|
+
this.config.reinit();
|
|
388
|
+
this.config.onResumeAfterBatch();
|
|
389
|
+
}, this.batchWindowMs + this.batchBufferMs);
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* 清理所有定时器,释放资源
|
|
394
|
+
*
|
|
395
|
+
* 在服务停止时调用,防止定时器在服务销毁后仍然触发回调
|
|
396
|
+
*/
|
|
397
|
+
clearTimers() {
|
|
398
|
+
if (this.updateTimer) {
|
|
399
|
+
clearTimeout(this.updateTimer);
|
|
400
|
+
this.updateTimer = null;
|
|
401
|
+
}
|
|
402
|
+
if (this.batchResumeTimer) {
|
|
403
|
+
clearTimeout(this.batchResumeTimer);
|
|
404
|
+
this.batchResumeTimer = null;
|
|
405
|
+
}
|
|
406
|
+
this.pendingEvents.clear();
|
|
407
|
+
this.recentChanges = [];
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
// services/file-watcher/renameDetector.ts
|
|
412
|
+
import { existsSync as existsSync2 } from "fs";
|
|
413
|
+
import { join as join2 } from "path";
|
|
414
|
+
var FOLDER_RENAME_DETECT_WINDOW_MS = 500;
|
|
415
|
+
var RenameDetector = class {
|
|
416
|
+
constructor(config) {
|
|
417
|
+
this.config = config;
|
|
418
|
+
/** 待处理的文件夹重命名 */
|
|
419
|
+
this.pendingFolderRename = null;
|
|
420
|
+
/** 文件夹重命名检测定时器 */
|
|
421
|
+
this.folderRenameTimer = null;
|
|
422
|
+
}
|
|
423
|
+
handleFsRename(folderName) {
|
|
424
|
+
const { notesDir, dirCache, logger: logger2, onDelete, onRename } = this.config;
|
|
425
|
+
const folderPath = join2(notesDir, folderName);
|
|
426
|
+
const folderExists = existsSync2(folderPath);
|
|
427
|
+
const noteIndex = NoteManager.extractNoteIndex(folderName);
|
|
428
|
+
if (!noteIndex) {
|
|
429
|
+
logger2.warn(`\u65E0\u6CD5\u4ECE\u6587\u4EF6\u5939\u540D\u79F0\u63D0\u53D6\u7B14\u8BB0\u7D22\u5F15: ${folderName}`);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (!folderExists) {
|
|
433
|
+
if (dirCache.has(folderName)) {
|
|
434
|
+
logger2.info(`\u68C0\u6D4B\u5230\u6587\u4EF6\u5939\u5220\u9664/\u91CD\u547D\u540D: ${folderName}`);
|
|
435
|
+
this.pendingFolderRename = { oldName: folderName, time: Date.now() };
|
|
436
|
+
if (this.folderRenameTimer) clearTimeout(this.folderRenameTimer);
|
|
437
|
+
this.folderRenameTimer = setTimeout(() => {
|
|
438
|
+
if (this.pendingFolderRename) {
|
|
439
|
+
logger2.warn(`\u68C0\u6D4B\u5230\u7B14\u8BB0\u5220\u9664: ${this.pendingFolderRename.oldName}`);
|
|
440
|
+
onDelete(this.pendingFolderRename.oldName);
|
|
441
|
+
}
|
|
442
|
+
this.pendingFolderRename = null;
|
|
443
|
+
this.folderRenameTimer = null;
|
|
444
|
+
}, FOLDER_RENAME_DETECT_WINDOW_MS);
|
|
445
|
+
}
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (!dirCache.has(folderName)) {
|
|
449
|
+
logger2.info(`\u68C0\u6D4B\u5230\u6587\u4EF6\u5939\u521B\u5EFA/\u91CD\u547D\u540D: ${folderName}`);
|
|
450
|
+
if (this.pendingFolderRename && Date.now() - this.pendingFolderRename.time < FOLDER_RENAME_DETECT_WINDOW_MS) {
|
|
451
|
+
const oldName = this.pendingFolderRename.oldName;
|
|
452
|
+
const oldNoteIndex = NoteManager.extractNoteIndex(oldName);
|
|
453
|
+
if (oldNoteIndex && oldNoteIndex === noteIndex) {
|
|
454
|
+
logger2.info(`\u68C0\u6D4B\u5230\u6587\u4EF6\u5939\u91CD\u547D\u540D: ${oldName} \u2192 ${folderName}`);
|
|
455
|
+
if (this.folderRenameTimer) {
|
|
456
|
+
clearTimeout(this.folderRenameTimer);
|
|
457
|
+
this.folderRenameTimer = null;
|
|
458
|
+
}
|
|
459
|
+
onRename(oldName, folderName);
|
|
460
|
+
this.pendingFolderRename = null;
|
|
461
|
+
} else if (oldNoteIndex && oldNoteIndex !== noteIndex) {
|
|
462
|
+
logger2.warn(`\u7D22\u5F15\u51B2\u7A81\uFF0C\u56DE\u9000: ${oldName} -> ${folderName}`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
dirCache.add(folderName);
|
|
466
|
+
if (this.pendingFolderRename) {
|
|
467
|
+
dirCache.delete(this.pendingFolderRename.oldName);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
clearTimers() {
|
|
472
|
+
if (this.folderRenameTimer) {
|
|
473
|
+
clearTimeout(this.folderRenameTimer);
|
|
474
|
+
this.folderRenameTimer = null;
|
|
475
|
+
}
|
|
476
|
+
this.pendingFolderRename = null;
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
// services/file-watcher/configChangeHandler.ts
|
|
481
|
+
var ConfigChangeHandler = class {
|
|
482
|
+
constructor(config) {
|
|
483
|
+
this.config = config;
|
|
484
|
+
}
|
|
485
|
+
async handle(events) {
|
|
486
|
+
if (events.length === 0) return [];
|
|
487
|
+
const changedIndexes = [];
|
|
488
|
+
const { state, noteService, noteIndexCache, logger: logger2 } = this.config;
|
|
489
|
+
for (const change of events) {
|
|
490
|
+
if (noteService.shouldIgnoreConfigChange(change.path)) {
|
|
491
|
+
logger2.debug(`\u5FFD\u7565 API \u5199\u5165\u7684\u914D\u7F6E\u6587\u4EF6: ${change.path}`);
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
const snapshot = state.readConfigSnapshot(change.path);
|
|
495
|
+
if (!snapshot) continue;
|
|
496
|
+
const cached = state.getConfigSnapshot(change.path);
|
|
497
|
+
state.setConfigSnapshot(change.path, snapshot);
|
|
498
|
+
noteIndexCache.updateConfig(change.noteIndex, snapshot);
|
|
499
|
+
if (!cached) continue;
|
|
500
|
+
const statusChanged = cached.done !== snapshot.done;
|
|
501
|
+
const otherChanged = cached.enableDiscussions !== snapshot.enableDiscussions || cached.description !== snapshot.description;
|
|
502
|
+
if (statusChanged) {
|
|
503
|
+
changedIndexes.push(change.noteIndex);
|
|
504
|
+
logger2.info(`\u68C0\u6D4B\u5230\u914D\u7F6E\u72B6\u6001\u53D8\u5316: done(${cached.done}\u2192${snapshot.done})`);
|
|
505
|
+
} else if (otherChanged) {
|
|
506
|
+
logger2.info("\u68C0\u6D4B\u5230\u914D\u7F6E\u975E\u72B6\u6001\u5B57\u6BB5\u53D8\u5316\uFF0C\u5DF2\u5237\u65B0\u7F13\u5B58\uFF08\u65E0\u9700\u5168\u5C40\u66F4\u65B0\uFF09");
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return changedIndexes;
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
// services/file-watcher/readmeChangeHandler.ts
|
|
514
|
+
var ReadmeChangeHandler = class {
|
|
515
|
+
constructor(config) {
|
|
516
|
+
this.config = config;
|
|
517
|
+
}
|
|
518
|
+
async handle(events) {
|
|
519
|
+
if (events.length === 0) return;
|
|
520
|
+
const indexes = [...new Set(events.map((c) => c.noteIndex))];
|
|
521
|
+
for (const noteIndex of indexes) {
|
|
522
|
+
const noteInfo = this.config.noteService.getNoteByIndex(noteIndex);
|
|
523
|
+
if (noteInfo) {
|
|
524
|
+
await this.config.noteService.fixNoteTitle(noteInfo);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
// services/file-watcher/globalUpdateCoordinator.ts
|
|
531
|
+
var GlobalUpdateCoordinator = class {
|
|
532
|
+
constructor(config) {
|
|
533
|
+
this.config = config;
|
|
534
|
+
}
|
|
535
|
+
async applyConfigUpdates(changedNoteIndexes) {
|
|
536
|
+
if (changedNoteIndexes.length === 0) return;
|
|
537
|
+
const { readmeService, noteIndexCache, logger: logger2 } = this.config;
|
|
538
|
+
logger2.info("\u68C0\u6D4B\u5230\u7B14\u8BB0\u72B6\u6001\u53D8\u5316\uFF0C\u589E\u91CF\u66F4\u65B0\u5168\u5C40\u6587\u4EF6...");
|
|
539
|
+
for (const noteIndex of changedNoteIndexes) {
|
|
540
|
+
await safeExecute(
|
|
541
|
+
`\u589E\u91CF\u66F4\u65B0 ${noteIndex}`,
|
|
542
|
+
async () => {
|
|
543
|
+
const item = noteIndexCache.getByNoteIndex(noteIndex);
|
|
544
|
+
await readmeService.updateNoteInReadme(
|
|
545
|
+
noteIndex,
|
|
546
|
+
item?.noteConfig || {}
|
|
547
|
+
);
|
|
548
|
+
logger2.info(`\u589E\u91CF\u66F4\u65B0 README \u4E2D\u7684\u7B14\u8BB0: ${noteIndex}`);
|
|
549
|
+
},
|
|
550
|
+
logger2
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
await readmeService.regenerateSidebar();
|
|
554
|
+
}
|
|
555
|
+
async updateNoteReadmesOnly(events) {
|
|
556
|
+
const noteIndexesToUpdate = [...new Set(events.map((c) => c.noteIndex))];
|
|
557
|
+
if (noteIndexesToUpdate.length === 0) return;
|
|
558
|
+
await this.config.readmeService.updateNoteReadmesOnly(noteIndexesToUpdate);
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
// services/file-watcher/folderChangeHandler.ts
|
|
563
|
+
import { existsSync as existsSync3, promises as fsPromises } from "fs";
|
|
564
|
+
import { join as join3 } from "path";
|
|
565
|
+
var RENAME_REVERT_DELAY_MS = 2e3;
|
|
566
|
+
var DELETE_REINIT_DELAY_MS = 1e3;
|
|
567
|
+
var UPDATE_UNLOCK_DELAY_MS = 500;
|
|
568
|
+
var FolderChangeHandler = class {
|
|
569
|
+
constructor(config) {
|
|
570
|
+
this.config = config;
|
|
571
|
+
/** 活跃的定时器 ID 集合,用于统一清理 */
|
|
572
|
+
this.activeTimers = /* @__PURE__ */ new Set();
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* 清理所有活跃定时器,释放资源
|
|
576
|
+
*
|
|
577
|
+
* 在服务停止时调用,防止定时器在服务销毁后仍然触发回调
|
|
578
|
+
*/
|
|
579
|
+
clearTimers() {
|
|
580
|
+
for (const timer of this.activeTimers) {
|
|
581
|
+
clearTimeout(timer);
|
|
582
|
+
}
|
|
583
|
+
this.activeTimers.clear();
|
|
584
|
+
}
|
|
585
|
+
scheduleTimer(fn, delay) {
|
|
586
|
+
const timer = setTimeout(() => {
|
|
587
|
+
this.activeTimers.delete(timer);
|
|
588
|
+
fn();
|
|
589
|
+
}, delay);
|
|
590
|
+
this.activeTimers.add(timer);
|
|
591
|
+
}
|
|
592
|
+
async handleFolderDeletion(deletedFolderName) {
|
|
593
|
+
const { scheduler, watchState, noteIndexCache, readmeService, logger: logger2 } = this.config;
|
|
594
|
+
if (scheduler.getUpdating()) return;
|
|
595
|
+
scheduler.setUpdating(true);
|
|
596
|
+
try {
|
|
597
|
+
const noteIndex = this.extractNoteIndexOrWarn(deletedFolderName);
|
|
598
|
+
if (!noteIndex) return;
|
|
599
|
+
logger2.info(`\u6B63\u5728\u5904\u7406\u7B14\u8BB0\u5220\u9664: ${noteIndex} (${deletedFolderName})`);
|
|
600
|
+
watchState.deleteNoteDir(deletedFolderName);
|
|
601
|
+
watchState.clearNoteCaches(deletedFolderName);
|
|
602
|
+
noteIndexCache.delete(noteIndex);
|
|
603
|
+
await safeExecute(
|
|
604
|
+
`\u5220\u9664\u7B14\u8BB0 ${noteIndex}`,
|
|
605
|
+
async () => {
|
|
606
|
+
await readmeService.deleteNoteFromReadme(noteIndex);
|
|
607
|
+
await readmeService.regenerateSidebar();
|
|
608
|
+
},
|
|
609
|
+
logger2
|
|
610
|
+
);
|
|
611
|
+
} finally {
|
|
612
|
+
this.scheduleTimer(() => {
|
|
613
|
+
scheduler.setUpdating(false);
|
|
614
|
+
watchState.initializeFromDisk();
|
|
615
|
+
}, DELETE_REINIT_DELAY_MS);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
async handleFolderRenameUpdate(oldName, newName) {
|
|
619
|
+
const { scheduler, watchState, logger: logger2 } = this.config;
|
|
620
|
+
if (scheduler.getUpdating()) return;
|
|
621
|
+
scheduler.setUpdating(true);
|
|
622
|
+
try {
|
|
623
|
+
const { oldNoteIndex, newNoteIndex } = this.validateRenameIndexes(
|
|
624
|
+
oldName,
|
|
625
|
+
newName
|
|
626
|
+
);
|
|
627
|
+
if (!oldNoteIndex || !newNoteIndex) return;
|
|
628
|
+
logger2.info(`\u6B63\u5728\u5904\u7406\u6587\u4EF6\u5939\u91CD\u547D\u540D: ${oldName} \u2192 ${newName}`);
|
|
629
|
+
if (oldNoteIndex === newNoteIndex) {
|
|
630
|
+
await safeExecute(
|
|
631
|
+
`\u6807\u9898\u91CD\u547D\u540D ${newNoteIndex}`,
|
|
632
|
+
() => this.handleTitleOnlyRename(newNoteIndex, newName),
|
|
633
|
+
logger2
|
|
634
|
+
);
|
|
635
|
+
} else {
|
|
636
|
+
await safeExecute(
|
|
637
|
+
`\u7D22\u5F15\u53D8\u66F4 ${oldNoteIndex}\u2192${newNoteIndex}`,
|
|
638
|
+
() => this.handleIndexChangedRename(oldNoteIndex, newNoteIndex),
|
|
639
|
+
logger2
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
} finally {
|
|
643
|
+
this.scheduleTimer(() => {
|
|
644
|
+
scheduler.setUpdating(false);
|
|
645
|
+
watchState.initializeFromDisk();
|
|
646
|
+
}, UPDATE_UNLOCK_DELAY_MS);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
// #region - 私有实现
|
|
650
|
+
validateRenameIndexes(oldName, newName) {
|
|
651
|
+
const { noteIndexCache, logger: logger2 } = this.config;
|
|
652
|
+
const oldNoteIndex = this.extractNoteIndexOrWarn(oldName);
|
|
653
|
+
const newNoteIndex = this.extractNoteIndexOrWarn(newName);
|
|
654
|
+
if (!oldNoteIndex || !newNoteIndex) {
|
|
655
|
+
return { oldNoteIndex: null, newNoteIndex: null };
|
|
656
|
+
}
|
|
657
|
+
if (!/^\d{4}$/.test(newNoteIndex)) {
|
|
658
|
+
logger2.error(`\u65B0\u7B14\u8BB0\u7D22\u5F15\u683C\u5F0F\u975E\u6CD5: ${newNoteIndex}\uFF0C\u81EA\u52A8\u56DE\u9000`);
|
|
659
|
+
this.revertFolderRename(oldName, newName);
|
|
660
|
+
return { oldNoteIndex: null, newNoteIndex: null };
|
|
661
|
+
}
|
|
662
|
+
if (oldNoteIndex !== newNoteIndex && noteIndexCache.has(newNoteIndex)) {
|
|
663
|
+
logger2.error(`\u65B0\u7B14\u8BB0\u7D22\u5F15 ${newNoteIndex} \u5DF2\u5B58\u5728\uFF0C\u81EA\u52A8\u56DE\u9000`);
|
|
664
|
+
this.revertFolderRename(oldName, newName);
|
|
665
|
+
return { oldNoteIndex: null, newNoteIndex: null };
|
|
666
|
+
}
|
|
667
|
+
return { oldNoteIndex, newNoteIndex };
|
|
668
|
+
}
|
|
669
|
+
async handleTitleOnlyRename(noteIndex, newName) {
|
|
670
|
+
const { noteIndexCache, readmeService, logger: logger2 } = this.config;
|
|
671
|
+
logger2.info(`\u7B14\u8BB0\u7D22\u5F15\u672A\u53D8 (${noteIndex})\uFF0C\u53EA\u66F4\u65B0\u6807\u9898`);
|
|
672
|
+
noteIndexCache.updateFolderName(noteIndex, newName);
|
|
673
|
+
const item = noteIndexCache.getByNoteIndex(noteIndex);
|
|
674
|
+
if (item) {
|
|
675
|
+
await readmeService.updateNoteInReadme(noteIndex, item.noteConfig);
|
|
676
|
+
}
|
|
677
|
+
await readmeService.regenerateSidebar();
|
|
678
|
+
logger2.success(`\u6807\u9898\u66F4\u65B0\u5B8C\u6210`);
|
|
679
|
+
}
|
|
680
|
+
async handleIndexChangedRename(oldNoteIndex, newNoteIndex) {
|
|
681
|
+
const { noteService, noteIndexCache, readmeService, logger: logger2 } = this.config;
|
|
682
|
+
logger2.info(`\u7B14\u8BB0\u7D22\u5F15\u53D8\u66F4: ${oldNoteIndex} \u2192 ${newNoteIndex}`);
|
|
683
|
+
await readmeService.deleteNoteFromReadme(oldNoteIndex);
|
|
684
|
+
const newNote = noteService.getNoteByIndex(newNoteIndex);
|
|
685
|
+
if (newNote) {
|
|
686
|
+
noteIndexCache.delete(oldNoteIndex);
|
|
687
|
+
noteIndexCache.add(newNote);
|
|
688
|
+
await readmeService.appendNoteToReadme(newNoteIndex);
|
|
689
|
+
await readmeService.regenerateSidebar();
|
|
690
|
+
logger2.success(`\u7B14\u8BB0\u7D22\u5F15\u53D8\u66F4\u5904\u7406\u5B8C\u6210`);
|
|
691
|
+
} else {
|
|
692
|
+
logger2.error(`\u672A\u627E\u5230\u65B0\u7B14\u8BB0: ${newNoteIndex}`);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
async revertFolderRename(oldName, newName) {
|
|
696
|
+
const { notesDir, scheduler, watchState, logger: logger2 } = this.config;
|
|
697
|
+
try {
|
|
698
|
+
const oldPath = join3(notesDir, oldName);
|
|
699
|
+
const newPath = join3(notesDir, newName);
|
|
700
|
+
if (existsSync3(newPath)) {
|
|
701
|
+
scheduler.setUpdating(true);
|
|
702
|
+
await fsPromises.rename(newPath, oldPath);
|
|
703
|
+
logger2.warn(`\u6587\u4EF6\u5939\u5DF2\u56DE\u9000: ${newName} \u2192 ${oldName}`);
|
|
704
|
+
this.scheduleTimer(() => {
|
|
705
|
+
scheduler.setUpdating(false);
|
|
706
|
+
watchState.initializeFromDisk();
|
|
707
|
+
}, RENAME_REVERT_DELAY_MS);
|
|
708
|
+
}
|
|
709
|
+
} catch (error) {
|
|
710
|
+
logger2.error(`\u56DE\u9000\u6587\u4EF6\u5939\u91CD\u547D\u540D\u5931\u8D25: ${error}`);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
extractNoteIndexOrWarn(name) {
|
|
714
|
+
const noteIndex = name.match(/^(\d{4})/)?.[1] || null;
|
|
715
|
+
if (!noteIndex) {
|
|
716
|
+
this.config.logger.warn(`\u65E0\u6CD5\u4ECE\u6587\u4EF6\u5939\u540D\u79F0\u63D0\u53D6\u7B14\u8BB0\u7D22\u5F15: ${name}`);
|
|
717
|
+
}
|
|
718
|
+
return noteIndex;
|
|
719
|
+
}
|
|
720
|
+
// #endregion - 私有实现
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
// services/file-watcher/fsWatcherAdapter.ts
|
|
724
|
+
import { watch } from "fs";
|
|
725
|
+
import { basename, dirname, join as join4, sep } from "path";
|
|
726
|
+
var FsWatcherAdapter = class {
|
|
727
|
+
constructor(config) {
|
|
728
|
+
this.config = config;
|
|
729
|
+
/** 文件系统监听器实例 */
|
|
730
|
+
this.watcher = null;
|
|
731
|
+
}
|
|
732
|
+
start() {
|
|
733
|
+
const { logger: logger2 } = this.config;
|
|
734
|
+
if (this.watcher) {
|
|
735
|
+
logger2.warn("\u6587\u4EF6\u76D1\u542C\u670D\u52A1\u5DF2\u542F\u52A8");
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
this.watcher = watch(
|
|
739
|
+
this.config.notesDir,
|
|
740
|
+
{ recursive: true },
|
|
741
|
+
(eventType, filename) => this.handleFsEvent(eventType, filename)
|
|
742
|
+
);
|
|
743
|
+
logger2.debug(`\u6587\u4EF6\u76D1\u542C\u5DF2\u542F\u52A8\uFF0C\u76D1\u542C\u76EE\u5F55\uFF1A${this.config.notesDir}`);
|
|
744
|
+
}
|
|
745
|
+
stop() {
|
|
746
|
+
if (!this.watcher) return;
|
|
747
|
+
this.watcher.close();
|
|
748
|
+
this.watcher = null;
|
|
749
|
+
}
|
|
750
|
+
isWatching() {
|
|
751
|
+
return this.watcher !== null;
|
|
752
|
+
}
|
|
753
|
+
handleFsEvent(eventType, filename) {
|
|
754
|
+
const { isUpdating, onRename, onNoteEvent } = this.config;
|
|
755
|
+
if (!filename || // 忽略无文件变更
|
|
756
|
+
isUpdating()) {
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
if (eventType === "rename" && // 检测文件夹 rename 事件
|
|
760
|
+
!filename.includes(sep)) {
|
|
761
|
+
onRename(filename);
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
const baseFilename = basename(filename);
|
|
765
|
+
if (baseFilename !== "README.md" && baseFilename !== ".tnotes.json") {
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
const fullPath = join4(this.config.notesDir, filename);
|
|
769
|
+
const event = this.buildWatchEvent(fullPath, filename);
|
|
770
|
+
if (!event) {
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
onNoteEvent(event);
|
|
774
|
+
}
|
|
775
|
+
buildWatchEvent(fullPath, filename) {
|
|
776
|
+
const noteDirName = basename(dirname(fullPath));
|
|
777
|
+
const noteIndex = NoteManager.extractNoteIndex(noteDirName);
|
|
778
|
+
if (!noteIndex) {
|
|
779
|
+
NoteManager.warnInvalidNoteIndex(noteDirName);
|
|
780
|
+
return null;
|
|
781
|
+
}
|
|
782
|
+
const fileType = filename.endsWith("README.md") ? WATCH_EVENT_TYPES.README : WATCH_EVENT_TYPES.CONFIG;
|
|
783
|
+
return {
|
|
784
|
+
path: fullPath,
|
|
785
|
+
type: fileType,
|
|
786
|
+
noteIndex,
|
|
787
|
+
noteDirName,
|
|
788
|
+
noteDirPath: dirname(fullPath)
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
// core/ReadmeGenerator.ts
|
|
794
|
+
import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync4 } from "fs";
|
|
795
|
+
|
|
796
|
+
// core/TocGenerator.ts
|
|
797
|
+
var BILIBILI_VIDEO_BASE_URL = "https://www.bilibili.com/video/";
|
|
798
|
+
var TNOTES_YUQUE_BASE_URL = "https://www.yuque.com/tdahuyou/tnotes.yuque/";
|
|
799
|
+
var NOTES_TOC_START_TAG = "<!-- region:toc -->";
|
|
800
|
+
var NOTES_TOC_END_TAG = "<!-- endregion:toc -->";
|
|
801
|
+
var TocGenerator = class {
|
|
802
|
+
/**
|
|
803
|
+
* 更新笔记目录
|
|
804
|
+
* @param noteIndex - 笔记索引
|
|
805
|
+
* @param lines - 笔记内容行数组
|
|
806
|
+
* @param noteConfig - 笔记配置
|
|
807
|
+
* @param repoName - 仓库名称
|
|
808
|
+
*/
|
|
809
|
+
updateNoteToc(noteIndex, lines, noteConfig, repoName) {
|
|
810
|
+
let startLineIdx = -1, endLineIdx = -1;
|
|
811
|
+
lines.forEach((line, idx) => {
|
|
812
|
+
if (line.startsWith(NOTES_TOC_START_TAG)) startLineIdx = idx;
|
|
813
|
+
if (line.startsWith(NOTES_TOC_END_TAG)) endLineIdx = idx;
|
|
814
|
+
});
|
|
815
|
+
if (startLineIdx === -1 || endLineIdx === -1) return;
|
|
816
|
+
const titles = [];
|
|
817
|
+
const numberedHeaders = ["## ", "### "];
|
|
818
|
+
const unnumberedHeaders = ["#### ", "##### ", "###### "];
|
|
819
|
+
const addNumberToTitle = createAddNumberToTitle();
|
|
820
|
+
let inCodeBlock = false;
|
|
821
|
+
let inHtmlComment = false;
|
|
822
|
+
for (let i = 0; i < lines.length; i++) {
|
|
823
|
+
const line = lines[i];
|
|
824
|
+
if (line.trim().startsWith("```") || line.trim().startsWith("~~~")) {
|
|
825
|
+
inCodeBlock = !inCodeBlock;
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
if (line.trim().startsWith("<!--")) {
|
|
829
|
+
inHtmlComment = true;
|
|
830
|
+
}
|
|
831
|
+
if (line.trim().includes("-->")) {
|
|
832
|
+
inHtmlComment = false;
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
if (inCodeBlock || inHtmlComment) {
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
const isNumberedHeader = numberedHeaders.some(
|
|
839
|
+
(header) => line.startsWith(header)
|
|
840
|
+
);
|
|
841
|
+
const isUnnumberedHeader = unnumberedHeaders.some(
|
|
842
|
+
(header) => line.startsWith(header)
|
|
843
|
+
);
|
|
844
|
+
if (isNumberedHeader) {
|
|
845
|
+
const [numberedTitle] = addNumberToTitle(line);
|
|
846
|
+
titles.push(numberedTitle);
|
|
847
|
+
lines[i] = numberedTitle;
|
|
848
|
+
} else if (isUnnumberedHeader) {
|
|
849
|
+
const match = line.match(/^(\#+)\s*(\d+(\.\d+)*\.\s*)?(.*)/);
|
|
850
|
+
if (match) {
|
|
851
|
+
const headerSymbol = match[1];
|
|
852
|
+
const plainTitle = match[4];
|
|
853
|
+
const cleanTitle = `${headerSymbol} ${plainTitle}`;
|
|
854
|
+
titles.push(cleanTitle);
|
|
855
|
+
lines[i] = cleanTitle;
|
|
856
|
+
} else {
|
|
857
|
+
titles.push(line);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
const toc = generateToc(titles);
|
|
862
|
+
const bilibiliTOCItems = [];
|
|
863
|
+
const tnotesTOCItems = [];
|
|
864
|
+
const yuqueTOCItems = [];
|
|
865
|
+
if (noteConfig) {
|
|
866
|
+
if (noteConfig.bilibili.length > 0) {
|
|
867
|
+
noteConfig.bilibili.forEach((bvid, i) => {
|
|
868
|
+
bilibiliTOCItems.push(
|
|
869
|
+
` - [bilibili.${repoName}.${noteIndex}.${i + 1}](${BILIBILI_VIDEO_BASE_URL + bvid})`
|
|
870
|
+
);
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
if (noteConfig.tnotes && noteConfig.tnotes.length > 0) {
|
|
874
|
+
tnotesTOCItems.push(
|
|
875
|
+
`- [\u{1F4D2} TNotes\uFF08\u76F8\u5173\u77E5\u8BC6\u5E93\uFF09](https://tnotesjs.github.io/TNotes/)`
|
|
876
|
+
);
|
|
877
|
+
noteConfig.tnotes.forEach((repoName2) => {
|
|
878
|
+
tnotesTOCItems.push(
|
|
879
|
+
` - [TNotes.${repoName2}](https://tnotesjs.github.io/TNotes.${repoName2}/)`
|
|
880
|
+
);
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
if (noteConfig.yuque.length > 0) {
|
|
884
|
+
noteConfig.yuque.forEach((slug, i) => {
|
|
885
|
+
yuqueTOCItems.push(
|
|
886
|
+
` - [TNotes.yuque.${repoName.replace(
|
|
887
|
+
"TNotes.",
|
|
888
|
+
""
|
|
889
|
+
)}.${noteIndex}](${TNOTES_YUQUE_BASE_URL + slug})`
|
|
890
|
+
);
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
const insertTocItems = [];
|
|
895
|
+
const hasExternalResources = bilibiliTOCItems.length > 0 || tnotesTOCItems.length > 0 || yuqueTOCItems.length > 0;
|
|
896
|
+
if (hasExternalResources) {
|
|
897
|
+
insertTocItems.push("::: details \u{1F4DA} \u76F8\u5173\u8D44\u6E90", "");
|
|
898
|
+
if (bilibiliTOCItems.length > 0) {
|
|
899
|
+
insertTocItems.push(
|
|
900
|
+
`- [\u{1F4FA} bilibili\uFF08\u7B14\u8BB0\u89C6\u9891\u8D44\u6E90\uFF09](https://space.bilibili.com/407241004)`,
|
|
901
|
+
...bilibiliTOCItems
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
if (tnotesTOCItems.length > 0) {
|
|
905
|
+
insertTocItems.push(...tnotesTOCItems);
|
|
906
|
+
}
|
|
907
|
+
if (yuqueTOCItems.length > 0) {
|
|
908
|
+
insertTocItems.push(
|
|
909
|
+
`- [\u{1F4C2} TNotes.yuque\uFF08\u7B14\u8BB0\u9644\u4EF6\u8D44\u6E90\uFF09](${TNOTES_YUQUE_BASE_URL})`,
|
|
910
|
+
...yuqueTOCItems
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
insertTocItems.push("", ":::", "");
|
|
914
|
+
}
|
|
915
|
+
lines.splice(
|
|
916
|
+
startLineIdx + 1,
|
|
917
|
+
endLineIdx - startLineIdx - 1,
|
|
918
|
+
"",
|
|
919
|
+
...insertTocItems,
|
|
920
|
+
...toc.replace(new RegExp(`^${EOL}`), "").split(EOL)
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* 更新首页目录
|
|
925
|
+
* @param lines - 首页内容行数组
|
|
926
|
+
* @param titles - 标题数组
|
|
927
|
+
* @param titlesNotesCount - 每个标题下的笔记数量
|
|
928
|
+
*/
|
|
929
|
+
updateHomeToc(lines, titles, titlesNotesCount) {
|
|
930
|
+
let startLineIdx = -1, endLineIdx = -1;
|
|
931
|
+
lines.forEach((line, idx) => {
|
|
932
|
+
if (line.startsWith(NOTES_TOC_START_TAG)) startLineIdx = idx;
|
|
933
|
+
if (line.startsWith(NOTES_TOC_END_TAG)) endLineIdx = idx;
|
|
934
|
+
});
|
|
935
|
+
if (startLineIdx === -1 || endLineIdx === -1) return;
|
|
936
|
+
const toc = generateToc(titles);
|
|
937
|
+
lines.splice(
|
|
938
|
+
startLineIdx + 1,
|
|
939
|
+
endLineIdx - startLineIdx - 1,
|
|
940
|
+
...toc.split(EOL)
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
};
|
|
944
|
+
|
|
945
|
+
// core/ReadmeGenerator.ts
|
|
946
|
+
var ReadmeGenerator = class {
|
|
947
|
+
constructor() {
|
|
948
|
+
this.tocGenerator = new TocGenerator();
|
|
949
|
+
this.configManager = ConfigManager.getInstance();
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* 更新笔记 README
|
|
953
|
+
* @param noteInfo - 笔记信息
|
|
954
|
+
*/
|
|
955
|
+
updateNoteReadme(noteInfo) {
|
|
956
|
+
if (!noteInfo.config) {
|
|
957
|
+
logger.warn(`\u7B14\u8BB0 ${noteInfo.dirName} \u7F3A\u5C11\u914D\u7F6E\u6587\u4EF6`);
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
const content = readFileSync2(noteInfo.readmePath, "utf-8");
|
|
961
|
+
if (content.length === 0) return;
|
|
962
|
+
const lines = content.split(EOL);
|
|
963
|
+
const repoName = this.configManager.get("repoName");
|
|
964
|
+
this.tocGenerator.updateNoteToc(
|
|
965
|
+
noteInfo.index,
|
|
966
|
+
lines,
|
|
967
|
+
noteInfo.config,
|
|
968
|
+
repoName
|
|
969
|
+
);
|
|
970
|
+
const updatedContent = lines.join(EOL);
|
|
971
|
+
writeFileSync(noteInfo.readmePath, updatedContent, "utf-8");
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* 更新首页 README
|
|
975
|
+
* 更新笔记链接的状态标记([x] 或 [ ]),同时更新 TOC 区域
|
|
976
|
+
* @param notes - 笔记信息数组
|
|
977
|
+
* @param homeReadmePath - 首页 README 路径
|
|
978
|
+
*/
|
|
979
|
+
updateHomeReadme(notes, homeReadmePath) {
|
|
980
|
+
if (!existsSync4(homeReadmePath)) {
|
|
981
|
+
logger.error(`\u6839\u76EE\u5F55\u4E0B\u7684 README.md \u6587\u4EF6\u672A\u627E\u5230\uFF1A${homeReadmePath}`);
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
const content = readFileSync2(homeReadmePath, "utf-8");
|
|
985
|
+
const lines = content.split(EOL);
|
|
986
|
+
const noteByIndexMap = /* @__PURE__ */ new Map();
|
|
987
|
+
for (const note of notes) {
|
|
988
|
+
noteByIndexMap.set(note.index, note);
|
|
989
|
+
}
|
|
990
|
+
const repoOwner = this.configManager.get("author");
|
|
991
|
+
const repoName = this.configManager.get("repoName");
|
|
992
|
+
const existingNoteIndexes = /* @__PURE__ */ new Set();
|
|
993
|
+
const linesToRemove = /* @__PURE__ */ new Set();
|
|
994
|
+
const titles = [];
|
|
995
|
+
const titlesNotesCount = [];
|
|
996
|
+
let inTocRegion = false;
|
|
997
|
+
let currentNoteCount = 0;
|
|
998
|
+
const addNumberToTitle = createAddNumberToTitle();
|
|
999
|
+
const numberedHeaders = ["## ", "### "];
|
|
1000
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1001
|
+
const line = lines[i];
|
|
1002
|
+
if (line.includes("<!-- region:toc -->")) {
|
|
1003
|
+
inTocRegion = true;
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
if (line.includes("<!-- endregion:toc -->")) {
|
|
1007
|
+
inTocRegion = false;
|
|
1008
|
+
continue;
|
|
1009
|
+
}
|
|
1010
|
+
if (inTocRegion) {
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
1013
|
+
const parsed = parseNoteLine(line);
|
|
1014
|
+
if (parsed.isMatch && parsed.noteIndex) {
|
|
1015
|
+
const note = noteByIndexMap.get(parsed.noteIndex);
|
|
1016
|
+
if (!note) {
|
|
1017
|
+
linesToRemove.add(i);
|
|
1018
|
+
logger.warn(`\u79FB\u9664\u4E0D\u5B58\u5728\u7684\u7B14\u8BB0: ${parsed.noteIndex}`);
|
|
1019
|
+
continue;
|
|
1020
|
+
}
|
|
1021
|
+
existingNoteIndexes.add(parsed.noteIndex);
|
|
1022
|
+
lines[i] = buildNoteLineMarkdown(note, repoOwner, repoName);
|
|
1023
|
+
currentNoteCount++;
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1026
|
+
const titleMatch = line.match(/^(#{2,})\s+(.+)$/);
|
|
1027
|
+
if (titleMatch) {
|
|
1028
|
+
const isNumberedHeader = numberedHeaders.some(
|
|
1029
|
+
(header) => line.startsWith(header)
|
|
1030
|
+
);
|
|
1031
|
+
if (isNumberedHeader) {
|
|
1032
|
+
const [numberedTitle] = addNumberToTitle(line);
|
|
1033
|
+
lines[i] = numberedTitle;
|
|
1034
|
+
if (titles.length > 0) {
|
|
1035
|
+
titlesNotesCount.push(currentNoteCount);
|
|
1036
|
+
}
|
|
1037
|
+
titles.push(numberedTitle);
|
|
1038
|
+
currentNoteCount = 0;
|
|
1039
|
+
} else {
|
|
1040
|
+
if (titles.length > 0) {
|
|
1041
|
+
titlesNotesCount.push(currentNoteCount);
|
|
1042
|
+
}
|
|
1043
|
+
titles.push(line);
|
|
1044
|
+
currentNoteCount = 0;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
const sortedLinesToRemove = Array.from(linesToRemove).sort((a, b) => b - a);
|
|
1049
|
+
for (const lineIndex of sortedLinesToRemove) {
|
|
1050
|
+
lines.splice(lineIndex, 1);
|
|
1051
|
+
if (currentNoteCount > 0) {
|
|
1052
|
+
currentNoteCount--;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
const missingNotes = [];
|
|
1056
|
+
for (const note of notes) {
|
|
1057
|
+
if (!existingNoteIndexes.has(note.index)) {
|
|
1058
|
+
missingNotes.push(note);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
if (missingNotes.length > 0) {
|
|
1062
|
+
logger.info(`\u6DFB\u52A0 ${missingNotes.length} \u7BC7\u7F3A\u5931\u7684\u7B14\u8BB0\u5230 README`);
|
|
1063
|
+
missingNotes.sort((a, b) => a.index.localeCompare(b.index));
|
|
1064
|
+
for (const note of missingNotes) {
|
|
1065
|
+
const noteLine = buildNoteLineMarkdown(note, repoOwner, repoName);
|
|
1066
|
+
lines.push(noteLine);
|
|
1067
|
+
currentNoteCount++;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
if (titles.length > 0) {
|
|
1071
|
+
titlesNotesCount.push(currentNoteCount);
|
|
1072
|
+
}
|
|
1073
|
+
this.tocGenerator.updateHomeToc(lines, titles, titlesNotesCount);
|
|
1074
|
+
const processedLines = processEmptyLines(lines);
|
|
1075
|
+
const updatedContent = processedLines.join(EOL);
|
|
1076
|
+
writeFileSync(homeReadmePath, updatedContent, "utf-8");
|
|
1077
|
+
logger.info("\u5DF2\u66F4\u65B0\u9996\u9875 README");
|
|
1078
|
+
}
|
|
1079
|
+
};
|
|
1080
|
+
|
|
1081
|
+
// core/NoteIndexCache.ts
|
|
1082
|
+
import { join as join5 } from "path";
|
|
1083
|
+
var NoteIndexCache = class _NoteIndexCache {
|
|
1084
|
+
constructor() {
|
|
1085
|
+
/** noteIndex -> NoteIndexItem 的映射 */
|
|
1086
|
+
this.byNoteIndex = /* @__PURE__ */ new Map();
|
|
1087
|
+
/** configId (UUID) -> noteIndex 的映射,用于快速反向查询 */
|
|
1088
|
+
this.byConfigId = /* @__PURE__ */ new Map();
|
|
1089
|
+
/** 是否已完成初始化 */
|
|
1090
|
+
this._initialized = false;
|
|
1091
|
+
}
|
|
1092
|
+
static {
|
|
1093
|
+
this.instance = null;
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* 获取单例实例
|
|
1097
|
+
*/
|
|
1098
|
+
static getInstance() {
|
|
1099
|
+
if (!_NoteIndexCache.instance) {
|
|
1100
|
+
_NoteIndexCache.instance = new _NoteIndexCache();
|
|
1101
|
+
}
|
|
1102
|
+
return _NoteIndexCache.instance;
|
|
1103
|
+
}
|
|
1104
|
+
/**
|
|
1105
|
+
* 初始化索引缓存
|
|
1106
|
+
* @param notes - 扫描得到的笔记列表(已由 NoteManager.scanNotes 完成重复检测)
|
|
1107
|
+
*/
|
|
1108
|
+
initialize(notes) {
|
|
1109
|
+
this.byNoteIndex.clear();
|
|
1110
|
+
this.byConfigId.clear();
|
|
1111
|
+
for (const note of notes) {
|
|
1112
|
+
const item = {
|
|
1113
|
+
noteIndex: note.index,
|
|
1114
|
+
folderName: note.dirName,
|
|
1115
|
+
noteConfig: note.config
|
|
1116
|
+
};
|
|
1117
|
+
this.byNoteIndex.set(note.index, item);
|
|
1118
|
+
this.byConfigId.set(note.config.id, note.index);
|
|
1119
|
+
}
|
|
1120
|
+
this._initialized = true;
|
|
1121
|
+
}
|
|
1122
|
+
/**
|
|
1123
|
+
* 是否已完成初始化
|
|
1124
|
+
*/
|
|
1125
|
+
isInitialized() {
|
|
1126
|
+
return this._initialized;
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* 从缓存构建 NoteInfo 列表(纯内存,零 I/O)
|
|
1130
|
+
* @returns 笔记信息数组
|
|
1131
|
+
*/
|
|
1132
|
+
toNoteInfoList() {
|
|
1133
|
+
const result = [];
|
|
1134
|
+
for (const item of this.byNoteIndex.values()) {
|
|
1135
|
+
const notePath = join5(NOTES_PATH, item.folderName);
|
|
1136
|
+
result.push({
|
|
1137
|
+
index: item.noteIndex,
|
|
1138
|
+
path: notePath,
|
|
1139
|
+
dirName: item.folderName,
|
|
1140
|
+
readmePath: join5(notePath, "README.md"),
|
|
1141
|
+
configPath: join5(notePath, ".tnotes.json"),
|
|
1142
|
+
config: item.noteConfig
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
return result;
|
|
1146
|
+
}
|
|
1147
|
+
/**
|
|
1148
|
+
* 根据 noteIndex 获取索引项
|
|
1149
|
+
*/
|
|
1150
|
+
getByNoteIndex(noteIndex) {
|
|
1151
|
+
return this.byNoteIndex.get(noteIndex);
|
|
1152
|
+
}
|
|
1153
|
+
/**
|
|
1154
|
+
* 根据 configId (UUID) 获取索引项
|
|
1155
|
+
*/
|
|
1156
|
+
getByConfigId(configId) {
|
|
1157
|
+
const noteIndex = this.byConfigId.get(configId);
|
|
1158
|
+
return noteIndex ? this.byNoteIndex.get(noteIndex) : void 0;
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
1161
|
+
* 检查 noteIndex 是否存在
|
|
1162
|
+
*/
|
|
1163
|
+
has(noteIndex) {
|
|
1164
|
+
return this.byNoteIndex.has(noteIndex);
|
|
1165
|
+
}
|
|
1166
|
+
/**
|
|
1167
|
+
* 更新笔记配置
|
|
1168
|
+
* @param noteIndex - 笔记索引
|
|
1169
|
+
* @param configUpdates - 要更新的配置字段
|
|
1170
|
+
*/
|
|
1171
|
+
updateConfig(noteIndex, configUpdates) {
|
|
1172
|
+
const item = this.byNoteIndex.get(noteIndex);
|
|
1173
|
+
if (!item) {
|
|
1174
|
+
logger.warn(`\u5C1D\u8BD5\u66F4\u65B0\u4E0D\u5B58\u5728\u7684\u7B14\u8BB0: ${noteIndex}`);
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
Object.assign(item.noteConfig, configUpdates);
|
|
1178
|
+
item.noteConfig.updated_at = Date.now();
|
|
1179
|
+
logger.debug(`\u66F4\u65B0\u7B14\u8BB0\u914D\u7F6E: ${noteIndex}`, configUpdates);
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* 删除笔记
|
|
1183
|
+
* @param noteIndex - 笔记索引
|
|
1184
|
+
*/
|
|
1185
|
+
delete(noteIndex) {
|
|
1186
|
+
const item = this.byNoteIndex.get(noteIndex);
|
|
1187
|
+
if (!item) {
|
|
1188
|
+
logger.warn(`\u5C1D\u8BD5\u5220\u9664\u4E0D\u5B58\u5728\u7684\u7B14\u8BB0: ${noteIndex}`);
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
this.byNoteIndex.delete(noteIndex);
|
|
1192
|
+
this.byConfigId.delete(item.noteConfig.id);
|
|
1193
|
+
logger.info(`\u5220\u9664\u7B14\u8BB0\u7D22\u5F15: ${noteIndex}`);
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* 添加新笔记
|
|
1197
|
+
* @param note - 笔记信息
|
|
1198
|
+
*/
|
|
1199
|
+
add(note) {
|
|
1200
|
+
const item = {
|
|
1201
|
+
noteIndex: note.index,
|
|
1202
|
+
folderName: note.dirName,
|
|
1203
|
+
noteConfig: note.config
|
|
1204
|
+
};
|
|
1205
|
+
this.byNoteIndex.set(note.index, item);
|
|
1206
|
+
this.byConfigId.set(note.config.id, note.index);
|
|
1207
|
+
logger.info(`\u6DFB\u52A0\u7B14\u8BB0\u7D22\u5F15: ${note.index}`);
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* 更新笔记的文件夹名称(标题变更时)
|
|
1211
|
+
* @param noteIndex - 笔记索引
|
|
1212
|
+
* @param newFolderName - 新的文件夹名称
|
|
1213
|
+
*/
|
|
1214
|
+
updateFolderName(noteIndex, newFolderName) {
|
|
1215
|
+
const item = this.byNoteIndex.get(noteIndex);
|
|
1216
|
+
if (!item) {
|
|
1217
|
+
logger.warn(`\u5C1D\u8BD5\u66F4\u65B0\u4E0D\u5B58\u5728\u7684\u7B14\u8BB0: ${noteIndex}`);
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
item.folderName = newFolderName;
|
|
1221
|
+
logger.debug(`\u66F4\u65B0\u7B14\u8BB0\u6587\u4EF6\u5939\u540D\u79F0: ${noteIndex} -> ${newFolderName}`);
|
|
1222
|
+
}
|
|
1223
|
+
};
|
|
1224
|
+
|
|
1225
|
+
// services/readme/service.ts
|
|
1226
|
+
import {
|
|
1227
|
+
existsSync as existsSync5,
|
|
1228
|
+
readFileSync as readFileSync3,
|
|
1229
|
+
writeFileSync as writeFileSync2,
|
|
1230
|
+
promises as fsPromises2
|
|
1231
|
+
} from "fs";
|
|
1232
|
+
var ReadmeService = class _ReadmeService {
|
|
1233
|
+
constructor() {
|
|
1234
|
+
this.noteManager = NoteManager.getInstance();
|
|
1235
|
+
this.readmeGenerator = new ReadmeGenerator();
|
|
1236
|
+
this.configManager = ConfigManager.getInstance();
|
|
1237
|
+
this.noteIndexCache = NoteIndexCache.getInstance();
|
|
1238
|
+
}
|
|
1239
|
+
static getInstance() {
|
|
1240
|
+
if (!_ReadmeService.instance) {
|
|
1241
|
+
_ReadmeService.instance = new _ReadmeService();
|
|
1242
|
+
}
|
|
1243
|
+
return _ReadmeService.instance;
|
|
1244
|
+
}
|
|
1245
|
+
/**
|
|
1246
|
+
* 更新所有笔记的 README
|
|
1247
|
+
* @param options - 更新选项
|
|
1248
|
+
*/
|
|
1249
|
+
async updateAllReadmes(options = {}) {
|
|
1250
|
+
const {
|
|
1251
|
+
updateSidebar = true,
|
|
1252
|
+
updateHome = true,
|
|
1253
|
+
notes: providedNotes
|
|
1254
|
+
} = options;
|
|
1255
|
+
logger.info("\u5F00\u59CB\u66F4\u65B0\u77E5\u8BC6\u5E93...");
|
|
1256
|
+
const notes = providedNotes ?? this.noteManager.scanNotes();
|
|
1257
|
+
logger.info(`\u626B\u63CF\u5230 ${notes.length} \u7BC7\u7B14\u8BB0`);
|
|
1258
|
+
const changedIndexes = await this.getChangedNoteIndexes();
|
|
1259
|
+
const shouldIncrementalUpdate = changedIndexes.size > 0 && changedIndexes.size < notes.length * 0.3;
|
|
1260
|
+
let notesToUpdate = notes;
|
|
1261
|
+
if (shouldIncrementalUpdate) {
|
|
1262
|
+
notesToUpdate = notes.filter((note) => changedIndexes.has(note.index));
|
|
1263
|
+
logger.info(
|
|
1264
|
+
`\u68C0\u6D4B\u5230 ${changedIndexes.size} \u7BC7\u7B14\u8BB0\u6709\u53D8\u66F4\uFF0C\u4F7F\u7528\u589E\u91CF\u66F4\u65B0\u6A21\u5F0F`
|
|
1265
|
+
);
|
|
1266
|
+
} else {
|
|
1267
|
+
logger.info("\u4F7F\u7528\u5168\u91CF\u66F4\u65B0\u6A21\u5F0F");
|
|
1268
|
+
}
|
|
1269
|
+
const startTime = Date.now();
|
|
1270
|
+
await this.updateNoteReadmesInParallel(notesToUpdate);
|
|
1271
|
+
const updateTime = Date.now() - startTime;
|
|
1272
|
+
logger.info(`\u66F4\u65B0\u4E86 ${notesToUpdate.length} \u7BC7\u7B14\u8BB0 (\u8017\u65F6 ${updateTime}ms)`);
|
|
1273
|
+
if (updateHome) {
|
|
1274
|
+
await this.updateHomeReadme(notes);
|
|
1275
|
+
}
|
|
1276
|
+
if (updateSidebar) {
|
|
1277
|
+
await this.updateSidebar(notes);
|
|
1278
|
+
}
|
|
1279
|
+
logger.info("\u77E5\u8BC6\u5E93\u66F4\u65B0\u5B8C\u6210\uFF01");
|
|
1280
|
+
}
|
|
1281
|
+
/**
|
|
1282
|
+
* 只更新指定笔记的 README(不更新 sidebar、home)
|
|
1283
|
+
* @param noteIndexes - 笔记索引数组,例如 ['0001', '0002']
|
|
1284
|
+
*/
|
|
1285
|
+
async updateNoteReadmesOnly(noteIndexes) {
|
|
1286
|
+
if (noteIndexes.length === 0) return;
|
|
1287
|
+
const notesToUpdate = [];
|
|
1288
|
+
for (const noteIndex of noteIndexes) {
|
|
1289
|
+
const note = this.noteManager.getNoteByIndex(noteIndex);
|
|
1290
|
+
if (note) {
|
|
1291
|
+
notesToUpdate.push(note);
|
|
1292
|
+
} else {
|
|
1293
|
+
logger.warn(`\u7B14\u8BB0\u672A\u627E\u5230: ${noteIndex}`);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
if (notesToUpdate.length === 0) {
|
|
1297
|
+
logger.warn("\u6CA1\u6709\u627E\u5230\u9700\u8981\u66F4\u65B0\u7684\u7B14\u8BB0");
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
for (const note of notesToUpdate) {
|
|
1301
|
+
try {
|
|
1302
|
+
this.readmeGenerator.updateNoteReadme(note);
|
|
1303
|
+
} catch (error) {
|
|
1304
|
+
logger.error(`\u66F4\u65B0\u7B14\u8BB0 ${note.dirName} \u5931\u8D25`, error);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1309
|
+
* 获取变更的笔记索引集合
|
|
1310
|
+
* @returns 变更的笔记索引集合
|
|
1311
|
+
*/
|
|
1312
|
+
async getChangedNoteIndexes() {
|
|
1313
|
+
try {
|
|
1314
|
+
return getChangedIds();
|
|
1315
|
+
} catch (error) {
|
|
1316
|
+
return /* @__PURE__ */ new Set();
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
/**
|
|
1320
|
+
* 并行更新多个笔记的 README
|
|
1321
|
+
* @param notes - 笔记信息数组
|
|
1322
|
+
*/
|
|
1323
|
+
async updateNoteReadmesInParallel(notes) {
|
|
1324
|
+
const batchSize = 10;
|
|
1325
|
+
const batches = [];
|
|
1326
|
+
for (let i = 0; i < notes.length; i += batchSize) {
|
|
1327
|
+
batches.push(notes.slice(i, i + batchSize));
|
|
1328
|
+
}
|
|
1329
|
+
let successCount = 0;
|
|
1330
|
+
let failCount = 0;
|
|
1331
|
+
for (const batch of batches) {
|
|
1332
|
+
const results = await Promise.allSettled(
|
|
1333
|
+
batch.map(
|
|
1334
|
+
(note) => Promise.resolve().then(() => {
|
|
1335
|
+
this.readmeGenerator.updateNoteReadme(note);
|
|
1336
|
+
})
|
|
1337
|
+
)
|
|
1338
|
+
);
|
|
1339
|
+
for (const result of results) {
|
|
1340
|
+
if (result.status === "fulfilled") {
|
|
1341
|
+
successCount++;
|
|
1342
|
+
} else {
|
|
1343
|
+
failCount++;
|
|
1344
|
+
logger.error("\u66F4\u65B0\u7B14\u8BB0\u5931\u8D25", result.reason);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
if (failCount > 0) {
|
|
1349
|
+
logger.warn(`\u66F4\u65B0\u5B8C\u6210\uFF1A\u6210\u529F ${successCount} \u7BC7\uFF0C\u5931\u8D25 ${failCount} \u7BC7`);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
/**
|
|
1353
|
+
* 更新侧边栏配置
|
|
1354
|
+
* @param notes - 笔记信息数组
|
|
1355
|
+
*/
|
|
1356
|
+
async updateSidebar(notes) {
|
|
1357
|
+
if (!existsSync5(ROOT_README_PATH)) {
|
|
1358
|
+
logger.error("\u672A\u627E\u5230\u9996\u9875 README\uFF0C\u65E0\u6CD5\u751F\u6210\u4FA7\u8FB9\u680F");
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
const content = readFileSync3(ROOT_README_PATH, "utf-8");
|
|
1362
|
+
const lines = content.split("\n");
|
|
1363
|
+
const itemList = [];
|
|
1364
|
+
const titles = [];
|
|
1365
|
+
const titlesNotesCount = [];
|
|
1366
|
+
let currentNoteCount = 0;
|
|
1367
|
+
let inTocRegion = false;
|
|
1368
|
+
for (const line of lines) {
|
|
1369
|
+
if (line.includes("<!-- region:toc -->")) {
|
|
1370
|
+
inTocRegion = true;
|
|
1371
|
+
continue;
|
|
1372
|
+
}
|
|
1373
|
+
if (line.includes("<!-- endregion:toc -->")) {
|
|
1374
|
+
inTocRegion = false;
|
|
1375
|
+
continue;
|
|
1376
|
+
}
|
|
1377
|
+
if (inTocRegion) {
|
|
1378
|
+
continue;
|
|
1379
|
+
}
|
|
1380
|
+
const parsed = parseNoteLine(line);
|
|
1381
|
+
if (parsed.isMatch && parsed.noteIndex) {
|
|
1382
|
+
const note = notes.find((n) => n.index === parsed.noteIndex);
|
|
1383
|
+
if (!note) {
|
|
1384
|
+
logger.warn(`\u672A\u627E\u5230\u7B14\u8BB0\u7D22\u5F15: ${parsed.noteIndex}`);
|
|
1385
|
+
continue;
|
|
1386
|
+
}
|
|
1387
|
+
let statusEmoji = "\u23F0 ";
|
|
1388
|
+
if (note?.config) {
|
|
1389
|
+
if (note.config.done) {
|
|
1390
|
+
statusEmoji = "\u2705 ";
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
const sidebarShowNoteId = this.configManager.get("sidebarShowNoteId");
|
|
1394
|
+
let displayText = note.dirName;
|
|
1395
|
+
if (!sidebarShowNoteId) {
|
|
1396
|
+
displayText = note.dirName.replace(/^\d{4}\.\s/, "");
|
|
1397
|
+
}
|
|
1398
|
+
itemList.push({
|
|
1399
|
+
text: statusEmoji + displayText,
|
|
1400
|
+
link: `/notes/${note.dirName}/README`
|
|
1401
|
+
});
|
|
1402
|
+
currentNoteCount++;
|
|
1403
|
+
continue;
|
|
1404
|
+
}
|
|
1405
|
+
const titleMatch = line.match(/^(#{2,})\s+(.+)$/);
|
|
1406
|
+
if (titleMatch) {
|
|
1407
|
+
if (titles.length > 0) {
|
|
1408
|
+
titlesNotesCount.push(currentNoteCount);
|
|
1409
|
+
}
|
|
1410
|
+
titles.push(line);
|
|
1411
|
+
currentNoteCount = 0;
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
if (titles.length > 0) {
|
|
1415
|
+
titlesNotesCount.push(currentNoteCount);
|
|
1416
|
+
}
|
|
1417
|
+
const sidebarIsCollapsed = true;
|
|
1418
|
+
const hierarchicalSidebar = genHierarchicalSidebar(
|
|
1419
|
+
itemList,
|
|
1420
|
+
titles,
|
|
1421
|
+
titlesNotesCount,
|
|
1422
|
+
sidebarIsCollapsed
|
|
1423
|
+
);
|
|
1424
|
+
writeFileSync2(
|
|
1425
|
+
VP_SIDEBAR_PATH,
|
|
1426
|
+
JSON.stringify(hierarchicalSidebar, null, 2),
|
|
1427
|
+
"utf-8"
|
|
1428
|
+
);
|
|
1429
|
+
logger.info("\u5DF2\u66F4\u65B0\u4FA7\u8FB9\u680F\u914D\u7F6E");
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* 更新首页 README
|
|
1433
|
+
* @param notes - 笔记信息数组
|
|
1434
|
+
*/
|
|
1435
|
+
async updateHomeReadme(notes) {
|
|
1436
|
+
this.readmeGenerator.updateHomeReadme(notes, ROOT_README_PATH);
|
|
1437
|
+
}
|
|
1438
|
+
/**
|
|
1439
|
+
* 增量更新首页 README 中的单个笔记
|
|
1440
|
+
* @param noteIndex - 笔记索引
|
|
1441
|
+
* @param updates - 需要更新的配置字段
|
|
1442
|
+
*/
|
|
1443
|
+
async updateNoteInReadme(noteIndex, updates) {
|
|
1444
|
+
const item = this.noteIndexCache.getByNoteIndex(noteIndex);
|
|
1445
|
+
if (!item) {
|
|
1446
|
+
logger.warn(`\u5C1D\u8BD5\u66F4\u65B0\u4E0D\u5B58\u5728\u7684\u7B14\u8BB0: ${noteIndex}`);
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
const content = await fsPromises2.readFile(ROOT_README_PATH, "utf-8");
|
|
1450
|
+
const lines = content.split("\n");
|
|
1451
|
+
const repoOwner = this.configManager.get("author");
|
|
1452
|
+
const repoName = this.configManager.get("repoName");
|
|
1453
|
+
const mergedConfig = { ...item.noteConfig, ...updates };
|
|
1454
|
+
const tempNoteInfo = {
|
|
1455
|
+
index: noteIndex,
|
|
1456
|
+
dirName: item.folderName,
|
|
1457
|
+
path: "",
|
|
1458
|
+
readmePath: "",
|
|
1459
|
+
configPath: "",
|
|
1460
|
+
config: mergedConfig
|
|
1461
|
+
};
|
|
1462
|
+
let updated = false;
|
|
1463
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1464
|
+
const parsed = parseNoteLine(lines[i]);
|
|
1465
|
+
if (parsed.noteIndex === noteIndex) {
|
|
1466
|
+
lines[i] = buildNoteLineMarkdown(tempNoteInfo, repoOwner, repoName);
|
|
1467
|
+
updated = true;
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
if (updated) {
|
|
1471
|
+
await fsPromises2.writeFile(ROOT_README_PATH, lines.join("\n"), "utf-8");
|
|
1472
|
+
logger.info(`\u589E\u91CF\u66F4\u65B0 README.md \u4E2D\u7684\u7B14\u8BB0: ${noteIndex}`);
|
|
1473
|
+
} else {
|
|
1474
|
+
logger.warn(`README.md \u4E2D\u672A\u627E\u5230\u7B14\u8BB0: ${noteIndex}`);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
/**
|
|
1478
|
+
* 从首页 README 中删除笔记
|
|
1479
|
+
* @param noteIndex - 笔记索引
|
|
1480
|
+
*/
|
|
1481
|
+
async deleteNoteFromReadme(noteIndex) {
|
|
1482
|
+
const content = await fsPromises2.readFile(ROOT_README_PATH, "utf-8");
|
|
1483
|
+
const lines = content.split("\n");
|
|
1484
|
+
const linesToRemove = [];
|
|
1485
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1486
|
+
const parsed = parseNoteLine(lines[i]);
|
|
1487
|
+
if (parsed.noteIndex === noteIndex) {
|
|
1488
|
+
linesToRemove.push(i);
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
if (linesToRemove.length > 0) {
|
|
1492
|
+
for (let i = linesToRemove.length - 1; i >= 0; i--) {
|
|
1493
|
+
lines.splice(linesToRemove[i], 1);
|
|
1494
|
+
}
|
|
1495
|
+
await fsPromises2.writeFile(ROOT_README_PATH, lines.join("\n"), "utf-8");
|
|
1496
|
+
logger.info(
|
|
1497
|
+
`\u4ECE README.md \u4E2D\u5220\u9664\u7B14\u8BB0: ${noteIndex} (${linesToRemove.length} \u5904\u5F15\u7528)`
|
|
1498
|
+
);
|
|
1499
|
+
} else {
|
|
1500
|
+
logger.warn(`README.md \u4E2D\u672A\u627E\u5230\u7B14\u8BB0: ${noteIndex}`);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
/**
|
|
1504
|
+
* 在首页 README 末尾添加新笔记
|
|
1505
|
+
* @param noteIndex - 笔记索引
|
|
1506
|
+
*/
|
|
1507
|
+
async appendNoteToReadme(noteIndex) {
|
|
1508
|
+
const item = this.noteIndexCache.getByNoteIndex(noteIndex);
|
|
1509
|
+
if (!item) {
|
|
1510
|
+
logger.warn(`\u5C1D\u8BD5\u6DFB\u52A0\u4E0D\u5B58\u5728\u7684\u7B14\u8BB0: ${noteIndex}`);
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
const content = await fsPromises2.readFile(ROOT_README_PATH, "utf-8");
|
|
1514
|
+
const lines = content.split("\n");
|
|
1515
|
+
const repoOwner = this.configManager.get("author");
|
|
1516
|
+
const repoName = this.configManager.get("repoName");
|
|
1517
|
+
const tempNoteInfo = {
|
|
1518
|
+
index: noteIndex,
|
|
1519
|
+
dirName: item.folderName,
|
|
1520
|
+
path: "",
|
|
1521
|
+
readmePath: "",
|
|
1522
|
+
configPath: "",
|
|
1523
|
+
config: item.noteConfig
|
|
1524
|
+
};
|
|
1525
|
+
const noteLine = buildNoteLineMarkdown(tempNoteInfo, repoOwner, repoName);
|
|
1526
|
+
lines.push(noteLine);
|
|
1527
|
+
await fsPromises2.writeFile(ROOT_README_PATH, lines.join("\n"), "utf-8");
|
|
1528
|
+
logger.info(`\u5728 README.md \u672B\u5C3E\u6DFB\u52A0\u7B14\u8BB0: ${noteIndex}`);
|
|
1529
|
+
}
|
|
1530
|
+
/**
|
|
1531
|
+
* 重新生成 sidebar.json(基于当前 README.md)
|
|
1532
|
+
* @param notes - 可选的笔记列表,不传则内部扫描
|
|
1533
|
+
*/
|
|
1534
|
+
async regenerateSidebar(notes) {
|
|
1535
|
+
const allNotes = notes ?? (this.noteIndexCache.isInitialized() ? this.noteIndexCache.toNoteInfoList() : this.noteManager.scanNotes());
|
|
1536
|
+
await this.updateSidebar(allNotes);
|
|
1537
|
+
logger.info("\u91CD\u65B0\u751F\u6210 sidebar.json");
|
|
1538
|
+
}
|
|
1539
|
+
};
|
|
1540
|
+
|
|
1541
|
+
// services/note/service.ts
|
|
1542
|
+
import { writeFileSync as writeFileSync3, readFileSync as readFileSync4 } from "fs";
|
|
1543
|
+
import { join as join6 } from "path";
|
|
1544
|
+
import { v4 as uuidv4 } from "uuid";
|
|
1545
|
+
|
|
1546
|
+
// config/templates.ts
|
|
1547
|
+
function generateNoteTitle(noteIndex, title, repoUrl) {
|
|
1548
|
+
const dirName = `${noteIndex}. ${title}`;
|
|
1549
|
+
const encodedDirName = encodeURIComponent(dirName);
|
|
1550
|
+
return `# [${dirName}](${repoUrl}/${encodedDirName})`;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
// services/note/service.ts
|
|
1554
|
+
var NEW_NOTES_README_MD_TEMPLATE = `
|
|
1555
|
+
<!-- region:toc -->
|
|
1556
|
+
|
|
1557
|
+
- [1. \u{1F3AF} \u672C\u8282\u5185\u5BB9](#1--\u672C\u8282\u5185\u5BB9)
|
|
1558
|
+
- [2. \u{1FAE7} \u8BC4\u4EF7](#2--\u8BC4\u4EF7)
|
|
1559
|
+
|
|
1560
|
+
<!-- endregion:toc -->
|
|
1561
|
+
|
|
1562
|
+
## 1. \u{1F3AF} \u672C\u8282\u5185\u5BB9
|
|
1563
|
+
|
|
1564
|
+
- todo
|
|
1565
|
+
|
|
1566
|
+
## 2. \u{1FAE7} \u8BC4\u4EF7
|
|
1567
|
+
|
|
1568
|
+
- todo
|
|
1569
|
+
`;
|
|
1570
|
+
var NoteService = class _NoteService {
|
|
1571
|
+
constructor() {
|
|
1572
|
+
this.ignoredConfigPaths = /* @__PURE__ */ new Set();
|
|
1573
|
+
this.noteManager = NoteManager.getInstance();
|
|
1574
|
+
this.noteIndexCache = NoteIndexCache.getInstance();
|
|
1575
|
+
}
|
|
1576
|
+
static getInstance() {
|
|
1577
|
+
if (!_NoteService.instance) {
|
|
1578
|
+
_NoteService.instance = new _NoteService();
|
|
1579
|
+
}
|
|
1580
|
+
return _NoteService.instance;
|
|
1581
|
+
}
|
|
1582
|
+
/**
|
|
1583
|
+
* 标记配置文件在下次变更时被忽略(防止 API 写入触发文件监听循环)
|
|
1584
|
+
* @param configPath - 配置文件路径
|
|
1585
|
+
*/
|
|
1586
|
+
ignoreNextConfigChange(configPath) {
|
|
1587
|
+
this.ignoredConfigPaths.add(configPath);
|
|
1588
|
+
}
|
|
1589
|
+
/**
|
|
1590
|
+
* 检查配置文件是否应该被忽略
|
|
1591
|
+
* @param configPath - 配置文件路径
|
|
1592
|
+
* @returns 是否应该忽略
|
|
1593
|
+
*/
|
|
1594
|
+
shouldIgnoreConfigChange(configPath) {
|
|
1595
|
+
if (this.ignoredConfigPaths.has(configPath)) {
|
|
1596
|
+
this.ignoredConfigPaths.delete(configPath);
|
|
1597
|
+
return true;
|
|
1598
|
+
}
|
|
1599
|
+
return false;
|
|
1600
|
+
}
|
|
1601
|
+
/**
|
|
1602
|
+
* 获取所有笔记
|
|
1603
|
+
* dev 模式下(缓存已初始化)从内存读取,其他模式回退到文件扫描
|
|
1604
|
+
* @returns 笔记信息数组
|
|
1605
|
+
*/
|
|
1606
|
+
getAllNotes() {
|
|
1607
|
+
if (this.noteIndexCache.isInitialized()) {
|
|
1608
|
+
return this.noteIndexCache.toNoteInfoList();
|
|
1609
|
+
}
|
|
1610
|
+
return this.noteManager.scanNotes();
|
|
1611
|
+
}
|
|
1612
|
+
/**
|
|
1613
|
+
* 获取笔记(通过索引)
|
|
1614
|
+
* @param noteIndex - 笔记索引(文件夹前 4 位数字)
|
|
1615
|
+
* @returns 笔记信息,未找到时返回 undefined
|
|
1616
|
+
*/
|
|
1617
|
+
getNoteByIndex(noteIndex) {
|
|
1618
|
+
return this.noteManager.getNoteByIndex(noteIndex);
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* 创建新笔记
|
|
1622
|
+
* @param options - 创建选项
|
|
1623
|
+
* @returns 新创建的笔记信息
|
|
1624
|
+
*/
|
|
1625
|
+
async createNote(options = {}) {
|
|
1626
|
+
const {
|
|
1627
|
+
title = "new",
|
|
1628
|
+
category,
|
|
1629
|
+
enableDiscussions = false,
|
|
1630
|
+
configId,
|
|
1631
|
+
usedIndexes
|
|
1632
|
+
} = options;
|
|
1633
|
+
const noteIndex = this.generateNextNoteIndex(usedIndexes);
|
|
1634
|
+
const dirName = `${noteIndex}. ${title}`;
|
|
1635
|
+
const notePath = join6(NOTES_PATH, dirName);
|
|
1636
|
+
await ensureDirectory(notePath);
|
|
1637
|
+
const readmePath = join6(notePath, "README.md");
|
|
1638
|
+
const noteTitle = generateNoteTitle(noteIndex, title, REPO_NOTES_URL);
|
|
1639
|
+
const readmeContent = noteTitle + "\n" + NEW_NOTES_README_MD_TEMPLATE;
|
|
1640
|
+
writeFileSync3(readmePath, readmeContent, "utf-8");
|
|
1641
|
+
const configPath = join6(notePath, ".tnotes.json");
|
|
1642
|
+
const now = Date.now();
|
|
1643
|
+
const config = {
|
|
1644
|
+
id: configId || uuidv4(),
|
|
1645
|
+
// 配置 ID 使用 UUID(跨知识库唯一)
|
|
1646
|
+
bilibili: [],
|
|
1647
|
+
tnotes: [],
|
|
1648
|
+
yuque: [],
|
|
1649
|
+
done: false,
|
|
1650
|
+
category,
|
|
1651
|
+
enableDiscussions,
|
|
1652
|
+
created_at: now,
|
|
1653
|
+
updated_at: now
|
|
1654
|
+
};
|
|
1655
|
+
this.noteManager.writeNoteConfig(configPath, config);
|
|
1656
|
+
logger.info(`Created new note: ${dirName}`);
|
|
1657
|
+
return {
|
|
1658
|
+
index: noteIndex,
|
|
1659
|
+
// 返回的 id 是笔记索引(目录前缀)
|
|
1660
|
+
path: notePath,
|
|
1661
|
+
dirName,
|
|
1662
|
+
readmePath,
|
|
1663
|
+
configPath,
|
|
1664
|
+
config
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
/**
|
|
1668
|
+
* 生成下一个笔记索引(填充空缺)
|
|
1669
|
+
* @param usedIndexes - 可选的已使用编号集合,不传则内部扫描
|
|
1670
|
+
* @returns 新的笔记索引(4位数字字符串,从 0001 到 9999)
|
|
1671
|
+
*/
|
|
1672
|
+
generateNextNoteIndex(usedIndexes) {
|
|
1673
|
+
if (!usedIndexes) {
|
|
1674
|
+
const notes = this.getAllNotes();
|
|
1675
|
+
usedIndexes = /* @__PURE__ */ new Set();
|
|
1676
|
+
for (const note of notes) {
|
|
1677
|
+
const id = parseInt(note.index, 10);
|
|
1678
|
+
if (!isNaN(id) && id >= 1 && id <= 9999) {
|
|
1679
|
+
usedIndexes.add(id);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
if (usedIndexes.size === 0) {
|
|
1684
|
+
return "0001";
|
|
1685
|
+
}
|
|
1686
|
+
for (let i = 1; i <= 9999; i++) {
|
|
1687
|
+
if (!usedIndexes.has(i)) {
|
|
1688
|
+
return i.toString().padStart(CONSTANTS.NOTE_INDEX_LENGTH, "0");
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
throw new Error("\u6240\u6709\u7B14\u8BB0\u7F16\u53F7 (0001-9999) \u5DF2\u88AB\u5360\u7528\uFF0C\u65E0\u6CD5\u521B\u5EFA\u65B0\u7B14\u8BB0");
|
|
1692
|
+
}
|
|
1693
|
+
/**
|
|
1694
|
+
* 更新笔记配置
|
|
1695
|
+
* @param noteIndex - 笔记索引
|
|
1696
|
+
* @param updates - 配置更新
|
|
1697
|
+
*/
|
|
1698
|
+
async updateNoteConfig(noteIndex, updates) {
|
|
1699
|
+
const note = this.getNoteByIndex(noteIndex);
|
|
1700
|
+
if (!note || !note.config) {
|
|
1701
|
+
throw new Error(`Note not found or no config: ${noteIndex}`);
|
|
1702
|
+
}
|
|
1703
|
+
const oldConfig = { ...note.config };
|
|
1704
|
+
const updatedConfig = {
|
|
1705
|
+
...note.config,
|
|
1706
|
+
...updates,
|
|
1707
|
+
updated_at: Date.now()
|
|
1708
|
+
};
|
|
1709
|
+
this.ignoreNextConfigChange(note.configPath);
|
|
1710
|
+
this.noteManager.updateNoteConfig(note, updatedConfig);
|
|
1711
|
+
this.noteIndexCache.updateConfig(noteIndex, updatedConfig);
|
|
1712
|
+
const needsGlobalUpdate = this.checkNeedsGlobalUpdate(
|
|
1713
|
+
oldConfig,
|
|
1714
|
+
updatedConfig
|
|
1715
|
+
);
|
|
1716
|
+
if (needsGlobalUpdate) {
|
|
1717
|
+
logger.info(`\u68C0\u6D4B\u5230\u5168\u5C40\u5B57\u6BB5\u53D8\u66F4 (${noteIndex})\uFF0C\u6B63\u5728\u589E\u91CF\u66F4\u65B0\u5168\u5C40\u6587\u4EF6...`);
|
|
1718
|
+
const readmeService = ReadmeService.getInstance();
|
|
1719
|
+
await readmeService.updateNoteInReadme(noteIndex, updates);
|
|
1720
|
+
await readmeService.regenerateSidebar();
|
|
1721
|
+
logger.info(`\u5168\u5C40\u6587\u4EF6\u589E\u91CF\u66F4\u65B0\u5B8C\u6210 (${noteIndex})`);
|
|
1722
|
+
} else {
|
|
1723
|
+
logger.debug(`\u914D\u7F6E\u66F4\u65B0\u4E0D\u5F71\u54CD\u5168\u5C40\u6587\u4EF6 (${noteIndex})`);
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
/**
|
|
1727
|
+
* 检查配置更新是否需要触发全局更新
|
|
1728
|
+
* @param oldConfig - 旧配置
|
|
1729
|
+
* @param newConfig - 新配置
|
|
1730
|
+
* @returns 是否需要全局更新
|
|
1731
|
+
*/
|
|
1732
|
+
checkNeedsGlobalUpdate(oldConfig, newConfig) {
|
|
1733
|
+
const globalFields = ["done"];
|
|
1734
|
+
for (const field of globalFields) {
|
|
1735
|
+
if (oldConfig[field] !== newConfig[field]) {
|
|
1736
|
+
return true;
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
return false;
|
|
1740
|
+
}
|
|
1741
|
+
/**
|
|
1742
|
+
* 修正笔记标题
|
|
1743
|
+
* @param noteInfo - 笔记信息
|
|
1744
|
+
* @returns 是否进行了修正
|
|
1745
|
+
*/
|
|
1746
|
+
async fixNoteTitle(noteInfo) {
|
|
1747
|
+
try {
|
|
1748
|
+
const readmeContent = readFileSync4(noteInfo.readmePath, "utf-8");
|
|
1749
|
+
if (readmeContent.length === 0) return false;
|
|
1750
|
+
const lines = readmeContent.split("\n");
|
|
1751
|
+
const match = noteInfo.dirName.match(/^\d{4}\.\s+(.+)$/);
|
|
1752
|
+
if (!match) {
|
|
1753
|
+
logger.warn(`\u68C0\u6D4B\u5230\u9519\u8BEF\u7684\u7B14\u8BB0\u76EE\u5F55\u540D\u79F0\uFF1A${noteInfo.dirName}`);
|
|
1754
|
+
return false;
|
|
1755
|
+
}
|
|
1756
|
+
const expectedTitle = match[1];
|
|
1757
|
+
const expectedH1 = generateNoteTitle(
|
|
1758
|
+
noteInfo.index,
|
|
1759
|
+
expectedTitle,
|
|
1760
|
+
REPO_NOTES_URL
|
|
1761
|
+
);
|
|
1762
|
+
const firstLine = lines[0].trim();
|
|
1763
|
+
if (!firstLine.startsWith("# ")) {
|
|
1764
|
+
lines.unshift(expectedH1);
|
|
1765
|
+
writeFileSync3(noteInfo.readmePath, lines.join("\n"), "utf-8");
|
|
1766
|
+
logger.info(`Added title to: ${noteInfo.dirName}`);
|
|
1767
|
+
return true;
|
|
1768
|
+
}
|
|
1769
|
+
if (firstLine !== expectedH1) {
|
|
1770
|
+
lines[0] = expectedH1;
|
|
1771
|
+
writeFileSync3(noteInfo.readmePath, lines.join("\n"), "utf-8");
|
|
1772
|
+
logger.info(`Fixed title for: ${noteInfo.dirName}`);
|
|
1773
|
+
return true;
|
|
1774
|
+
}
|
|
1775
|
+
return false;
|
|
1776
|
+
} catch (error) {
|
|
1777
|
+
logger.error(`Failed to fix title for: ${noteInfo.dirName}`, error);
|
|
1778
|
+
return false;
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
/**
|
|
1782
|
+
* 修正所有笔记的标题
|
|
1783
|
+
* @param providedNotes - 可选的笔记列表,不传则内部扫描
|
|
1784
|
+
* @returns 修正的笔记数量
|
|
1785
|
+
*/
|
|
1786
|
+
async fixAllNoteTitles(providedNotes) {
|
|
1787
|
+
const notes = providedNotes ?? this.getAllNotes();
|
|
1788
|
+
let fixedCount = 0;
|
|
1789
|
+
for (const note of notes) {
|
|
1790
|
+
const fixed = await this.fixNoteTitle(note);
|
|
1791
|
+
if (fixed) {
|
|
1792
|
+
fixedCount++;
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
if (fixedCount > 0) {
|
|
1796
|
+
logger.info(`Fixed ${fixedCount} note titles`);
|
|
1797
|
+
}
|
|
1798
|
+
return fixedCount;
|
|
1799
|
+
}
|
|
1800
|
+
};
|
|
1801
|
+
|
|
1802
|
+
// services/file-watcher/service.ts
|
|
1803
|
+
var NOTES_DIR_NOT_SET_ERROR = "NOTES_DIR_PATH \u672A\u8BBE\u7F6E\uFF0C\u65E0\u6CD5\u542F\u52A8\u6587\u4EF6\u76D1\u542C";
|
|
1804
|
+
var UPDATE_UNLOCK_DELAY_MS2 = 500;
|
|
1805
|
+
var FileWatcherService = class {
|
|
1806
|
+
constructor(notesDir = NOTES_DIR_PATH) {
|
|
1807
|
+
this.notesDir = notesDir;
|
|
1808
|
+
this.unlockTimer = null;
|
|
1809
|
+
if (!this.notesDir) {
|
|
1810
|
+
throw new Error(NOTES_DIR_NOT_SET_ERROR);
|
|
1811
|
+
}
|
|
1812
|
+
this.init();
|
|
1813
|
+
}
|
|
1814
|
+
init() {
|
|
1815
|
+
this.noteService = NoteService.getInstance();
|
|
1816
|
+
this.readmeService = ReadmeService.getInstance();
|
|
1817
|
+
this.noteIndexCache = NoteIndexCache.getInstance();
|
|
1818
|
+
this.watchState = this.initWatchState();
|
|
1819
|
+
this.scheduler = this.initScheduler();
|
|
1820
|
+
this.folderHandler = this.initFolderHandler();
|
|
1821
|
+
this.renameDetector = this.initRenameDetector();
|
|
1822
|
+
this.configHandler = this.initConfigHandler();
|
|
1823
|
+
this.readmeHandler = this.initReadmeHandler();
|
|
1824
|
+
this.coordinator = this.initCoordinator();
|
|
1825
|
+
this.adapter = this.initAdapter();
|
|
1826
|
+
}
|
|
1827
|
+
initWatchState() {
|
|
1828
|
+
const watchState = new WatchState({ notesDir: this.notesDir, logger });
|
|
1829
|
+
watchState.initializeFromDisk();
|
|
1830
|
+
return watchState;
|
|
1831
|
+
}
|
|
1832
|
+
initScheduler() {
|
|
1833
|
+
return new EventScheduler({
|
|
1834
|
+
onFlush: (events) => this.handleFileChange(events),
|
|
1835
|
+
onPauseForBatch: () => logger.warn("\u76D1\u542C\u670D\u52A1\u6682\u505C 3s \u7B49\u5F85\u6279\u91CF\u66F4\u65B0\u5B8C\u6210..."),
|
|
1836
|
+
onResumeAfterBatch: () => logger.info("\u6062\u590D\u81EA\u52A8\u76D1\u542C"),
|
|
1837
|
+
reinit: () => this.watchState.initializeFromDisk()
|
|
1838
|
+
});
|
|
1839
|
+
}
|
|
1840
|
+
initFolderHandler() {
|
|
1841
|
+
return new FolderChangeHandler({
|
|
1842
|
+
notesDir: this.notesDir,
|
|
1843
|
+
watchState: this.watchState,
|
|
1844
|
+
scheduler: this.scheduler,
|
|
1845
|
+
noteService: this.noteService,
|
|
1846
|
+
readmeService: this.readmeService,
|
|
1847
|
+
noteIndexCache: this.noteIndexCache,
|
|
1848
|
+
logger
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
initRenameDetector() {
|
|
1852
|
+
return new RenameDetector({
|
|
1853
|
+
notesDir: this.notesDir,
|
|
1854
|
+
dirCache: {
|
|
1855
|
+
has: (name) => this.watchState.hasNoteDir(name),
|
|
1856
|
+
add: (name) => this.watchState.addNoteDir(name),
|
|
1857
|
+
delete: (name) => this.watchState.deleteNoteDir(name)
|
|
1858
|
+
},
|
|
1859
|
+
logger,
|
|
1860
|
+
onDelete: (oldName) => this.folderHandler.handleFolderDeletion(oldName),
|
|
1861
|
+
onRename: (oldName, newName) => this.folderHandler.handleFolderRenameUpdate(oldName, newName)
|
|
1862
|
+
});
|
|
1863
|
+
}
|
|
1864
|
+
initConfigHandler() {
|
|
1865
|
+
return new ConfigChangeHandler({
|
|
1866
|
+
state: this.watchState,
|
|
1867
|
+
noteService: this.noteService,
|
|
1868
|
+
noteIndexCache: this.noteIndexCache,
|
|
1869
|
+
logger
|
|
1870
|
+
});
|
|
1871
|
+
}
|
|
1872
|
+
initReadmeHandler() {
|
|
1873
|
+
return new ReadmeChangeHandler({ noteService: this.noteService });
|
|
1874
|
+
}
|
|
1875
|
+
initCoordinator() {
|
|
1876
|
+
return new GlobalUpdateCoordinator({
|
|
1877
|
+
readmeService: this.readmeService,
|
|
1878
|
+
noteIndexCache: this.noteIndexCache,
|
|
1879
|
+
logger
|
|
1880
|
+
});
|
|
1881
|
+
}
|
|
1882
|
+
initAdapter() {
|
|
1883
|
+
return new FsWatcherAdapter({
|
|
1884
|
+
notesDir: this.notesDir,
|
|
1885
|
+
isUpdating: () => this.scheduler.getUpdating(),
|
|
1886
|
+
onRename: (folderName) => this.renameDetector.handleFsRename(folderName),
|
|
1887
|
+
onNoteEvent: (event) => this.onNoteEvent(event),
|
|
1888
|
+
logger
|
|
1889
|
+
});
|
|
1890
|
+
}
|
|
1891
|
+
start() {
|
|
1892
|
+
this.watchState.initializeFromDisk();
|
|
1893
|
+
this.adapter.start();
|
|
1894
|
+
}
|
|
1895
|
+
stop() {
|
|
1896
|
+
this.adapter.stop();
|
|
1897
|
+
this.scheduler.clearTimers();
|
|
1898
|
+
this.renameDetector.clearTimers();
|
|
1899
|
+
this.folderHandler.clearTimers();
|
|
1900
|
+
if (this.unlockTimer) {
|
|
1901
|
+
clearTimeout(this.unlockTimer);
|
|
1902
|
+
this.unlockTimer = null;
|
|
1903
|
+
}
|
|
1904
|
+
logger.info("\u6587\u4EF6\u76D1\u542C\u670D\u52A1\u5DF2\u505C\u6B62");
|
|
1905
|
+
}
|
|
1906
|
+
pause() {
|
|
1907
|
+
this.scheduler.setUpdating(true);
|
|
1908
|
+
logger.info("\u6587\u4EF6\u76D1\u542C\u5DF2\u6682\u505C");
|
|
1909
|
+
}
|
|
1910
|
+
resume() {
|
|
1911
|
+
this.watchState.initializeFromDisk();
|
|
1912
|
+
this.scheduler.setUpdating(false);
|
|
1913
|
+
logger.info("\u6587\u4EF6\u76D1\u542C\u5DF2\u6062\u590D");
|
|
1914
|
+
}
|
|
1915
|
+
isWatching() {
|
|
1916
|
+
return this.adapter.isWatching();
|
|
1917
|
+
}
|
|
1918
|
+
// #region - 私有实现
|
|
1919
|
+
onNoteEvent(event) {
|
|
1920
|
+
if (!this.isNoteFile(event.path)) return;
|
|
1921
|
+
if (!this.watchState.updateFileHash(event.path)) return;
|
|
1922
|
+
if (this.scheduler.recordChangeAndDetectBatch()) return;
|
|
1923
|
+
this.scheduler.enqueue(event);
|
|
1924
|
+
}
|
|
1925
|
+
async handleFileChange(events) {
|
|
1926
|
+
try {
|
|
1927
|
+
const configChanges = events.filter(
|
|
1928
|
+
(e) => e.type === WATCH_EVENT_TYPES.CONFIG
|
|
1929
|
+
);
|
|
1930
|
+
const readmeChanges = events.filter(
|
|
1931
|
+
(e) => e.type === WATCH_EVENT_TYPES.README
|
|
1932
|
+
);
|
|
1933
|
+
const changedNoteIndexes = await this.configHandler.handle(configChanges);
|
|
1934
|
+
if (changedNoteIndexes.length > 0) {
|
|
1935
|
+
await safeExecute(
|
|
1936
|
+
"\u914D\u7F6E\u53D8\u66F4\u66F4\u65B0",
|
|
1937
|
+
() => this.coordinator.applyConfigUpdates(changedNoteIndexes),
|
|
1938
|
+
logger
|
|
1939
|
+
);
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
await safeExecute(
|
|
1943
|
+
"README \u53D8\u66F4\u66F4\u65B0",
|
|
1944
|
+
async () => {
|
|
1945
|
+
await this.readmeHandler.handle(readmeChanges);
|
|
1946
|
+
await this.coordinator.updateNoteReadmesOnly(events);
|
|
1947
|
+
},
|
|
1948
|
+
logger
|
|
1949
|
+
);
|
|
1950
|
+
} finally {
|
|
1951
|
+
if (this.unlockTimer) clearTimeout(this.unlockTimer);
|
|
1952
|
+
this.unlockTimer = setTimeout(() => {
|
|
1953
|
+
this.unlockTimer = null;
|
|
1954
|
+
this.scheduler.setUpdating(false);
|
|
1955
|
+
}, UPDATE_UNLOCK_DELAY_MS2);
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
isNoteFile(filePath) {
|
|
1959
|
+
return filePath.endsWith("README.md") || filePath.endsWith(".tnotes.json");
|
|
1960
|
+
}
|
|
1961
|
+
// #endregion - 私有实现
|
|
1962
|
+
};
|
|
1963
|
+
|
|
1964
|
+
// core/GitManager.ts
|
|
1965
|
+
import { resolve } from "path";
|
|
1966
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1967
|
+
var GitManager = class {
|
|
1968
|
+
constructor(dir, logger2) {
|
|
1969
|
+
this.dir = dir;
|
|
1970
|
+
this.logger = logger2?.child("git") || new Logger({ prefix: "git" });
|
|
1971
|
+
}
|
|
1972
|
+
/**
|
|
1973
|
+
* 检查是否为有效的 Git 仓库
|
|
1974
|
+
*/
|
|
1975
|
+
async isValidRepo() {
|
|
1976
|
+
try {
|
|
1977
|
+
const result = await runCommand(
|
|
1978
|
+
"git rev-parse --is-inside-work-tree",
|
|
1979
|
+
this.dir
|
|
1980
|
+
);
|
|
1981
|
+
return result.trim() === "true";
|
|
1982
|
+
} catch {
|
|
1983
|
+
return false;
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
/**
|
|
1987
|
+
* 确保是有效的 Git 仓库,否则抛出错误
|
|
1988
|
+
*/
|
|
1989
|
+
async ensureValidRepo() {
|
|
1990
|
+
if (!await this.isValidRepo()) {
|
|
1991
|
+
throw createError.gitNotRepo(this.dir);
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
/**
|
|
1995
|
+
* 获取 Git 状态
|
|
1996
|
+
*/
|
|
1997
|
+
async getStatus() {
|
|
1998
|
+
await this.ensureValidRepo();
|
|
1999
|
+
const statusOutput = await runCommand(
|
|
2000
|
+
"git -c core.quotePath=false status --porcelain",
|
|
2001
|
+
this.dir
|
|
2002
|
+
);
|
|
2003
|
+
const lines = statusOutput.trim().split("\n").filter((line) => line);
|
|
2004
|
+
const files = lines.map((line) => {
|
|
2005
|
+
const statusCode = line.substring(0, 2);
|
|
2006
|
+
let path = line.substring(3);
|
|
2007
|
+
path = path.replace(/^"(.*)"$/, "$1");
|
|
2008
|
+
let status = "modified";
|
|
2009
|
+
if (line.startsWith("??")) {
|
|
2010
|
+
status = "untracked";
|
|
2011
|
+
} else if (/^[MADRC]/.test(statusCode)) {
|
|
2012
|
+
status = "staged";
|
|
2013
|
+
} else if (/^.[MD]/.test(statusCode)) {
|
|
2014
|
+
status = "unstaged";
|
|
2015
|
+
}
|
|
2016
|
+
return { path, status, statusCode };
|
|
2017
|
+
});
|
|
2018
|
+
const staged = files.filter((f) => f.status === "staged").length;
|
|
2019
|
+
const unstaged = files.filter((f) => f.status === "unstaged").length;
|
|
2020
|
+
const untracked = files.filter((f) => f.status === "untracked").length;
|
|
2021
|
+
const branch = await runCommand("git branch --show-current", this.dir);
|
|
2022
|
+
let ahead = 0;
|
|
2023
|
+
let behind = 0;
|
|
2024
|
+
try {
|
|
2025
|
+
const aheadBehind = await runCommand(
|
|
2026
|
+
"git rev-list --left-right --count @{upstream}...HEAD",
|
|
2027
|
+
this.dir
|
|
2028
|
+
);
|
|
2029
|
+
const [behindStr, aheadStr] = aheadBehind.trim().split(" ");
|
|
2030
|
+
behind = parseInt(behindStr) || 0;
|
|
2031
|
+
ahead = parseInt(aheadStr) || 0;
|
|
2032
|
+
} catch {
|
|
2033
|
+
}
|
|
2034
|
+
return {
|
|
2035
|
+
hasChanges: lines.length > 0,
|
|
2036
|
+
changedFiles: lines.length,
|
|
2037
|
+
staged,
|
|
2038
|
+
unstaged,
|
|
2039
|
+
untracked,
|
|
2040
|
+
branch: branch.trim(),
|
|
2041
|
+
ahead,
|
|
2042
|
+
behind,
|
|
2043
|
+
files
|
|
2044
|
+
};
|
|
2045
|
+
}
|
|
2046
|
+
/**
|
|
2047
|
+
* 获取远程仓库信息
|
|
2048
|
+
*/
|
|
2049
|
+
async getRemoteInfo() {
|
|
2050
|
+
try {
|
|
2051
|
+
await this.ensureValidRepo();
|
|
2052
|
+
const remoteUrl = await runCommand(
|
|
2053
|
+
"git config --get remote.origin.url",
|
|
2054
|
+
this.dir
|
|
2055
|
+
);
|
|
2056
|
+
const url = remoteUrl.trim();
|
|
2057
|
+
if (!url) return null;
|
|
2058
|
+
const httpsMatch = url.match(
|
|
2059
|
+
/https:\/\/(?:www\.)?github\.com\/([^/]+)\/(.+?)(?:\.git)?$/
|
|
2060
|
+
);
|
|
2061
|
+
if (httpsMatch) {
|
|
2062
|
+
return {
|
|
2063
|
+
url,
|
|
2064
|
+
type: "https",
|
|
2065
|
+
owner: httpsMatch[1],
|
|
2066
|
+
repo: httpsMatch[2]
|
|
2067
|
+
};
|
|
2068
|
+
}
|
|
2069
|
+
const sshMatch = url.match(/git@github\.com:([^/]+)\/(.+?)(?:\.git)?$/);
|
|
2070
|
+
if (sshMatch) {
|
|
2071
|
+
return {
|
|
2072
|
+
url,
|
|
2073
|
+
type: "ssh",
|
|
2074
|
+
owner: sshMatch[1],
|
|
2075
|
+
repo: sshMatch[2]
|
|
2076
|
+
};
|
|
2077
|
+
}
|
|
2078
|
+
return { url, type: "unknown" };
|
|
2079
|
+
} catch {
|
|
2080
|
+
return null;
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
/**
|
|
2084
|
+
* 检查是否有未提交的更改
|
|
2085
|
+
*/
|
|
2086
|
+
async hasUncommittedChanges() {
|
|
2087
|
+
const status = await this.getStatus();
|
|
2088
|
+
return status.hasChanges;
|
|
2089
|
+
}
|
|
2090
|
+
/**
|
|
2091
|
+
* Stash 当前更改
|
|
2092
|
+
*/
|
|
2093
|
+
async stash(message) {
|
|
2094
|
+
try {
|
|
2095
|
+
await this.ensureValidRepo();
|
|
2096
|
+
const cmd = message ? `git stash push -m "${message}"` : "git stash push";
|
|
2097
|
+
await runCommand(cmd, this.dir);
|
|
2098
|
+
this.logger.info("Stashed uncommitted changes");
|
|
2099
|
+
return true;
|
|
2100
|
+
} catch (error) {
|
|
2101
|
+
this.logger.warn("Failed to stash changes");
|
|
2102
|
+
return false;
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
/**
|
|
2106
|
+
* Pop stash
|
|
2107
|
+
*/
|
|
2108
|
+
async stashPop() {
|
|
2109
|
+
try {
|
|
2110
|
+
await this.ensureValidRepo();
|
|
2111
|
+
await runCommand("git stash pop", this.dir);
|
|
2112
|
+
this.logger.info("Restored stashed changes");
|
|
2113
|
+
return true;
|
|
2114
|
+
} catch (error) {
|
|
2115
|
+
this.logger.warn("Failed to restore stashed changes");
|
|
2116
|
+
return false;
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
/**
|
|
2120
|
+
* 拉取远程更新
|
|
2121
|
+
*/
|
|
2122
|
+
async pull(options) {
|
|
2123
|
+
await this.ensureValidRepo();
|
|
2124
|
+
const { rebase = true, autostash = true } = options || {};
|
|
2125
|
+
const hasChanges = await this.hasUncommittedChanges();
|
|
2126
|
+
let didStash = false;
|
|
2127
|
+
if (hasChanges && !autostash) {
|
|
2128
|
+
this.logger.warn("Repository has uncommitted changes");
|
|
2129
|
+
didStash = await this.stash("Auto-stash before pull");
|
|
2130
|
+
}
|
|
2131
|
+
try {
|
|
2132
|
+
const status = await this.getStatus();
|
|
2133
|
+
const beforeCommit = await runCommand("git rev-parse HEAD", this.dir);
|
|
2134
|
+
this.logger.info("\u6B63\u5728\u62C9\u53D6\u8FDC\u7A0B\u66F4\u65B0...");
|
|
2135
|
+
const cmd = `git pull ${rebase ? "--rebase" : ""} ${autostash ? "--autostash" : ""}`.trim();
|
|
2136
|
+
await runCommand(cmd, this.dir);
|
|
2137
|
+
const afterCommit = await runCommand("git rev-parse HEAD", this.dir);
|
|
2138
|
+
if (beforeCommit.trim() !== afterCommit.trim()) {
|
|
2139
|
+
try {
|
|
2140
|
+
const diffOutput = await runCommand(
|
|
2141
|
+
`git diff --name-only ${beforeCommit.trim()}..${afterCommit.trim()}`,
|
|
2142
|
+
this.dir
|
|
2143
|
+
);
|
|
2144
|
+
const changedFiles = diffOutput.trim().split("\n").filter((f) => f);
|
|
2145
|
+
if (changedFiles.length > 0) {
|
|
2146
|
+
console.log(` \u66F4\u65B0\u4E86 ${changedFiles.length} \u4E2A\u6587\u4EF6:`);
|
|
2147
|
+
changedFiles.forEach((file, index) => {
|
|
2148
|
+
console.log(` ${index + 1}. ${file}`);
|
|
2149
|
+
});
|
|
2150
|
+
}
|
|
2151
|
+
this.logger.success(`\u62C9\u53D6\u6210\u529F: ${changedFiles.length} \u4E2A\u6587\u4EF6\u5DF2\u66F4\u65B0`);
|
|
2152
|
+
} catch {
|
|
2153
|
+
this.logger.success("\u62C9\u53D6\u6210\u529F");
|
|
2154
|
+
}
|
|
2155
|
+
} else {
|
|
2156
|
+
this.logger.info("\u5DF2\u662F\u6700\u65B0\uFF0C\u6CA1\u6709\u9700\u8981\u62C9\u53D6\u7684\u66F4\u65B0");
|
|
2157
|
+
}
|
|
2158
|
+
await this.updateSubmodules();
|
|
2159
|
+
} catch (error) {
|
|
2160
|
+
this.logger.error("\u62C9\u53D6\u5931\u8D25");
|
|
2161
|
+
handleError(error);
|
|
2162
|
+
throw error;
|
|
2163
|
+
} finally {
|
|
2164
|
+
if (didStash) {
|
|
2165
|
+
await this.stashPop();
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
/**
|
|
2170
|
+
* 提交更改
|
|
2171
|
+
*/
|
|
2172
|
+
async commit(message) {
|
|
2173
|
+
await this.ensureValidRepo();
|
|
2174
|
+
try {
|
|
2175
|
+
await runCommand(`git commit -m "${message}"`, this.dir);
|
|
2176
|
+
this.logger.success(`Committed: ${message}`);
|
|
2177
|
+
} catch (error) {
|
|
2178
|
+
handleError(error);
|
|
2179
|
+
throw error;
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
/**
|
|
2183
|
+
* 添加文件到暂存区
|
|
2184
|
+
*/
|
|
2185
|
+
async add(files = ".") {
|
|
2186
|
+
await this.ensureValidRepo();
|
|
2187
|
+
const fileList = Array.isArray(files) ? files.join(" ") : files;
|
|
2188
|
+
try {
|
|
2189
|
+
await runCommand(`git add ${fileList}`, this.dir);
|
|
2190
|
+
this.logger.info(`Staged changes: ${fileList}`);
|
|
2191
|
+
} catch (error) {
|
|
2192
|
+
handleError(error);
|
|
2193
|
+
throw error;
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
/**
|
|
2197
|
+
* 推送到远程仓库
|
|
2198
|
+
*/
|
|
2199
|
+
async push(options) {
|
|
2200
|
+
await this.ensureValidRepo();
|
|
2201
|
+
const { force = false, setUpstream = false } = options || {};
|
|
2202
|
+
try {
|
|
2203
|
+
const status = await this.getStatus();
|
|
2204
|
+
this.logger.progress(`\u6B63\u5728\u63A8\u9001\u5230\u8FDC\u7A0B (${status.branch})...`);
|
|
2205
|
+
let cmd = "git push";
|
|
2206
|
+
if (force) cmd += " --force";
|
|
2207
|
+
if (setUpstream) cmd += ` --set-upstream origin ${status.branch}`;
|
|
2208
|
+
await runCommand(cmd, this.dir);
|
|
2209
|
+
const remoteInfo = await this.getRemoteInfo();
|
|
2210
|
+
if (remoteInfo) {
|
|
2211
|
+
this.logger.success(`\u63A8\u9001\u6210\u529F \u2192 ${remoteInfo.owner}/${remoteInfo.repo}`);
|
|
2212
|
+
} else {
|
|
2213
|
+
this.logger.success("\u63A8\u9001\u6210\u529F");
|
|
2214
|
+
}
|
|
2215
|
+
} catch (error) {
|
|
2216
|
+
this.logger.error("\u63A8\u9001\u5931\u8D25");
|
|
2217
|
+
handleError(error);
|
|
2218
|
+
throw error;
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
/**
|
|
2222
|
+
* 完整的推送流程:检查 -> 添加 -> 提交 -> 推送
|
|
2223
|
+
*/
|
|
2224
|
+
async pushWithCommit(commitMessage, options) {
|
|
2225
|
+
await this.ensureValidRepo();
|
|
2226
|
+
const status = await this.getStatus();
|
|
2227
|
+
if (!status.hasChanges) {
|
|
2228
|
+
this.logger.info("\u6CA1\u6709\u9700\u8981\u63D0\u4EA4\u7684\u66F4\u6539");
|
|
2229
|
+
return;
|
|
2230
|
+
}
|
|
2231
|
+
try {
|
|
2232
|
+
await this.pushSubmodules(commitMessage);
|
|
2233
|
+
const latestStatus = await this.getStatus();
|
|
2234
|
+
if (!latestStatus.hasChanges) {
|
|
2235
|
+
this.logger.info("\u6CA1\u6709\u9700\u8981\u63D0\u4EA4\u7684\u66F4\u6539");
|
|
2236
|
+
return;
|
|
2237
|
+
}
|
|
2238
|
+
this.logger.info(`\u6B63\u5728\u63A8\u9001 ${latestStatus.changedFiles} \u4E2A\u6587\u4EF6...`);
|
|
2239
|
+
latestStatus.files.forEach((file, index) => {
|
|
2240
|
+
console.log(` ${index + 1}. ${file.path}`);
|
|
2241
|
+
});
|
|
2242
|
+
await runCommand("git add .", this.dir);
|
|
2243
|
+
const message = commitMessage || `update: ${latestStatus.changedFiles} files modified`;
|
|
2244
|
+
await runCommand(`git commit -m "${message}"`, this.dir);
|
|
2245
|
+
let cmd = "git push";
|
|
2246
|
+
if (options?.force) cmd += " --force";
|
|
2247
|
+
await runCommand(cmd, this.dir);
|
|
2248
|
+
const remoteInfo = await this.getRemoteInfo();
|
|
2249
|
+
if (remoteInfo) {
|
|
2250
|
+
this.logger.success(
|
|
2251
|
+
`\u63A8\u9001\u6210\u529F: ${latestStatus.changedFiles} \u4E2A\u6587\u4EF6 \u2192 https://github.com/${remoteInfo.owner}/${remoteInfo.repo}`
|
|
2252
|
+
);
|
|
2253
|
+
} else {
|
|
2254
|
+
this.logger.success(`\u63A8\u9001\u6210\u529F: ${latestStatus.changedFiles} \u4E2A\u6587\u4EF6`);
|
|
2255
|
+
}
|
|
2256
|
+
} catch (error) {
|
|
2257
|
+
this.logger.error(`\u63A8\u9001\u5931\u8D25`);
|
|
2258
|
+
handleError(error);
|
|
2259
|
+
throw error;
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
/**
|
|
2263
|
+
* 完整的同步流程:拉取 -> 推送
|
|
2264
|
+
*/
|
|
2265
|
+
async sync(options) {
|
|
2266
|
+
const { commitMessage, rebase = true } = options || {};
|
|
2267
|
+
try {
|
|
2268
|
+
await this.pull({ rebase, autostash: true });
|
|
2269
|
+
await this.pushWithCommit(commitMessage);
|
|
2270
|
+
} catch (error) {
|
|
2271
|
+
this.logger.error("Sync failed");
|
|
2272
|
+
handleError(error);
|
|
2273
|
+
throw error;
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
// ==================== Submodule 操作 ====================
|
|
2277
|
+
/**
|
|
2278
|
+
* 检查仓库是否包含 submodule
|
|
2279
|
+
*/
|
|
2280
|
+
hasSubmodules() {
|
|
2281
|
+
return existsSync6(resolve(this.dir, ".gitmodules"));
|
|
2282
|
+
}
|
|
2283
|
+
/**
|
|
2284
|
+
* 获取所有 submodule 的路径
|
|
2285
|
+
*/
|
|
2286
|
+
async getSubmodulePaths() {
|
|
2287
|
+
if (!this.hasSubmodules()) return [];
|
|
2288
|
+
try {
|
|
2289
|
+
const output = await runCommand(
|
|
2290
|
+
"git config --file .gitmodules --get-regexp path",
|
|
2291
|
+
this.dir
|
|
2292
|
+
);
|
|
2293
|
+
return output.trim().split("\n").filter((line) => line).map((line) => line.replace(/^submodule\..*\.path\s+/, ""));
|
|
2294
|
+
} catch {
|
|
2295
|
+
return [];
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
/**
|
|
2299
|
+
* 推送前处理 submodule:检查未提交/未推送的更改,自动提交并推送
|
|
2300
|
+
*/
|
|
2301
|
+
async pushSubmodules(commitMessage) {
|
|
2302
|
+
const paths = await this.getSubmodulePaths();
|
|
2303
|
+
if (paths.length === 0) return;
|
|
2304
|
+
for (const subPath of paths) {
|
|
2305
|
+
const absPath = resolve(this.dir, subPath);
|
|
2306
|
+
let hasChanges = false;
|
|
2307
|
+
try {
|
|
2308
|
+
const status = await runCommand("git status --porcelain", absPath);
|
|
2309
|
+
hasChanges = status.trim().length > 0;
|
|
2310
|
+
} catch {
|
|
2311
|
+
continue;
|
|
2312
|
+
}
|
|
2313
|
+
if (hasChanges) {
|
|
2314
|
+
const message = commitMessage || "update";
|
|
2315
|
+
this.logger.info(`Submodule [${subPath}] \u6709\u672A\u63D0\u4EA4\u7684\u66F4\u6539\uFF0C\u6B63\u5728\u63D0\u4EA4...`);
|
|
2316
|
+
await runCommand("git add -A", absPath);
|
|
2317
|
+
await runCommand(`git commit -m "${message}"`, absPath);
|
|
2318
|
+
}
|
|
2319
|
+
let unpushed = 0;
|
|
2320
|
+
try {
|
|
2321
|
+
const output = await runCommand(
|
|
2322
|
+
"git rev-list @{u}..HEAD --count",
|
|
2323
|
+
absPath
|
|
2324
|
+
);
|
|
2325
|
+
unpushed = parseInt(output.trim()) || 0;
|
|
2326
|
+
} catch {
|
|
2327
|
+
unpushed = 1;
|
|
2328
|
+
}
|
|
2329
|
+
if (unpushed > 0) {
|
|
2330
|
+
this.logger.info(
|
|
2331
|
+
`Submodule [${subPath}] \u6709 ${unpushed} \u4E2A\u672A\u63A8\u9001\u7684\u63D0\u4EA4\uFF0C\u6B63\u5728\u63A8\u9001...`
|
|
2332
|
+
);
|
|
2333
|
+
await runCommand("git push", absPath);
|
|
2334
|
+
this.logger.success(`Submodule [${subPath}] \u63A8\u9001\u6210\u529F`);
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
/**
|
|
2339
|
+
* 拉取后更新 submodule 到父仓库指针指向的 commit
|
|
2340
|
+
*/
|
|
2341
|
+
async updateSubmodules() {
|
|
2342
|
+
if (!this.hasSubmodules()) return;
|
|
2343
|
+
try {
|
|
2344
|
+
this.logger.info("\u6B63\u5728\u66F4\u65B0 submodule...");
|
|
2345
|
+
await runCommand("git submodule update --init", this.dir);
|
|
2346
|
+
this.logger.success("Submodule \u5DF2\u540C\u6B65\u5230\u6700\u65B0\u6307\u9488");
|
|
2347
|
+
} catch (error) {
|
|
2348
|
+
this.logger.warn(
|
|
2349
|
+
"Submodule \u66F4\u65B0\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u6267\u884C git submodule update --init"
|
|
2350
|
+
);
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
/**
|
|
2354
|
+
* 显示状态摘要
|
|
2355
|
+
*/
|
|
2356
|
+
async showStatus(options) {
|
|
2357
|
+
const { showFiles = true } = options || {};
|
|
2358
|
+
const status = await this.getStatus();
|
|
2359
|
+
const remoteInfo = await this.getRemoteInfo();
|
|
2360
|
+
console.log("\n\u{1F4CA} Git \u72B6\u6001:");
|
|
2361
|
+
console.log(` \u5206\u652F: ${status.branch}`);
|
|
2362
|
+
if (remoteInfo) {
|
|
2363
|
+
console.log(
|
|
2364
|
+
` \u8FDC\u7A0B: ${remoteInfo.owner}/${remoteInfo.repo} (${remoteInfo.type})`
|
|
2365
|
+
);
|
|
2366
|
+
}
|
|
2367
|
+
if (status.hasChanges) {
|
|
2368
|
+
console.log(
|
|
2369
|
+
` \u53D8\u66F4: ${status.changedFiles} \u4E2A\u6587\u4EF6 (\u5DF2\u6682\u5B58 ${status.staged}, \u672A\u6682\u5B58 ${status.unstaged}, \u672A\u8DDF\u8E2A ${status.untracked})`
|
|
2370
|
+
);
|
|
2371
|
+
if (showFiles && status.files.length > 0) {
|
|
2372
|
+
console.log(" \u53D8\u66F4\u6587\u4EF6\u5217\u8868:");
|
|
2373
|
+
const stagedFiles = status.files.filter((f) => f.status === "staged");
|
|
2374
|
+
const unstagedFiles = status.files.filter(
|
|
2375
|
+
(f) => f.status === "unstaged"
|
|
2376
|
+
);
|
|
2377
|
+
const untrackedFiles = status.files.filter(
|
|
2378
|
+
(f) => f.status === "untracked"
|
|
2379
|
+
);
|
|
2380
|
+
if (stagedFiles.length > 0) {
|
|
2381
|
+
console.log(" \u5DF2\u6682\u5B58:");
|
|
2382
|
+
stagedFiles.forEach((f) => console.log(` \u2713 ${f.path}`));
|
|
2383
|
+
}
|
|
2384
|
+
if (unstagedFiles.length > 0) {
|
|
2385
|
+
console.log(" \u672A\u6682\u5B58:");
|
|
2386
|
+
unstagedFiles.forEach((f) => console.log(` \u2022 ${f.path}`));
|
|
2387
|
+
}
|
|
2388
|
+
if (untrackedFiles.length > 0) {
|
|
2389
|
+
console.log(" \u672A\u8DDF\u8E2A:");
|
|
2390
|
+
untrackedFiles.forEach((f) => console.log(` ? ${f.path}`));
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
} else {
|
|
2394
|
+
console.log(" \u72B6\u6001: \u5DE5\u4F5C\u533A\u5E72\u51C0\uFF0C\u6CA1\u6709\u53D8\u66F4");
|
|
2395
|
+
}
|
|
2396
|
+
if (status.ahead > 0 || status.behind > 0) {
|
|
2397
|
+
const syncInfo = [];
|
|
2398
|
+
if (status.ahead > 0) syncInfo.push(`\u9886\u5148 ${status.ahead} \u4E2A\u63D0\u4EA4`);
|
|
2399
|
+
if (status.behind > 0) syncInfo.push(`\u843D\u540E ${status.behind} \u4E2A\u63D0\u4EA4`);
|
|
2400
|
+
console.log(` \u540C\u6B65: ${syncInfo.join(", ")}`);
|
|
2401
|
+
}
|
|
2402
|
+
console.log();
|
|
2403
|
+
}
|
|
2404
|
+
};
|
|
2405
|
+
|
|
2406
|
+
// core/ProcessManager.ts
|
|
2407
|
+
import { spawn } from "child_process";
|
|
2408
|
+
var ProcessManager = class {
|
|
2409
|
+
constructor() {
|
|
2410
|
+
this.processes = /* @__PURE__ */ new Map();
|
|
2411
|
+
this.logger = new Logger({ prefix: "process" });
|
|
2412
|
+
process.on("exit", () => {
|
|
2413
|
+
this.killAll();
|
|
2414
|
+
});
|
|
2415
|
+
process.on("SIGINT", () => {
|
|
2416
|
+
this.killAll();
|
|
2417
|
+
process.exit(0);
|
|
2418
|
+
});
|
|
2419
|
+
process.on("SIGTERM", () => {
|
|
2420
|
+
this.killAll();
|
|
2421
|
+
process.exit(0);
|
|
2422
|
+
});
|
|
2423
|
+
}
|
|
2424
|
+
/**
|
|
2425
|
+
* 启动进程
|
|
2426
|
+
* @param id - 进程ID
|
|
2427
|
+
* @param command - 命令
|
|
2428
|
+
* @param args - 参数列表
|
|
2429
|
+
* @param options - spawn 选项
|
|
2430
|
+
* @returns ProcessInfo
|
|
2431
|
+
*/
|
|
2432
|
+
spawn(id, command, args = [], options) {
|
|
2433
|
+
if (this.processes.has(id)) {
|
|
2434
|
+
this.logger.warn(`\u8FDB\u7A0B ${id} \u5DF2\u5B58\u5728\uFF0C\u5148\u505C\u6B62\u65E7\u8FDB\u7A0B`);
|
|
2435
|
+
this.kill(id);
|
|
2436
|
+
}
|
|
2437
|
+
const proc = spawn(command, args, {
|
|
2438
|
+
stdio: "inherit",
|
|
2439
|
+
shell: true,
|
|
2440
|
+
...options
|
|
2441
|
+
});
|
|
2442
|
+
const processInfo = {
|
|
2443
|
+
id,
|
|
2444
|
+
pid: proc.pid,
|
|
2445
|
+
command,
|
|
2446
|
+
args,
|
|
2447
|
+
startTime: Date.now(),
|
|
2448
|
+
process: proc
|
|
2449
|
+
};
|
|
2450
|
+
this.processes.set(id, processInfo);
|
|
2451
|
+
proc.on("exit", (code, signal) => {
|
|
2452
|
+
this.logger.info(`\u8FDB\u7A0B ${id} \u5DF2\u9000\u51FA (code: ${code}, signal: ${signal})`);
|
|
2453
|
+
this.processes.delete(id);
|
|
2454
|
+
});
|
|
2455
|
+
proc.on("error", (err) => {
|
|
2456
|
+
this.logger.error(`\u8FDB\u7A0B ${id} \u51FA\u9519: ${err.message}`);
|
|
2457
|
+
this.processes.delete(id);
|
|
2458
|
+
});
|
|
2459
|
+
return processInfo;
|
|
2460
|
+
}
|
|
2461
|
+
/**
|
|
2462
|
+
* 停止进程
|
|
2463
|
+
* @param id - 进程ID
|
|
2464
|
+
* @param signal - 信号(默认为 SIGTERM)
|
|
2465
|
+
* @returns 是否成功停止
|
|
2466
|
+
*/
|
|
2467
|
+
kill(id, signal = "SIGTERM") {
|
|
2468
|
+
const processInfo = this.processes.get(id);
|
|
2469
|
+
if (!processInfo) {
|
|
2470
|
+
this.logger.warn(`\u8FDB\u7A0B ${id} \u4E0D\u5B58\u5728`);
|
|
2471
|
+
return false;
|
|
2472
|
+
}
|
|
2473
|
+
this.logger.info(`\u505C\u6B62\u8FDB\u7A0B: ${id} (PID: ${processInfo.pid})`);
|
|
2474
|
+
try {
|
|
2475
|
+
const killed = processInfo.process.kill(signal);
|
|
2476
|
+
if (killed) {
|
|
2477
|
+
this.processes.delete(id);
|
|
2478
|
+
return true;
|
|
2479
|
+
}
|
|
2480
|
+
return false;
|
|
2481
|
+
} catch (error) {
|
|
2482
|
+
this.logger.error(`\u505C\u6B62\u8FDB\u7A0B ${id} \u5931\u8D25: ${error}`);
|
|
2483
|
+
return false;
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
/**
|
|
2487
|
+
* 检查进程是否存在
|
|
2488
|
+
* @param id - 进程ID
|
|
2489
|
+
* @returns 是否存在
|
|
2490
|
+
*/
|
|
2491
|
+
has(id) {
|
|
2492
|
+
return this.processes.has(id);
|
|
2493
|
+
}
|
|
2494
|
+
/**
|
|
2495
|
+
* 检查进程是否在运行
|
|
2496
|
+
* @param id - 进程 ID
|
|
2497
|
+
* @returns 是否在运行
|
|
2498
|
+
*/
|
|
2499
|
+
isRunning(id) {
|
|
2500
|
+
const processInfo = this.processes.get(id);
|
|
2501
|
+
if (!processInfo) return false;
|
|
2502
|
+
try {
|
|
2503
|
+
return process.kill(processInfo.pid, 0);
|
|
2504
|
+
} catch {
|
|
2505
|
+
return false;
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
/**
|
|
2509
|
+
* 停止所有进程
|
|
2510
|
+
* @param signal - 信号(默认为 SIGTERM)
|
|
2511
|
+
*/
|
|
2512
|
+
killAll(signal = "SIGTERM") {
|
|
2513
|
+
if (this.processes.size === 0) {
|
|
2514
|
+
return;
|
|
2515
|
+
}
|
|
2516
|
+
this.logger.info(`\u505C\u6B62\u6240\u6709\u8FDB\u7A0B (${this.processes.size} \u4E2A)`);
|
|
2517
|
+
for (const [id, processInfo] of this.processes) {
|
|
2518
|
+
try {
|
|
2519
|
+
processInfo.process.kill(signal);
|
|
2520
|
+
this.logger.info(`\u5DF2\u505C\u6B62\u8FDB\u7A0B: ${id}`);
|
|
2521
|
+
} catch (error) {
|
|
2522
|
+
this.logger.error(`\u505C\u6B62\u8FDB\u7A0B ${id} \u5931\u8D25: ${error}`);
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
this.processes.clear();
|
|
2526
|
+
}
|
|
2527
|
+
};
|
|
2528
|
+
|
|
2529
|
+
// services/git/service.ts
|
|
2530
|
+
var GitService = class {
|
|
2531
|
+
constructor() {
|
|
2532
|
+
this.gitManager = new GitManager(ROOT_DIR_PATH);
|
|
2533
|
+
}
|
|
2534
|
+
/**
|
|
2535
|
+
* 推送到远程仓库
|
|
2536
|
+
* @param options - 推送选项
|
|
2537
|
+
*/
|
|
2538
|
+
async push(options = {}) {
|
|
2539
|
+
const { message, branch, force = false } = options;
|
|
2540
|
+
logger.info("Pushing to remote repository...");
|
|
2541
|
+
if (message) {
|
|
2542
|
+
await this.gitManager.pushWithCommit(message, { force });
|
|
2543
|
+
} else {
|
|
2544
|
+
await this.gitManager.push({ setUpstream: !!branch, force });
|
|
2545
|
+
}
|
|
2546
|
+
logger.info("Push completed successfully");
|
|
2547
|
+
}
|
|
2548
|
+
/**
|
|
2549
|
+
* 从远程仓库拉取
|
|
2550
|
+
* @param options - 拉取选项
|
|
2551
|
+
*/
|
|
2552
|
+
async pull(options = {}) {
|
|
2553
|
+
const { rebase = false } = options;
|
|
2554
|
+
logger.info("Pulling from remote repository...");
|
|
2555
|
+
await this.gitManager.pull({ rebase });
|
|
2556
|
+
logger.info("Pull completed successfully");
|
|
2557
|
+
}
|
|
2558
|
+
/**
|
|
2559
|
+
* 同步本地和远程仓库(先拉取后推送)
|
|
2560
|
+
* @param commitMessage - 可选的提交信息
|
|
2561
|
+
*/
|
|
2562
|
+
async sync(commitMessage) {
|
|
2563
|
+
logger.info("Syncing with remote repository...");
|
|
2564
|
+
await this.gitManager.sync({ commitMessage });
|
|
2565
|
+
logger.info("Sync completed successfully");
|
|
2566
|
+
}
|
|
2567
|
+
/**
|
|
2568
|
+
* 获取 Git 状态
|
|
2569
|
+
* @returns Git 状态信息
|
|
2570
|
+
*/
|
|
2571
|
+
async getStatus() {
|
|
2572
|
+
return await this.gitManager.getStatus();
|
|
2573
|
+
}
|
|
2574
|
+
/**
|
|
2575
|
+
* 检查是否有未提交的更改
|
|
2576
|
+
* @returns 是否有未提交的更改
|
|
2577
|
+
*/
|
|
2578
|
+
async hasChanges() {
|
|
2579
|
+
const status = await this.getStatus();
|
|
2580
|
+
return status.hasChanges;
|
|
2581
|
+
}
|
|
2582
|
+
/**
|
|
2583
|
+
* 生成自动提交信息
|
|
2584
|
+
* @returns 自动生成的提交信息
|
|
2585
|
+
*/
|
|
2586
|
+
generateCommitMessage() {
|
|
2587
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
2588
|
+
const time = (/* @__PURE__ */ new Date()).toTimeString().split(" ")[0];
|
|
2589
|
+
return `\u{1F4DD} Update notes - ${date} ${time}`;
|
|
2590
|
+
}
|
|
2591
|
+
/**
|
|
2592
|
+
* 快速提交并推送(使用自动生成的提交信息)
|
|
2593
|
+
* @param options - 推送选项
|
|
2594
|
+
*/
|
|
2595
|
+
async quickPush(options = {}) {
|
|
2596
|
+
if (!options.skipCheck && !await this.hasChanges()) {
|
|
2597
|
+
logger.info("No changes to commit");
|
|
2598
|
+
return;
|
|
2599
|
+
}
|
|
2600
|
+
const message = this.generateCommitMessage();
|
|
2601
|
+
await this.push({ message, force: options.force });
|
|
2602
|
+
}
|
|
2603
|
+
};
|
|
2604
|
+
|
|
2605
|
+
// services/sync-core/service.ts
|
|
2606
|
+
import { existsSync as existsSync7 } from "fs";
|
|
2607
|
+
import { join as join7, basename as basename2 } from "path";
|
|
2608
|
+
var SyncCoreService = class {
|
|
2609
|
+
/**
|
|
2610
|
+
* 同步单个仓库的 submodule 到最新版本
|
|
2611
|
+
*/
|
|
2612
|
+
async syncSingleRepo(targetDir) {
|
|
2613
|
+
const repoName = basename2(targetDir);
|
|
2614
|
+
const submodulePath = join7(targetDir, ".vitepress", "tnotes");
|
|
2615
|
+
try {
|
|
2616
|
+
if (!existsSync7(join7(targetDir, ".gitmodules"))) {
|
|
2617
|
+
return {
|
|
2618
|
+
dir: targetDir,
|
|
2619
|
+
repoName,
|
|
2620
|
+
success: false,
|
|
2621
|
+
updated: false,
|
|
2622
|
+
error: "\u672A\u627E\u5230 .gitmodules\uFF0C\u8BE5\u4ED3\u5E93\u672A\u914D\u7F6E submodule"
|
|
2623
|
+
};
|
|
2624
|
+
}
|
|
2625
|
+
if (!existsSync7(submodulePath)) {
|
|
2626
|
+
await runCommand("git submodule update --init", targetDir);
|
|
2627
|
+
}
|
|
2628
|
+
const beforeHash = (await runCommand("git rev-parse HEAD", submodulePath)).trim();
|
|
2629
|
+
const beforeTime = (await runCommand("git log -1 --format=%ci HEAD", submodulePath)).trim().replace(/ [+-]\d{4}$/, "");
|
|
2630
|
+
await runCommand("git fetch origin", submodulePath);
|
|
2631
|
+
await runCommand("git reset --hard origin/main", submodulePath);
|
|
2632
|
+
const afterHash = (await runCommand("git rev-parse HEAD", submodulePath)).trim();
|
|
2633
|
+
const afterTime = (await runCommand("git log -1 --format=%ci HEAD", submodulePath)).trim().replace(/ [+-]\d{4}$/, "");
|
|
2634
|
+
const updated = beforeHash !== afterHash;
|
|
2635
|
+
if (updated) {
|
|
2636
|
+
await runCommand("git add .vitepress/tnotes", targetDir);
|
|
2637
|
+
await runCommand('git commit -m "chore: update TNotes.core"', targetDir);
|
|
2638
|
+
}
|
|
2639
|
+
return {
|
|
2640
|
+
dir: targetDir,
|
|
2641
|
+
repoName,
|
|
2642
|
+
success: true,
|
|
2643
|
+
updated,
|
|
2644
|
+
beforeHash: beforeHash.substring(0, 7),
|
|
2645
|
+
beforeTime,
|
|
2646
|
+
afterHash: afterHash.substring(0, 7),
|
|
2647
|
+
afterTime
|
|
2648
|
+
};
|
|
2649
|
+
} catch (error) {
|
|
2650
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2651
|
+
return {
|
|
2652
|
+
dir: targetDir,
|
|
2653
|
+
repoName,
|
|
2654
|
+
success: false,
|
|
2655
|
+
updated: false,
|
|
2656
|
+
error: errorMessage
|
|
2657
|
+
};
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
/**
|
|
2661
|
+
* 同步所有兄弟仓库的 TNotes.core 到最新版本
|
|
2662
|
+
*/
|
|
2663
|
+
async syncToAllRepos() {
|
|
2664
|
+
try {
|
|
2665
|
+
const targetDirs = getTargetDirs(TNOTES_BASE_DIR, "TNotes.", [
|
|
2666
|
+
ROOT_DIR_PATH,
|
|
2667
|
+
TNOTES_CORE_DIR,
|
|
2668
|
+
EN_WORDS_DIR
|
|
2669
|
+
]);
|
|
2670
|
+
if (targetDirs.length === 0) {
|
|
2671
|
+
logger.warn("\u672A\u627E\u5230\u7B26\u5408\u6761\u4EF6\u7684\u76EE\u6807\u76EE\u5F55");
|
|
2672
|
+
return;
|
|
2673
|
+
}
|
|
2674
|
+
logger.info(`\u6B63\u5728\u540C\u6B65 ${targetDirs.length} \u4E2A\u4ED3\u5E93\u7684 TNotes.core...`);
|
|
2675
|
+
console.log();
|
|
2676
|
+
const results = [];
|
|
2677
|
+
for (let i = 0; i < targetDirs.length; i++) {
|
|
2678
|
+
const dir = targetDirs[i];
|
|
2679
|
+
const repoName = basename2(dir);
|
|
2680
|
+
logger.info(`[${i + 1}/${targetDirs.length}] ${repoName}`);
|
|
2681
|
+
const result = await this.syncSingleRepo(dir);
|
|
2682
|
+
results.push(result);
|
|
2683
|
+
if (result.success) {
|
|
2684
|
+
if (result.updated) {
|
|
2685
|
+
logger.success(
|
|
2686
|
+
` \u2713 \u5DF2\u66F4\u65B0 ${result.beforeHash}(${result.beforeTime}) \u2192 ${result.afterHash}(${result.afterTime})
|
|
2687
|
+
`
|
|
2688
|
+
);
|
|
2689
|
+
} else {
|
|
2690
|
+
logger.info(
|
|
2691
|
+
` - \u5DF2\u662F\u6700\u65B0 ${result.afterHash}(${result.afterTime})
|
|
2692
|
+
`
|
|
2693
|
+
);
|
|
2694
|
+
}
|
|
2695
|
+
} else {
|
|
2696
|
+
logger.error(` \u2717 \u5931\u8D25: ${result.error}
|
|
2697
|
+
`);
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
const successCount = results.filter((r) => r.success).length;
|
|
2701
|
+
const updatedCount = results.filter((r) => r.updated).length;
|
|
2702
|
+
const failCount = results.length - successCount;
|
|
2703
|
+
console.log("\u2501".repeat(50));
|
|
2704
|
+
if (failCount === 0) {
|
|
2705
|
+
logger.success(
|
|
2706
|
+
`\u2728 \u540C\u6B65\u5B8C\u6210: ${updatedCount} \u4E2A\u4ED3\u5E93\u5DF2\u66F4\u65B0, ${successCount - updatedCount} \u4E2A\u5DF2\u662F\u6700\u65B0 (\u5171 ${results.length} \u4E2A)`
|
|
2707
|
+
);
|
|
2708
|
+
} else {
|
|
2709
|
+
logger.warn(
|
|
2710
|
+
`\u26A0\uFE0F \u540C\u6B65\u5B8C\u6210: ${successCount} \u6210\u529F (${updatedCount} \u66F4\u65B0), ${failCount} \u5931\u8D25 (\u5171 ${results.length} \u4E2A)`
|
|
2711
|
+
);
|
|
2712
|
+
console.log("\n\u5931\u8D25\u7684\u4ED3\u5E93:");
|
|
2713
|
+
results.filter((r) => !r.success).forEach((r, index) => {
|
|
2714
|
+
console.log(` ${index + 1}. ${r.repoName}`);
|
|
2715
|
+
console.log(` \u9519\u8BEF: ${r.error}`);
|
|
2716
|
+
});
|
|
2717
|
+
}
|
|
2718
|
+
} catch (error) {
|
|
2719
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2720
|
+
logger.error(`TNotes.core \u540C\u6B65\u5931\u8D25: ${errorMessage}`);
|
|
2721
|
+
throw error;
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
};
|
|
2725
|
+
|
|
2726
|
+
// services/timestamp/service.ts
|
|
2727
|
+
import {
|
|
2728
|
+
existsSync as existsSync8,
|
|
2729
|
+
readFileSync as readFileSync5,
|
|
2730
|
+
writeFileSync as writeFileSync4,
|
|
2731
|
+
readdirSync as readdirSync2,
|
|
2732
|
+
statSync as statSync2
|
|
2733
|
+
} from "fs";
|
|
2734
|
+
import { join as join8 } from "path";
|
|
2735
|
+
import { execSync } from "child_process";
|
|
2736
|
+
var BIRTH_DATE = (/* @__PURE__ */ new Date("1999-06-29T00:00:00+08:00")).getTime();
|
|
2737
|
+
var TimestampService = class {
|
|
2738
|
+
constructor() {
|
|
2739
|
+
this.noteManager = NoteManager.getInstance();
|
|
2740
|
+
}
|
|
2741
|
+
/**
|
|
2742
|
+
* 从 git 获取文件的创建时间和最后修改时间
|
|
2743
|
+
* @param noteDirPath - 笔记目录路径
|
|
2744
|
+
* @returns 时间戳对象,包含 created_at 和 updated_at
|
|
2745
|
+
*/
|
|
2746
|
+
getGitTimestamps(noteDirPath) {
|
|
2747
|
+
try {
|
|
2748
|
+
const readmePath = join8(noteDirPath, "README.md");
|
|
2749
|
+
const createdAtOutput = execSync(
|
|
2750
|
+
`git log --diff-filter=A --follow --format=%ct -- "${readmePath}"`,
|
|
2751
|
+
{
|
|
2752
|
+
cwd: ROOT_DIR_PATH,
|
|
2753
|
+
encoding: "utf-8",
|
|
2754
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
2755
|
+
}
|
|
2756
|
+
).split(/\r?\n/).filter(Boolean).pop();
|
|
2757
|
+
const updatedAtOutput = execSync(
|
|
2758
|
+
`git log -1 --format=%ct -- "${readmePath}"`,
|
|
2759
|
+
{
|
|
2760
|
+
cwd: ROOT_DIR_PATH,
|
|
2761
|
+
encoding: "utf-8",
|
|
2762
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
2763
|
+
}
|
|
2764
|
+
).trim();
|
|
2765
|
+
if (!createdAtOutput || !updatedAtOutput) {
|
|
2766
|
+
return null;
|
|
2767
|
+
}
|
|
2768
|
+
return {
|
|
2769
|
+
created_at: parseInt(createdAtOutput) * 1e3,
|
|
2770
|
+
// 转换为毫秒
|
|
2771
|
+
updated_at: parseInt(updatedAtOutput) * 1e3
|
|
2772
|
+
};
|
|
2773
|
+
} catch (error) {
|
|
2774
|
+
logger.debug?.(`getGitTimestamps failed: ${noteDirPath}`, error);
|
|
2775
|
+
return null;
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
/**
|
|
2779
|
+
* 修复单个笔记的时间戳
|
|
2780
|
+
* @param noteDir - 笔记目录名
|
|
2781
|
+
* @param forceUpdate - 是否强制更新(忽略现有值)
|
|
2782
|
+
* @returns 是否进行了修复
|
|
2783
|
+
*/
|
|
2784
|
+
fixNoteTimestamps(noteDir, forceUpdate = false) {
|
|
2785
|
+
const configPath = join8(NOTES_DIR_PATH, noteDir, ".tnotes.json");
|
|
2786
|
+
if (!existsSync8(configPath)) {
|
|
2787
|
+
return false;
|
|
2788
|
+
}
|
|
2789
|
+
try {
|
|
2790
|
+
const configContent = readFileSync5(configPath, "utf-8");
|
|
2791
|
+
const config = JSON.parse(configContent);
|
|
2792
|
+
const noteDirPath = join8(NOTES_DIR_PATH, noteDir);
|
|
2793
|
+
const timestamps = this.getGitTimestamps(noteDirPath);
|
|
2794
|
+
if (!timestamps) {
|
|
2795
|
+
return false;
|
|
2796
|
+
}
|
|
2797
|
+
let modified = false;
|
|
2798
|
+
if (forceUpdate || !config.created_at || config.created_at !== timestamps.created_at) {
|
|
2799
|
+
config.created_at = timestamps.created_at;
|
|
2800
|
+
modified = true;
|
|
2801
|
+
}
|
|
2802
|
+
if (forceUpdate) {
|
|
2803
|
+
if (config.updated_at !== timestamps.updated_at) {
|
|
2804
|
+
config.updated_at = timestamps.updated_at;
|
|
2805
|
+
modified = true;
|
|
2806
|
+
}
|
|
2807
|
+
} else {
|
|
2808
|
+
if (!config.updated_at) {
|
|
2809
|
+
config.updated_at = timestamps.updated_at;
|
|
2810
|
+
modified = true;
|
|
2811
|
+
} else if (timestamps.updated_at > config.updated_at) {
|
|
2812
|
+
config.updated_at = timestamps.updated_at;
|
|
2813
|
+
modified = true;
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
if (modified) {
|
|
2817
|
+
this.noteManager.writeNoteConfig(configPath, config);
|
|
2818
|
+
return true;
|
|
2819
|
+
}
|
|
2820
|
+
return false;
|
|
2821
|
+
} catch (error) {
|
|
2822
|
+
logger.error(`\u4FEE\u590D\u65F6\u95F4\u6233\u5931\u8D25: ${noteDir}`, error);
|
|
2823
|
+
return false;
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
/**
|
|
2827
|
+
* 修复根配置文件的时间戳
|
|
2828
|
+
* @param forceUpdate - 是否强制更新
|
|
2829
|
+
* @returns 是否进行了修复
|
|
2830
|
+
*/
|
|
2831
|
+
fixRootConfigTimestamps(forceUpdate = false) {
|
|
2832
|
+
try {
|
|
2833
|
+
const configContent = readFileSync5(ROOT_CONFIG_PATH, "utf-8");
|
|
2834
|
+
const config = JSON.parse(configContent);
|
|
2835
|
+
const createdAtOutput = execSync("git log --reverse --format=%ct", {
|
|
2836
|
+
cwd: ROOT_DIR_PATH,
|
|
2837
|
+
encoding: "utf-8",
|
|
2838
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
2839
|
+
}).trim();
|
|
2840
|
+
const updatedAtOutput = execSync("git log -1 --format=%ct", {
|
|
2841
|
+
cwd: ROOT_DIR_PATH,
|
|
2842
|
+
encoding: "utf-8",
|
|
2843
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
2844
|
+
}).trim();
|
|
2845
|
+
if (!createdAtOutput || !updatedAtOutput) {
|
|
2846
|
+
return false;
|
|
2847
|
+
}
|
|
2848
|
+
const firstTimestamp = createdAtOutput.split("\n")[0];
|
|
2849
|
+
const createdAt = parseInt(firstTimestamp) * 1e3;
|
|
2850
|
+
const updatedAt = parseInt(updatedAtOutput) * 1e3;
|
|
2851
|
+
let modified = false;
|
|
2852
|
+
if (forceUpdate || !config.root_item.created_at || config.root_item.created_at !== createdAt) {
|
|
2853
|
+
config.root_item.created_at = createdAt;
|
|
2854
|
+
modified = true;
|
|
2855
|
+
}
|
|
2856
|
+
if (forceUpdate || !config.root_item.updated_at || config.root_item.updated_at !== updatedAt) {
|
|
2857
|
+
config.root_item.updated_at = updatedAt;
|
|
2858
|
+
modified = true;
|
|
2859
|
+
}
|
|
2860
|
+
if (modified) {
|
|
2861
|
+
const daysSinceBirth = Math.floor((updatedAt - BIRTH_DATE) / (1e3 * 60 * 60 * 24)) + 1;
|
|
2862
|
+
config.root_item.days_since_birth = daysSinceBirth;
|
|
2863
|
+
writeFileSync4(
|
|
2864
|
+
ROOT_CONFIG_PATH,
|
|
2865
|
+
JSON.stringify(config, null, 2) + "\n",
|
|
2866
|
+
"utf-8"
|
|
2867
|
+
);
|
|
2868
|
+
return true;
|
|
2869
|
+
}
|
|
2870
|
+
return false;
|
|
2871
|
+
} catch (error) {
|
|
2872
|
+
logger.error("\u4FEE\u590D\u6839\u914D\u7F6E\u6587\u4EF6\u65F6\u95F4\u6233\u5931\u8D25", error);
|
|
2873
|
+
return false;
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
/**
|
|
2877
|
+
* 修复所有笔记的时间戳
|
|
2878
|
+
* @param forceUpdate - 是否强制更新(忽略现有值,用于修复历史错误数据)
|
|
2879
|
+
* @returns 修复统计信息
|
|
2880
|
+
*/
|
|
2881
|
+
async fixAllTimestamps(forceUpdate = false) {
|
|
2882
|
+
if (forceUpdate) {
|
|
2883
|
+
logger.info("\u6B63\u5728\u5F3A\u5236\u4FEE\u590D\u7B14\u8BB0\u65F6\u95F4\u6233\uFF08\u4F7F\u7528 git \u771F\u5B9E\u65F6\u95F4\uFF09...");
|
|
2884
|
+
} else {
|
|
2885
|
+
logger.info("\u6B63\u5728\u4FEE\u590D\u7B14\u8BB0\u65F6\u95F4\u6233...");
|
|
2886
|
+
}
|
|
2887
|
+
const rootConfigFixed = this.fixRootConfigTimestamps(forceUpdate);
|
|
2888
|
+
if (rootConfigFixed) {
|
|
2889
|
+
logger.success("\u2705 \u6839\u914D\u7F6E\u6587\u4EF6\u65F6\u95F4\u6233\u5DF2\u4FEE\u590D");
|
|
2890
|
+
}
|
|
2891
|
+
if (!existsSync8(NOTES_DIR_PATH)) {
|
|
2892
|
+
logger.error("notes \u76EE\u5F55\u4E0D\u5B58\u5728");
|
|
2893
|
+
return { fixed: 0, skipped: 0, total: 0, rootConfigFixed };
|
|
2894
|
+
}
|
|
2895
|
+
const noteDirs = readdirSync2(NOTES_DIR_PATH).filter((name) => {
|
|
2896
|
+
const fullPath = join8(NOTES_DIR_PATH, name);
|
|
2897
|
+
return statSync2(fullPath).isDirectory() && /^\d{4}\./.test(name);
|
|
2898
|
+
}).sort();
|
|
2899
|
+
let fixedCount = 0;
|
|
2900
|
+
let skippedCount = 0;
|
|
2901
|
+
for (const noteDir of noteDirs) {
|
|
2902
|
+
const fixed = this.fixNoteTimestamps(noteDir, forceUpdate);
|
|
2903
|
+
if (fixed) {
|
|
2904
|
+
fixedCount++;
|
|
2905
|
+
} else {
|
|
2906
|
+
skippedCount++;
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2909
|
+
if (fixedCount > 0) {
|
|
2910
|
+
logger.success(`\u65F6\u95F4\u6233\u4FEE\u590D\u5B8C\u6210: ${fixedCount} \u4E2A\u7B14\u8BB0\u5DF2\u66F4\u65B0`);
|
|
2911
|
+
} else {
|
|
2912
|
+
logger.info("\u6240\u6709\u7B14\u8BB0\u65F6\u95F4\u6233\u5747\u5DF2\u6B63\u786E");
|
|
2913
|
+
}
|
|
2914
|
+
return {
|
|
2915
|
+
fixed: fixedCount,
|
|
2916
|
+
skipped: skippedCount,
|
|
2917
|
+
total: noteDirs.length,
|
|
2918
|
+
rootConfigFixed
|
|
2919
|
+
};
|
|
2920
|
+
}
|
|
2921
|
+
/**
|
|
2922
|
+
* 更新指定笔记的时间戳为当前时间
|
|
2923
|
+
* @param noteDirNames - 笔记目录名数组
|
|
2924
|
+
* @returns 更新的笔记数量
|
|
2925
|
+
*/
|
|
2926
|
+
async updateNotesTimestamp(noteDirNames) {
|
|
2927
|
+
if (noteDirNames.length === 0) {
|
|
2928
|
+
return 0;
|
|
2929
|
+
}
|
|
2930
|
+
const now = Date.now();
|
|
2931
|
+
let updatedCount = 0;
|
|
2932
|
+
for (const noteDir of noteDirNames) {
|
|
2933
|
+
const configPath = join8(NOTES_DIR_PATH, noteDir, ".tnotes.json");
|
|
2934
|
+
if (!existsSync8(configPath)) {
|
|
2935
|
+
continue;
|
|
2936
|
+
}
|
|
2937
|
+
try {
|
|
2938
|
+
const configContent = readFileSync5(configPath, "utf-8");
|
|
2939
|
+
const config = JSON.parse(configContent);
|
|
2940
|
+
config.updated_at = now;
|
|
2941
|
+
this.noteManager.writeNoteConfig(configPath, config);
|
|
2942
|
+
updatedCount++;
|
|
2943
|
+
} catch (error) {
|
|
2944
|
+
logger.error(`\u66F4\u65B0\u65F6\u95F4\u6233\u5931\u8D25: ${noteDir}`, error);
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
return updatedCount;
|
|
2948
|
+
}
|
|
2949
|
+
/**
|
|
2950
|
+
* 获取本次变更中包含 README.md 的笔记列表
|
|
2951
|
+
* @param changedFiles - git status 返回的变更文件列表
|
|
2952
|
+
* @returns 变更的笔记目录名数组
|
|
2953
|
+
*/
|
|
2954
|
+
getChangedNotes(changedFiles) {
|
|
2955
|
+
const changedNotes = /* @__PURE__ */ new Set();
|
|
2956
|
+
for (let file of changedFiles) {
|
|
2957
|
+
file = file.replace(/^"(.*)"$/, "$1").replace(/"$/, "");
|
|
2958
|
+
const match = file.match(/^notes\/([^/]+)\/README\.md$/);
|
|
2959
|
+
if (match) {
|
|
2960
|
+
changedNotes.add(match[1]);
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
return Array.from(changedNotes);
|
|
2964
|
+
}
|
|
2965
|
+
};
|
|
2966
|
+
|
|
2967
|
+
// services/vitepress/service.ts
|
|
2968
|
+
import { spawn as spawn2 } from "child_process";
|
|
2969
|
+
var VitepressService = class _VitepressService {
|
|
2970
|
+
static {
|
|
2971
|
+
/** VitePress 开发服务器默认端口 */
|
|
2972
|
+
this.DEFAULT_DEV_PORT = 5173;
|
|
2973
|
+
}
|
|
2974
|
+
static {
|
|
2975
|
+
/** VitePress 预览服务器默认端口 */
|
|
2976
|
+
this.DEFAULT_PREVIEW_PORT = 4173;
|
|
2977
|
+
}
|
|
2978
|
+
static {
|
|
2979
|
+
/** 开发服务器进程 ID 后缀 */
|
|
2980
|
+
this.PROCESS_ID_DEV_SUFFIX = "vitepress-dev";
|
|
2981
|
+
}
|
|
2982
|
+
static {
|
|
2983
|
+
/** 预览服务器进程 ID 后缀 */
|
|
2984
|
+
this.PROCESS_ID_PREVIEW_SUFFIX = "vitepress-preview";
|
|
2985
|
+
}
|
|
2986
|
+
static {
|
|
2987
|
+
/** 服务启动超时时间(毫秒) */
|
|
2988
|
+
this.SERVER_STARTUP_TIMEOUT = 6e4;
|
|
2989
|
+
}
|
|
2990
|
+
static {
|
|
2991
|
+
/** 端口释放等待超时时间(毫秒) */
|
|
2992
|
+
this.PORT_RELEASE_TIMEOUT = 3e3;
|
|
2993
|
+
}
|
|
2994
|
+
static {
|
|
2995
|
+
/** 进程清理等待时间(毫秒) */
|
|
2996
|
+
this.PROCESS_CLEANUP_DELAY = 3e3;
|
|
2997
|
+
}
|
|
2998
|
+
static {
|
|
2999
|
+
/** 显示启动服务状态行间隔(毫秒) */
|
|
3000
|
+
this.SERVER_STATUS_LINE_INTERVAL = 1e3;
|
|
3001
|
+
}
|
|
3002
|
+
static {
|
|
3003
|
+
/** 默认包管理器 */
|
|
3004
|
+
this.DEFAULT_PACKAGE_MANAGER = "pnpm";
|
|
3005
|
+
}
|
|
3006
|
+
constructor() {
|
|
3007
|
+
this.processManager = new ProcessManager();
|
|
3008
|
+
this.configManager = ConfigManager.getInstance();
|
|
3009
|
+
}
|
|
3010
|
+
/**
|
|
3011
|
+
* 启动 VitePress 开发服务器
|
|
3012
|
+
* @returns 启动结果(服务就绪后返回),失败时返回 undefined
|
|
3013
|
+
*/
|
|
3014
|
+
async startServer() {
|
|
3015
|
+
const port = this.configManager.get("port") || _VitepressService.DEFAULT_DEV_PORT;
|
|
3016
|
+
const repoName = this.configManager.get("repoName");
|
|
3017
|
+
const processId = `${repoName}-${_VitepressService.PROCESS_ID_DEV_SUFFIX}`;
|
|
3018
|
+
if (this.processManager.has(processId) && this.processManager.isRunning(processId)) {
|
|
3019
|
+
this.processManager.kill(processId);
|
|
3020
|
+
await new Promise(
|
|
3021
|
+
(resolve3) => setTimeout(resolve3, _VitepressService.PROCESS_CLEANUP_DELAY)
|
|
3022
|
+
);
|
|
3023
|
+
}
|
|
3024
|
+
if (isPortInUse(port)) {
|
|
3025
|
+
logger.warn(`\u7AEF\u53E3 ${port} \u88AB\u5360\u7528\uFF0C\u6B63\u5728\u6E05\u7406...`);
|
|
3026
|
+
killPortProcess(port);
|
|
3027
|
+
const available = await waitForPort(
|
|
3028
|
+
port,
|
|
3029
|
+
_VitepressService.PORT_RELEASE_TIMEOUT
|
|
3030
|
+
);
|
|
3031
|
+
if (available) {
|
|
3032
|
+
logger.info(`\u7AEF\u53E3 ${port} \u5DF2\u91CA\u653E\uFF0C\u7EE7\u7EED\u542F\u52A8\u670D\u52A1`);
|
|
3033
|
+
} else {
|
|
3034
|
+
logger.warn(
|
|
3035
|
+
`\u7AEF\u53E3 ${port} \u672A\u786E\u8BA4\u91CA\u653E\uFF0C\u4ECD\u5C06\u5C1D\u8BD5\u542F\u52A8\uFF1B\u5982\u542F\u52A8\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u6E05\u7406\u8BE5\u7AEF\u53E3`
|
|
3036
|
+
);
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
const pm = this.configManager.get("packageManager") || _VitepressService.DEFAULT_PACKAGE_MANAGER;
|
|
3040
|
+
const args = ["vitepress", "dev", "--port", port.toString()];
|
|
3041
|
+
const processInfo = this.processManager.spawn(processId, pm, args, {
|
|
3042
|
+
cwd: ROOT_DIR_PATH,
|
|
3043
|
+
stdio: ["inherit", "pipe", "pipe"]
|
|
3044
|
+
// stdin 继承,stdout/stderr 管道捕获
|
|
3045
|
+
});
|
|
3046
|
+
const serverInfo = await this.waitForServerReady(processInfo.process);
|
|
3047
|
+
if (!processInfo.pid) return void 0;
|
|
3048
|
+
return {
|
|
3049
|
+
pid: processInfo.pid,
|
|
3050
|
+
version: serverInfo.version,
|
|
3051
|
+
elapsed: serverInfo.elapsed
|
|
3052
|
+
};
|
|
3053
|
+
}
|
|
3054
|
+
/**
|
|
3055
|
+
* 等待服务就绪,显示启动状态
|
|
3056
|
+
* @param childProcess - 子进程
|
|
3057
|
+
*/
|
|
3058
|
+
waitForServerReady(childProcess) {
|
|
3059
|
+
return new Promise((resolve3) => {
|
|
3060
|
+
const startTime = Date.now();
|
|
3061
|
+
let serverReady = false;
|
|
3062
|
+
let version = "";
|
|
3063
|
+
const statusTimer = setInterval(() => {
|
|
3064
|
+
if (serverReady) {
|
|
3065
|
+
clearInterval(statusTimer);
|
|
3066
|
+
return;
|
|
3067
|
+
}
|
|
3068
|
+
const elapsed = Date.now() - startTime;
|
|
3069
|
+
const seconds = (elapsed / 1e3).toFixed(0);
|
|
3070
|
+
process.stderr.clearLine?.(0);
|
|
3071
|
+
process.stderr.cursorTo?.(0);
|
|
3072
|
+
process.stderr.write(`\u23F3 \u542F\u52A8\u4E2D: \u5DF2\u7528 ${seconds}s...`);
|
|
3073
|
+
}, _VitepressService.SERVER_STATUS_LINE_INTERVAL);
|
|
3074
|
+
const handleOutput = (data) => {
|
|
3075
|
+
const text = data.toString();
|
|
3076
|
+
const versionMatch = text.match(/vitepress v([\d.]+)/);
|
|
3077
|
+
if (versionMatch) {
|
|
3078
|
+
version = versionMatch[1];
|
|
3079
|
+
}
|
|
3080
|
+
if (!serverReady && (text.includes("Local:") || text.includes("http://localhost") || text.includes("\u279C") && text.includes("Local"))) {
|
|
3081
|
+
serverReady = true;
|
|
3082
|
+
clearInterval(statusTimer);
|
|
3083
|
+
process.stderr.clearLine?.(0);
|
|
3084
|
+
process.stderr.cursorTo?.(0);
|
|
3085
|
+
const elapsed = Date.now() - startTime;
|
|
3086
|
+
setTimeout(() => resolve3({ version, elapsed }), 200);
|
|
3087
|
+
return;
|
|
3088
|
+
}
|
|
3089
|
+
if (!serverReady) {
|
|
3090
|
+
if (text.includes("error") || text.includes("Error") || text.includes("Port") && text.includes("is in use")) {
|
|
3091
|
+
process.stderr.clearLine?.(0);
|
|
3092
|
+
process.stderr.cursorTo?.(0);
|
|
3093
|
+
process.stdout.write(data);
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
};
|
|
3097
|
+
if (childProcess.stdout) {
|
|
3098
|
+
childProcess.stdout.setEncoding("utf8");
|
|
3099
|
+
childProcess.stdout.on("data", handleOutput);
|
|
3100
|
+
}
|
|
3101
|
+
if (childProcess.stderr) {
|
|
3102
|
+
childProcess.stderr.setEncoding("utf8");
|
|
3103
|
+
childProcess.stderr.on("data", handleOutput);
|
|
3104
|
+
}
|
|
3105
|
+
setTimeout(() => {
|
|
3106
|
+
if (!serverReady) {
|
|
3107
|
+
serverReady = true;
|
|
3108
|
+
clearInterval(statusTimer);
|
|
3109
|
+
process.stderr.clearLine?.(0);
|
|
3110
|
+
process.stderr.cursorTo?.(0);
|
|
3111
|
+
logger.warn("\u542F\u52A8\u8D85\u65F6\uFF0C\u8BF7\u68C0\u67E5 VitePress \u8F93\u51FA");
|
|
3112
|
+
resolve3({ version, elapsed: _VitepressService.SERVER_STARTUP_TIMEOUT });
|
|
3113
|
+
}
|
|
3114
|
+
}, _VitepressService.SERVER_STARTUP_TIMEOUT);
|
|
3115
|
+
});
|
|
3116
|
+
}
|
|
3117
|
+
/**
|
|
3118
|
+
* 构建生产版本
|
|
3119
|
+
*/
|
|
3120
|
+
build() {
|
|
3121
|
+
return new Promise((resolve3, reject) => {
|
|
3122
|
+
const pm = this.configManager.get("packageManager") || _VitepressService.DEFAULT_PACKAGE_MANAGER;
|
|
3123
|
+
const child = spawn2(pm, ["vitepress", "build"], {
|
|
3124
|
+
cwd: ROOT_DIR_PATH,
|
|
3125
|
+
shell: true,
|
|
3126
|
+
stdio: ["inherit", "pipe", "pipe"]
|
|
3127
|
+
});
|
|
3128
|
+
const filterOutput = (data) => {
|
|
3129
|
+
const str = data.toString();
|
|
3130
|
+
if (str.includes("\u{1F528}") || str.includes("\u2705 \u6784\u5EFA\u6210\u529F") || str.includes("\u274C \u6784\u5EFA\u5931\u8D25") || str.includes("\u{1F4C1}") || str.includes("\u{1F4CA}") || str.includes("\u{1F4E6}") || str.includes("\u23F1\uFE0F") || str.includes("Building [") || str.includes("error") || str.includes("Error")) {
|
|
3131
|
+
process.stdout.write(data);
|
|
3132
|
+
return;
|
|
3133
|
+
}
|
|
3134
|
+
if (/^[\s⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏✓\r\n]*$/.test(str) || str.includes("building client + server") || str.includes("rendering pages") || str.includes("generating sitemap") || str.includes("build complete in") || str.includes("vitepress v")) {
|
|
3135
|
+
return;
|
|
3136
|
+
}
|
|
3137
|
+
};
|
|
3138
|
+
child.stdout?.on("data", filterOutput);
|
|
3139
|
+
child.stderr?.on("data", filterOutput);
|
|
3140
|
+
child.on("error", (err) => {
|
|
3141
|
+
reject(err);
|
|
3142
|
+
});
|
|
3143
|
+
child.on("close", (code) => {
|
|
3144
|
+
if (code === 0) {
|
|
3145
|
+
resolve3();
|
|
3146
|
+
} else {
|
|
3147
|
+
reject(new Error(`Command failed with code ${code}`));
|
|
3148
|
+
}
|
|
3149
|
+
});
|
|
3150
|
+
});
|
|
3151
|
+
}
|
|
3152
|
+
/**
|
|
3153
|
+
* 预览构建后的站点
|
|
3154
|
+
*/
|
|
3155
|
+
async preview() {
|
|
3156
|
+
const repoName = this.configManager.get("repoName");
|
|
3157
|
+
const processId = `${repoName}-${_VitepressService.PROCESS_ID_PREVIEW_SUFFIX}`;
|
|
3158
|
+
const pm = this.configManager.get("packageManager") || _VitepressService.DEFAULT_PACKAGE_MANAGER;
|
|
3159
|
+
const args = ["vitepress", "preview"];
|
|
3160
|
+
const previewPort = _VitepressService.DEFAULT_PREVIEW_PORT;
|
|
3161
|
+
if (isPortInUse(previewPort)) {
|
|
3162
|
+
logger.warn(`\u7AEF\u53E3 ${previewPort} \u5DF2\u88AB\u5360\u7528\uFF0C\u6B63\u5728\u5C1D\u8BD5\u6E05\u7406...`);
|
|
3163
|
+
const killed = killPortProcess(previewPort);
|
|
3164
|
+
if (killed) {
|
|
3165
|
+
const available = await waitForPort(
|
|
3166
|
+
previewPort,
|
|
3167
|
+
_VitepressService.PORT_RELEASE_TIMEOUT
|
|
3168
|
+
);
|
|
3169
|
+
if (!available) {
|
|
3170
|
+
logger.error(`\u7AEF\u53E3 ${previewPort} \u91CA\u653E\u8D85\u65F6\uFF0C\u8BF7\u624B\u52A8\u6E05\u7406`);
|
|
3171
|
+
return void 0;
|
|
3172
|
+
}
|
|
3173
|
+
logger.info(`\u7AEF\u53E3 ${previewPort} \u5DF2\u91CA\u653E`);
|
|
3174
|
+
} else {
|
|
3175
|
+
logger.error(
|
|
3176
|
+
`\u65E0\u6CD5\u6E05\u7406\u7AEF\u53E3 ${previewPort}\uFF0C\u8BF7\u624B\u52A8\u6267\u884C: taskkill /F /PID <PID>`
|
|
3177
|
+
);
|
|
3178
|
+
return void 0;
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
const processInfo = this.processManager.spawn(processId, pm, args, {
|
|
3182
|
+
cwd: ROOT_DIR_PATH,
|
|
3183
|
+
stdio: "inherit"
|
|
3184
|
+
});
|
|
3185
|
+
return processInfo.pid;
|
|
3186
|
+
}
|
|
3187
|
+
};
|
|
3188
|
+
|
|
3189
|
+
// commands/update/UpdateCommand.ts
|
|
3190
|
+
import { existsSync as existsSync9, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
3191
|
+
import { resolve as resolve2 } from "path";
|
|
3192
|
+
var UpdateCommand = class extends BaseCommand {
|
|
3193
|
+
constructor() {
|
|
3194
|
+
super("update");
|
|
3195
|
+
this.quiet = false;
|
|
3196
|
+
this.updateAll = false;
|
|
3197
|
+
this.readmeService = ReadmeService.getInstance();
|
|
3198
|
+
this.noteService = NoteService.getInstance();
|
|
3199
|
+
}
|
|
3200
|
+
/**
|
|
3201
|
+
* 设置 quiet 模式
|
|
3202
|
+
*
|
|
3203
|
+
* 在 quiet 模式下,只显示 WARN 级别以上的日志
|
|
3204
|
+
*/
|
|
3205
|
+
setQuiet(quiet) {
|
|
3206
|
+
this.quiet = quiet;
|
|
3207
|
+
if (quiet) {
|
|
3208
|
+
logger.setLevel(2 /* WARN */);
|
|
3209
|
+
} else {
|
|
3210
|
+
logger.setLevel(1 /* INFO */);
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
/**
|
|
3214
|
+
* 设置是否更新所有知识库
|
|
3215
|
+
*/
|
|
3216
|
+
setUpdateAll(updateAll) {
|
|
3217
|
+
this.updateAll = updateAll;
|
|
3218
|
+
}
|
|
3219
|
+
async run() {
|
|
3220
|
+
if (this.updateAll) {
|
|
3221
|
+
await this.updateAllRepos();
|
|
3222
|
+
} else {
|
|
3223
|
+
await this.updateCurrentRepo();
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3226
|
+
/**
|
|
3227
|
+
* 更新当前知识库
|
|
3228
|
+
*/
|
|
3229
|
+
async updateCurrentRepo() {
|
|
3230
|
+
const startTime = Date.now();
|
|
3231
|
+
const notes = this.noteService.getAllNotes();
|
|
3232
|
+
if (!this.quiet) {
|
|
3233
|
+
this.logger.info("\u6B63\u5728\u4FEE\u6B63\u7B14\u8BB0\u6807\u9898...");
|
|
3234
|
+
}
|
|
3235
|
+
const fixedCount = await this.noteService.fixAllNoteTitles(notes);
|
|
3236
|
+
if (!this.quiet && fixedCount > 0) {
|
|
3237
|
+
this.logger.success(`\u4FEE\u6B63\u4E86 ${fixedCount} \u4E2A\u7B14\u8BB0\u6807\u9898`);
|
|
3238
|
+
}
|
|
3239
|
+
await this.readmeService.updateAllReadmes({ notes });
|
|
3240
|
+
await this.updateRootItem();
|
|
3241
|
+
const duration = Date.now() - startTime;
|
|
3242
|
+
if (this.quiet) {
|
|
3243
|
+
this.logger.success(`\u77E5\u8BC6\u5E93\u66F4\u65B0\u5B8C\u6210 (${duration}ms)`);
|
|
3244
|
+
} else {
|
|
3245
|
+
this.logger.success("\u77E5\u8BC6\u5E93\u66F4\u65B0\u5B8C\u6210");
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
/**
|
|
3249
|
+
* 更新所有知识库
|
|
3250
|
+
*/
|
|
3251
|
+
async updateAllRepos() {
|
|
3252
|
+
try {
|
|
3253
|
+
const targetDirs = getTargetDirs(TNOTES_BASE_DIR, "TNotes.", [
|
|
3254
|
+
ROOT_DIR_PATH,
|
|
3255
|
+
EN_WORDS_DIR
|
|
3256
|
+
]);
|
|
3257
|
+
if (targetDirs.length === 0) {
|
|
3258
|
+
this.logger.warn("\u672A\u627E\u5230\u7B26\u5408\u6761\u4EF6\u7684\u77E5\u8BC6\u5E93");
|
|
3259
|
+
return;
|
|
3260
|
+
}
|
|
3261
|
+
this.logger.info(`\u6B63\u5728\u66F4\u65B0 ${targetDirs.length} \u4E2A\u77E5\u8BC6\u5E93...`);
|
|
3262
|
+
let successCount = 0;
|
|
3263
|
+
let failCount = 0;
|
|
3264
|
+
for (let i = 0; i < targetDirs.length; i++) {
|
|
3265
|
+
const dir = targetDirs[i];
|
|
3266
|
+
const repoName = dir.split("/").pop() || dir;
|
|
3267
|
+
try {
|
|
3268
|
+
process.stdout.write(
|
|
3269
|
+
`\r [${i + 1}/${targetDirs.length}] \u6B63\u5728\u66F4\u65B0: ${repoName}...`
|
|
3270
|
+
);
|
|
3271
|
+
await runCommand("pnpm tn:update --quiet", dir);
|
|
3272
|
+
successCount++;
|
|
3273
|
+
} catch (error) {
|
|
3274
|
+
failCount++;
|
|
3275
|
+
console.log();
|
|
3276
|
+
this.logger.error(
|
|
3277
|
+
`\u66F4\u65B0\u5931\u8D25: ${repoName} - ${error instanceof Error ? error.message : String(error)}`
|
|
3278
|
+
);
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
console.log();
|
|
3282
|
+
if (failCount === 0) {
|
|
3283
|
+
this.logger.success(
|
|
3284
|
+
`\u2705 \u6240\u6709\u77E5\u8BC6\u5E93\u66F4\u65B0\u5B8C\u6210: ${successCount}/${targetDirs.length}`
|
|
3285
|
+
);
|
|
3286
|
+
} else {
|
|
3287
|
+
this.logger.warn(
|
|
3288
|
+
`\u26A0\uFE0F \u66F4\u65B0\u5B8C\u6210: ${successCount} \u6210\u529F, ${failCount} \u5931\u8D25 (\u5171 ${targetDirs.length} \u4E2A)`
|
|
3289
|
+
);
|
|
3290
|
+
}
|
|
3291
|
+
} catch (error) {
|
|
3292
|
+
this.logger.error(
|
|
3293
|
+
`\u6279\u91CF\u66F4\u65B0\u5931\u8D25: ${error instanceof Error ? error.message : String(error)}`
|
|
3294
|
+
);
|
|
3295
|
+
throw error;
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
/**
|
|
3299
|
+
* 更新 root_item 配置
|
|
3300
|
+
* 只更新当前月份的完成笔记数量
|
|
3301
|
+
*/
|
|
3302
|
+
async updateRootItem() {
|
|
3303
|
+
try {
|
|
3304
|
+
const configContent = readFileSync6(ROOT_CONFIG_PATH, "utf-8");
|
|
3305
|
+
const config = JSON.parse(configContent);
|
|
3306
|
+
const readmePath = resolve2(ROOT_DIR_PATH, "README.md");
|
|
3307
|
+
if (!existsSync9(readmePath)) {
|
|
3308
|
+
throw new Error("\u6839\u76EE\u5F55 README.md \u4E0D\u5B58\u5728");
|
|
3309
|
+
}
|
|
3310
|
+
const readmeContent = readFileSync6(readmePath, "utf-8");
|
|
3311
|
+
const { completedCount } = parseReadmeCompletedNotes(readmeContent);
|
|
3312
|
+
const now = /* @__PURE__ */ new Date();
|
|
3313
|
+
const yearShort = String(now.getFullYear()).slice(-2);
|
|
3314
|
+
const monthStr = String(now.getMonth() + 1).padStart(2, "0");
|
|
3315
|
+
const currentKey = `${yearShort}.${monthStr}`;
|
|
3316
|
+
const completedNotesCount = {
|
|
3317
|
+
...config.root_item.completed_notes_count || {},
|
|
3318
|
+
[currentKey]: completedCount
|
|
3319
|
+
};
|
|
3320
|
+
const updatedAt = Date.now();
|
|
3321
|
+
config.root_item = {
|
|
3322
|
+
...config.root_item,
|
|
3323
|
+
completed_notes_count: completedNotesCount,
|
|
3324
|
+
updated_at: updatedAt
|
|
3325
|
+
};
|
|
3326
|
+
delete config.root_item.completed_notes_count_last_month;
|
|
3327
|
+
writeFileSync5(ROOT_CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
|
|
3328
|
+
if (!this.quiet) {
|
|
3329
|
+
this.logger.success(
|
|
3330
|
+
`root_item \u914D\u7F6E\u5DF2\u66F4\u65B0: ${currentKey} \u6708\u5B8C\u6210 ${completedCount} \u7BC7\u7B14\u8BB0`
|
|
3331
|
+
);
|
|
3332
|
+
}
|
|
3333
|
+
} catch (error) {
|
|
3334
|
+
if (!this.quiet) {
|
|
3335
|
+
this.logger.error(
|
|
3336
|
+
`\u66F4\u65B0 root_item \u5931\u8D25: ${error instanceof Error ? error.message : String(error)}`
|
|
3337
|
+
);
|
|
3338
|
+
}
|
|
3339
|
+
throw error;
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
};
|
|
3343
|
+
|
|
3344
|
+
// commands/update-completed-count/UpdateCompletedCountCommand.ts
|
|
3345
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
|
|
3346
|
+
import { execSync as execSync2 } from "child_process";
|
|
3347
|
+
var UpdateCompletedCountCommand = class extends BaseCommand {
|
|
3348
|
+
constructor() {
|
|
3349
|
+
super("update-completed-count");
|
|
3350
|
+
this.updateAll = false;
|
|
3351
|
+
}
|
|
3352
|
+
/**
|
|
3353
|
+
* 设置是否更新所有知识库
|
|
3354
|
+
*/
|
|
3355
|
+
setUpdateAll(updateAll) {
|
|
3356
|
+
this.updateAll = updateAll;
|
|
3357
|
+
}
|
|
3358
|
+
async run() {
|
|
3359
|
+
if (this.updateAll) {
|
|
3360
|
+
await this.updateAllRepos();
|
|
3361
|
+
} else {
|
|
3362
|
+
await this.updateCurrentRepo();
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
/**
|
|
3366
|
+
* 更新当前知识库
|
|
3367
|
+
*/
|
|
3368
|
+
async updateCurrentRepo() {
|
|
3369
|
+
const startTime = Date.now();
|
|
3370
|
+
try {
|
|
3371
|
+
const configContent = readFileSync7(ROOT_CONFIG_PATH, "utf-8");
|
|
3372
|
+
const config = JSON.parse(configContent);
|
|
3373
|
+
this.logger.info("\u5F00\u59CB\u66F4\u65B0\u5B8C\u6210\u7B14\u8BB0\u6570\u91CF\u5386\u53F2\u8BB0\u5F55...");
|
|
3374
|
+
const completedNotesCountHistory = await this.getCompletedNotesCountHistory(config.root_item.created_at);
|
|
3375
|
+
config.root_item = {
|
|
3376
|
+
...config.root_item,
|
|
3377
|
+
completed_notes_count: completedNotesCountHistory,
|
|
3378
|
+
updated_at: Date.now()
|
|
3379
|
+
};
|
|
3380
|
+
writeFileSync6(ROOT_CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
|
|
3381
|
+
const duration = Date.now() - startTime;
|
|
3382
|
+
const monthKeys = Object.keys(completedNotesCountHistory);
|
|
3383
|
+
const currentKey = monthKeys[monthKeys.length - 1];
|
|
3384
|
+
const currentCount = completedNotesCountHistory[currentKey] || 0;
|
|
3385
|
+
this.logger.success(
|
|
3386
|
+
`\u5386\u53F2\u6570\u636E\u66F4\u65B0\u5B8C\u6210: \u5171 ${monthKeys.length} \u4E2A\u6708, \u5F53\u524D ${currentKey} \u6708\u5B8C\u6210 ${currentCount} \u7BC7\u7B14\u8BB0 (${duration}ms)`
|
|
3387
|
+
);
|
|
3388
|
+
} catch (error) {
|
|
3389
|
+
this.logger.error(
|
|
3390
|
+
`\u66F4\u65B0\u5931\u8D25: ${error instanceof Error ? error.message : String(error)}`
|
|
3391
|
+
);
|
|
3392
|
+
throw error;
|
|
3393
|
+
}
|
|
3394
|
+
}
|
|
3395
|
+
/**
|
|
3396
|
+
* 更新所有知识库
|
|
3397
|
+
*/
|
|
3398
|
+
async updateAllRepos() {
|
|
3399
|
+
try {
|
|
3400
|
+
const targetDirs = getTargetDirs(TNOTES_BASE_DIR, "TNotes.", [
|
|
3401
|
+
ROOT_DIR_PATH,
|
|
3402
|
+
EN_WORDS_DIR
|
|
3403
|
+
]);
|
|
3404
|
+
if (targetDirs.length === 0) {
|
|
3405
|
+
this.logger.warn("\u672A\u627E\u5230\u7B26\u5408\u6761\u4EF6\u7684\u77E5\u8BC6\u5E93");
|
|
3406
|
+
return;
|
|
3407
|
+
}
|
|
3408
|
+
this.logger.info(
|
|
3409
|
+
`\u6B63\u5728\u66F4\u65B0 ${targetDirs.length} \u4E2A\u77E5\u8BC6\u5E93\u7684\u5B8C\u6210\u6570\u91CF\u5386\u53F2\u8BB0\u5F55...`
|
|
3410
|
+
);
|
|
3411
|
+
let successCount = 0;
|
|
3412
|
+
let failCount = 0;
|
|
3413
|
+
for (let i = 0; i < targetDirs.length; i++) {
|
|
3414
|
+
const dir = targetDirs[i];
|
|
3415
|
+
const repoName = dir.split("/").pop() || dir;
|
|
3416
|
+
try {
|
|
3417
|
+
process.stdout.write(
|
|
3418
|
+
`\r [${i + 1}/${targetDirs.length}] \u6B63\u5728\u66F4\u65B0: ${repoName}...`
|
|
3419
|
+
);
|
|
3420
|
+
await runCommand("pnpm tn:update-completed-count", dir);
|
|
3421
|
+
successCount++;
|
|
3422
|
+
} catch (error) {
|
|
3423
|
+
failCount++;
|
|
3424
|
+
console.log();
|
|
3425
|
+
this.logger.error(
|
|
3426
|
+
`\u66F4\u65B0\u5931\u8D25: ${repoName} - ${error instanceof Error ? error.message : String(error)}`
|
|
3427
|
+
);
|
|
3428
|
+
}
|
|
3429
|
+
}
|
|
3430
|
+
console.log();
|
|
3431
|
+
if (failCount === 0) {
|
|
3432
|
+
this.logger.success(
|
|
3433
|
+
`\u2705 \u6240\u6709\u77E5\u8BC6\u5E93\u5386\u53F2\u6570\u636E\u66F4\u65B0\u5B8C\u6210: ${successCount}/${targetDirs.length}`
|
|
3434
|
+
);
|
|
3435
|
+
} else {
|
|
3436
|
+
this.logger.warn(
|
|
3437
|
+
`\u26A0\uFE0F \u66F4\u65B0\u5B8C\u6210: ${successCount} \u6210\u529F, ${failCount} \u5931\u8D25 (\u5171 ${targetDirs.length} \u4E2A)`
|
|
3438
|
+
);
|
|
3439
|
+
}
|
|
3440
|
+
} catch (error) {
|
|
3441
|
+
this.logger.error(
|
|
3442
|
+
`\u6279\u91CF\u66F4\u65B0\u5931\u8D25: ${error instanceof Error ? error.message : String(error)}`
|
|
3443
|
+
);
|
|
3444
|
+
throw error;
|
|
3445
|
+
}
|
|
3446
|
+
}
|
|
3447
|
+
/**
|
|
3448
|
+
* 获取历史每个月的 completed_notes_count(最近12个月)
|
|
3449
|
+
*
|
|
3450
|
+
* 逻辑:
|
|
3451
|
+
* 1. 计算最近12个月的范围(当前月份往前推11个月)
|
|
3452
|
+
* 2. 遍历这12个月,从 Git 历史中读取 README.md
|
|
3453
|
+
* 3. 解析 README.md 获取完成笔记数量
|
|
3454
|
+
* 4. 如果知识库创建时间在这12个月内,之前的月份补0
|
|
3455
|
+
* 5. 返回对象 { '25.01': 0, '25.02': 1, ..., '25.12': 15 }
|
|
3456
|
+
*/
|
|
3457
|
+
async getCompletedNotesCountHistory(createdAt) {
|
|
3458
|
+
try {
|
|
3459
|
+
const now = /* @__PURE__ */ new Date();
|
|
3460
|
+
const currentYear = now.getFullYear();
|
|
3461
|
+
const currentMonth = now.getMonth();
|
|
3462
|
+
let firstYear = currentYear;
|
|
3463
|
+
let firstMonth = currentMonth - 11;
|
|
3464
|
+
if (firstMonth < 0) {
|
|
3465
|
+
firstYear = currentYear - 1;
|
|
3466
|
+
firstMonth = 12 + firstMonth;
|
|
3467
|
+
}
|
|
3468
|
+
const createdDate = new Date(createdAt);
|
|
3469
|
+
const createdYear = createdDate.getFullYear();
|
|
3470
|
+
const createdMonth = createdDate.getMonth();
|
|
3471
|
+
const result = {};
|
|
3472
|
+
let prevCount = 0;
|
|
3473
|
+
for (let i = 0; i < 12; i++) {
|
|
3474
|
+
const targetYear = firstYear + Math.floor((firstMonth + i) / 12);
|
|
3475
|
+
const targetMonth = (firstMonth + i) % 12;
|
|
3476
|
+
const yearShort = String(targetYear).slice(-2);
|
|
3477
|
+
const monthStr = String(targetMonth + 1).padStart(2, "0");
|
|
3478
|
+
const key = `${yearShort}.${monthStr}`;
|
|
3479
|
+
const isBeforeCreation = targetYear < createdYear || targetYear === createdYear && targetMonth < createdMonth;
|
|
3480
|
+
if (isBeforeCreation) {
|
|
3481
|
+
result[key] = 0;
|
|
3482
|
+
prevCount = 0;
|
|
3483
|
+
this.logger.info(`\u2713 ${key}: 0 \u7BC7\uFF08\u65E9\u4E8E\u521B\u5EFA\u65F6\u95F4\uFF09`);
|
|
3484
|
+
} else {
|
|
3485
|
+
try {
|
|
3486
|
+
const count = await this.getMonthCompletedCount(
|
|
3487
|
+
targetYear,
|
|
3488
|
+
targetMonth,
|
|
3489
|
+
prevCount
|
|
3490
|
+
);
|
|
3491
|
+
result[key] = count;
|
|
3492
|
+
prevCount = count;
|
|
3493
|
+
this.logger.info(`\u2713 ${key}: ${count} \u7BC7`);
|
|
3494
|
+
} catch (error) {
|
|
3495
|
+
result[key] = prevCount;
|
|
3496
|
+
this.logger.warn(`${key}: \u65E0\u6570\u636E\uFF0C\u4F7F\u7528 ${prevCount}\uFF08\u4E0A\u6708\u503C\uFF09`);
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
3499
|
+
}
|
|
3500
|
+
return result;
|
|
3501
|
+
} catch (error) {
|
|
3502
|
+
this.logger.error(
|
|
3503
|
+
`\u83B7\u53D6\u5386\u53F2\u6570\u636E\u5931\u8D25: ${error instanceof Error ? error.message : String(error)}`
|
|
3504
|
+
);
|
|
3505
|
+
return {};
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3508
|
+
/**
|
|
3509
|
+
* 获取指定月份的完成笔记数量
|
|
3510
|
+
* @param year - 年份
|
|
3511
|
+
* @param month - 月份 (0-11)
|
|
3512
|
+
* @param fallbackCount - 回退值(如果该月没有数据)
|
|
3513
|
+
* @returns 完成笔记数量
|
|
3514
|
+
*/
|
|
3515
|
+
async getMonthCompletedCount(year, month, fallbackCount = 0) {
|
|
3516
|
+
const lastDayOfMonth = new Date(year, month + 1, 0, 23, 59, 59);
|
|
3517
|
+
const yearStr = lastDayOfMonth.getFullYear();
|
|
3518
|
+
const monthStr = String(lastDayOfMonth.getMonth() + 1).padStart(2, "0");
|
|
3519
|
+
const dayStr = String(lastDayOfMonth.getDate()).padStart(2, "0");
|
|
3520
|
+
const untilDate = `${yearStr}-${monthStr}-${dayStr} 23:59:59 +0800`;
|
|
3521
|
+
const commitHash = execSync2(
|
|
3522
|
+
`git log --until="${untilDate}" --format=%H -1 -- README.md`,
|
|
3523
|
+
{
|
|
3524
|
+
cwd: ROOT_DIR_PATH,
|
|
3525
|
+
encoding: "utf-8"
|
|
3526
|
+
}
|
|
3527
|
+
).trim();
|
|
3528
|
+
if (!commitHash) {
|
|
3529
|
+
return fallbackCount;
|
|
3530
|
+
}
|
|
3531
|
+
let readmeContent;
|
|
3532
|
+
try {
|
|
3533
|
+
readmeContent = execSync2(`git show ${commitHash}:README.md`, {
|
|
3534
|
+
cwd: ROOT_DIR_PATH,
|
|
3535
|
+
encoding: "utf-8"
|
|
3536
|
+
});
|
|
3537
|
+
} catch (error) {
|
|
3538
|
+
return fallbackCount;
|
|
3539
|
+
}
|
|
3540
|
+
const { completedCount } = parseReadmeCompletedNotes(readmeContent);
|
|
3541
|
+
return completedCount;
|
|
3542
|
+
}
|
|
3543
|
+
};
|
|
3544
|
+
|
|
3545
|
+
// commands/git/PushCommand.ts
|
|
3546
|
+
var PushCommand = class extends BaseCommand {
|
|
3547
|
+
constructor() {
|
|
3548
|
+
super("push");
|
|
3549
|
+
this.pushAll = false;
|
|
3550
|
+
this.gitService = new GitService();
|
|
3551
|
+
this.timestampService = new TimestampService();
|
|
3552
|
+
}
|
|
3553
|
+
/**
|
|
3554
|
+
* 设置是否推送所有仓库
|
|
3555
|
+
*/
|
|
3556
|
+
setPushAll(value) {
|
|
3557
|
+
this.pushAll = value;
|
|
3558
|
+
}
|
|
3559
|
+
async run() {
|
|
3560
|
+
if (this.pushAll) {
|
|
3561
|
+
const parallel = process.env.PARALLEL_PUSH === "true";
|
|
3562
|
+
const force = this.options.force === true;
|
|
3563
|
+
if (parallel) {
|
|
3564
|
+
this.logger.info("Parallel push mode enabled");
|
|
3565
|
+
}
|
|
3566
|
+
if (force) {
|
|
3567
|
+
this.logger.warn("\u4F7F\u7528\u5F3A\u5236\u63A8\u9001\u6A21\u5F0F (--force)");
|
|
3568
|
+
}
|
|
3569
|
+
await pushAllRepos({ parallel, force });
|
|
3570
|
+
return;
|
|
3571
|
+
}
|
|
3572
|
+
try {
|
|
3573
|
+
this.logger.info("\u68C0\u67E5\u662F\u5426\u6709\u66F4\u6539...");
|
|
3574
|
+
const status = await this.gitService.getStatus();
|
|
3575
|
+
const hasPendingCommits = (status.ahead ?? 0) > 0;
|
|
3576
|
+
if (!status.hasChanges && !hasPendingCommits) {
|
|
3577
|
+
this.logger.info("\u6CA1\u6709\u66F4\u6539\u9700\u8981\u63A8\u9001");
|
|
3578
|
+
return;
|
|
3579
|
+
}
|
|
3580
|
+
if (hasPendingCommits && !status.hasChanges) {
|
|
3581
|
+
this.logger.info(`\u68C0\u6D4B\u5230 ${status.ahead} \u4E2A\u672A\u63A8\u9001\u7684\u63D0\u4EA4\uFF0C\u76F4\u63A5\u63A8\u9001...`);
|
|
3582
|
+
}
|
|
3583
|
+
if (status.hasChanges) {
|
|
3584
|
+
const changedFiles = status.files.map((f) => f.path);
|
|
3585
|
+
this.logger.info(`\u5171\u8BA1\u68C0\u6D4B\u5230 ${changedFiles.length} \u4E2A\u53D8\u66F4\u6587\u4EF6`);
|
|
3586
|
+
const changedNotes = this.timestampService.getChangedNotes(changedFiles);
|
|
3587
|
+
this.logger.info(
|
|
3588
|
+
`\u5DE5\u5177\u68C0\u6D4B\u5230 ${changedNotes.length} \u4E2A\u53D8\u66F4\u7B14\u8BB0\uFF08README.md\uFF09`
|
|
3589
|
+
);
|
|
3590
|
+
if (changedNotes.length > 0) {
|
|
3591
|
+
this.logger.info(
|
|
3592
|
+
`\u68C0\u6D4B\u5230 ${changedNotes.length} \u7BC7\u7B14\u8BB0\u7684 README.md \u6709\u53D8\u66F4\uFF0C\u66F4\u65B0\u65F6\u95F4\u6233...`
|
|
3593
|
+
);
|
|
3594
|
+
await this.timestampService.updateNotesTimestamp(changedNotes);
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
const force = this.options.force === true;
|
|
3598
|
+
if (force) {
|
|
3599
|
+
this.logger.warn("\u4F7F\u7528\u5F3A\u5236\u63A8\u9001\u6A21\u5F0F (--force)");
|
|
3600
|
+
}
|
|
3601
|
+
this.logger.info("\u6B63\u5728\u63A8\u9001\u5230\u8FDC\u7A0B\u4ED3\u5E93...");
|
|
3602
|
+
await this.gitService.quickPush({ force, skipCheck: true });
|
|
3603
|
+
this.logger.success("\u63A8\u9001\u5B8C\u6210");
|
|
3604
|
+
} catch (error) {
|
|
3605
|
+
this.logger.error("\u63A8\u9001\u5931\u8D25:", error);
|
|
3606
|
+
throw error;
|
|
3607
|
+
}
|
|
3608
|
+
}
|
|
3609
|
+
};
|
|
3610
|
+
|
|
3611
|
+
// commands/git/PullCommand.ts
|
|
3612
|
+
var PullCommand = class extends BaseCommand {
|
|
3613
|
+
constructor() {
|
|
3614
|
+
super("pull");
|
|
3615
|
+
this.pullAll = false;
|
|
3616
|
+
this.gitService = new GitService();
|
|
3617
|
+
}
|
|
3618
|
+
/**
|
|
3619
|
+
* 设置是否拉取所有仓库
|
|
3620
|
+
*/
|
|
3621
|
+
setPullAll(value) {
|
|
3622
|
+
this.pullAll = value;
|
|
3623
|
+
}
|
|
3624
|
+
async run() {
|
|
3625
|
+
if (this.pullAll) {
|
|
3626
|
+
const parallel = process.env.PARALLEL_PULL === "true";
|
|
3627
|
+
if (parallel) {
|
|
3628
|
+
this.logger.info("Parallel pull mode enabled");
|
|
3629
|
+
}
|
|
3630
|
+
await pullAllRepos({ parallel });
|
|
3631
|
+
return;
|
|
3632
|
+
}
|
|
3633
|
+
this.logger.info("\u6B63\u5728\u4ECE\u8FDC\u7A0B\u4ED3\u5E93\u62C9\u53D6...");
|
|
3634
|
+
await this.gitService.pull();
|
|
3635
|
+
this.logger.success("\u62C9\u53D6\u5B8C\u6210");
|
|
3636
|
+
}
|
|
3637
|
+
};
|
|
3638
|
+
|
|
3639
|
+
// commands/git/SyncCommand.ts
|
|
3640
|
+
var SyncCommand = class extends BaseCommand {
|
|
3641
|
+
constructor() {
|
|
3642
|
+
super("sync");
|
|
3643
|
+
this.syncAll = false;
|
|
3644
|
+
this.gitService = new GitService();
|
|
3645
|
+
}
|
|
3646
|
+
/**
|
|
3647
|
+
* 设置是否同步所有仓库
|
|
3648
|
+
*/
|
|
3649
|
+
setSyncAll(value) {
|
|
3650
|
+
this.syncAll = value;
|
|
3651
|
+
}
|
|
3652
|
+
async run() {
|
|
3653
|
+
if (this.syncAll) {
|
|
3654
|
+
await syncAllRepos({ parallel: false });
|
|
3655
|
+
return;
|
|
3656
|
+
}
|
|
3657
|
+
this.logger.info("\u6B63\u5728\u540C\u6B65\u4ED3\u5E93...");
|
|
3658
|
+
const hasChanges = await this.gitService.hasChanges();
|
|
3659
|
+
if (hasChanges) {
|
|
3660
|
+
const message = this.gitService.generateCommitMessage();
|
|
3661
|
+
await this.gitService.sync(message);
|
|
3662
|
+
} else {
|
|
3663
|
+
await this.gitService.sync();
|
|
3664
|
+
}
|
|
3665
|
+
this.logger.success("\u540C\u6B65\u5B8C\u6210");
|
|
3666
|
+
}
|
|
3667
|
+
};
|
|
3668
|
+
|
|
3669
|
+
// commands/dev/DevCommand.ts
|
|
3670
|
+
var DevCommand = class extends BaseCommand {
|
|
3671
|
+
constructor() {
|
|
3672
|
+
super("dev");
|
|
3673
|
+
this.fileWatcherService = new FileWatcherService();
|
|
3674
|
+
this.vitepressService = new VitepressService();
|
|
3675
|
+
this.noteManager = NoteManager.getInstance();
|
|
3676
|
+
this.noteIndexCache = NoteIndexCache.getInstance();
|
|
3677
|
+
this.configManager = ConfigManager.getInstance();
|
|
3678
|
+
}
|
|
3679
|
+
async run() {
|
|
3680
|
+
const notes = this.noteManager.scanNotes();
|
|
3681
|
+
this.logger.info(`\u626B\u63CF\u5230 ${notes.length} \u7BC7\u7B14\u8BB0`);
|
|
3682
|
+
this.noteIndexCache.initialize(notes);
|
|
3683
|
+
const result = await this.vitepressService.startServer();
|
|
3684
|
+
if (result) {
|
|
3685
|
+
const versionInfo = result.version ? `\uFF08v${result.version}\uFF09` : "";
|
|
3686
|
+
this.logger.success(
|
|
3687
|
+
`VitePress \u670D\u52A1${versionInfo}\u5DF2\u5C31\u7EEA\uFF0C\u8017\u65F6\uFF1A${result.elapsed} ms`
|
|
3688
|
+
);
|
|
3689
|
+
const watcherStart = Date.now();
|
|
3690
|
+
this.fileWatcherService.start();
|
|
3691
|
+
const watcherElapsed = Date.now() - watcherStart;
|
|
3692
|
+
this.logger.success(`\u6587\u4EF6\u76D1\u542C\u670D\u52A1\u5DF2\u5C31\u7EEA\uFF0C\u8017\u65F6\uFF1A${watcherElapsed} ms`);
|
|
3693
|
+
const port = this.configManager.get("port") || VitepressService.DEFAULT_DEV_PORT;
|
|
3694
|
+
const repoName = this.configManager.get("repoName");
|
|
3695
|
+
this.logger.info(
|
|
3696
|
+
`\u672C\u5730\u5F00\u53D1\u670D\u52A1\u5730\u5740\uFF1Ahttp://localhost:${port}/${repoName}/`
|
|
3697
|
+
);
|
|
3698
|
+
} else {
|
|
3699
|
+
this.logger.error("\u542F\u52A8\u670D\u52A1\u5668\u5931\u8D25");
|
|
3700
|
+
}
|
|
3701
|
+
}
|
|
3702
|
+
};
|
|
3703
|
+
|
|
3704
|
+
// commands/build/BuildCommand.ts
|
|
3705
|
+
var BuildCommand = class extends BaseCommand {
|
|
3706
|
+
constructor() {
|
|
3707
|
+
super("build");
|
|
3708
|
+
this.vitepressService = new VitepressService();
|
|
3709
|
+
}
|
|
3710
|
+
async run() {
|
|
3711
|
+
this.logger.info("\u5F00\u59CB\u6784\u5EFA\u77E5\u8BC6\u5E93...");
|
|
3712
|
+
await this.vitepressService.build();
|
|
3713
|
+
this.logger.success("\u77E5\u8BC6\u5E93\u6784\u5EFA\u5B8C\u6210");
|
|
3714
|
+
}
|
|
3715
|
+
};
|
|
3716
|
+
|
|
3717
|
+
// commands/build/PreviewCommand.ts
|
|
3718
|
+
var PreviewCommand = class extends BaseCommand {
|
|
3719
|
+
constructor() {
|
|
3720
|
+
super("preview");
|
|
3721
|
+
this.vitepressService = new VitepressService();
|
|
3722
|
+
}
|
|
3723
|
+
async run() {
|
|
3724
|
+
this.logger.info("\u542F\u52A8\u9884\u89C8\u670D\u52A1\u5668...");
|
|
3725
|
+
const pid = await this.vitepressService.preview();
|
|
3726
|
+
if (pid) {
|
|
3727
|
+
this.logger.success(`\u9884\u89C8\u670D\u52A1\u5668\u5DF2\u542F\u52A8 (PID: ${pid})`);
|
|
3728
|
+
} else {
|
|
3729
|
+
this.logger.error("\u542F\u52A8\u9884\u89C8\u670D\u52A1\u5668\u5931\u8D25");
|
|
3730
|
+
}
|
|
3731
|
+
}
|
|
3732
|
+
};
|
|
3733
|
+
|
|
3734
|
+
// commands/note/CreateNoteCommand.ts
|
|
3735
|
+
import { createInterface } from "readline";
|
|
3736
|
+
import { v4 as uuidv42 } from "uuid";
|
|
3737
|
+
var CreateNoteCommand = class extends BaseCommand {
|
|
3738
|
+
constructor() {
|
|
3739
|
+
super("create-notes");
|
|
3740
|
+
this.noteService = NoteService.getInstance();
|
|
3741
|
+
this.readmeService = ReadmeService.getInstance();
|
|
3742
|
+
}
|
|
3743
|
+
async run() {
|
|
3744
|
+
this.logger.info("\u521B\u5EFA\u65B0\u7B14\u8BB0...");
|
|
3745
|
+
const count = await this.promptForCount();
|
|
3746
|
+
let successCount = 0;
|
|
3747
|
+
let failCount = 0;
|
|
3748
|
+
const createdNotes = [];
|
|
3749
|
+
const existingNotes = this.noteService.getAllNotes();
|
|
3750
|
+
const usedIndexes = /* @__PURE__ */ new Set();
|
|
3751
|
+
for (const note of existingNotes) {
|
|
3752
|
+
const index = parseInt(note.index, 10);
|
|
3753
|
+
if (!isNaN(index) && index >= 1 && index <= 9999) {
|
|
3754
|
+
usedIndexes.add(index);
|
|
3755
|
+
}
|
|
3756
|
+
}
|
|
3757
|
+
for (let i = 1; i <= count; i++) {
|
|
3758
|
+
try {
|
|
3759
|
+
let title;
|
|
3760
|
+
if (count === 1) {
|
|
3761
|
+
title = await this.promptForTitle();
|
|
3762
|
+
} else {
|
|
3763
|
+
title = `new`;
|
|
3764
|
+
this.logger.info(`[${i}/${count}] \u521B\u5EFA\u7B14\u8BB0: ${title}`);
|
|
3765
|
+
}
|
|
3766
|
+
const configId = uuidv42();
|
|
3767
|
+
const note = await this.noteService.createNote({
|
|
3768
|
+
title: title || `new`,
|
|
3769
|
+
enableDiscussions: false,
|
|
3770
|
+
configId,
|
|
3771
|
+
// 传递 UUID 作为配置 ID(跨知识库唯一)
|
|
3772
|
+
usedIndexes
|
|
3773
|
+
// 传入已使用编号集合,避免重复扫描
|
|
3774
|
+
});
|
|
3775
|
+
const newIndex = parseInt(note.index, 10);
|
|
3776
|
+
if (!isNaN(newIndex)) {
|
|
3777
|
+
usedIndexes.add(newIndex);
|
|
3778
|
+
}
|
|
3779
|
+
createdNotes.push(note.dirName);
|
|
3780
|
+
successCount++;
|
|
3781
|
+
if (count === 1) {
|
|
3782
|
+
this.logger.success(`\u7B14\u8BB0\u521B\u5EFA\u6210\u529F: ${note.dirName}`);
|
|
3783
|
+
this.logger.info(`\u7B14\u8BB0\u8DEF\u5F84: ${note.path}`);
|
|
3784
|
+
this.logger.info(`\u7B14\u8BB0\u7F16\u53F7: ${note.index}`);
|
|
3785
|
+
this.logger.info(`\u914D\u7F6EID: ${configId}`);
|
|
3786
|
+
} else {
|
|
3787
|
+
this.logger.success(`[${i}/${count}] \u521B\u5EFA\u6210\u529F: ${note.dirName}`);
|
|
3788
|
+
}
|
|
3789
|
+
} catch (error) {
|
|
3790
|
+
failCount++;
|
|
3791
|
+
this.logger.error(`[${i}/${count}] \u521B\u5EFA\u5931\u8D25`, error);
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3794
|
+
if (count > 1) {
|
|
3795
|
+
console.log("");
|
|
3796
|
+
this.logger.info(
|
|
3797
|
+
`\u{1F4CA} \u521B\u5EFA\u5B8C\u6210: \u6210\u529F ${successCount} \u7BC7, \u5931\u8D25 ${failCount} \u7BC7`
|
|
3798
|
+
);
|
|
3799
|
+
}
|
|
3800
|
+
if (successCount > 0) {
|
|
3801
|
+
this.logger.info("\u6B63\u5728\u66F4\u65B0\u77E5\u8BC6\u5E93\u7D22\u5F15...");
|
|
3802
|
+
await this.readmeService.updateAllReadmes();
|
|
3803
|
+
this.logger.success("\u77E5\u8BC6\u5E93\u7D22\u5F15\u66F4\u65B0\u5B8C\u6210");
|
|
3804
|
+
}
|
|
3805
|
+
}
|
|
3806
|
+
/**
|
|
3807
|
+
* 提示用户输入要创建的笔记数量
|
|
3808
|
+
*/
|
|
3809
|
+
async promptForCount() {
|
|
3810
|
+
const rl = createInterface({
|
|
3811
|
+
input: process.stdin,
|
|
3812
|
+
output: process.stdout
|
|
3813
|
+
});
|
|
3814
|
+
return new Promise((resolve3) => {
|
|
3815
|
+
rl.question("\n\u{1F4DD} \u8BF7\u8F93\u5165\u8981\u521B\u5EFA\u7684\u7B14\u8BB0\u6570\u91CF\uFF08\u9ED8\u8BA4\u4E3A 1\uFF09: ", (answer) => {
|
|
3816
|
+
rl.close();
|
|
3817
|
+
const trimmed = answer.trim();
|
|
3818
|
+
if (!trimmed) {
|
|
3819
|
+
this.logger.info("\u4F7F\u7528\u9ED8\u8BA4\u6570\u91CF: 1");
|
|
3820
|
+
resolve3(1);
|
|
3821
|
+
return;
|
|
3822
|
+
}
|
|
3823
|
+
const num = parseInt(trimmed, 10);
|
|
3824
|
+
if (isNaN(num) || num < 1 || !Number.isInteger(num)) {
|
|
3825
|
+
this.logger.warn(`\u8F93\u5165 "${trimmed}" \u4E0D\u662F\u6709\u6548\u7684\u6B63\u6574\u6570\uFF0C\u4F7F\u7528\u9ED8\u8BA4\u503C: 1`);
|
|
3826
|
+
resolve3(1);
|
|
3827
|
+
return;
|
|
3828
|
+
}
|
|
3829
|
+
this.logger.info(`\u5C06\u521B\u5EFA ${num} \u7BC7\u7B14\u8BB0`);
|
|
3830
|
+
resolve3(num);
|
|
3831
|
+
});
|
|
3832
|
+
});
|
|
3833
|
+
}
|
|
3834
|
+
/**
|
|
3835
|
+
* 提示用户输入笔记标题
|
|
3836
|
+
*/
|
|
3837
|
+
async promptForTitle() {
|
|
3838
|
+
const rl = createInterface({
|
|
3839
|
+
input: process.stdin,
|
|
3840
|
+
output: process.stdout
|
|
3841
|
+
});
|
|
3842
|
+
return new Promise((resolve3) => {
|
|
3843
|
+
rl.question("\u8BF7\u8F93\u5165\u7B14\u8BB0\u6807\u9898: ", (answer) => {
|
|
3844
|
+
rl.close();
|
|
3845
|
+
resolve3(answer.trim());
|
|
3846
|
+
});
|
|
3847
|
+
});
|
|
3848
|
+
}
|
|
3849
|
+
};
|
|
3850
|
+
|
|
3851
|
+
// commands/note/UpdateNoteConfigCommand.ts
|
|
3852
|
+
var UpdateNoteConfigCommand = class extends BaseCommand {
|
|
3853
|
+
constructor() {
|
|
3854
|
+
super("update-note-config");
|
|
3855
|
+
this.noteService = NoteService.getInstance();
|
|
3856
|
+
}
|
|
3857
|
+
async run() {
|
|
3858
|
+
const noteIndex = process.env.NOTE_ID;
|
|
3859
|
+
const done = process.env.NOTE_DONE === "true";
|
|
3860
|
+
const enableDiscussions = process.env.NOTE_DISCUSSIONS === "true";
|
|
3861
|
+
const description = process.env.NOTE_DESCRIPTION || "";
|
|
3862
|
+
if (!noteIndex) {
|
|
3863
|
+
throw new Error("\u7F3A\u5C11 NOTE_ID \u53C2\u6570");
|
|
3864
|
+
}
|
|
3865
|
+
try {
|
|
3866
|
+
await this.updateConfig({
|
|
3867
|
+
noteIndex,
|
|
3868
|
+
config: {
|
|
3869
|
+
done,
|
|
3870
|
+
enableDiscussions,
|
|
3871
|
+
description
|
|
3872
|
+
}
|
|
3873
|
+
});
|
|
3874
|
+
this.logger.success(`\u7B14\u8BB0 ${noteIndex} \u914D\u7F6E\u5DF2\u66F4\u65B0`);
|
|
3875
|
+
} catch (error) {
|
|
3876
|
+
this.logger.error("\u66F4\u65B0\u914D\u7F6E\u5931\u8D25", error);
|
|
3877
|
+
throw error;
|
|
3878
|
+
}
|
|
3879
|
+
}
|
|
3880
|
+
/**
|
|
3881
|
+
* 更新笔记配置(可被外部调用)
|
|
3882
|
+
*/
|
|
3883
|
+
async updateConfig(params) {
|
|
3884
|
+
const { noteIndex, config } = params;
|
|
3885
|
+
const note = this.noteService.getNoteByIndex(noteIndex);
|
|
3886
|
+
if (!note) {
|
|
3887
|
+
throw new Error(`\u7B14\u8BB0\u672A\u627E\u5230: ${noteIndex}`);
|
|
3888
|
+
}
|
|
3889
|
+
await this.noteService.updateNoteConfig(noteIndex, config);
|
|
3890
|
+
this.logger.info(`\u2705 \u7B14\u8BB0 ${noteIndex} \u914D\u7F6E\u5DF2\u66F4\u65B0:`);
|
|
3891
|
+
if (config.done !== void 0)
|
|
3892
|
+
this.logger.info(` - \u5B8C\u6210\u72B6\u6001: ${config.done}`);
|
|
3893
|
+
if (config.enableDiscussions !== void 0)
|
|
3894
|
+
this.logger.info(` - \u8BC4\u8BBA\u72B6\u6001: ${config.enableDiscussions}`);
|
|
3895
|
+
if (config.description !== void 0)
|
|
3896
|
+
this.logger.info(` - \u7B14\u8BB0\u7B80\u4ECB: ${config.description || "(\u7A7A)"}`);
|
|
3897
|
+
}
|
|
3898
|
+
};
|
|
3899
|
+
|
|
3900
|
+
// commands/note/RenameNoteCommand.ts
|
|
3901
|
+
import { existsSync as existsSync10, renameSync, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
3902
|
+
import { join as join9 } from "path";
|
|
3903
|
+
var RenameNoteCommand = class extends BaseCommand {
|
|
3904
|
+
constructor() {
|
|
3905
|
+
super("rename-note");
|
|
3906
|
+
this.noteService = NoteService.getInstance();
|
|
3907
|
+
this.readmeService = ReadmeService.getInstance();
|
|
3908
|
+
}
|
|
3909
|
+
async run() {
|
|
3910
|
+
const noteIndex = process.env.NOTE_ID;
|
|
3911
|
+
const newTitle = process.env.NOTE_TITLE;
|
|
3912
|
+
if (!noteIndex || !newTitle) {
|
|
3913
|
+
throw new Error("\u7F3A\u5C11 NOTE_ID \u6216 NOTE_TITLE \u53C2\u6570");
|
|
3914
|
+
}
|
|
3915
|
+
try {
|
|
3916
|
+
await this.renameNote({ noteIndex, newTitle });
|
|
3917
|
+
this.logger.success(`\u7B14\u8BB0 ${noteIndex} \u5DF2\u91CD\u547D\u540D\u4E3A: ${newTitle}`);
|
|
3918
|
+
} catch (error) {
|
|
3919
|
+
this.logger.error("\u91CD\u547D\u540D\u5931\u8D25", error);
|
|
3920
|
+
throw error;
|
|
3921
|
+
}
|
|
3922
|
+
}
|
|
3923
|
+
/**
|
|
3924
|
+
* 重命名笔记(可被外部调用)
|
|
3925
|
+
*/
|
|
3926
|
+
async renameNote(params) {
|
|
3927
|
+
const { noteIndex, newTitle } = params;
|
|
3928
|
+
const note = this.noteService.getNoteByIndex(noteIndex);
|
|
3929
|
+
if (!note) {
|
|
3930
|
+
throw new Error(`\u7B14\u8BB0\u672A\u627E\u5230: ${noteIndex}`);
|
|
3931
|
+
}
|
|
3932
|
+
const validation = validateNoteTitle(newTitle);
|
|
3933
|
+
if (!validation.valid) {
|
|
3934
|
+
throw new Error(validation.error || "\u6807\u9898\u683C\u5F0F\u65E0\u6548");
|
|
3935
|
+
}
|
|
3936
|
+
const newDirName = `${noteIndex}. ${newTitle.trim()}`;
|
|
3937
|
+
const newPath = join9(NOTES_PATH, newDirName);
|
|
3938
|
+
if (existsSync10(newPath)) {
|
|
3939
|
+
throw new Error(`\u76EE\u6807\u6587\u4EF6\u5939\u5DF2\u5B58\u5728: ${newDirName}`);
|
|
3940
|
+
}
|
|
3941
|
+
try {
|
|
3942
|
+
renameSync(note.path, newPath);
|
|
3943
|
+
this.logger.info(`\u2705 \u6587\u4EF6\u5939\u5DF2\u91CD\u547D\u540D:`);
|
|
3944
|
+
this.logger.info(` \u539F\u540D\u79F0: ${note.dirName}`);
|
|
3945
|
+
this.logger.info(` \u65B0\u540D\u79F0: ${newDirName}`);
|
|
3946
|
+
} catch (error) {
|
|
3947
|
+
throw new Error(
|
|
3948
|
+
`\u91CD\u547D\u540D\u6587\u4EF6\u5939\u5931\u8D25: ${error instanceof Error ? error.message : String(error)}`
|
|
3949
|
+
);
|
|
3950
|
+
}
|
|
3951
|
+
try {
|
|
3952
|
+
this.logger.info("\u6B63\u5728\u66F4\u65B0\u7B14\u8BB0\u5185\u90E8\u6807\u9898...");
|
|
3953
|
+
const readmePath = join9(newPath, "README.md");
|
|
3954
|
+
if (existsSync10(readmePath)) {
|
|
3955
|
+
const content = readFileSync8(readmePath, "utf-8");
|
|
3956
|
+
const lines = content.split("\n");
|
|
3957
|
+
let h1Index = -1;
|
|
3958
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3959
|
+
if (lines[i].trim().startsWith("# ")) {
|
|
3960
|
+
h1Index = i;
|
|
3961
|
+
break;
|
|
3962
|
+
}
|
|
3963
|
+
}
|
|
3964
|
+
if (h1Index !== -1) {
|
|
3965
|
+
const newH1 = generateNoteTitle(
|
|
3966
|
+
noteIndex,
|
|
3967
|
+
newTitle.trim(),
|
|
3968
|
+
REPO_NOTES_URL
|
|
3969
|
+
);
|
|
3970
|
+
lines[h1Index] = newH1;
|
|
3971
|
+
writeFileSync7(readmePath, lines.join("\n"), "utf-8");
|
|
3972
|
+
this.logger.success("\u2705 \u7B14\u8BB0\u6807\u9898\u5DF2\u66F4\u65B0");
|
|
3973
|
+
} else {
|
|
3974
|
+
this.logger.warn(
|
|
3975
|
+
`\u26A0\uFE0F \u7B14\u8BB0\u6807\u9898\u683C\u5F0F\u4E0D\u7B26\u5408\u89C4\u8303\uFF0C\u672A\u627E\u5230\u4E00\u7EA7\u6807\u9898\uFF0C\u8BF7\u624B\u52A8\u68C0\u67E5\u4FEE\u6B63: ${readmePath}`
|
|
3976
|
+
);
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
} catch (error) {
|
|
3980
|
+
this.logger.warn("\u26A0\uFE0F \u66F4\u65B0\u7B14\u8BB0\u6807\u9898\u65F6\u51FA\u9519:", error);
|
|
3981
|
+
}
|
|
3982
|
+
try {
|
|
3983
|
+
this.logger.info("\u6B63\u5728\u66F4\u65B0\u5168\u5C40 README.md \u548C sidebar.json...");
|
|
3984
|
+
await this.readmeService.updateAllReadmes();
|
|
3985
|
+
this.logger.success("\u2705 \u5168\u5C40\u6587\u4EF6\u5DF2\u66F4\u65B0");
|
|
3986
|
+
} catch (error) {
|
|
3987
|
+
this.logger.warn("\u26A0\uFE0F \u6587\u4EF6\u5939\u91CD\u547D\u540D\u6210\u529F,\u4F46\u66F4\u65B0\u5168\u5C40\u6587\u4EF6\u65F6\u51FA\u9519:", error);
|
|
3988
|
+
}
|
|
3989
|
+
}
|
|
3990
|
+
};
|
|
3991
|
+
|
|
3992
|
+
// commands/maintenance/SyncCoreCommand.ts
|
|
3993
|
+
var SyncCoreCommand = class extends BaseCommand {
|
|
3994
|
+
constructor() {
|
|
3995
|
+
super("sync-core");
|
|
3996
|
+
this.syncCoreService = new SyncCoreService();
|
|
3997
|
+
}
|
|
3998
|
+
async run() {
|
|
3999
|
+
await this.syncCoreService.syncToAllRepos();
|
|
4000
|
+
}
|
|
4001
|
+
};
|
|
4002
|
+
|
|
4003
|
+
// commands/maintenance/FixTimestampsCommand.ts
|
|
4004
|
+
var FixTimestampsCommand = class extends BaseCommand {
|
|
4005
|
+
constructor() {
|
|
4006
|
+
super("fix-timestamps");
|
|
4007
|
+
this.timestampService = new TimestampService();
|
|
4008
|
+
}
|
|
4009
|
+
async run() {
|
|
4010
|
+
this.logger.info("\u5F00\u59CB\u4FEE\u590D\u6240\u6709\u7B14\u8BB0\u7684\u65F6\u95F4\u6233...");
|
|
4011
|
+
this.logger.info("\u{1F4CC} \u6B64\u64CD\u4F5C\u4F1A\u5C06\u6240\u6709\u65F6\u95F4\u6233\u66F4\u65B0\u4E3A git \u771F\u5B9E\u65F6\u95F4");
|
|
4012
|
+
this.logger.info("");
|
|
4013
|
+
const result = await this.timestampService.fixAllTimestamps(true);
|
|
4014
|
+
this.logger.info("");
|
|
4015
|
+
this.logger.info("\u{1F4CA} \u4FEE\u590D\u7EDF\u8BA1:");
|
|
4016
|
+
this.logger.info(
|
|
4017
|
+
` - \u6839\u914D\u7F6E\u6587\u4EF6: ${result.rootConfigFixed ? "\u5DF2\u4FEE\u590D" : "\u65E0\u9700\u4FEE\u590D"}`
|
|
4018
|
+
);
|
|
4019
|
+
this.logger.info(` - \u603B\u7B14\u8BB0\u6570: ${result.total}`);
|
|
4020
|
+
this.logger.info(` - \u5DF2\u4FEE\u590D: ${result.fixed}`);
|
|
4021
|
+
this.logger.info(` - \u8DF3\u8FC7: ${result.skipped}`);
|
|
4022
|
+
this.logger.info("");
|
|
4023
|
+
if (result.fixed > 0 || result.rootConfigFixed) {
|
|
4024
|
+
this.logger.success(
|
|
4025
|
+
`\u2705 \u6210\u529F\u4FEE\u590D ${result.fixed} \u4E2A\u7B14\u8BB0${result.rootConfigFixed ? " + \u6839\u914D\u7F6E\u6587\u4EF6" : ""}\u7684\u65F6\u95F4\u6233\uFF01`
|
|
4026
|
+
);
|
|
4027
|
+
this.logger.info("\u{1F4A1} \u63D0\u793A: \u8FD0\u884C pnpm tn:push \u63D0\u4EA4\u66F4\u6539");
|
|
4028
|
+
} else {
|
|
4029
|
+
this.logger.success("\u2705 \u6240\u6709\u65F6\u95F4\u6233\u5747\u5DF2\u6B63\u786E\uFF01");
|
|
4030
|
+
}
|
|
4031
|
+
}
|
|
4032
|
+
};
|
|
4033
|
+
|
|
4034
|
+
// commands/misc/HelpCommand.ts
|
|
4035
|
+
var COMMAND_CATEGORIES = {
|
|
4036
|
+
\u5F00\u53D1\u548C\u6784\u5EFA: [COMMAND_NAMES.DEV, COMMAND_NAMES.BUILD, COMMAND_NAMES.PREVIEW],
|
|
4037
|
+
\u5185\u5BB9\u7BA1\u7406: [
|
|
4038
|
+
COMMAND_NAMES.UPDATE,
|
|
4039
|
+
COMMAND_NAMES.UPDATE_COMPLETED_COUNT,
|
|
4040
|
+
COMMAND_NAMES.CREATE_NOTES
|
|
4041
|
+
],
|
|
4042
|
+
"Git \u64CD\u4F5C": [COMMAND_NAMES.PUSH, COMMAND_NAMES.PULL, COMMAND_NAMES.SYNC],
|
|
4043
|
+
\u5176\u4ED6: [
|
|
4044
|
+
COMMAND_NAMES.SYNC_SCRIPTS,
|
|
4045
|
+
COMMAND_NAMES.FIX_TIMESTAMPS,
|
|
4046
|
+
COMMAND_NAMES.HELP
|
|
4047
|
+
]
|
|
4048
|
+
};
|
|
4049
|
+
var COMMAND_OPTIONS_INFO = {
|
|
4050
|
+
[COMMAND_OPTIONS.ALL]: {
|
|
4051
|
+
description: "\u6279\u91CF\u64CD\u4F5C\u6240\u6709\u77E5\u8BC6\u5E93",
|
|
4052
|
+
applicableTo: "update/update-completed-count/push/pull/sync"
|
|
4053
|
+
},
|
|
4054
|
+
[COMMAND_OPTIONS.QUIET]: {
|
|
4055
|
+
description: "\u9759\u9ED8\u6A21\u5F0F",
|
|
4056
|
+
applicableTo: "update"
|
|
4057
|
+
},
|
|
4058
|
+
[COMMAND_OPTIONS.FORCE]: {
|
|
4059
|
+
description: "\u5F3A\u5236\u63A8\u9001",
|
|
4060
|
+
applicableTo: "push"
|
|
4061
|
+
}
|
|
4062
|
+
};
|
|
4063
|
+
var HelpCommand = class extends BaseCommand {
|
|
4064
|
+
constructor() {
|
|
4065
|
+
super("help");
|
|
4066
|
+
this.logger = createLogger("help", {
|
|
4067
|
+
timestamp: false,
|
|
4068
|
+
level: process.env.DEBUG ? 0 /* DEBUG */ : 1 /* INFO */
|
|
4069
|
+
});
|
|
4070
|
+
}
|
|
4071
|
+
async run() {
|
|
4072
|
+
this.logger.info("TNotes \u547D\u4EE4\u884C\u5DE5\u5177");
|
|
4073
|
+
this.logger.info("");
|
|
4074
|
+
this.logger.info("\u7528\u6CD5\uFF1Apnpm tn:<command> # \u63A8\u8350");
|
|
4075
|
+
this.logger.info("\u6216\u8005\uFF1Anpx tsx ./.vitepress/tnotes/index.ts --<command>");
|
|
4076
|
+
this.logger.info("");
|
|
4077
|
+
this.logger.info("\u53EF\u7528\u547D\u4EE4\uFF1A");
|
|
4078
|
+
this.logger.info("");
|
|
4079
|
+
for (const [category, cmdNames] of Object.entries(COMMAND_CATEGORIES)) {
|
|
4080
|
+
this.logger.info(` ${category}:`);
|
|
4081
|
+
for (const cmdName of cmdNames) {
|
|
4082
|
+
const description = COMMAND_DESCRIPTIONS[cmdName];
|
|
4083
|
+
const paddingLength = Math.max(25 - cmdName.length, 1);
|
|
4084
|
+
const padding = " ".repeat(paddingLength);
|
|
4085
|
+
this.logger.info(` --${cmdName}${padding}${description}`);
|
|
4086
|
+
}
|
|
4087
|
+
this.logger.info("");
|
|
4088
|
+
}
|
|
4089
|
+
this.logger.info("\u793A\u4F8B\uFF1A");
|
|
4090
|
+
this.logger.info(" npx tsx ./.vitepress/tnotes/index.ts --dev");
|
|
4091
|
+
this.logger.info(" pnpm tn:build");
|
|
4092
|
+
this.logger.info(" pnpm tn:create-notes # \u6279\u91CF\u521B\u5EFA\u7B14\u8BB0");
|
|
4093
|
+
this.logger.info(" pnpm tn:update");
|
|
4094
|
+
this.logger.info(" pnpm tn:update --all # \u66F4\u65B0\u6240\u6709\u77E5\u8BC6\u5E93");
|
|
4095
|
+
this.logger.info(
|
|
4096
|
+
" pnpm tn:update-completed-count # \u751F\u6210\u5F53\u524D\u77E5\u8BC6\u5E93\u6700\u8FD1 12 \u4E2A\u6708\u7684\u5B8C\u6210\u7B14\u8BB0\u6570\u91CF\u7EDF\u8BA1"
|
|
4097
|
+
);
|
|
4098
|
+
this.logger.info(
|
|
4099
|
+
" pnpm tn:update-completed-count --all # \u751F\u6210\u6240\u6709\u77E5\u8BC6\u5E93\u6700\u8FD1 12 \u4E2A\u6708\u7684\u5B8C\u6210\u7B14\u8BB0\u6570\u91CF\u7EDF\u8BA1"
|
|
4100
|
+
);
|
|
4101
|
+
this.logger.info(" pnpm tn:push --all # \u63A8\u9001\u6240\u6709\u77E5\u8BC6\u5E93");
|
|
4102
|
+
this.logger.info("");
|
|
4103
|
+
this.logger.info("\u53C2\u6570\uFF1A");
|
|
4104
|
+
for (const [option, info] of Object.entries(COMMAND_OPTIONS_INFO)) {
|
|
4105
|
+
const paddingLength = Math.max(13 - option.length, 1);
|
|
4106
|
+
const padding = " ".repeat(paddingLength);
|
|
4107
|
+
this.logger.info(
|
|
4108
|
+
` --${option}${padding}${info.description} (\u9002\u7528\u4E8E ${info.applicableTo})`
|
|
4109
|
+
);
|
|
4110
|
+
}
|
|
4111
|
+
this.logger.info("");
|
|
4112
|
+
this.logger.info("\u73AF\u5883\u53D8\u91CF\uFF1A");
|
|
4113
|
+
this.logger.info(" DEBUG=1 \u542F\u7528\u8C03\u8BD5\u6A21\u5F0F\uFF0C\u663E\u793A\u8BE6\u7EC6\u65E5\u5FD7");
|
|
4114
|
+
this.logger.info("");
|
|
4115
|
+
this.logger.info("\u66F4\u591A\u4FE1\u606F\u8BF7\u67E5\u770B: .vitepress/tnotes/README.md");
|
|
4116
|
+
}
|
|
4117
|
+
};
|
|
4118
|
+
|
|
4119
|
+
// commands/registry.ts
|
|
4120
|
+
var commandFactories = {
|
|
4121
|
+
dev: () => new DevCommand(),
|
|
4122
|
+
build: () => new BuildCommand(),
|
|
4123
|
+
preview: () => new PreviewCommand(),
|
|
4124
|
+
update: () => new UpdateCommand(),
|
|
4125
|
+
"update-completed-count": () => new UpdateCompletedCountCommand(),
|
|
4126
|
+
push: () => new PushCommand(),
|
|
4127
|
+
pull: () => new PullCommand(),
|
|
4128
|
+
sync: () => new SyncCommand(),
|
|
4129
|
+
"create-notes": () => new CreateNoteCommand(),
|
|
4130
|
+
"sync-core": () => new SyncCoreCommand(),
|
|
4131
|
+
"fix-timestamps": () => new FixTimestampsCommand(),
|
|
4132
|
+
"update-note-config": () => new UpdateNoteConfigCommand(),
|
|
4133
|
+
"rename-note": () => new RenameNoteCommand(),
|
|
4134
|
+
help: () => new HelpCommand()
|
|
4135
|
+
};
|
|
4136
|
+
var commandCache = /* @__PURE__ */ new Map();
|
|
4137
|
+
function getCommand(name) {
|
|
4138
|
+
if (!commandFactories[name]) return void 0;
|
|
4139
|
+
if (!commandCache.has(name)) commandCache.set(name, commandFactories[name]());
|
|
4140
|
+
return commandCache.get(name);
|
|
4141
|
+
}
|
|
4142
|
+
|
|
4143
|
+
// index.ts
|
|
4144
|
+
(async () => {
|
|
4145
|
+
try {
|
|
4146
|
+
const args = parseArgs(process.argv.slice(2));
|
|
4147
|
+
const commandName = Object.keys(args).find(
|
|
4148
|
+
(key) => key !== "_" && args[key] === true
|
|
4149
|
+
);
|
|
4150
|
+
const command = commandName ? getCommand(commandName) : null;
|
|
4151
|
+
if (!command) {
|
|
4152
|
+
const logger2 = createLogger("command-not-found", {
|
|
4153
|
+
timestamp: false
|
|
4154
|
+
});
|
|
4155
|
+
console.log(`
|
|
4156
|
+
${"-".repeat(66)}
|
|
4157
|
+
`);
|
|
4158
|
+
if (commandName) {
|
|
4159
|
+
logger2.warn(`\u672A\u627E\u5230\u547D\u4EE4\uFF1A${commandName}`);
|
|
4160
|
+
logger2.info(`\u8BF7\u68C0\u67E5\u547D\u4EE4\u540D\u662F\u5426\u6B63\u786E\uFF01`);
|
|
4161
|
+
} else {
|
|
4162
|
+
logger2.warn(`\u672A\u68C0\u6D4B\u5230\u547D\u4EE4\u540D\uFF0C\u8BF7\u68C0\u67E5\u547D\u4EE4\u8F93\u5165\u662F\u5426\u6B63\u786E`);
|
|
4163
|
+
logger2.info(`\u793A\u4F8B\uFF1A`);
|
|
4164
|
+
logger2.info(`pnpm tn:<\u547D\u4EE4\u540D>`);
|
|
4165
|
+
logger2.info(`npx tsx ./.vitepress/tnotes/index.ts --<\u547D\u4EE4\u540D>`);
|
|
4166
|
+
}
|
|
4167
|
+
console.log(`
|
|
4168
|
+
${"-".repeat(66)}
|
|
4169
|
+
`);
|
|
4170
|
+
const helpCommand = getCommand(COMMAND_NAMES.HELP);
|
|
4171
|
+
if (helpCommand) {
|
|
4172
|
+
console.log("\n\u6B63\u5728\u6267\u884C pnpm tn:help \u6253\u5370\u5E2E\u52A9\u4FE1\u606F...\n");
|
|
4173
|
+
await helpCommand.execute();
|
|
4174
|
+
}
|
|
4175
|
+
return;
|
|
4176
|
+
}
|
|
4177
|
+
if (commandName === COMMAND_NAMES.UPDATE) {
|
|
4178
|
+
const cmd = command;
|
|
4179
|
+
if (args.quiet) cmd.setQuiet(true);
|
|
4180
|
+
if (args.all) cmd.setUpdateAll(true);
|
|
4181
|
+
} else if (commandName === COMMAND_NAMES.UPDATE_COMPLETED_COUNT) {
|
|
4182
|
+
const cmd = command;
|
|
4183
|
+
if (args.all) cmd.setUpdateAll(true);
|
|
4184
|
+
} else if (commandName === COMMAND_NAMES.PUSH) {
|
|
4185
|
+
const cmd = command;
|
|
4186
|
+
if (args.force) cmd.setOptions({ force: true });
|
|
4187
|
+
if (args.all) cmd.setPushAll(true);
|
|
4188
|
+
} else if (commandName === COMMAND_NAMES.PULL) {
|
|
4189
|
+
const cmd = command;
|
|
4190
|
+
if (args.all) cmd.setPullAll(true);
|
|
4191
|
+
} else if (commandName === COMMAND_NAMES.SYNC) {
|
|
4192
|
+
const cmd = command;
|
|
4193
|
+
if (args.all) cmd.setSyncAll(true);
|
|
4194
|
+
}
|
|
4195
|
+
await command.execute();
|
|
4196
|
+
} catch (error) {
|
|
4197
|
+
handleError(error, true);
|
|
4198
|
+
}
|
|
4199
|
+
})();
|