@dongfangw/study-kit 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/LICENSE +671 -0
  2. package/README.md +28 -2
  3. package/cordis.patch.yml +3 -0
  4. package/ext/adoc-convert/index.mjs +62 -0
  5. package/lib/client.js +5917 -0
  6. package/lib/client.js.map +1 -0
  7. package/lib/docsplit.d.ts +73 -0
  8. package/lib/docsplit.d.ts.map +1 -0
  9. package/lib/docsplit.js +241 -0
  10. package/lib/docsplit.js.map +1 -0
  11. package/lib/git.d.ts +29 -0
  12. package/lib/git.d.ts.map +1 -0
  13. package/lib/git.js +305 -0
  14. package/lib/git.js.map +1 -0
  15. package/lib/index.d.ts +10 -0
  16. package/lib/index.d.ts.map +1 -0
  17. package/lib/index.js +324 -0
  18. package/lib/index.js.map +1 -0
  19. package/lib/isolation.d.ts +33 -0
  20. package/lib/isolation.d.ts.map +1 -0
  21. package/lib/isolation.js +30 -0
  22. package/lib/isolation.js.map +1 -0
  23. package/lib/libraries.d.ts +36 -0
  24. package/lib/libraries.d.ts.map +1 -0
  25. package/lib/libraries.js +118 -0
  26. package/lib/libraries.js.map +1 -0
  27. package/lib/render.d.ts +22 -0
  28. package/lib/render.d.ts.map +1 -0
  29. package/lib/render.js +568 -0
  30. package/lib/render.js.map +1 -0
  31. package/lib/session.d.ts +23 -0
  32. package/lib/session.d.ts.map +1 -0
  33. package/lib/session.js +35 -0
  34. package/lib/session.js.map +1 -0
  35. package/lib/skills.d.ts +24 -0
  36. package/lib/skills.d.ts.map +1 -0
  37. package/lib/skills.js +111 -0
  38. package/lib/skills.js.map +1 -0
  39. package/lib/slidev.d.ts +13 -0
  40. package/lib/slidev.d.ts.map +1 -0
  41. package/lib/slidev.js +755 -0
  42. package/lib/slidev.js.map +1 -0
  43. package/lib/slidevBin.d.ts +7 -0
  44. package/lib/slidevBin.d.ts.map +1 -0
  45. package/lib/slidevBin.js +130 -0
  46. package/lib/slidevBin.js.map +1 -0
  47. package/lib/slidevEnv.d.ts +56 -0
  48. package/lib/slidevEnv.d.ts.map +1 -0
  49. package/lib/slidevEnv.js +702 -0
  50. package/lib/slidevEnv.js.map +1 -0
  51. package/lib/static.d.ts +8 -0
  52. package/lib/static.d.ts.map +1 -0
  53. package/lib/static.js +79 -0
  54. package/lib/static.js.map +1 -0
  55. package/lib/tools.d.ts +4 -0
  56. package/lib/tools.d.ts.map +1 -0
  57. package/lib/tools.js +160 -0
  58. package/lib/tools.js.map +1 -0
  59. package/lib/types/client/DocMarks.d.ts +76 -0
  60. package/lib/types/client/DocMarks.d.ts.map +1 -0
  61. package/lib/types/client/DocViewer.d.ts +21 -0
  62. package/lib/types/client/DocViewer.d.ts.map +1 -0
  63. package/lib/types/client/GitPanel.d.ts +20 -0
  64. package/lib/types/client/GitPanel.d.ts.map +1 -0
  65. package/lib/types/client/LibraryWorkspace.d.ts +5 -0
  66. package/lib/types/client/LibraryWorkspace.d.ts.map +1 -0
  67. package/lib/types/client/MyLibraries.d.ts +5 -0
  68. package/lib/types/client/MyLibraries.d.ts.map +1 -0
  69. package/lib/types/client/SlidevDeckViewer.d.ts +25 -0
  70. package/lib/types/client/SlidevDeckViewer.d.ts.map +1 -0
  71. package/lib/types/client/SlidevEnvPanel.d.ts +11 -0
  72. package/lib/types/client/SlidevEnvPanel.d.ts.map +1 -0
  73. package/lib/types/client/annotations.d.ts +161 -0
  74. package/lib/types/client/annotations.d.ts.map +1 -0
  75. package/lib/types/client/docEnhance.d.ts +55 -0
  76. package/lib/types/client/docEnhance.d.ts.map +1 -0
  77. package/lib/types/client/docKey.d.ts +22 -0
  78. package/lib/types/client/docKey.d.ts.map +1 -0
  79. package/lib/types/client/git.module.css.gen.d.ts +3 -0
  80. package/lib/types/client/git.module.css.gen.d.ts.map +1 -0
  81. package/lib/types/client/icons.d.ts +50 -0
  82. package/lib/types/client/icons.d.ts.map +1 -0
  83. package/lib/types/client/index.d.ts +12 -0
  84. package/lib/types/client/index.d.ts.map +1 -0
  85. package/lib/types/client/prose.d.ts +19 -0
  86. package/lib/types/client/prose.d.ts.map +1 -0
  87. package/lib/types/client/readerControls.d.ts +55 -0
  88. package/lib/types/client/readerControls.d.ts.map +1 -0
  89. package/lib/types/client/readerPrefs.d.ts +73 -0
  90. package/lib/types/client/readerPrefs.d.ts.map +1 -0
  91. package/lib/types/client/readingPos.d.ts +82 -0
  92. package/lib/types/client/readingPos.d.ts.map +1 -0
  93. package/lib/types/client/styles.module.css.gen.d.ts +3 -0
  94. package/lib/types/client/styles.module.css.gen.d.ts.map +1 -0
  95. package/lib/types/client/submitDraft.d.ts +29 -0
  96. package/lib/types/client/submitDraft.d.ts.map +1 -0
  97. package/lib/types/client/viewActive.d.ts +55 -0
  98. package/lib/types/client/viewActive.d.ts.map +1 -0
  99. package/lib/types/types.d.ts +351 -0
  100. package/lib/types/types.d.ts.map +1 -0
  101. package/lib/types.d.ts +351 -0
  102. package/lib/types.d.ts.map +1 -0
  103. package/lib/types.js +6 -0
  104. package/lib/types.js.map +1 -0
  105. package/package.json +101 -6
  106. package/skills/aero-concept-qa/SKILL.md +65 -0
  107. package/skills/aero-learning-path/SKILL.md +61 -0
  108. package/skills/aero-study-guide/SKILL.md +47 -0
  109. package/skills/aero-think/SKILL.md +59 -0
  110. package/skills/kb-organize/SKILL.md +73 -0
