@dongfangw/study-kit 0.1.1 → 0.1.3

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 (48) hide show
  1. package/README.md +46 -2
  2. package/ext/adoc-convert/index.mjs +62 -62
  3. package/lib/client.js +1862 -164
  4. package/lib/client.js.map +1 -1
  5. package/lib/index.d.ts +1 -1
  6. package/lib/index.d.ts.map +1 -1
  7. package/lib/index.js +32 -17
  8. package/lib/index.js.map +1 -1
  9. package/lib/isolation.d.ts +1 -1
  10. package/lib/libPath.d.ts +28 -0
  11. package/lib/libPath.d.ts.map +1 -0
  12. package/lib/libPath.js +36 -0
  13. package/lib/libPath.js.map +1 -0
  14. package/lib/libraries.d.ts +1 -1
  15. package/lib/libraries.js +1 -1
  16. package/lib/pdf.d.ts +31 -0
  17. package/lib/pdf.d.ts.map +1 -0
  18. package/lib/pdf.js +174 -0
  19. package/lib/pdf.js.map +1 -0
  20. package/lib/render.d.ts.map +1 -1
  21. package/lib/render.js +58 -1
  22. package/lib/render.js.map +1 -1
  23. package/lib/session.d.ts +1 -1
  24. package/lib/slidev.js +1 -1
  25. package/lib/tools.d.ts.map +1 -1
  26. package/lib/tools.js +10 -2
  27. package/lib/tools.js.map +1 -1
  28. package/lib/types/client/LibraryWorkspace.d.ts.map +1 -1
  29. package/lib/types/client/PdfViewer.d.ts +15 -0
  30. package/lib/types/client/PdfViewer.d.ts.map +1 -0
  31. package/lib/types/client/annotations.d.ts +26 -2
  32. package/lib/types/client/annotations.d.ts.map +1 -1
  33. package/lib/types/client/docEnhance.d.ts.map +1 -1
  34. package/lib/types/client/docKey.d.ts +8 -0
  35. package/lib/types/client/docKey.d.ts.map +1 -1
  36. package/lib/types/client/pdfAnno.d.ts +44 -0
  37. package/lib/types/client/pdfAnno.d.ts.map +1 -0
  38. package/lib/types/client/pdfPos.d.ts +27 -0
  39. package/lib/types/client/pdfPos.d.ts.map +1 -0
  40. package/lib/types/client/pdfjs.d.ts +87 -0
  41. package/lib/types/client/pdfjs.d.ts.map +1 -0
  42. package/lib/types/client/styles.module.css.gen.d.ts.map +1 -1
  43. package/package.json +140 -101
  44. package/skills/aero-concept-qa/SKILL.md +59 -59
  45. package/skills/aero-learning-path/SKILL.md +55 -55
  46. package/skills/aero-study-guide/SKILL.md +47 -47
  47. package/skills/aero-think/SKILL.md +59 -59
  48. package/skills/kb-organize/SKILL.md +2 -2
package/README.md CHANGED
@@ -14,16 +14,60 @@ dsh plugin install @dongfangw/study-kit
14
14
  ## 能力
15
15
 
16
16
  - 书籍版式读 adoc / md,公式走 KaTeX;超长文档自动分段
17
+ - **PDF 阅读器**:浏览器的 pdf.js 渲染(矢量清晰、可缩放、可翻页),选中文字即可取色标记 /
18
+ 画重点线 / 写批注 / 问 AI——与 md/adoc 共用同一套标注面板与「带着这段问 AI」
17
19
  - 荧光笔 / 重点线 / 便签提问(标注只落本机)
18
- - 阅读位置记住(按文档、按段)
20
+ - 阅读位置记住(文档按段,PDF 按页 + 页内比例)
19
21
  - 「我的库」:加库向导、库隔离(`visibility: private` 三出口全隐)、每库 Git 状态
20
22
  - 4 个工具:`kb_libraries` / `kb_catalog` / `kb_read` / `kb_search`
21
23
  - 5 个技能:学习导航、学习路径、概念答疑、物理直觉、建库向导
22
24
 
