@tnotesjs/core 0.1.27 → 0.2.0

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