package/lib/slidev.js ADDED
@@ -0,0 +1,755 @@
1
+ /**
2
+ * Slidev 课件按需构建 + 静态托管。
3
+ *
4
+ * 背景:teaching-case/mdPPT 各节是独立 Slidev 工程(slides.md + 本地主题 ../../theme-neversink)。
5
+ * 但部分历史工作区的 mdPPT/theme-neversink 曾是指向外部主题目录的符号链接,
6
+ * 位于只读挂载(/mnt/data)上;vite 构建时要在主题 node_modules 下写 .vite-temp/.vite 缓存,
7
+ * 直接构建会 EROFS。因此这里把 deck 与主题复制到可写缓存(默认 os.tmpdir())构成镜像树,
8
+ * 保持 `../../theme-neversink` 的相对布局,再用全局 slidev CLI 构建出静态 SPA,
9
+ * 经 /aero-kb/slidev/<key>/ 托管(--base 重写资源前缀)。
10
+ *
11
+ * 流程:meta?p=... → 判断 slides.md → 计算 key → 过期则入队构建(并发上限 2,超时 8 分钟)
12
+ * → 构建产物 dist/ 静态托管(index.html 回退);meta.json 记录 srcMtime 用于失效判断。
13
+ */
14
+ import { createHash } from 'node:crypto';
15
+ import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
16
+ import os from 'node:os';
17
+ import path from 'node:path';
18
+ import { spawn } from 'node:child_process';
19
+ import { resolveSlidevBin } from './slidevBin.js';
20
+ import { getSlidevEnv } from './slidevEnv.js';
21
+ import { sendFile } from './static.js';
22
+ import { isInside } from '@aero/library-core';
23
+ // ---------------------------------------------------------------------------
24
+ // 常量与缓存根
25
+ // ---------------------------------------------------------------------------
26
+ /** 单个构建进程允许的最长时间(首次需复制主题镜像 700MB+ 并跑 vite,比较慢)。 */
27
+ const BUILD_TIMEOUT_MS = 8 * 60_000;
28
+ /** 并发构建上限(构建很吃 CPU/内存)。 */
29
+ const MAX_PARALLEL = 2;
30
+ /** 构建日志环形缓冲上限(字符)。 */
31
+ const MAX_LOG_CHARS = 40_000;
32
+ /** meta.json 中保留的日志尾部。 */
33
+ const META_LOG_TAIL = 16_000;
34
+ /** 静态资源 key:sha1 十六进制。 */
35
+ const KEY_RE = /^[0-9a-f]{8,64}$/;
36
+ const CACHE_ROOT = process.env.AERO_SLIDEV_CACHE
37
+ ? path.resolve(process.env.AERO_SLIDEV_CACHE)
38
+ : path.join(os.tmpdir(), 'aero-slidev');
39
+ const THEMES_DIR = path.join(CACHE_ROOT, 'themes');
40
+ function sha1(text) {
41
+ return createHash('sha1').update(text).digest('hex');
42
+ }
43
+ // ---------------------------------------------------------------------------
44
+ // 工具函数
45
+ // ---------------------------------------------------------------------------
46
+ /** 目录下文件的最近 mtime(跳过指定子目录/扩展名,用于判断源是否变化)。 */
47
+ function maxMtimeOf(dir, opts) {
48
+ let max = 0;
49
+ const walk = (d, depth) => {
50
+ if (depth > opts.maxDepth)
51
+ return;
52
+ let entries;
53
+ try {
54
+ entries = readdirSync(d, { withFileTypes: true });
55
+ }
56
+ catch {
57
+ return;
58
+ }
59
+ for (const e of entries) {
60
+ if (e.name.startsWith('.'))
61
+ continue;
62
+ const abs = path.join(d, e.name);
63
+ if (e.isDirectory()) {
64
+ if (!opts.skipDirs.has(e.name))
65
+ walk(abs, depth + 1);
66
+ continue;
67
+ }
68
+ if (!e.isFile())
69
+ continue;
70
+ if (opts.skipExts.has(path.extname(e.name).toLowerCase()))
71
+ continue;
72
+ try {
73
+ max = Math.max(max, statSync(abs).mtimeMs);
74
+ }
75
+ catch {
76
+ /* 忽略竞态 */
77
+ }
78
+ }
79
+ };
80
+ walk(dir, 0);
81
+ try {
82
+ max = Math.max(max, statSync(dir).mtimeMs);
83
+ }
84
+ catch {
85
+ /* ignore */
86
+ }
87
+ return max;
88
+ }
89
+ /** 读取 slides.md 前置 YAML 中的 theme 字段(只读文件头 4KB)。 */
90
+ function readThemeRef(slidesAbs) {
91
+ let text;
92
+ try {
93
+ text = readFileSync(slidesAbs, 'utf8').slice(0, 4096);
94
+ }
95
+ catch {
96
+ return null;
97
+ }
98
+ const m = /^\s*theme:\s*(\S+)\s*$/m.exec(text);
99
+ if (m === null || m[1] === undefined)
100
+ return null;
101
+ return m[1].replace(/^['"]|['"]$/g, '');
102
+ }
103
+ /** 安全地限制相对路径在 base 内。 */
104
+ function safeJoin(base, rel) {
105
+ const abs = path.resolve(base, rel);
106
+ if (!isInside(base, abs))
107
+ throw new Error('非法路径');
108
+ return abs;
109
+ }
110
+ function trimLog(log) {
111
+ let total = 0;
112
+ for (let i = log.length - 1; i >= 0; i--) {
113
+ total += (log[i] ?? '').length;
114
+ if (total > MAX_LOG_CHARS) {
115
+ log.splice(0, i + 1);
116
+ return;
117
+ }
118
+ }
119
+ }
120
+ // ---------------------------------------------------------------------------
121
+ // 主题镜像(复制到可写缓存,跨 deck 共享)
122
+ // ---------------------------------------------------------------------------
123
+ const themeMirrors = new Map();
124
+ async function mirrorTheme(themeAbs) {
125
+ const id = sha1(themeAbs).slice(0, 16);
126
+ const dest = path.join(THEMES_DIR, id);
127
+ const inflight = themeMirrors.get(id);
128
+ if (inflight !== undefined)
129
+ return inflight;
130
+ const job = (async () => {
131
+ const metaFile = path.join(dest, '.aero-mirror.json');
132
+ const srcReal = realpathSync(themeAbs);
133
+ const srcMtime = maxMtimeOf(srcReal, {
134
+ skipDirs: new Set(['node_modules', '.git', 'docs']),
135
+ skipExts: new Set(),
136
+ maxDepth: 6,
137
+ });
138
+ let fresh = false;
139
+ try {
140
+ const m = JSON.parse(readFileSync(metaFile, 'utf8'));
141
+ if (typeof m.mtime === 'number' && m.mtime >= srcMtime && existsSync(path.join(dest, 'package.json')))
142
+ fresh = true;
143
+ }
144
+ catch {
145
+ /* 无 meta → 重建 */
146
+ }
147
+ if (!fresh) {
148
+ const tmp = `${dest}.tmp-${process.pid}`;
149
+ rmSync(tmp, { recursive: true, force: true });
150
+ mkdirSync(tmp, { recursive: true });
151
+ // 复制整个主题(含 node_modules:vite 需要主题自身依赖解析 @slidev/types、unocss 等)。
152
+ cpSync(srcReal, tmp, {
153
+ recursive: true,
154
+ dereference: true,
155
+ filter: (src) => {
156
+ const base = path.basename(src);
157
+ if (base === '.git' || base === 'docs')
158
+ return false;
159
+ // 跳过悬空符号链接:dereference: true 会 stat 其目标,目标缺失时整个镜像拷贝
160
+ // 以 ENOENT 中止(曾因 theme node_modules/node_modules -> /mnt/newdata/… 悬空,
161
+ // 导致所有课件构建失败、工作台长时间无结果)。
162
+ try {
163
+ if (lstatSync(src).isSymbolicLink())
164
+ statSync(src);
165
+ }
166
+ catch {
167
+ return false;
168
+ }
169
+ return true;
170
+ },
171
+ });
172
+ writeFileSync(path.join(tmp, '.aero-mirror.json'), JSON.stringify({ mtime: srcMtime, src: themeAbs }));
173
+ rmSync(dest, { recursive: true, force: true });
174
+ renameSync(tmp, dest);
175
+ }
176
+ return dest;
177
+ })();
178
+ themeMirrors.set(id, job);
179
+ try {
180
+ return await job;
181
+ }
182
+ finally {
183
+ themeMirrors.delete(id);
184
+ }
185
+ }
186
+ const jobs = new Map();
187
+ const queue = [];
188
+ const pendingRebuild = new Set();
189
+ const activeChildren = new Set();
190
+ let running = 0;
191
+ /** 构建签名:hash 路由 + 前缀 base + JS 内 BASE_URL 常量改写(见 attemptBuild 注释)。 */
192
+ const BUILDER_SIG = 'slidev-hash-v9';
193
+ function readDeckMeta(key) {
194
+ try {
195
+ return JSON.parse(readFileSync(path.join(CACHE_ROOT, key, 'meta.json'), 'utf8'));
196
+ }
197
+ catch {
198
+ return null;
199
+ }
200
+ }
201
+ function writeDeckMeta(key, meta) {
202
+ mkdirSync(path.join(CACHE_ROOT, key), { recursive: true });
203
+ writeFileSync(path.join(CACHE_ROOT, key, 'meta.json'), JSON.stringify(meta));
204
+ }
205
+ /** 启动一个构建(调用方需保证并发/去重)。 */
206
+ function startBuild(req) {
207
+ running++;
208
+ const log = [];
209
+ const job = buildOne(req, log).finally(() => {
210
+ running--;
211
+ jobs.delete(req.key);
212
+ drainQueue();
213
+ });
214
+ jobs.set(req.key, { promise: job, log });
215
+ void job;
216
+ }
217
+ function drainQueue() {
218
+ while (running < MAX_PARALLEL && queue.length > 0) {
219
+ const next = queue.shift();
220
+ if (next !== undefined)
221
+ startBuild(next);
222
+ }
223
+ }
224
+ function ensureBuild(req) {
225
+ if (jobs.has(req.key))
226
+ return;
227
+ if (running >= MAX_PARALLEL) {
228
+ queue.push(req);
229
+ return;
230
+ }
231
+ startBuild(req);
232
+ }
233
+ /** 把 JS 产物里内联的 BASE_URL 字符串常量替换为空串(路由/资源相对解析,见 attemptBuild 注释)。 */
234
+ const REWRITE_EXTS = new Set(['.js', '.mjs', '.cjs']);
235
+ function rewriteBaseConstant(key, distDir) {
236
+ const literal = `/aero-kb/slidev/${key}/`;
237
+ let count = 0;
238
+ const walk = (dir) => {
239
+ let entries;
240
+ try {
241
+ entries = readdirSync(dir, { withFileTypes: true });
242
+ }
243
+ catch {
244
+ return;
245
+ }
246
+ for (const e of entries) {
247
+ const abs = path.join(dir, e.name);
248
+ if (e.isDirectory()) {
249
+ walk(abs);
250
+ continue;
251
+ }
252
+ if (!e.isFile() || !REWRITE_EXTS.has(path.extname(e.name).toLowerCase()))
253
+ continue;
254
+ let text;
255
+ try {
256
+ text = readFileSync(abs, 'utf8');
257
+ }
258
+ catch {
259
+ continue;
260
+ }
261
+ let changed = false;
262
+ for (const q of ['`', '"', "'"]) {
263
+ // 形态 1:完整字面量 `LIT` → 空字面量(getSlidePath 简单拼接 / createWebHashHistory)
264
+ const needle1 = q + literal + q;
265
+ if (text.includes(needle1)) {
266
+ count += text.split(needle1).length - 1;
267
+ text = text.replaceAll(needle1, q + q);
268
+ changed = true;
269
+ }
270
+ // 形态 2:模板前缀 `LIT${…}` → `${…}`(getSlidePath 的模板拼接形态;literal 自带结尾斜杠)
271
+ const needle2 = q + literal + '${';
272
+ if (text.includes(needle2)) {
273
+ count += text.split(needle2).length - 1;
274
+ text = text.replaceAll(needle2, q + '${');
275
+ changed = true;
276
+ }
277
+ }
278
+ // 修正:Vite 的资源前缀助手 `function(e){return BASE + e}` 会被形态 1 剥成 `return``+e`,
279
+ // 其后的 import.meta.resolve(e)(或 new URL(e, import.meta.url))无法解析「无前导 /、./」的相对路径,
280
+ // 抛 TypeError 导致整份课件运行时白屏。这里把 `return``+` 还原为绝对 BASE 前缀(资源仍正确落在 <key>/assets/ 下),
281
+ // 而 createWebHashHistory(`LIT`) 这类「BASE 作函数实参」的形态 1 剥离不受影响。
282
+ if (text.includes('return``+')) {
283
+ text = text.replaceAll('return``+', 'return`' + literal + '`+');
284
+ changed = true;
285
+ }
286
+ if (changed)
287
+ writeFileSync(abs, text);
288
+ }
289
+ };
290
+ walk(distDir);
291
+ return count;
292
+ }
293
+ /** 移除产物 HTML 中的 <base> 标签:vue-router hash 路由会把它当作 hash base 前缀写入 URL 导致 404。 */
294
+ function removeBaseTags(distDir) {
295
+ const walk = (dir) => {
296
+ let entries;
297
+ try {
298
+ entries = readdirSync(dir, { withFileTypes: true });
299
+ }
300
+ catch {
301
+ return;
302
+ }
303
+ for (const e of entries) {
304
+ const abs = path.join(dir, e.name);
305
+ if (e.isDirectory()) {
306
+ walk(abs);
307
+ continue;
308
+ }
309
+ if (!e.isFile() || path.extname(e.name).toLowerCase() !== '.html')
310
+ continue;
311
+ let text;
312
+ try {
313
+ text = readFileSync(abs, 'utf8');
314
+ }
315
+ catch {
316
+ continue;
317
+ }
318
+ const next = text.replace(/<base[^>]*>/g, '');
319
+ if (next !== text)
320
+ writeFileSync(abs, next);
321
+ }
322
+ };
323
+ walk(distDir);
324
+ }
325
+ /** 单 deck 构建:镜像复制 → slidev build → 记录 meta。重试直到 pendingRebuild 清空。 */
326
+ async function buildOne(req, log) {
327
+ do {
328
+ pendingRebuild.delete(req.key);
329
+ await attemptBuild(req, log);
330
+ } while (pendingRebuild.has(req.key));
331
+ }
332
+ /**
333
+ * 在镜像副本的 slides.md 前置 YAML 里强制 `routerMode: hash`(不动源文件)。
334
+ *
335
+ * Slidev 52 起 `--router-mode hash` CLI 参数已移除,且 parser 默认 routerMode 为 "history";
336
+ * 而本插件的构建后处理(改写 BASE_URL / 去 <base>)是按 hash 路由设计的。这里直接向
337
+ * resolveConfig 会并入的 frontmatter 注入 routerMode: hash 来保证 hash 路由。
338
+ */
339
+ function forceHashRoute(deckMirror) {
340
+ const slidesAbs = path.join(deckMirror, 'slides.md');
341
+ if (!existsSync(slidesAbs))
342
+ return;
343
+ let text;
344
+ try {
345
+ text = readFileSync(slidesAbs, 'utf8');
346
+ }
347
+ catch {
348
+ return;
349
+ }
350
+ if (!/^\s*---\s*\r?\n/.test(text))
351
+ return;
352
+ if (/^\s*routerMode:\s*hash\b/m.test(text))
353
+ return;
354
+ text = text.replace(/^(\s*---\s*\r?\n)/, '$1routerMode: hash\n');
355
+ try {
356
+ writeFileSync(slidesAbs, text);
357
+ }
358
+ catch {
359
+ /* 注入失败不致命:退化为 history 路由 */
360
+ }
361
+ }
362
+ async function attemptBuild(req, log) {
363
+ const { key, relPath, deckAbs } = req;
364
+ const keyDir = path.join(CACHE_ROOT, key);
365
+ const distDir = path.join(keyDir, 'dist');
366
+ const workDir = path.join(keyDir, '.work');
367
+ const base = `/aero-kb/slidev/${key}/`;
368
+ const bin = resolveSlidevBin();
369
+ rmSync(distDir, { recursive: true, force: true });
370
+ mkdirSync(keyDir, { recursive: true });
371
+ if (bin === null) {
372
+ writeDeckMeta(key, {
373
+ builtAt: 0,
374
+ srcMtime: 0,
375
+ error: '未找到 slidev CLI。请安装:npm i -g @slidev/cli(或设置 AERO_SLIDEV_BIN 指向 slidev 可执行文件)。',
376
+ log: '',
377
+ builder: BUILDER_SIG,
378
+ });
379
+ return;
380
+ }
381
+ const slidesAbs = path.join(deckAbs, 'slides.md');
382
+ try {
383
+ const dbg = process.env.AERO_SLIDEV_DEBUG === '1' ? (m) => console.error(`[slidev:${key.slice(0, 6)}] ${m}`) : () => { };
384
+ dbg(`attemptBuild start deckAbs=${deckAbs}`);
385
+ // 1) 源新鲜度(deck 全部文件 + 主题源码,不含 node_modules/导出物)
386
+ const deckMtime = maxMtimeOf(deckAbs, {
387
+ skipDirs: new Set(['node_modules', 'dist', '.git']),
388
+ skipExts: new Set(['.pdf']),
389
+ maxDepth: 8,
390
+ });
391
+ dbg('mtime ok');
392
+ // 2) 构建镜像树:deck 副本 + 主题符号链接(相对布局与源一致)
393
+ rmSync(workDir, { recursive: true, force: true });
394
+ mkdirSync(workDir, { recursive: true });
395
+ const deckMirror = path.join(workDir, path.dirname(relPath));
396
+ dbg('copy deck start');
397
+ cpSync(deckAbs, deckMirror, {
398
+ recursive: true,
399
+ dereference: false,
400
+ filter: (src) => {
401
+ const baseName = path.basename(src);
402
+ if (baseName === 'node_modules' || baseName === 'dist' || baseName === '.git')
403
+ return false;
404
+ if (path.extname(baseName).toLowerCase() === '.pdf')
405
+ return false;
406
+ return true;
407
+ },
408
+ });
409
+ dbg('copy deck done');
410
+ // 在镜像副本注入 routerMode: hash(Slidev 52 默认 history,插件后处理按 hash 设计)。
411
+ forceHashRoute(deckMirror);
412
+ const themeRef = readThemeRef(slidesAbs);
413
+ if (themeRef === null || !themeRef.startsWith('.')) {
414
+ throw new Error('slides.md 未声明本地主题路径(theme: ./…),暂不支持 npm 包名主题');
415
+ }
416
+ const themeAbs = path.resolve(deckAbs, themeRef);
417
+ if (!existsSync(path.join(themeAbs, 'package.json'))) {
418
+ throw new Error(`主题目录不存在:${themeAbs}`);
419
+ }
420
+ const linkTarget = path.resolve(workDir, path.dirname(relPath), themeRef);
421
+ const workPrefix = workDir.endsWith(path.sep) ? workDir : workDir + path.sep;
422
+ if (!linkTarget.startsWith(workPrefix))
423
+ throw new Error(`主题路径越界:${themeRef}`);
424
+ mkdirSync(path.dirname(linkTarget), { recursive: true });
425
+ rmSync(linkTarget, { recursive: true, force: true });
426
+ dbg('mirror theme start');
427
+ // Windows 创建目录符号链接需要管理员/开发者模式(否则 EPERM)。
428
+ // 目录型 junction 无需提权即可创建,作为 Windows 下的等价替代。
429
+ symlinkSync(await mirrorTheme(themeAbs), linkTarget, process.platform === 'win32' ? 'junction' : undefined);
430
+ dbg('mirror theme done');
431
+ // 3) 构建:hash 路由(frontmatter 已注入 routerMode: hash)+ 前缀 base。
432
+ // history 路由与 vue-router 的 base 叠加会产生 base 重复的路径(导航直接 404)。
433
+ // hash 路由下 BASE_URL 前缀会被 Slidev 的 getSlidePath 拼进路由造成 404,
434
+ // 因此构建后把 JS 里内联的 BASE_URL 常量 "/aero-kb/slidev/<key>/" 替换为空串:
435
+ // · 资源拼接处:""+"assets/…" 相对当前模块/文档解析,仍落在 <key>/assets/ 下 ✓
436
+ // · 路由拼接处:""+"2" → 相对导航 → 干净的 #/2 ✓
437
+ // 注:Slidev 52 起 `--router-mode hash` CLI 参数已移除,只能靠配置控制,故不在 args 里传。
438
+ await runBuild(bin, ['build', '--base', base, '--out', distDir], deckMirror, log);
439
+ dbg('build done');
440
+ const rewrites = rewriteBaseConstant(key, distDir);
441
+ if (rewrites === 0)
442
+ throw new Error('构建产物中未找到 BASE_URL 内联常量,路由改写失败(slidev 版本行为变化?)');
443
+ removeBaseTags(distDir);
444
+ // srcMtime 必须与 currentStatus 的新鲜度口径一致:max(deck, 主题)。
445
+ let themeMtime = 0;
446
+ try {
447
+ themeMtime = maxMtimeOf(realpathSync(themeAbs), {
448
+ skipDirs: new Set(['node_modules', '.git', 'docs']),
449
+ skipExts: new Set(),
450
+ maxDepth: 6,
451
+ });
452
+ }
453
+ catch {
454
+ /* 主题 mtime 不可读时按 0 处理 */
455
+ }
456
+ writeDeckMeta(key, {
457
+ builtAt: Date.now(),
458
+ srcMtime: Math.max(deckMtime, themeMtime),
459
+ log: log.join('').slice(-META_LOG_TAIL),
460
+ builder: BUILDER_SIG,
461
+ });
462
+ }
463
+ catch (err) {
464
+ rmSync(distDir, { recursive: true, force: true });
465
+ const msg = err instanceof Error ? err.message : String(err);
466
+ writeDeckMeta(key, {
467
+ builtAt: 0,
468
+ srcMtime: 0,
469
+ error: msg,
470
+ log: log.join('').slice(-META_LOG_TAIL),
471
+ builder: BUILDER_SIG,
472
+ });
473
+ }
474
+ finally {
475
+ rmSync(workDir, { recursive: true, force: true });
476
+ }
477
+ }
478
+ function runBuild(bin, args, cwd, log) {
479
+ return new Promise((resolve, reject) => {
480
+ const dbg = process.env.AERO_SLIDEV_DEBUG === '1' ? (m) => console.error(`[slidev:spawn] ${m}`) : () => { };
481
+ dbg(`spawn ${process.execPath} ${bin} ${args.join(' ')} cwd=${cwd}`);
482
+ const child = spawn(process.execPath, [bin, ...args], {
483
+ cwd,
484
+ env: { ...process.env, NO_COLOR: '1', FORCE_COLOR: '0', GIT_TERMINAL_PROMPT: '0' },
485
+ stdio: ['ignore', 'pipe', 'pipe'],
486
+ });
487
+ activeChildren.add(child);
488
+ dbg(`spawned pid=${child.pid ?? '?'}`);
489
+ const onData = (b) => {
490
+ log.push(b.toString('utf8'));
491
+ trimLog(log);
492
+ };
493
+ child.stdout?.on('data', onData);
494
+ child.stderr?.on('data', onData);
495
+ const timer = setTimeout(() => {
496
+ child.kill('SIGKILL');
497
+ }, BUILD_TIMEOUT_MS);
498
+ child.on('error', (err) => {
499
+ clearTimeout(timer);
500
+ activeChildren.delete(child);
501
+ reject(err);
502
+ });
503
+ child.on('close', (code) => {
504
+ clearTimeout(timer);
505
+ activeChildren.delete(child);
506
+ dbg(`closed code=${String(code)}`);
507
+ if (code === 0)
508
+ resolve();
509
+ else
510
+ reject(new Error(code === null ? '构建超时(超过 8 分钟)或被终止' : `构建进程退出码 ${code}`));
511
+ });
512
+ });
513
+ }
514
+ // ---------------------------------------------------------------------------
515
+ // 状态查询
516
+ // ---------------------------------------------------------------------------
517
+ function metaBody(partial) {
518
+ return {
519
+ ok: true,
520
+ isDeck: false,
521
+ relPath: '',
522
+ key: '',
523
+ status: 'not-deck',
524
+ builtAt: 0,
525
+ slidesUrl: '',
526
+ presenterUrl: '',
527
+ error: '',
528
+ log: '',
529
+ ...partial,
530
+ };
531
+ }
532
+ function currentStatus(key, root, relPath) {
533
+ const job = jobs.get(key);
534
+ if (job !== undefined)
535
+ return { status: 'building', builtAt: 0, error: '', log: job.log.join('').slice(-META_LOG_TAIL) };
536
+ if (queue.some((q) => q.key === key))
537
+ return { status: 'queued', builtAt: 0, error: '', log: '' };
538
+ const deckMeta = readDeckMeta(key);
539
+ const distIndex = path.join(CACHE_ROOT, key, 'dist', 'index.html');
540
+ const built = deckMeta !== null && deckMeta.builtAt > 0 && deckMeta.builder === BUILDER_SIG && (deckMeta.error === undefined || deckMeta.error === '') && existsSync(distIndex);
541
+ // 已构建且源未变化 → ready;源变化 → 自动重建
542
+ if (built && deckMeta !== null) {
543
+ const deckAbs = safeJoin(root, path.dirname(relPath));
544
+ const srcMtime = maxMtimeOf(deckAbs, {
545
+ skipDirs: new Set(['node_modules', 'dist', '.git']),
546
+ skipExts: new Set(['.pdf']),
547
+ maxDepth: 8,
548
+ });
549
+ const themeRef = readThemeRef(path.join(deckAbs, 'slides.md'));
550
+ let fresh = false;
551
+ if (themeRef !== null && themeRef.startsWith('.')) {
552
+ const themeAbs = path.resolve(deckAbs, themeRef);
553
+ try {
554
+ const t = maxMtimeOf(realpathSync(themeAbs), {
555
+ skipDirs: new Set(['node_modules', '.git', 'docs']),
556
+ skipExts: new Set(),
557
+ maxDepth: 6,
558
+ });
559
+ fresh = Math.max(srcMtime, t) <= deckMeta.srcMtime;
560
+ }
561
+ catch {
562
+ fresh = false;
563
+ }
564
+ }
565
+ else {
566
+ fresh = srcMtime <= deckMeta.srcMtime;
567
+ }
568
+ if (fresh)
569
+ return { status: 'ready', builtAt: deckMeta.builtAt, error: '', log: deckMeta.log };
570
+ return { status: 'missing', builtAt: 0, error: '', log: '' };
571
+ }
572
+ if (deckMeta !== null && deckMeta.error !== undefined && deckMeta.error !== '') {
573
+ return { status: 'error', builtAt: 0, error: deckMeta.error, log: deckMeta.log };
574
+ }
575
+ return { status: 'missing', builtAt: 0, error: '', log: '' };
576
+ }
577
+ // ---------------------------------------------------------------------------
578
+ // HTTP 处理
579
+ // ---------------------------------------------------------------------------
580
+ const DIST_TYPES = {
581
+ '.html': 'text/html; charset=utf-8',
582
+ '.js': 'text/javascript; charset=utf-8',
583
+ '.mjs': 'text/javascript; charset=utf-8',
584
+ '.css': 'text/css; charset=utf-8',
585
+ '.json': 'application/json',
586
+ '.map': 'application/json',
587
+ '.svg': 'image/svg+xml',
588
+ '.png': 'image/png',
589
+ '.jpg': 'image/jpeg',
590
+ '.jpeg': 'image/jpeg',
591
+ '.webp': 'image/webp',
592
+ '.avif': 'image/avif',
593
+ '.gif': 'image/gif',
594
+ '.ico': 'image/x-icon',
595
+ '.woff': 'font/woff',
596
+ '.woff2': 'font/woff2',
597
+ '.ttf': 'font/ttf',
598
+ '.mp4': 'video/mp4',
599
+ '.webm': 'video/webm',
600
+ '.wasm': 'application/wasm',
601
+ '.xml': 'application/xml',
602
+ '.txt': 'text/plain; charset=utf-8',
603
+ '.webmanifest': 'application/manifest+json',
604
+ };
605
+ function sendJson(res, code, body) {
606
+ const data = JSON.stringify(body);
607
+ res.statusCode = code;
608
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
609
+ res.setHeader('Cache-Control', 'no-store');
610
+ res.end(data);
611
+ }
612
+ function sendText(res, code, text) {
613
+ res.statusCode = code;
614
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
615
+ res.setHeader('Cache-Control', 'no-store');
616
+ res.end(text);
617
+ }
618
+ function serveDist(key, file, req, res) {
619
+ const distDir = path.join(CACHE_ROOT, key, 'dist');
620
+ if (!existsSync(path.join(distDir, 'index.html'))) {
621
+ sendText(res, 404, '课件尚未构建完成,请稍后刷新');
622
+ return;
623
+ }
624
+ let abs;
625
+ try {
626
+ const rel = file !== '' ? decodeURIComponent(file) : '';
627
+ abs = safeJoin(distDir, rel);
628
+ }
629
+ catch {
630
+ sendText(res, 403, 'forbidden');
631
+ return;
632
+ }
633
+ try {
634
+ if (!existsSync(abs) || statSync(abs).isDirectory()) {
635
+ abs = path.join(distDir, 'index.html');
636
+ res.setHeader('Cache-Control', 'no-store');
637
+ }
638
+ else if (path.extname(abs).toLowerCase() === '.html') {
639
+ res.setHeader('Cache-Control', 'no-store');
640
+ }
641
+ else {
642
+ // 产物经构建后改写(文件名哈希不代表最终内容),用协商缓存避免浏览器命中旧构建。
643
+ res.setHeader('Cache-Control', 'no-cache');
644
+ }
645
+ const ext = path.extname(abs).toLowerCase();
646
+ sendFile(req, res, abs, DIST_TYPES[ext] ?? 'application/octet-stream');
647
+ }
648
+ catch {
649
+ sendText(res, 404, 'not found');
650
+ }
651
+ }
652
+ /**
653
+ * /aero-kb/slidev 路由:
654
+ * - GET /aero-kb/slidev/meta?p=<rel>&cwd=…&rebuild=1 → 状态/触发构建
655
+ * - GET /aero-kb/slidev/<key>/<file…> → 构建产物静态托管
656
+ */
657
+ export function createSlidevHandler(opts) {
658
+ return (req, res) => {
659
+ void handleSlidev(req, res, opts);
660
+ };
661
+ }
662
+ /**
663
+ * 实际处理逻辑(异步):meta 分支在缺构建链时要跑环境体检,因此整体是 async。
664
+ * 静态产物分支是同步读写,但同样走这里以保持单一入口。
665
+ */
666
+ async function handleSlidev(req, res, opts) {
667
+ const url = new URL(req.url ?? '/', 'http://localhost');
668
+ const parts = url.pathname.split('/').filter(Boolean);
669
+ const seg = parts[2] ?? '';
670
+ const hintCwd = url.searchParams.get('cwd') ?? undefined;
671
+ try {
672
+ if (seg === 'meta') {
673
+ const rel = url.searchParams.get('p') ?? '';
674
+ const root = opts.resolveKbRoot(hintCwd);
675
+ if (root === null) {
676
+ sendJson(res, 200, metaBody({ ok: false, relPath: rel, error: '未找到空气动力学知识库目录' }));
677
+ return;
678
+ }
679
+ const abs = safeJoin(root, rel);
680
+ if (path.basename(abs).toLowerCase() !== 'slides.md' || !existsSync(abs)) {
681
+ sendJson(res, 200, metaBody({ isDeck: false, relPath: rel, status: 'not-deck', error: '不是 Slidev 课件(slides.md)' }));
682
+ return;
683
+ }
684
+ const key = sha1(`${root}\n${rel}`).slice(0, 16);
685
+ const deckAbs = path.dirname(abs);
686
+ if (resolveSlidevBin() === null) {
687
+ // 顺带把体检结果一起回给前端:用户不必再点一次「检查环境」才能看到缺什么、怎么装。
688
+ const env = await getSlidevEnv({ slidesAbs: abs });
689
+ sendJson(res, 200, metaBody({
690
+ isDeck: true, relPath: rel, key, status: 'no-toolchain',
691
+ error: env.summary === '' ? '本机 Slidev 构建链未就绪。' : `本机 Slidev 构建链未就绪:${env.summary}`,
692
+ env,
693
+ }));
694
+ return;
695
+ }
696
+ const force = url.searchParams.get('rebuild') === '1';
697
+ const dbgM = process.env.AERO_SLIDEV_DEBUG === '1' ? (m) => console.error(`[slidev:meta ${key.slice(0, 6)}] ${m}`) : () => { };
698
+ dbgM(`meta p=${rel.slice(-30)} rebuild=${url.searchParams.get('rebuild') ?? ''}`);
699
+ if (force) {
700
+ if (jobs.has(key) || pendingRebuild.has(key)) {
701
+ // 正在构建:标记完成后自动再构建一次
702
+ dbgM('force during build -> pendingRebuild.add');
703
+ pendingRebuild.add(key);
704
+ }
705
+ else {
706
+ dbgM('force idle -> rm cache');
707
+ rmSync(path.join(CACHE_ROOT, key), { recursive: true, force: true });
708
+ }
709
+ }
710
+ const st = currentStatus(key, root, rel);
711
+ if (st.status === 'missing') {
712
+ ensureBuild({ key, relPath: rel, deckAbs });
713
+ }
714
+ const status = st.status === 'missing' ? 'building' : st.status;
715
+ const distDir = path.join(CACHE_ROOT, key, 'dist');
716
+ sendJson(res, 200, metaBody({
717
+ isDeck: true,
718
+ relPath: rel,
719
+ key,
720
+ status,
721
+ builtAt: st.builtAt,
722
+ slidesUrl: `/aero-kb/slidev/${key}/index.html`,
723
+ presenterUrl: existsSync(path.join(distDir, 'presenter.html')) ? `/aero-kb/slidev/${key}/presenter.html` : `/aero-kb/slidev/${key}/index.html#/presenter/1`,
724
+ error: st.error,
725
+ log: st.log,
726
+ }));
727
+ return;
728
+ }
729
+ if (KEY_RE.test(seg)) {
730
+ serveDist(seg, parts.slice(3).join('/'), req, res);
731
+ return;
732
+ }
733
+ sendText(res, 404, 'not found');
734
+ }
735
+ catch (err) {
736
+ sendJson(res, 500, { ok: false, error: err instanceof Error ? err.message : String(err) });
737
+ }
738
+ }
739
+ /** 插件卸载时终止所有构建子进程。 */
740
+ export function disposeSlidev() {
741
+ for (const child of activeChildren) {
742
+ try {
743
+ child.kill('SIGKILL');
744
+ }
745
+ catch {
746
+ /* ignore */
747
+ }
748
+ }
749
+ activeChildren.clear();
750
+ jobs.clear();
751
+ queue.length = 0;
752
+ pendingRebuild.clear();
753
+ running = 0;
754
+ }
755
+ //# sourceMappingURL=slidev.js.map