25
+ ## PDF 怎么用
26
+
27
+ 1. 在库的 `library.yaml` 里把 `pdf` 加进某个分组的 `exts`(例:`exts: [md, pdf]`),
28
+ 目录树里就会出现 PDF 节点(徽章 `PDF`)。
29
+ 2. 点开即读:默认**适宽**;顶栏有页码跳转、上一页/下一页、缩小/适宽/放大(`Ctrl` + 滚轮也行)。
30
+ 3. 选中一段文字 → 工具条浮出:四种荧光色 / 重点线 / 写想法 / 问 AI。
31
+ 标记按「第几页 + 页内文字偏移」存,换缩放、换窗口宽度后仍贴在同一段话上。
32
+ 4. 右下角「✍ N」打开标记面板:按页列出、可定位、删除、复制全部(Markdown)、清空。
33
+
34
+ 实现要点(改这块之前先读,否则很容易踩回去):
35
+
36
+ | 组件 | 职责 |
37
+ |---|---|
38
+ | 宿主 `src/pdf.ts` | `/kb/pdf` 送 PDF 字节(Range 感知,pdf.js 靠它分块拉大文件);`/kb/pdfjs/*` 按白名单伺服 pdf.js 运行时(含 `cmaps/` 与 `standard_fonts/`,中文 PDF 缺 cmaps 会抽不出文字);`loader.js` 带版本号 |
39
+ | 客户端 `pdfjs.ts` | 注入宿主 loader、等 `window.__aeroPdfjs`;**pdf.js 不进客户端 bundle**(主库 350KB + worker 1.4MB,且 worker 必须是独立文件) |
40
+ | 客户端 `PdfViewer.tsx` | 逐页 canvas + 文字层(只渲视口附近两屏的页)、缩放/翻页/位置记忆、选区 → 页内锚点 |
41
+ | 客户端 `pdfAnno.ts` | 页内文字索引、选区 → 偏移锚点、标注 → 色块矩形(空白归一后按原文兜底找回) |
42
+ | 客户端 `annotations.ts` | 标注库(`aero.anno.v1`):PDF 用 `paintPageTextAnnotation`,键是 `pdf:<relPath>` |
43
+
44
+ - 色块画在 canvas 与文字层**之间**,且 `pointer-events: none`:文字层在最上面,
45
+ 所以拖选永远不被色块拦住;点色块靠矩形反查(`hitTestAnnotations`)。
46
+ - 文字层正在重建时(缩放/重新进视口)代码块会先摘掉,避免旧色块挂在新排版上。
47
+ - 只有 `md/adoc` 走 `/kb/doc`;`.pdf` 走 `/kb/pdf`——把二进制喂给 markdown 渲染器只会得到乱码。
48
+
49
+ **安装体积**:`pdfjs-dist` 带来约 36MB(含 `.map` 与 legacy 构建),另有它的可选依赖
50
+ `@napi-rs/canvas` 约 36MB(**只有 Node 端渲染才用**,浏览器里用不到)。想省掉后者:
51
+ `npm install --omit=optional`,或装完后删掉 `<profile>/node_modules/@napi-rs`。
52
+
53
+ **已知限制**:扫描件(没有文字层)里选不中文字,因此只能翻看、不能取色标注——页面上会显示
54
+ 「第 N 页」占位;PDF 也不进 `kb_search`(全文检索只认 md/adoc)。
55
+
56
+ ## 门禁与自测
57
+
58
+ ```bash
59
+ npm test # 含 PDF 路由的行为约定(字节/Range/白名单/教师端分组)
60
+ npm run audit # 套件铁律(零课程专名、路由、技能完整性)
61
+ npm run probe:pdf # 真实浏览器实测:真 bundle + 真路由 + 真 PDF,断言渲染/选中/标注/缩放
62
+ ```
63
+
64
+ `probe:pdf`(`scripts/pdf-harness.mjs`)需要工作区根装有 `puppeteer-core` 与本机 Chrome/Edge;
65
+ 它把「学习资料」组件按 DSH 槽位约定挂到一个空白页里跑,截图落在 `tmp/pdf-harness/`。
66
+
23
67
  ## 状态
24
68
 
25
69
  施工中(阶段 2)。本仓从 `student-pack` 的阅读器部分拆出并去课程化。
26
70
 
27
71
  ## 许可
28
72
 
29
- MIT。
73
+ MIT。
@@ -1,62 +1,62 @@
1
- #!/usr/bin/env node
2
- /**
3
- * AsciiDoc → HTML 转换的**隔离执行器**(供 src/render.ts 以子进程方式调用)。
4
- *
5
- * 为什么必须隔离(血泪教训,勿改回进程内 require):
6
- * `@asciidoctor/core` 内含 Opal(Ruby 的 JS 实现)运行时,加载后会把内建原型链改写掉
7
- * (实测:`Object.getPrototypeOf(Array.prototype) !== Object.prototype`)。而 DSH 宿主用
8
- * `@deepseek-ai/dsh-util-values` 的「无损 JSON」校验(snapshotJsonValue/isJsonValue)来把关
9
- * 会话事件落盘,该校验会逐层核对内建原型(hasPlainArrayPrototype/hasIntrinsicObjectPrototype)。
10
- * 原型链一旦被 Opal 改写,**宿主进程里所有数组都不再算合法 JSON**:
11
- * · 任何 prompt 都无法写入会话日志 → 上层把错误包装成 session/agent-busy "prompt rejected"
12
- * → 学生端看到的就是「提问报错 agent busy」;
13
- * · 新会话的 api-session/added 事件也无法转发 → 新建会话失败。
14
- * 触发时机:学生首次打开任意 `.adoc` 教案(本插件的渲染库是惰性加载的)之后,进程即"中毒"。
15
- *
16
- * 注意:事后恢复原型链(Object.setPrototypeOf 回退)虽然能让 DSH 校验恢复正常,但会让
17
- * Opal 自身的后续调用崩溃(Ruby 方法沿被改写后的原型链查找)。所以唯一稳妥的做法是
18
- * 把整个 asciidoctor 运行时挡在宿主进程之外——本文件的子进程就是这层隔离。
19
- *
20
- * 协议:stdin 收 JSON `{ text, options }`,stdout 回 JSON `{ ok: true, html }` 或
21
- * `{ ok: false, error }`;退出码 0/1 仅作辅助,调用方以 JSON 为准。
22
- */
23
- import { readFileSync } from 'node:fs';
24
- import { createRequire } from 'node:module';
25
-
26
- const require = createRequire(import.meta.url);
27
-
28
- function readStdin() {
29
- try {
30
- return readFileSync(0, 'utf8');
31
- }
32
- catch {
33
- return '';
34
- }
35
- }
36
-
37
- function main() {
38
- // 容忍 UTF-8 BOM(手工用 `< file` / Get-Content 喂请求时会带上)。
39
- const raw = readStdin().replace(/^\uFEFF/u, '');
40
- if (raw.trim() === '')
41
- return { ok: false, error: 'asciidoctor 隔离执行器:stdin 为空' };
42
- let request;
43
- try {
44
- request = JSON.parse(raw);
45
- }
46
- catch (error) {
47
- return { ok: false, error: `请求不是合法 JSON:${error instanceof Error ? error.message : String(error)}` };
48
- }
49
- try {
50
- const factory = require('@asciidoctor/core');
51
- const api = factory();
52
- const doc = api.load(String(request.text ?? ''), request.options ?? {});
53
- return { ok: true, html: String(doc.convert()) };
54
- }
55
- catch (error) {
56
- return { ok: false, error: error instanceof Error ? error.message : String(error) };
57
- }
58
- }
59
-
60
- const result = main();
61
- process.stdout.write(JSON.stringify(result));
62
- process.exitCode = result.ok ? 0 : 1;
1
+ #!/usr/bin/env node
2
+ /**
3
+ * AsciiDoc → HTML 转换的**隔离执行器**(供 src/render.ts 以子进程方式调用)。
4
+ *
5
+ * 为什么必须隔离(血泪教训,勿改回进程内 require):
6
+ * `@asciidoctor/core` 内含 Opal(Ruby 的 JS 实现)运行时,加载后会把内建原型链改写掉
7
+ * (实测:`Object.getPrototypeOf(Array.prototype) !== Object.prototype`)。而 DSH 宿主用
8
+ * `@deepseek-ai/dsh-util-values` 的「无损 JSON」校验(snapshotJsonValue/isJsonValue)来把关
9
+ * 会话事件落盘,该校验会逐层核对内建原型(hasPlainArrayPrototype/hasIntrinsicObjectPrototype)。
10
+ * 原型链一旦被 Opal 改写,**宿主进程里所有数组都不再算合法 JSON**:
11
+ * · 任何 prompt 都无法写入会话日志 → 上层把错误包装成 session/agent-busy "prompt rejected"
12
+ * → 学生端看到的就是「提问报错 agent busy」;
13
+ * · 新会话的 api-session/added 事件也无法转发 → 新建会话失败。
14
+ * 触发时机:学生首次打开任意 `.adoc` 教案(本插件的渲染库是惰性加载的)之后,进程即"中毒"。
15
+ *
16
+ * 注意:事后恢复原型链(Object.setPrototypeOf 回退)虽然能让 DSH 校验恢复正常,但会让
17
+ * Opal 自身的后续调用崩溃(Ruby 方法沿被改写后的原型链查找)。所以唯一稳妥的做法是
18
+ * 把整个 asciidoctor 运行时挡在宿主进程之外——本文件的子进程就是这层隔离。
19
+ *
20
+ * 协议:stdin 收 JSON `{ text, options }`,stdout 回 JSON `{ ok: true, html }` 或
21
+ * `{ ok: false, error }`;退出码 0/1 仅作辅助,调用方以 JSON 为准。
22
+ */
23
+ import { readFileSync } from 'node:fs';
24
+ import { createRequire } from 'node:module';
25
+
26
+ const require = createRequire(import.meta.url);
27
+
28
+ function readStdin() {
29
+ try {
30
+ return readFileSync(0, 'utf8');
31
+ }
32
+ catch {
33
+ return '';
34
+ }
35
+ }
36
+
37
+ function main() {
38
+ // 容忍 UTF-8 BOM(手工用 `< file` / Get-Content 喂请求时会带上)。
39
+ const raw = readStdin().replace(/^\uFEFF/u, '');
40
+ if (raw.trim() === '')
41
+ return { ok: false, error: 'asciidoctor 隔离执行器:stdin 为空' };
42
+ let request;
43
+ try {
44
+ request = JSON.parse(raw);
45
+ }
46
+ catch (error) {
47
+ return { ok: false, error: `请求不是合法 JSON:${error instanceof Error ? error.message : String(error)}` };
48
+ }
49
+ try {
50
+ const factory = require('@asciidoctor/core');
51
+ const api = factory();
52
+ const doc = api.load(String(request.text ?? ''), request.options ?? {});
53
+ return { ok: true, html: String(doc.convert()) };
54
+ }
55
+ catch (error) {
56
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
57
+ }
58
+ }
59
+
60
+ const result = main();
61
+ process.stdout.write(JSON.stringify(result));
62
+ process.exitCode = result.ok ? 0 : 1;