dsh-book-reader 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -1
- package/README.md +60 -454
- package/SKILL.md +1 -2
- package/cordis.patch.yml +4 -5
- package/lib/client.js +1 -22
- package/lib/index.js +17 -103
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,29 @@ All notable changes to this project are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
|
|
5
5
|
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.1.1] — 2026-10-18
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Simplified, feature-focused README (English + 中文) that leads with what the
|
|
12
|
+
plugin does instead of full internals documentation.
|
|
13
|
+
- The automatic reading-position hint is now sent **at most once per turn**, and
|
|
14
|
+
only when the page changed. The per-step guard and the 3-minute default
|
|
15
|
+
throttle are gone: `autoShareMinSteps` was removed and
|
|
16
|
+
`autoShareMinIntervalMs` now defaults to `0` (optional extra throttle).
|
|
17
|
+
- Repository, homepage, and issue links now point to
|
|
18
|
+
`github.com/Strepsiades2/dsh-book-reader`.
|
|
19
|
+
|
|
20
|
+
### Removed
|
|
21
|
+
|
|
22
|
+
- The `pdf_current_page` tool. The per-turn position hint (or a page reference
|
|
23
|
+
you insert yourself) now covers “which page am I on”, leaving three tools:
|
|
24
|
+
`pdf_info`, `pdf_search`, and `pdf_read_pages`.
|
|
25
|
+
- The toolbar's manual “insert current page reference” (↗) button. The
|
|
26
|
+
automatic per-turn position hint already tells the model the current page, so
|
|
27
|
+
the button was redundant. The search panel's per-result “→ AI” insertion
|
|
28
|
+
remains.
|
|
29
|
+
|
|
7
30
|
## [0.1.0] — 2026-10-18
|
|
8
31
|
|
|
9
32
|
First public release.
|
|
@@ -33,4 +56,5 @@ First public release.
|
|
|
33
56
|
- `pdfjs-dist` is pinned to `6.3.289` to match the host process.
|
|
34
57
|
- MIT licensed.
|
|
35
58
|
|
|
36
|
-
[0.1.
|
|
59
|
+
[0.1.1]: https://github.com/Strepsiades2/dsh-book-reader/releases/tag/v0.1.1
|
|
60
|
+
[0.1.0]: https://github.com/Strepsiades2/dsh-book-reader/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -1,495 +1,101 @@
|
|
|
1
1
|
# dsh-book-reader
|
|
2
2
|
|
|
3
|
-
Read books and papers in DSH:
|
|
3
|
+
Read books and papers in DSH: a reading toolbar on the official PDF preview, plus three PDF tools for the model.
|
|
4
4
|
|
|
5
5
|
**English** | [中文](#中文说明)
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Features
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
**Reading toolbar** — appears only on `.pdf` previews in the right sidebar; the stock previewer (zoom, selection, reload) is untouched.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
- Previous/next page, a page readout that follows scrolling, and a jump box that accepts printed page numbers (or `p152` for PDF page 152).
|
|
12
|
+
- Full-text search panel (`Cmd/Ctrl+Shift+F`): click a result to jump there, or insert its page reference into the chat.
|
|
13
|
+
- **AI position hint** (on by default): before a turn starts, the model may receive one short line with the page you are reading — at most once per turn, and only when the page changed since the last hint. Turn it off if you do not want the model to know your page automatically.
|
|
14
|
+
- Every control has a hover/focus explanation in English or Chinese, following the DSH UI language.
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
┌─ official PDF preview (PDF.js) ──────────────────────────┐
|
|
15
|
-
│ reload zoom … │ ‹ › [132] / 410 ⌕ ↗ AI● │ ← added by this plugin
|
|
16
|
-
├──────────────────────────────────────────────────────────┤
|
|
17
|
-
│ page content … │
|
|
18
|
-
└──────────────────────────────────────────────────────────┘
|
|
19
|
-
```
|
|
16
|
+
**Model tools** — the model can look up the page map, search the text, or read chosen pages (as text or as page images) when you ask about a PDF.
|
|
20
17
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
### Features
|
|
24
|
-
|
|
25
|
-
| Control | Behavior |
|
|
18
|
+
| Tool | What it does |
|
|
26
19
|
| --- | --- |
|
|
27
|
-
|
|
|
28
|
-
| `
|
|
29
|
-
|
|
|
30
|
-
| `↗` | Inserts `[PDF p.132]` at the composer caret as one undoable edit; it does not overwrite the draft. |
|
|
31
|
-
| `AI` + dot | Reading-position reporting toggle (on by default). Dot: green = Host acknowledged, amber = sending, red = Host route failed, faded = off. Shared by all mounted PDF previews and not persisted across reloads. |
|
|
32
|
-
| `Cmd/Ctrl + Shift + F` | Opens the search panel for the preview the pointer last claimed, and focuses/selects the query. `Cmd/Ctrl + F` is deliberately not intercepted. |
|
|
33
|
-
|
|
34
|
-
`Esc` closes the panel; `Enter` in the page box jumps; `Enter` in the search box jumps to the first result. Tooltips explain every control on hover or keyboard focus, in the active DSH UI language (English or Chinese).
|
|
35
|
-
|
|
36
|
-
After the readout resolves, the browser reports page changes to the Host with a 400 ms trailing debounce. When auto-share is enabled, the Host may inject at most one short, source-attributed position line per step (and, with the default interval, at most one every 3 minutes):
|
|
37
|
-
|
|
38
|
-
```text
|
|
39
|
-
[dsh-pdf-reader] 正在阅读《book》printed 132 (PDF 152) · Chapter 5 › 5B
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
That line is a **position marker only**; it does not tell the model to read anything. The model decides whether, what, and how to read, and can ask again with `pdf_current_page`. Injection is throttled by page change, a minimum interval (3 minutes by default), and step distance.
|
|
43
|
-
|
|
44
|
-
### Install
|
|
45
|
-
|
|
46
|
-
#### a) Published package
|
|
47
|
-
|
|
48
|
-
Install from the DSH **Plugins** page: **Add plugin** → `dsh-book-reader` (or pin the version with `dsh-book-reader@0.1.0`). The CLI equivalent is:
|
|
49
|
-
|
|
50
|
-
```sh
|
|
51
|
-
dsh plugin --profile <name> add dsh-book-reader
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
Restart DSH afterwards; the host half is loaded at startup. With the published package, consumers need **no manual package symlink and no npm command** — the optional skill registration in (c) is the only manual step.
|
|
55
|
-
|
|
56
|
-
#### b) Local development checkout
|
|
57
|
-
|
|
58
|
-
```sh
|
|
59
|
-
dsh plugin add "/abs/path/to/dsh-book-reader"
|
|
60
|
-
# add --profile <name> after "plugin" when targeting a non-default profile
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
Restart DSH after installing. The profile stores a `link:` to the checkout, so keep the checkout **and its `node_modules/`** in place: at runtime the Host imports `pdfjs-dist` and `@napi-rs/canvas` from that directory. The `node_modules/` in this checkout is macOS arm64; on another architecture run `npm install` in the checkout first. `engines.node` requires Node >= 20.
|
|
64
|
-
|
|
65
|
-
Uninstall either way with `dsh plugin --profile <name> remove dsh-book-reader`, then restart DSH. Prefer enabling the plugin through the GUI toggle over hand-editing `dsh.profile.bundles`; if a profile fails to start after installing, remove the plugin row from the command line and restart.
|
|
66
|
-
|
|
67
|
-
#### c) Skill registration
|
|
68
|
-
|
|
69
|
-
```sh
|
|
70
|
-
mkdir -p "<workspace>/.dsh/skills"
|
|
71
|
-
ln -sfn "/abs/path/to/dsh-book-reader" "<workspace>/.dsh/skills/pdf-reader"
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
The package ships `SKILL.md`, but DSH packaging does **not** auto-register skills. The link name must be `pdf-reader` (the `name` in `SKILL.md`); for an npm-installed package, point it at the installed package directory. The tools work without the skill, but the skill gives the model the text-first loop, the token costs, and the pitfalls. Register it once per workspace.
|
|
75
|
-
|
|
76
|
-
After publishing, installing the plugin by package name needs no manual symlink and no npm command for consumers; only this optional skill registration remains manual.
|
|
77
|
-
|
|
78
|
-
### Tools
|
|
79
|
-
|
|
80
|
-
Four tools, three cost tiers. The default is text.
|
|
81
|
-
|
|
82
|
-
| Tool | Cost | Use for |
|
|
83
|
-
| --- | --- | --- |
|
|
84
|
-
| `pdf_info` | ~0 (text) | Page count, printed↔physical offset, outline/TOC; also title, author, file size, page size, and `textCoverage.isLikelyScanned`. |
|
|
85
|
-
| `pdf_search` | ~0 (text) | Locating a phrase, theorem number, or figure caption. |
|
|
86
|
-
| `pdf_read_pages` + `include_image: false` | ~0 (text, per page) | Prose, theorem statements, exercise text. |
|
|
87
|
-
| `pdf_read_pages` (images on) | **~350–1024 tokens/page** | Formulas, diagrams, tables, layout. |
|
|
88
|
-
| `pdf_current_page` | ~0 | Which page and section the user is looking at. |
|
|
89
|
-
|
|
90
|
-
`pdf_read_pages` is the only image-returning tool. It is registered only while `ctx.attachments` is mounted (so PNGs can become durable image attachments), and every call fails if the routed model does not declare image input — even with `include_image: false`.
|
|
91
|
-
|
|
92
|
-
Page numbers are **1-based**, and relative paths resolve against the session working directory. `pages` accepts `"132-134"`, `"132,135"`, `132`, `[132,133,134]`, or an array of range strings. Other useful arguments:
|
|
93
|
-
|
|
94
|
-
- `page_numbers`: `"printed"` or `"pdf"`; defaults to printed when the PDF exposes numeric labels/offset.
|
|
95
|
-
- `scale`: `1`–`3`, default `2.5` (about 180 DPI).
|
|
96
|
-
- `crop`: fractions with a top-left origin — `"x0,y0,x1,y1"`, `[x0,y0,x1,y1]`, or `{x,y,w,h}`.
|
|
97
|
-
- `image_quality`: `"cheap"` (1.5×, about 350 tokens/page) or `"standard"` (`defaultScale`).
|
|
98
|
-
- `include_image`: default `true`; `false` returns page text only and adds no image tokens.
|
|
99
|
-
- `include_text`: default `true`, capped by `maxTextCharsPerPage`.
|
|
100
|
-
|
|
101
|
-
A call is capped at `maxPagesPerCall` (default **4**, hard maximum **8**) **before** rendering. Exceeding it is an error; pages are never silently dropped. `pdf_read_pages` returns per-page `tokens` and a total `imageTokens`, plus `text`, `textTruncated`, and `textUnavailable` per page. `pdf_info` caps its outline at 200 entries, and `pdf_search` defaults to 20 hits (clamped to `maxSearchResults`).
|
|
102
|
-
|
|
103
|
-
#### Text-first loop
|
|
104
|
-
|
|
105
|
-
1. `pdf_info` once per document. Note `printedOffset`, `pageNumberMode`, and the outline. Never hardcode the offset.
|
|
106
|
-
2. `pdf_search` for a distinctive phrase to get physical page numbers.
|
|
107
|
-
3. `pdf_read_pages` with **`include_image: false`** to read those pages as text.
|
|
108
|
-
4. Only when the text is ambiguous or the question is about typography (a formula, diagram, table, or theorem box) → call again with images, ideally with `crop` and `image_quality: "cheap"`.
|
|
109
|
-
|
|
110
|
-
For exact formulas or dense subscripts, `scale: 3` plus `crop` is the exception, not the default.
|
|
111
|
-
|
|
112
|
-
### Configuration
|
|
113
|
-
|
|
114
|
-
All keys are optional. Set them in the plugin row's `config` in the profile, using the same names as the comments in `cordis.patch.yml`.
|
|
115
|
-
|
|
116
|
-
| Key | Default | Meaning |
|
|
117
|
-
| --- | --- | --- |
|
|
118
|
-
| `enabled` | `true` | Master switch; `false` mounts no tools, routes, or listener. |
|
|
119
|
-
| `autoShare` | `true` | Inject the current reading position into `agent/pre-step`; `false` keeps the tools and routes but stops injection. |
|
|
120
|
-
| `autoShareSection` | `true` | Resolve the outline section for the injected line. |
|
|
121
|
-
| `autoShareMinIntervalMs` | `180000` | Minimum milliseconds between two injections (3 minutes). |
|
|
122
|
-
| `autoSharePageDelta` | `1` | Minimum absolute page change before another injection. |
|
|
123
|
-
| `autoShareMinSteps` | `1` | Minimum step distance between injections in the same turn. |
|
|
124
|
-
| `autoShareMaxAgeMs` | `0` | Ignore reported positions older than this; `0` = no age limit. |
|
|
125
|
-
| `allowOutsideWorkspace` | `false` | Let tools read PDFs outside the session working directory. |
|
|
126
|
-
| `routeAllowedRoots` | `[]` | Extra directory allowlist for the HTTP routes; `[]` = no extra restriction. |
|
|
127
|
-
| `maxPagesPerCall` | `4` | Hard page cap per `pdf_read_pages` call (`1`–`8`), checked before rendering. |
|
|
128
|
-
| `defaultScale` | `2.5` | Default render scale (`1`–`3`, about 180 DPI); no token gain above about `2.7`. |
|
|
129
|
-
| `maxTextCharsPerPage` | `1200` | Extracted text kept per page; `0` disables text. |
|
|
130
|
-
| `maxSearchResults` | `100` | Cap for the tool's and route's search limits (`1`–`500`). |
|
|
131
|
-
| `maxOutlineEntries` | `500` | Route outline cap (`1`–`5000`); `pdf_info` additionally caps at 200. |
|
|
132
|
-
| `maxLabels` | `0` | Page labels included by `pdf_info`; `0` omits them (cheapest). |
|
|
133
|
-
| `outlineDepth` | `1` | `0` = chapters, `1` = sections (default), `2` = subsections. |
|
|
134
|
-
| `debugCost` | `false` | Append one JSON cost line per image-returning `pdf_read_pages` call to `.debug-cost.log` beside the package. |
|
|
135
|
-
| `debug` | `false` | Include internal error detail in HTTP 500 bodies (development only). |
|
|
136
|
-
| `maxFileBytes` | `268435456` | Largest PDF touched by the tools or routes; 256 MiB. |
|
|
137
|
-
|
|
138
|
-
The code also accepts two advanced keys that are not shown in the sample comments in `cordis.patch.yml`: `maxRouteBodyBytes` (default `65536`, the `POST /current` body cap) and `infoCacheSize` (default `8`, parsed `getInfo` results cached by path + mtime + size).
|
|
139
|
-
|
|
140
|
-
### Page numbering
|
|
141
|
-
|
|
142
|
-
Tools and the UI use 1-based pages. `pdf_info` returns `printedOffset`, defined as physical PDF page minus printed page:
|
|
143
|
-
|
|
144
|
-
```text
|
|
145
|
-
physical = printed + printedOffset
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
The test book (`Linear Algebra Done Right`) has `printedOffset = 20`, so printed 132 = PDF 152. The offset is detected from numeric page labels (the mode of the per-page deltas), not hardcoded.
|
|
149
|
-
|
|
150
|
-
`pdf_info.pageNumberMode` is `"printed"` when numeric labels/offset exist and `"pdf"` otherwise. Tools default to printed numbering in that case; pass `page_numbers: "pdf"` for roman-numeral front matter, PDFs without numeric labels, or whenever you mean physical pages.
|
|
151
|
-
|
|
152
|
-
The browser readout uses printed numbering when an offset is known and falls back to physical numbering in front matter. In the jump box, a bare `132` means printed 132 when an offset is known; `p152`, `p.152`, `PDF 152`, and `#152` always mean physical PDF 152. Out-of-range input is rejected rather than clamped, so a mistyped printed page cannot silently land on the last page.
|
|
153
|
-
|
|
154
|
-
### Token economics
|
|
155
|
-
|
|
156
|
-
DeepSeek vision billing projects an image onto a 14 px patch grid, downsamples 3:1, and caps it at **1024 tokens**:
|
|
157
|
-
|
|
158
|
-
```text
|
|
159
|
-
tokens = gridHeight × (gridWidth + 1) + 2
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
Images smaller than 544×544 are scaled up first. Measured on a 6×9 in book page:
|
|
163
|
-
|
|
164
|
-
| Render | Pixels | Tokens/page | Notes |
|
|
165
|
-
| --- | --- | --- | --- |
|
|
166
|
-
| 1.5× (`image_quality: "cheap"`) | 563×954 | **347** | Enough for prose. |
|
|
167
|
-
| **2.5× (default)** | 938×1590 | **914** | Highest resolution before downsampling. |
|
|
168
|
-
| 3× | 1125×1908 | **1002** | Capped; no gain above about 2.7×. |
|
|
169
|
-
| 4× | 1500×2544 | **1002** | Still capped — pure waste. |
|
|
170
|
-
|
|
171
|
-
- `maxPagesPerCall` (default 4) is enforced before rendering.
|
|
172
|
-
- `pdf_read_pages` reports `tokens` per page and `imageTokens` in total; check them.
|
|
173
|
-
- Images stay in the conversation and are re-billed on later turns. **Read once, then answer in text.**
|
|
174
|
-
- Prefer `crop` and `image_quality: "cheap"` for a quick look; use `scale: 3` with `crop` only for dense subscripts.
|
|
175
|
-
- If the context grows too large, DSH's `image-offload` may replace old images with a file name and path; `read_image` can reload them.
|
|
176
|
-
- Set `debugCost: true` to append a JSON line per image call to `.debug-cost.log` and measure your own distribution.
|
|
177
|
-
|
|
178
|
-
### Security
|
|
179
|
-
|
|
180
|
-
- **Tools are workspace-scoped by default.** The resolved path must stay inside the session working directory, or the call fails with `PDF_OUTSIDE_WORKSPACE`. Set `allowOutsideWorkspace: true` only when reading outside is intentional. Relative tool paths use the session's original `header.cwd`; a later `working_directory` change is not consulted, so prefer absolute paths.
|
|
181
|
-
- **HTTP routes are loopback-only but unauthenticated.** They accept only loopback `Host` values (`127.0.0.1`, `localhost`, `::1`), absolute paths, `.pdf` suffixes, regular files, and files within `maxFileBytes`; responses carry no `Access-Control-Allow-Origin` header. Any local process can still use them to read local PDFs — the same tradeoff as `dsh-pdf-search` and other local preview plugins. Set `routeAllowedRoots: ["/path/to/books"]` to restrict the routes (this does **not** restrict model tools).
|
|
182
|
-
- **Installing a plugin runs local code with the Host process's privileges.** Install only packages you trust, and review a local checkout before adding it.
|
|
183
|
-
- **Renderer hardening:** 60 s document-load timeout, 60 s per-page render timeout, a 50,000,000-pixel canvas cap, at most 2 concurrent renders, at most 4 cached open documents, and the 256 MiB default file cap.
|
|
184
|
-
- **`debug: false` by default**, so HTTP 500 bodies do not expose internal detail or paths.
|
|
185
|
-
|
|
186
|
-
### Known limitations
|
|
187
|
-
|
|
188
|
-
- **Official PDF preview only.** The controls key off a `.pdf` `absolutePath`; Word/PowerPoint previews (even when internally converted to PDF), images, Markdown, and code previews get no toolbar. The tools can still read `.pdf` files directly.
|
|
189
|
-
- **Private DOM markers.** The browser half depends on `data-pdf-page`, `data-textpreview-body`, and `data-document-zoom-scrollport`. A DSH upgrade that renames them can make the controls inert; the preview itself keeps working.
|
|
190
|
-
- **Scanned PDFs have no text layer.** `pdf_search` finds nothing and text reads report `textUnavailable`; only page images help. Check `pdf_info.textCoverage.isLikelyScanned` before searching. There is **no OCR**.
|
|
191
|
-
- **Search is literal.** The UI uses case-insensitive substring matching: no regex, no whole-word mode, no hyphenation unfolding, and no match-to-match navigation inside a page.
|
|
192
|
-
- **The page offset is detected.** PDFs without numeric page labels fall back to physical numbering, and roman front-matter labels cannot be passed to `pdf_read_pages` as printed pages; use `page_numbers: "pdf"` (the browser jump box also accepts `pN`).
|
|
193
|
-
- **`pdfjs-dist` must match the Host process.** The package pins `6.3.289`. Other PDF plugins in the same process (for example `dsh-pdf-search`) must use the same version; mixing versions can fail with `API version does not match Worker version`. Upgrade them together.
|
|
194
|
-
- **Visual overlap with other preview add-ons.** This search panel is fixed at the top right, as is `dsh-pdf-search`'s find bar; opening both makes them overlap. Their shortcuts are deliberately different (`Cmd/Ctrl+Shift+F` here, `Cmd/Ctrl+F` there).
|
|
195
|
-
- **Auto-share state is process-local.** The throttle is not written to the session log, and staying on the same page does not produce a second message; ask `pdf_current_page` when the position is needed later. The reporting toggle is also not persisted across reloads.
|
|
196
|
-
- **Page reads require an image-capable model and an attachment store.** Without `ctx.attachments`, `pdf_read_pages` is not registered. Its route check runs before every call — including `include_image: false` — so a text-only model can still use `pdf_info` and `pdf_search` but not page reads.
|
|
197
|
-
- **Read-only.** There is no annotation, notes, editing, or OCR. The plugin reads and renders only.
|
|
198
|
-
|
|
199
|
-
### Development
|
|
200
|
-
|
|
201
|
-
```
|
|
202
|
-
dsh-book-reader/
|
|
203
|
-
├─ lib/index.js host half: four tools, /dsh-pdf-reader routes, auto-share
|
|
204
|
-
├─ lib/core/pdf.js document LRU, page labels/offset, outline, page text, search
|
|
205
|
-
├─ lib/core/render.js pdfjs + @napi-rs/canvas → RGB PNG, with timeouts and a pixel cap
|
|
206
|
-
├─ lib/core/tokens.js DeepSeek vision token accounting
|
|
207
|
-
├─ lib/client.js browser half: header controls, jump, search, share, reporting
|
|
208
|
-
├─ lib/host-README.md host internals: arguments, return shapes, routes
|
|
209
|
-
├─ lib/client-README.md browser internals: UI, shortcuts, DOM markers
|
|
210
|
-
├─ SKILL.md model-facing usage guide
|
|
211
|
-
├─ cordis.patch.yml profile mount + commented defaults
|
|
212
|
-
└─ test/*.mjs headless core / host / client tests
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
`package.json` points `main` at `lib/index.js`, exposes `./client` as `lib/client.js`, sets `dsh.bundle.patch` to `./cordis.patch.yml`, and marks `dsh.client.platform` as `web`. `cordis.patch.yml` inserts plugin id `pdf-reader` under package name `dsh-book-reader`.
|
|
216
|
-
|
|
217
|
-
There is **no bundler step**: the host half is hand-written ESM, and `lib/client.js` is the browser bundle in the `window.__ModuleLoader__.load` shape, with React as the only external. Runtime dependencies are `pdfjs-dist` pinned to `6.3.289` and `@napi-rs/canvas` `^1.0.11`; `engines.node` is `>=20`. Restart DSH to reload host code, and reload the page for browser-side changes.
|
|
218
|
-
|
|
219
|
-
#### Host routes
|
|
220
|
-
|
|
221
|
-
All routes share the historical prefix `/dsh-pdf-reader`, even though the package is now `dsh-book-reader`; the `data-dsh-pdf-reader` DOM markers keep the old name too. Use these exact paths:
|
|
222
|
-
|
|
223
|
-
- `GET /dsh-pdf-reader/info?path=<absolute .pdf>`
|
|
224
|
-
- `GET /dsh-pdf-reader/search?path=<absolute .pdf>&q=<text>&ignoreCase=1&limit=100`
|
|
225
|
-
- `POST /dsh-pdf-reader/current` with `{ "path": "...", "pdfPage": 152, "printedPage": 132, "sessionId": "..." }`
|
|
226
|
-
- `GET /dsh-pdf-reader/current?sessionId=<id>` (or `?path=<absolute .pdf>` as a fallback)
|
|
20
|
+
| `pdf_info` | Page count, printed↔physical page offset, outline/TOC. Text only. |
|
|
21
|
+
| `pdf_search` | Literal full-text search; returns page numbers and snippets. |
|
|
22
|
+
| `pdf_read_pages` | Text of chosen pages, optionally as images (`include_image: false` for text only; images cost ~350–1024 tokens/page). |
|
|
227
23
|
|
|
228
|
-
|
|
24
|
+
Typical loop: `pdf_info` → `pdf_search` → `pdf_read_pages` (text) → images only for formulas, diagrams, or tables.
|
|
229
25
|
|
|
230
|
-
|
|
26
|
+
## Install
|
|
231
27
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
28
|
+
- **From npm**: DSH **Plugins → Add plugin** → `dsh-book-reader` (or pin `dsh-book-reader@0.1.1`), or `dsh plugin add dsh-book-reader`.
|
|
29
|
+
- A fresh install applies live (this profile has HMR); **upgrading a plugin that is already loaded needs a DSH restart** to load the new code.
|
|
30
|
+
- **Optional skill** (gives the model the built-in PDF reading guide): symlink the package into the workspace:
|
|
31
|
+
```sh
|
|
32
|
+
ln -sfn "<installed package dir>" "<workspace>/.dsh/skills/pdf-reader"
|
|
33
|
+
```
|
|
34
|
+
- **Local checkout**: `dsh plugin add "/abs/path/to/dsh-book-reader"`; keep the checkout and its `node_modules/` in place.
|
|
237
35
|
|
|
238
|
-
|
|
36
|
+
## Configuration
|
|
239
37
|
|
|
240
|
-
|
|
38
|
+
Every option is optional and the defaults are safe. Common ones: `maxPagesPerCall` (default 4), `defaultScale` (2.5), `autoShare` (automatic position hint), `allowOutsideWorkspace` (tools stay inside the session workspace by default), `routeAllowedRoots` (restrict the local HTTP routes). The full list with defaults is commented in [`cordis.patch.yml`](cordis.patch.yml).
|
|
241
39
|
|
|
242
|
-
|
|
40
|
+
## Notes
|
|
243
41
|
|
|
244
|
-
|
|
42
|
+
- HTTP routes are loopback-only and unauthenticated; the tools are workspace-scoped by default. Other local preview plugins make the same tradeoff.
|
|
43
|
+
- Works only with the official PDF preview: it relies on that preview's DOM markers, which a DSH upgrade may change.
|
|
44
|
+
- Scanned PDFs have no text layer — there is no OCR, so only page images help.
|
|
45
|
+
- `pdfjs-dist` is pinned to `6.3.289` to match the host process.
|
|
245
46
|
|
|
246
|
-
|
|
47
|
+
## More
|
|
247
48
|
|
|
248
|
-
|
|
49
|
+
- [`SKILL.md`](SKILL.md) — model-facing guide: costs, page numbering, pitfalls.
|
|
50
|
+
- [`CHANGELOG.md`](CHANGELOG.md) · [`LICENSE`](LICENSE) (MIT)
|
|
249
51
|
|
|
250
52
|
---
|
|
251
53
|
|
|
252
54
|
## 中文说明
|
|
253
55
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
`dsh-book-reader` 在 DSH 右侧栏的官方 PDF 预览上加一组读书控件,同时注册四个模型可调用的工具,让模型**先读文本**。它不替换官方预览器:缩放、适应宽度、选中文字、刷新、自动刷新都保持原样;删掉插件行,预览恢复原状。
|
|
257
|
-
|
|
258
|
-
控件只挂在 `absolutePath` 以 `.pdf` 结尾的预览上。PDF 在宿主进程内用 `pdfjs-dist` + `@napi-rs/canvas` 解析和渲染;浏览器端把控件加进预览的 document-header 插槽。
|
|
259
|
-
|
|
260
|
-
```
|
|
261
|
-
┌─ 官方 PDF 预览(PDF.js)──────────────────────────────────┐
|
|
262
|
-
│ 刷新 缩放 … │ ‹ › [132] / 410 ⌕ ↗ AI● │ ← 本插件注入
|
|
263
|
-
├──────────────────────────────────────────────────────────┤
|
|
264
|
-
│ 页面内容 … │
|
|
265
|
-
└──────────────────────────────────────────────────────────┘
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
设计原则:**默认只花文本的 token;图片是需要时才付的钱,不是第一步。**
|
|
56
|
+
在 DSH 里读 PDF 书和论文:官方 PDF 预览上加一条读书工具栏,外加三个 PDF 工具给模型。
|
|
269
57
|
|
|
270
58
|
### 功能
|
|
271
59
|
|
|
272
|
-
|
|
273
|
-
| --- | --- |
|
|
274
|
-
| `‹` / `›` | 上一页 / 下一页(按**物理** PDF 页连续翻,与印刷页码无关)。 |
|
|
275
|
-
| `[132] / 410` | 当前页读数,随滚动更新;也是跳页框(输入页码后按 `Enter`)。已知偏移时显示印刷页码,否则显示物理页码。 |
|
|
276
|
-
| `⌕` | 打开全文搜索面板。结果显示为 `p.132 — 摘要`,点击跳转;每行还有 **`→ AI`**,把 `PDF p.132: <摘要>` 插到对话输入框光标处。 |
|
|
277
|
-
| `↗` | 把 `[PDF p.132]` 插到输入框光标处,是一次可撤销的编辑,不覆盖草稿。 |
|
|
278
|
-
| `AI` + 状态点 | 阅读位置上报开关(默认开启)。状态点:绿色 = 宿主已确认,琥珀 = 发送中,红色 = 宿主路由失败,变暗 = 关闭。所有已挂载的 PDF 预览共用这个开关,刷新后不保留。 |
|
|
279
|
-
| `Cmd/Ctrl + Shift + F` | 对指针最后停留/聚焦的预览打开搜索面板,并聚焦、全选查询词。`Cmd/Ctrl + F` 故意不拦截。 |
|
|
60
|
+
**阅读工具栏** —— 只在右侧栏的 `.pdf` 预览上出现;不替换官方预览器(缩放、选中、刷新都保持原样)。
|
|
280
61
|
|
|
281
|
-
|
|
62
|
+
- 上一页/下一页、随滚动更新的页码读数、跳页框(输入印刷页码,或 `p152` 表示 PDF 第 152 页)。
|
|
63
|
+
- 全文搜索面板(`Cmd/Ctrl+Shift+F`):点结果跳页,或把该页引用插入对话框。
|
|
64
|
+
- **AI 位置提示**(默认开):每轮对话开始时,模型可能收到一行“你正在看第 N 页”的短提示 —— 每轮最多一次,且页码变了才发。关掉后,模型不会自动知道你在哪页。
|
|
65
|
+
- 每个控件都有 hover / 键盘聚焦提示,跟随 DSH 界面语言(中文或英文)。
|
|
282
66
|
|
|
283
|
-
|
|
67
|
+
**模型工具** —— 默认只花文本 token,图片是按需付的钱。
|
|
284
68
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
69
|
+
| 工具 | 作用 |
|
|
70
|
+
| --- | --- |
|
|
71
|
+
| `pdf_info` | 页数、印刷页↔物理页偏移、目录/TOC;纯文本。 |
|
|
72
|
+
| `pdf_search` | 字面全文搜索;返回页码和摘要。 |
|
|
73
|
+
| `pdf_read_pages` | 选中页的文本,也可返回页面图片(`include_image: false` 只取文本;图片约 350–1024 token/页)。 |
|
|
288
74
|
|
|
289
|
-
|
|
75
|
+
典型流程:`pdf_info` → `pdf_search` → `pdf_read_pages`(文本)→ 只有公式、图表、表格才看图。
|
|
290
76
|
|
|
291
77
|
### 安装
|
|
292
78
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
装完后完全重启 DSH;宿主端随进程启动加载。用发布包安装时,消费者**不需要手动给包建软链,也不需要 npm 命令**;如果还需要 skill,唯一的手动步骤是下面的可选注册。
|
|
302
|
-
|
|
303
|
-
#### b) 本地开发目录
|
|
304
|
-
|
|
305
|
-
```sh
|
|
306
|
-
dsh plugin add "/abs/path/to/dsh-book-reader"
|
|
307
|
-
# 需要指定非默认 profile 时,在 plugin 后加 --profile <name>
|
|
308
|
-
```
|
|
309
|
-
|
|
310
|
-
然后重启 DSH。profile 记录的是指向源码目录的 `link:`,所以源码目录**和它的 `node_modules/`** 都必须留在原处:宿主运行时要从那里 `import 'pdfjs-dist'` 和 `@napi-rs/canvas`。当前目录里的 `node_modules/` 是 macOS arm64 的;其他架构请先在目录里执行 `npm install`。`engines.node` 要求 Node >= 20。
|
|
311
|
-
|
|
312
|
-
卸载:`dsh plugin --profile <name> remove dsh-book-reader`,再重启 DSH。首次启用建议走 GUI 开关,而不是手改 `dsh.profile.bundles`;如果安装后 profile 启动失败,可先用命令行移除插件行再重启。
|
|
313
|
-
|
|
314
|
-
#### c) 注册 skill
|
|
315
|
-
|
|
316
|
-
```sh
|
|
317
|
-
mkdir -p "<workspace>/.dsh/skills"
|
|
318
|
-
ln -sfn "/abs/path/to/dsh-book-reader" "<workspace>/.dsh/skills/pdf-reader"
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
包里带了 `SKILL.md`,但 DSH 的打包**不会自动注册 skill**。软链名必须是 `pdf-reader`(即 `SKILL.md` 里的 `name`);如果是 npm 安装的包,就指向已安装的包目录。不注册 skill 也能用这些工具,但注册后模型才知道文本优先的流程、成本表和常见陷阱。每个工作区注册一次即可。
|
|
322
|
-
|
|
323
|
-
发布之后,消费者用包名安装插件不需要手动软链,也不需要 npm 命令;只有这个可选的 skill 注册仍需手动。
|
|
324
|
-
|
|
325
|
-
### 工具
|
|
326
|
-
|
|
327
|
-
四个工具,三档成本。默认走文本。
|
|
328
|
-
|
|
329
|
-
| 工具 | 成本 | 用途 |
|
|
330
|
-
| --- | --- | --- |
|
|
331
|
-
| `pdf_info` | ~0(纯文本) | 页数、印刷页↔物理页偏移、目录/TOC;也返回标题、作者、文件大小、页面尺寸和 `textCoverage.isLikelyScanned`。 |
|
|
332
|
-
| `pdf_search` | ~0(纯文本) | 定位短语、定理编号、图表标题。 |
|
|
333
|
-
| `pdf_read_pages` + `include_image: false` | ~0(纯文本,按页) | 正文、定理陈述、习题文字。 |
|
|
334
|
-
| `pdf_read_pages`(带图) | **~350–1024 token/页** | 公式、图表、表格、版式。 |
|
|
335
|
-
| `pdf_current_page` | ~0 | 用户当前在看哪一页、哪一节。 |
|
|
336
|
-
|
|
337
|
-
`pdf_read_pages` 是唯一返回图片的工具。它只在宿主挂载了 `ctx.attachments` 时注册(PNG 才能成为持久的图片附件);如果当前路由的模型不接受图片输入,任何调用都会失败,`include_image: false` 也不例外。
|
|
338
|
-
|
|
339
|
-
页码是 **1-based**,相对路径按会话工作目录解析。`pages` 支持 `"132-134"`、`"132,135"`、`132`、`[132,133,134]`,也支持范围字符串组成的数组。其他常用参数:
|
|
340
|
-
|
|
341
|
-
- `page_numbers`:`"printed"` 或 `"pdf"`;PDF 有数字页标签/偏移时默认 `"printed"`。
|
|
342
|
-
- `scale`:`1`–`3`,默认 `2.5`(约 180 DPI)。
|
|
343
|
-
- `crop`:以左上角为原点的分数——`"x0,y0,x1,y1"`、`[x0,y0,x1,y1]` 或 `{x,y,w,h}`。
|
|
344
|
-
- `image_quality`:`"cheap"`(1.5×,约 350 token/页)或 `"standard"`(用 `defaultScale`)。
|
|
345
|
-
- `include_image`:默认 `true`;`false` 只返回页文本,不产生图片 token。
|
|
346
|
-
- `include_text`:默认 `true`,受 `maxTextCharsPerPage` 限制。
|
|
347
|
-
|
|
348
|
-
单次调用在**渲染之前**就按 `maxPagesPerCall`(默认 **4**,硬上限 **8**)校验;超了直接报错,不会悄悄丢页。`pdf_read_pages` 返回每页的 `tokens` 和合计 `imageTokens`,以及每页的 `text`、`textTruncated`、`textUnavailable`。`pdf_info` 的目录最多 200 条;`pdf_search` 默认 20 条(受 `maxSearchResults` 限制)。
|
|
349
|
-
|
|
350
|
-
#### 文本优先流程
|
|
351
|
-
|
|
352
|
-
1. 每份文档先调一次 `pdf_info`,记下 `printedOffset`、`pageNumberMode` 和目录。不要把偏移量写死。
|
|
353
|
-
2. 用 `pdf_search` 搜一个有辨识度的短语,拿到物理页码。
|
|
354
|
-
3. 用 `pdf_read_pages` 配 **`include_image: false`**,把这几页当文本读。
|
|
355
|
-
4. 只有文本有歧义,或问题本身与排版有关(公式、图、表、定理框)时,才带图再调一次,优先用 `crop` 和 `image_quality: "cheap"`。
|
|
356
|
-
|
|
357
|
-
精确公式或密集下标是例外:用 `scale: 3` 配 `crop`,但不要当成默认。
|
|
79
|
+
- **从 npm 安装**:DSH **Plugins → Add plugin** → 输入 `dsh-book-reader`(或锁定版本 `dsh-book-reader@0.1.1`),或命令行 `dsh plugin add dsh-book-reader`。
|
|
80
|
+
- 全新安装即时生效(本 profile 已启用 HMR);**升级已加载的插件需要完整重启 DSH** 才能加载新代码。
|
|
81
|
+
- **可选 skill**(给模型一份内置的 PDF 阅读指南):把包目录软链到工作区:
|
|
82
|
+
```sh
|
|
83
|
+
ln -sfn "<已安装的包目录>" "<workspace>/.dsh/skills/pdf-reader"
|
|
84
|
+
```
|
|
85
|
+
- **本地开发**:`dsh plugin add "/abs/path/to/dsh-book-reader"`;源码目录和 `node_modules/` 必须留在原处。
|
|
358
86
|
|
|
359
87
|
### 配置
|
|
360
88
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
| 键 | 默认 | 含义 |
|
|
364
|
-
| --- | --- | --- |
|
|
365
|
-
| `enabled` | `true` | 总开关;`false` 时不挂载任何工具、路由或监听器。 |
|
|
366
|
-
| `autoShare` | `true` | 把当前阅读位置注入 `agent/pre-step`;`false` 保留工具和路由,只停注入。 |
|
|
367
|
-
| `autoShareSection` | `true` | 为注入行解析目录中的小节。 |
|
|
368
|
-
| `autoShareMinIntervalMs` | `180000` | 两次注入的最小间隔(3 分钟)。 |
|
|
369
|
-
| `autoSharePageDelta` | `1` | 再次注入前要求的最小绝对页变化。 |
|
|
370
|
-
| `autoShareMinSteps` | `1` | 同一轮内两次注入的最小步距。 |
|
|
371
|
-
| `autoShareMaxAgeMs` | `0` | 忽略早于此时长的位置;`0` = 不限。 |
|
|
372
|
-
| `allowOutsideWorkspace` | `false` | 允许工具读取会话工作区外的 PDF。 |
|
|
373
|
-
| `routeAllowedRoots` | `[]` | HTTP 路由的额外目录白名单;`[]` = 不额外限制。 |
|
|
374
|
-
| `maxPagesPerCall` | `4` | 单次 `pdf_read_pages` 的硬页数上限(`1`–`8`),渲染前校验。 |
|
|
375
|
-
| `defaultScale` | `2.5` | 默认渲染倍率(`1`–`3`,约 180 DPI);超过约 `2.7` 没有 token 收益。 |
|
|
376
|
-
| `maxTextCharsPerPage` | `1200` | 每页保留的抽取文本字符数;`0` 关闭文本。 |
|
|
377
|
-
| `maxSearchResults` | `100` | 工具和路由搜索条数的上限(`1`–`500`)。 |
|
|
378
|
-
| `maxOutlineEntries` | `500` | 路由目录条数上限(`1`–`5000`);`pdf_info` 另有 200 条上限。 |
|
|
379
|
-
| `maxLabels` | `0` | `pdf_info` 返回的页标签数量;`0` = 不返回(最省)。 |
|
|
380
|
-
| `outlineDepth` | `1` | `0` = 章,`1` = 节(默认),`2` = 小节。 |
|
|
381
|
-
| `debugCost` | `false` | 每次带图的 `pdf_read_pages` 往包目录下的 `.debug-cost.log` 追加一行 JSON 成本记录。 |
|
|
382
|
-
| `debug` | `false` | 在 HTTP 500 响应里带内部错误细节(仅开发用)。 |
|
|
383
|
-
| `maxFileBytes` | `268435456` | 工具或路由能碰的最大 PDF;256 MiB。 |
|
|
384
|
-
|
|
385
|
-
代码还接受两个键,只是 `cordis.patch.yml` 的示例注释里没有列出:`maxRouteBodyBytes`(默认 `65536`,`POST /current` 的请求体上限)和 `infoCacheSize`(默认 `8`,按路径 + mtime + size 缓存的 `getInfo` 结果数)。
|
|
386
|
-
|
|
387
|
-
### 页码
|
|
388
|
-
|
|
389
|
-
工具和界面都用 1-based 页码。`pdf_info` 返回 `printedOffset`,定义是「物理 PDF 页 − 印刷页」:
|
|
390
|
-
|
|
391
|
-
```text
|
|
392
|
-
物理页 = 印刷页 + printedOffset
|
|
393
|
-
```
|
|
394
|
-
|
|
395
|
-
测试用的《Linear Algebra Done Right》`printedOffset = 20`,所以 printed 132 = PDF 152。偏移量是从数字页标签的逐页差值中探测出来的,不要写死。
|
|
396
|
-
|
|
397
|
-
`pdf_info.pageNumberMode` 在有数字标签/偏移时为 `"printed"`,否则为 `"pdf"`。可探测时工具默认用印刷页码;前言页、罗马数字标签、没有数字标签的 PDF,或者你就是想说物理页时,传 `page_numbers: "pdf"`。
|
|
398
|
-
|
|
399
|
-
浏览器读数在已知偏移时显示印刷页码,前言部分退回物理页码。跳页框里,裸数字 `132` 在已知偏移时按 printed 132 处理;`p152`、`p.152`、`PDF 152`、`#152` 强制物理页 152。超出 `1..pageCount` 的输入会被拒绝,不会截断到最后一页,所以打错的印刷页不会悄悄跳到全书末尾。
|
|
400
|
-
|
|
401
|
-
### Token 成本
|
|
402
|
-
|
|
403
|
-
DeepSeek 的视觉计费:图片先对齐到 14 px patch 网格,按 3:1 降采样,单图上限 **1024 token**:
|
|
404
|
-
|
|
405
|
-
```text
|
|
406
|
-
tokens = 网格高 × (网格宽 + 1) + 2
|
|
407
|
-
```
|
|
408
|
-
|
|
409
|
-
小于 544×544 的图会先放大。6×9 英寸书页实测:
|
|
410
|
-
|
|
411
|
-
| 渲染 | 像素 | token/页 | 说明 |
|
|
412
|
-
| --- | --- | --- | --- |
|
|
413
|
-
| 1.5×(`image_quality: "cheap"`) | 563×954 | **347** | 只读正文够用。 |
|
|
414
|
-
| **2.5×(默认)** | 938×1590 | **914** | 未被降采样的最高分辨率。 |
|
|
415
|
-
| 3× | 1125×1908 | **1002** | 已触顶;超过约 2.7× 没有收益。 |
|
|
416
|
-
| 4× | 1500×2544 | **1002** | 仍然触顶,纯属浪费。 |
|
|
417
|
-
|
|
418
|
-
- `maxPagesPerCall`(默认 4)在渲染前校验。
|
|
419
|
-
- `pdf_read_pages` 返回每页 `tokens` 和合计 `imageTokens`,记得看。
|
|
420
|
-
- 图片会留在对话里,后续轮次仍会计费。**读一次,然后用文本回答。**
|
|
421
|
-
- 快速看一眼用 `crop` + `image_quality: "cheap"`;只有密集下标才需要 `scale: 3` 配 `crop`。
|
|
422
|
-
- 上下文太大时,DSH 的 `image-offload` 可能把旧图替换成文件名+路径;仍可用 `read_image` 重读。
|
|
423
|
-
- 设 `debugCost: true` 后,每次带图调用会往 `.debug-cost.log` 追加一行 JSON,便于统计自己的真实成本分布。
|
|
424
|
-
|
|
425
|
-
### 安全
|
|
426
|
-
|
|
427
|
-
- **工具默认只在工作区内。** 解析后的路径必须落在当前会话工作目录内,否则返回 `PDF_OUTSIDE_WORKSPACE`。确实要读工作区外的 PDF 时,才显式设 `allowOutsideWorkspace: true`。相对路径按会话最初的 `header.cwd` 解析,之后改 `working_directory` 不会影响它,所以建议用绝对路径。
|
|
428
|
-
- **HTTP 路由只认回环 Host,但没有鉴权。** 只接受 `127.0.0.1`、`localhost`、`::1` 这类回环 `Host`,路径必须是绝对路径、`.pdf` 后缀、普通文件,且不超过 `maxFileBytes`;响应不带 `Access-Control-Allow-Origin`。本机任何进程仍可借它读取本机 PDF,这与 `dsh-pdf-search` 等本地预览插件是同一权衡。要收紧就设 `routeAllowedRoots: ["/path/to/books"]`;它只管 HTTP 路由,**不管**模型工具。
|
|
429
|
-
- **安装插件等于在宿主进程里运行本地代码。** 只装你信任的包;从本地目录安装前先看代码。
|
|
430
|
-
- **渲染保护:** 文档加载 60 s 超时,单页渲染 60 s 超时,画布上限 5000 万像素,同时最多 2 个渲染任务,最多缓存 4 个已打开文档,默认文件上限 256 MiB。
|
|
431
|
-
- **默认 `debug: false`**,HTTP 500 不返回内部错误细节和路径。
|
|
432
|
-
|
|
433
|
-
### 已知限制
|
|
434
|
-
|
|
435
|
-
- **只支持官方 PDF 预览。** 控件按 `.pdf` 的 `absolutePath` 匹配;Word/PowerPoint(即使预览时内部转成了 PDF)、图片、Markdown、代码预览都不会出现工具栏。工具仍可直接读 `.pdf` 文件。
|
|
436
|
-
- **依赖官方预览的私有 DOM 标记。** 浏览器端依赖 `data-pdf-page`、`data-textpreview-body`、`data-document-zoom-scrollport`;DSH 升级若改名,控件会失效,但预览本身不受影响。
|
|
437
|
-
- **扫描件没有文本层。** `pdf_search` 搜不到,文本读页返回 `textUnavailable`,只能看图;**没有 OCR**。搜之前先看 `pdf_info.textCoverage.isLikelyScanned`。
|
|
438
|
-
- **搜索是字面匹配。** 界面用大小写不敏感的子串匹配:没有正则、整词模式,不会展开连字符断行,也不能在一页内逐个跳匹配。
|
|
439
|
-
- **页码偏移是探测的。** 没有数字页标签的 PDF 退化为物理页模式;前言页的罗马数字标签不能直接传给 `pdf_read_pages`,请用 `page_numbers: "pdf"`(浏览器跳页框还可以用 `pN`)。
|
|
440
|
-
- **`pdfjs-dist` 版本必须和宿主进程一致。** 本包锁定 `6.3.289`;同一进程里的其他 PDF 插件(例如 `dsh-pdf-search`)必须用同一版本,混用会报 `API version does not match Worker version`。升级时一起升。
|
|
441
|
-
- **可能和其他预览插件视觉重叠。** 搜索面板固定在右上角,和 `dsh-pdf-search` 的查找条同时打开会重叠;快捷键故意错开(这里是 `Cmd/Ctrl+Shift+F`,那边是 `Cmd/Ctrl+F`)。
|
|
442
|
-
- **自动上报状态在进程内存里。** 节流状态不写入会话日志;用户停在同一页时不会重复注入,需要位置时模型应调用 `pdf_current_page`。上报开关也不跨刷新保留。
|
|
443
|
-
- **读页需要附件服务和支持图片的模型。** 没有 `ctx.attachments` 时 `pdf_read_pages` 不注册。每次调用都会先做路由检查,`include_image: false` 也不例外;所以纯文本模型仍可用 `pdf_info` 和 `pdf_search`,但不能读页。
|
|
444
|
-
- **只读。** 没有批注、笔记、编辑、OCR;插件只负责读取和渲染。
|
|
445
|
-
|
|
446
|
-
### 开发
|
|
447
|
-
|
|
448
|
-
```
|
|
449
|
-
dsh-book-reader/
|
|
450
|
-
├─ lib/index.js 宿主端:4 个工具、/dsh-pdf-reader 路由、自动位置注入
|
|
451
|
-
├─ lib/core/pdf.js 文档 LRU、页标签/偏移、目录、页文本、搜索
|
|
452
|
-
├─ lib/core/render.js pdfjs + @napi-rs/canvas → RGB PNG,带超时与像素上限
|
|
453
|
-
├─ lib/core/tokens.js DeepSeek 视觉 token 计算
|
|
454
|
-
├─ lib/client.js 浏览器端:header 控件、跳页、搜索、分享、上报
|
|
455
|
-
├─ lib/host-README.md 宿主内部文档:参数、返回结构、路由
|
|
456
|
-
├─ lib/client-README.md 浏览器内部文档:界面、快捷键、DOM 标记
|
|
457
|
-
├─ SKILL.md 模型使用手册
|
|
458
|
-
├─ cordis.patch.yml profile 挂载 + 注释掉的默认值
|
|
459
|
-
└─ test/*.mjs core / host / client 无头测试
|
|
460
|
-
```
|
|
461
|
-
|
|
462
|
-
`package.json` 的 `main` 是 `lib/index.js`,`exports["./client"]` 指向 `lib/client.js`,`dsh.bundle.patch` 指向 `./cordis.patch.yml`,`dsh.client.platform` 是 `web`。`cordis.patch.yml` 以插件 id `pdf-reader`、包名 `dsh-book-reader` 插入。
|
|
463
|
-
|
|
464
|
-
**没有打包步骤**:宿主端是手写 ESM,浏览器端 `lib/client.js` 直接是 `window.__ModuleLoader__.load` 形式的 bundle,React 是唯一外部依赖。运行时依赖:`pdfjs-dist` 锁定 `6.3.289`,`@napi-rs/canvas` `^1.0.11`;`engines.node >= 20`。改宿主代码后重启 DSH 才生效;改浏览器端后刷新页面。
|
|
465
|
-
|
|
466
|
-
#### 宿主路由
|
|
467
|
-
|
|
468
|
-
所有路由共用一个历史前缀 `/dsh-pdf-reader`:包名已经是 `dsh-book-reader`,但线上路径和 `data-dsh-pdf-reader` DOM 标记仍保留旧名。调用时用下面的精确路径:
|
|
469
|
-
|
|
470
|
-
- `GET /dsh-pdf-reader/info?path=<absolute .pdf>`
|
|
471
|
-
- `GET /dsh-pdf-reader/search?path=<absolute .pdf>&q=<text>&ignoreCase=1&limit=100`
|
|
472
|
-
- `POST /dsh-pdf-reader/current`,请求体 `{ "path": "...", "pdfPage": 152, "printedPage": 132, "sessionId": "..." }`
|
|
473
|
-
- `GET /dsh-pdf-reader/current?sessionId=<id>`(没有会话 id 时可用 `?path=<absolute .pdf>` 兜底)
|
|
474
|
-
|
|
475
|
-
响应都是 JSON,带 `cache-control: no-store` 和 `x-content-type-options: nosniff`,不带 `Access-Control-Allow-Origin`。浏览器端会传 `sessionId`;自动上报只读调用会话的位置。`POST /current` 不带 `sessionId` 时只按路径记位置,不会被自动上报使用。
|
|
476
|
-
|
|
477
|
-
#### 测试
|
|
478
|
-
|
|
479
|
-
```sh
|
|
480
|
-
node test/core.test.mjs
|
|
481
|
-
node test/host.test.mjs
|
|
482
|
-
node test/client.test.mjs
|
|
483
|
-
```
|
|
484
|
-
|
|
485
|
-
纯 Node,不用测试框架。core 测试会渲染真实的 LADR PDF,所以工作区里要有那两个文件;client 测试复用同级 `dsh-pdf-search/node_modules` 里的 `jsdom`/React。host 测试会替换宿主服务,jsdom 也没有布局引擎,因此整套测试不能代替真实 GUI 里对插槽布局、快捷键冲突和 PDF.js 懒渲染的冒烟验证。
|
|
486
|
-
|
|
487
|
-
#### 发布
|
|
89
|
+
全部可选,默认值即安全值。常用:`maxPagesPerCall`(默认 4)、`defaultScale`(2.5)、`autoShare`(自动位置提示)、`allowOutsideWorkspace`(工具默认只读会话工作区)、`routeAllowedRoots`(限制本地 HTTP 路由)。完整键和默认值见 [`cordis.patch.yml`](cordis.patch.yml) 注释。
|
|
488
90
|
|
|
489
|
-
|
|
91
|
+
### 说明
|
|
490
92
|
|
|
491
|
-
|
|
93
|
+
- HTTP 路由只认回环地址且无鉴权;工具默认只在工作区内。其他本地预览插件也是同样的权衡。
|
|
94
|
+
- 只支持官方 PDF 预览(依赖它的私有 DOM 标记,DSH 升级可能失效)。
|
|
95
|
+
- 扫描件没有文本层,**没有 OCR**,只能看图。
|
|
96
|
+
- `pdfjs-dist` 锁定 `6.3.289`,以和宿主进程保持一致。
|
|
492
97
|
|
|
493
|
-
###
|
|
98
|
+
### 更多
|
|
494
99
|
|
|
495
|
-
|
|
100
|
+
- [`SKILL.md`](SKILL.md) —— 模型使用手册:成本、页码、常见陷阱。
|
|
101
|
+
- [`CHANGELOG.md`](CHANGELOG.md) · [`LICENSE`](LICENSE)(MIT)
|
package/SKILL.md
CHANGED
|
@@ -5,7 +5,7 @@ description: 'Read a PDF (book, paper, manual) cheaply and accurately with the d
|
|
|
5
5
|
|
|
6
6
|
# Reading a PDF with `dsh-book-reader`
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Three tools, two cost tiers. **The default is text.** Images are a deliberate
|
|
9
9
|
expense, not the first move.
|
|
10
10
|
|
|
11
11
|
| Tool | Cost | Use for |
|
|
@@ -14,7 +14,6 @@ expense, not the first move.
|
|
|
14
14
|
| `pdf_search` | ~0 (text) | locating a phrase, theorem number, or figure caption |
|
|
15
15
|
| `pdf_read_pages` + `include_image:false` | ~0 (text, per page) | prose, theorem statements, exercise text |
|
|
16
16
|
| `pdf_read_pages` (images on) | **~350–1024 tokens/page** | formulas, diagrams, tables, layout |
|
|
17
|
-
| `pdf_current_page` | ~0 | which page the user is looking at |
|
|
18
17
|
|
|
19
18
|
## Default loop (text-first)
|
|
20
19
|
|
package/cordis.patch.yml
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# dsh-book-reader bundle patch: mount the plugin into the active profile.
|
|
2
|
-
# Host half: pdf_info / pdf_search / pdf_read_pages
|
|
2
|
+
# Host half: pdf_info / pdf_search / pdf_read_pages tools,
|
|
3
3
|
# /dsh-book-reader/* routes, and a minimal per-turn reading-position
|
|
4
4
|
# line injected through agent/pre-step.
|
|
5
5
|
# Browser half: page readout, jump, search panel, and share-to-chat on top of
|
|
@@ -11,11 +11,10 @@
|
|
|
11
11
|
name: 'dsh-book-reader'
|
|
12
12
|
# config:
|
|
13
13
|
# enabled: true
|
|
14
|
-
# autoShare: true # false stops position
|
|
14
|
+
# autoShare: true # false stops position hints
|
|
15
15
|
# autoShareSection: true # include the outline section in the line
|
|
16
|
-
# autoShareMinIntervalMs:
|
|
17
|
-
# autoSharePageDelta: 1 # require a page change
|
|
18
|
-
# autoShareMinSteps: 1 # at most once per step
|
|
16
|
+
# autoShareMinIntervalMs: 0 # optional: min ms between hints (0 = no time throttle)
|
|
17
|
+
# autoSharePageDelta: 1 # require a page change before another hint
|
|
19
18
|
# autoShareMaxAgeMs: 0 # 0 = no age limit on a reported position
|
|
20
19
|
# allowOutsideWorkspace: false # tools may only read inside the session cwd
|
|
21
20
|
# routeAllowedRoots: [] # extra dirs the HTTP routes may serve; [] = any
|
package/lib/client.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Decorates DSH's official PDF preview; it never replaces the renderer:
|
|
5
5
|
* - current-page readout, prev/next page, and a page-jump box;
|
|
6
6
|
* - a search panel (Cmd/Ctrl+Shift+F) backed by the Host text route;
|
|
7
|
-
* - "→ AI"
|
|
7
|
+
* - "→ AI" insertion of a search hit into the chat composer;
|
|
8
8
|
* - debounced current-page reporting for AI context, with an on/off toggle.
|
|
9
9
|
*
|
|
10
10
|
* The contribution mounts into the preview's own header slot
|
|
@@ -134,11 +134,6 @@ window.__ModuleLoader__.load({
|
|
|
134
134
|
return page === undefined ? 'p.?' : `p.${page}`
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
/** @returns `[PDF p.132]` for the share button. */
|
|
138
|
-
function buildShareReference(pdfPage, printedOffset) {
|
|
139
|
-
return `[PDF p.${displayPage(pdfPage, printedOffset)}]`
|
|
140
|
-
}
|
|
141
|
-
|
|
142
137
|
/** @returns `PDF p.132: <snippet>` (or `[PDF p.132]` when there is no snippet). */
|
|
143
138
|
function buildSearchReference(hit) {
|
|
144
139
|
const page = searchResultPage(hit)
|
|
@@ -679,10 +674,6 @@ window.__ModuleLoader__.load({
|
|
|
679
674
|
zh: ['搜索', '搜索这个 PDF 的文字层(Cmd/Ctrl+Shift+F)'],
|
|
680
675
|
en: ['Search', "Search this PDF's text layer (Cmd/Ctrl+Shift+F)"],
|
|
681
676
|
},
|
|
682
|
-
share: {
|
|
683
|
-
zh: ['引用当前页', '把当前位置引用插入输入框'],
|
|
684
|
-
en: ['Insert reference', 'Insert this page reference into the chat'],
|
|
685
|
-
},
|
|
686
677
|
follow: {
|
|
687
678
|
zh: ['AI 上下文', '把当前阅读页报告给模型'],
|
|
688
679
|
en: ['AI context', 'Report the page you are reading to the model'],
|
|
@@ -1536,17 +1527,6 @@ window.__ModuleLoader__.load({
|
|
|
1536
1527
|
},
|
|
1537
1528
|
style: BUTTON_STYLE,
|
|
1538
1529
|
}, SEARCH_ICON),
|
|
1539
|
-
h('button', {
|
|
1540
|
-
key: 'share',
|
|
1541
|
-
type: 'button',
|
|
1542
|
-
'aria-label': 'Insert this page reference into the chat',
|
|
1543
|
-
'data-tip': 'share',
|
|
1544
|
-
'data-dsh-pdf-reader': 'share',
|
|
1545
|
-
onClick: () => {
|
|
1546
|
-
deliverReference(buildShareReference(page, printedOffset), 'Inserted')
|
|
1547
|
-
},
|
|
1548
|
-
style: BUTTON_STYLE,
|
|
1549
|
-
}, h('span', { 'aria-hidden': 'true', style: { fontSize: '14px' } }, '↗')),
|
|
1550
1530
|
h('button', {
|
|
1551
1531
|
key: 'follow',
|
|
1552
1532
|
type: 'button',
|
|
@@ -1764,7 +1744,6 @@ window.__ModuleLoader__.load({
|
|
|
1764
1744
|
AI_BUTTON_STYLE,
|
|
1765
1745
|
PANEL_STYLE,
|
|
1766
1746
|
buildSearchReference,
|
|
1767
|
-
buildShareReference,
|
|
1768
1747
|
cachedInfo,
|
|
1769
1748
|
copyText,
|
|
1770
1749
|
createDebouncer,
|
package/lib/index.js
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
* `.pdf` suffix, regular file, size cap, JSON only, no CORS header.
|
|
13
13
|
* - Page images use the same attachment route as `read_image`: a durable
|
|
14
14
|
* `attachments.saveImage` reference plus an `ImageBlock` from `render()`.
|
|
15
|
-
* - Auto-share is per-session and deliberately conservative:
|
|
16
|
-
*
|
|
15
|
+
* - Auto-share is per-session and deliberately conservative: at most one
|
|
16
|
+
* message per turn, and only when the page changed since the last hint.
|
|
17
17
|
*
|
|
18
18
|
* @module dsh-pdf-reader
|
|
19
19
|
*/
|
|
@@ -51,16 +51,14 @@ const PAGE_SELECTION_HARD_LIMIT = 256
|
|
|
51
51
|
export const Config = z.object({
|
|
52
52
|
/** Master switch: false mounts no tool, route, or listener. */
|
|
53
53
|
enabled: z.boolean().default(true),
|
|
54
|
-
/** Inject the current reading page into `agent/pre-step
|
|
54
|
+
/** Inject the current reading page into `agent/pre-step`, at most once per turn. */
|
|
55
55
|
autoShare: z.boolean().default(true),
|
|
56
56
|
/** Resolve the outline section for the injected message (one cached getInfo). */
|
|
57
57
|
autoShareSection: z.boolean().default(true),
|
|
58
|
-
/**
|
|
59
|
-
autoShareMinIntervalMs: z.number().default(
|
|
60
|
-
/** Minimum absolute page change required before another
|
|
58
|
+
/** Optional minimum milliseconds between hints; 0 = no time throttle. */
|
|
59
|
+
autoShareMinIntervalMs: z.number().default(0),
|
|
60
|
+
/** Minimum absolute page change required before another hint. */
|
|
61
61
|
autoSharePageDelta: z.number().default(1),
|
|
62
|
-
/** Minimum step distance between injections in the same turn. */
|
|
63
|
-
autoShareMinSteps: z.number().default(1),
|
|
64
62
|
/** Ignore a reported position older than this; 0 means no age limit. */
|
|
65
63
|
autoShareMaxAgeMs: z.number().default(0),
|
|
66
64
|
/** Hard cap on pages rendered by one `pdf_read_pages` call. */
|
|
@@ -98,9 +96,8 @@ export const DEFAULT_CONFIG = Object.freeze({
|
|
|
98
96
|
enabled: true,
|
|
99
97
|
autoShare: true,
|
|
100
98
|
autoShareSection: true,
|
|
101
|
-
autoShareMinIntervalMs:
|
|
99
|
+
autoShareMinIntervalMs: 0,
|
|
102
100
|
autoSharePageDelta: 1,
|
|
103
|
-
autoShareMinSteps: 1,
|
|
104
101
|
autoShareMaxAgeMs: 0,
|
|
105
102
|
maxPagesPerCall: 4,
|
|
106
103
|
defaultScale: 2.5,
|
|
@@ -124,7 +121,6 @@ export const INITIAL_AUTO_SHARE_STATE = Object.freeze({
|
|
|
124
121
|
lastInjectedPdfPage: null,
|
|
125
122
|
lastInjectedAt: null,
|
|
126
123
|
lastInjectedTurn: null,
|
|
127
|
-
lastInjectedStep: null,
|
|
128
124
|
})
|
|
129
125
|
|
|
130
126
|
/** Create an Error carrying a stable code, for tests and route mapping. */
|
|
@@ -180,7 +176,6 @@ export function normalizeConfig(raw) {
|
|
|
180
176
|
86_400_000,
|
|
181
177
|
),
|
|
182
178
|
autoSharePageDelta: configInteger(source.autoSharePageDelta, DEFAULT_CONFIG.autoSharePageDelta, 'autoSharePageDelta', 1, 1000),
|
|
183
|
-
autoShareMinSteps: configInteger(source.autoShareMinSteps, DEFAULT_CONFIG.autoShareMinSteps, 'autoShareMinSteps', 1, 1000),
|
|
184
179
|
autoShareMaxAgeMs: configInteger(source.autoShareMaxAgeMs, DEFAULT_CONFIG.autoShareMaxAgeMs, 'autoShareMaxAgeMs', 0, 31_536_000_000),
|
|
185
180
|
maxPagesPerCall: configInteger(
|
|
186
181
|
source.maxPagesPerCall,
|
|
@@ -738,12 +733,15 @@ function sameInjectedPage(state, position) {
|
|
|
738
733
|
}
|
|
739
734
|
|
|
740
735
|
/**
|
|
741
|
-
* Decide whether this
|
|
736
|
+
* Decide whether this turn should receive a reading-position message.
|
|
737
|
+
*
|
|
738
|
+
* One hint per turn at most: the guard is the turn number, not the step, and
|
|
739
|
+
* the hint is only sent when the reported page changed since the last one.
|
|
742
740
|
* Pure: callers own the state map and the clock.
|
|
743
|
-
* @param options - position, prior state, turn
|
|
741
|
+
* @param options - position, prior state, turn, clock, and config.
|
|
744
742
|
* @returns `{ inject, reason, nextState }`; `nextState` is unchanged when false.
|
|
745
743
|
*/
|
|
746
|
-
export function decideAutoShare({ position, state = INITIAL_AUTO_SHARE_STATE, now, turn,
|
|
744
|
+
export function decideAutoShare({ position, state = INITIAL_AUTO_SHARE_STATE, now, turn, config = DEFAULT_CONFIG }) {
|
|
747
745
|
const keep = { inject: false, reason: '', nextState: state }
|
|
748
746
|
if (!config.autoShare) return { ...keep, reason: 'disabled' }
|
|
749
747
|
if (position === null || position === undefined) return { ...keep, reason: 'no-position' }
|
|
@@ -752,15 +750,11 @@ export function decideAutoShare({ position, state = INITIAL_AUTO_SHARE_STATE, no
|
|
|
752
750
|
if (typeof position.updatedAt !== 'number') return { ...keep, reason: 'unknown-age' }
|
|
753
751
|
if (now - position.updatedAt > config.autoShareMaxAgeMs) return { ...keep, reason: 'stale' }
|
|
754
752
|
}
|
|
755
|
-
if (
|
|
753
|
+
if (Number.isInteger(turn) && state.lastInjectedTurn === turn) return { ...keep, reason: 'same-turn' }
|
|
754
|
+
if (config.autoShareMinIntervalMs > 0 && state.lastInjectedAt !== null && state.lastInjectedAt !== undefined) {
|
|
756
755
|
const elapsed = now - state.lastInjectedAt
|
|
757
756
|
if (!(elapsed >= config.autoShareMinIntervalMs)) return { ...keep, reason: 'throttled' }
|
|
758
757
|
}
|
|
759
|
-
const sameTurn = Number.isInteger(turn) && Number.isInteger(step) && state.lastInjectedTurn === turn
|
|
760
|
-
if (sameTurn && state.lastInjectedStep === step) return { ...keep, reason: 'same-step' }
|
|
761
|
-
if (sameTurn && state.lastInjectedStep !== null && step - state.lastInjectedStep < config.autoShareMinSteps) {
|
|
762
|
-
return { ...keep, reason: 'too-few-steps' }
|
|
763
|
-
}
|
|
764
758
|
if (
|
|
765
759
|
config.autoSharePageDelta > 1 &&
|
|
766
760
|
state.lastInjectedPdfPage !== null &&
|
|
@@ -777,7 +771,6 @@ export function decideAutoShare({ position, state = INITIAL_AUTO_SHARE_STATE, no
|
|
|
777
771
|
lastInjectedPdfPage: position.pdfPage,
|
|
778
772
|
lastInjectedAt: now,
|
|
779
773
|
lastInjectedTurn: Number.isInteger(turn) ? turn : null,
|
|
780
|
-
lastInjectedStep: Number.isInteger(step) ? step : null,
|
|
781
774
|
},
|
|
782
775
|
}
|
|
783
776
|
}
|
|
@@ -823,7 +816,6 @@ export function createAutoShareListener({
|
|
|
823
816
|
state,
|
|
824
817
|
now: now(),
|
|
825
818
|
turn: payload.turn,
|
|
826
|
-
step: payload.step,
|
|
827
819
|
config,
|
|
828
820
|
})
|
|
829
821
|
if (!verdict.inject) return decision
|
|
@@ -1339,8 +1331,8 @@ export function trimPageText(raw, maxChars) {
|
|
|
1339
1331
|
return { text: normalized.slice(0, maxChars), truncated: true, unavailable: false }
|
|
1340
1332
|
}
|
|
1341
1333
|
|
|
1342
|
-
/** Register `pdf_info
|
|
1343
|
-
export function createCheapTools({ defineTool: makeTool, config, core, infoCache,
|
|
1334
|
+
/** Register `pdf_info` and `pdf_search`. */
|
|
1335
|
+
export function createCheapTools({ defineTool: makeTool, config, core, infoCache, statFile = stat }) {
|
|
1344
1336
|
return [
|
|
1345
1337
|
makeTool({
|
|
1346
1338
|
name: 'pdf_info',
|
|
@@ -1522,83 +1514,6 @@ export function createCheapTools({ defineTool: makeTool, config, core, infoCache
|
|
|
1522
1514
|
locations: [{ path: args.path }],
|
|
1523
1515
|
}),
|
|
1524
1516
|
}),
|
|
1525
|
-
|
|
1526
|
-
makeTool({
|
|
1527
|
-
name: 'pdf_current_page',
|
|
1528
|
-
description:
|
|
1529
|
-
'The page the user is reading in the preview, plus its section. Text only, cheap.',
|
|
1530
|
-
parameters: {
|
|
1531
|
-
path: {
|
|
1532
|
-
type: 'string',
|
|
1533
|
-
description: 'Optional expected PDF path. When supplied and it differs from the reported book, returns current:null.',
|
|
1534
|
-
},
|
|
1535
|
-
},
|
|
1536
|
-
output: {
|
|
1537
|
-
schema: {
|
|
1538
|
-
type: 'object',
|
|
1539
|
-
additionalProperties: false,
|
|
1540
|
-
properties: {
|
|
1541
|
-
current: {
|
|
1542
|
-
oneOf: [
|
|
1543
|
-
{ type: 'null' },
|
|
1544
|
-
{
|
|
1545
|
-
type: 'object',
|
|
1546
|
-
additionalProperties: false,
|
|
1547
|
-
properties: {
|
|
1548
|
-
path: { type: 'string', required: true },
|
|
1549
|
-
book: { type: 'string', required: true },
|
|
1550
|
-
pdfPage: { type: 'integer', required: true },
|
|
1551
|
-
printedPage: { ...PRINTED_PAGE_SCHEMA, required: true },
|
|
1552
|
-
pageNumberMode: { type: 'string', enum: ['printed', 'pdf'], required: true },
|
|
1553
|
-
section: { ...SECTION_SCHEMA },
|
|
1554
|
-
sectionTrail: { type: 'array', items: { type: 'string' } },
|
|
1555
|
-
updatedAt: { type: 'integer', required: true },
|
|
1556
|
-
},
|
|
1557
|
-
},
|
|
1558
|
-
],
|
|
1559
|
-
required: true,
|
|
1560
|
-
},
|
|
1561
|
-
reason: { type: 'string' },
|
|
1562
|
-
},
|
|
1563
|
-
},
|
|
1564
|
-
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }],
|
|
1565
|
-
presentationMeta: (_args, value) => ({ path: value?.current?.path }),
|
|
1566
|
-
},
|
|
1567
|
-
isConcurrencySafe: () => true,
|
|
1568
|
-
async execute(args, exec) {
|
|
1569
|
-
const sessionId = exec?.agent?.session?.id
|
|
1570
|
-
const position = typeof sessionId === 'string' ? positions.forSession(sessionId) : undefined
|
|
1571
|
-
if (position === undefined) return { current: null, reason: 'no-reading-position' }
|
|
1572
|
-
if (args.path !== undefined) {
|
|
1573
|
-
const expected = resolveToolPath(args.path, exec)
|
|
1574
|
-
if (expected !== position.path) return { current: null, reason: 'path-mismatch' }
|
|
1575
|
-
}
|
|
1576
|
-
let info = null
|
|
1577
|
-
try {
|
|
1578
|
-
info = (await infoCache.load(position.path)).info
|
|
1579
|
-
} catch {
|
|
1580
|
-
info = null
|
|
1581
|
-
}
|
|
1582
|
-
const trail = info === null ? [] : sectionTrailForPage(info.outline, position.pdfPage)
|
|
1583
|
-
const printedPage = printedPageFor(info, position.pdfPage, position.printedPage)
|
|
1584
|
-
return {
|
|
1585
|
-
current: {
|
|
1586
|
-
path: position.path,
|
|
1587
|
-
book: titleFromPath(position.path),
|
|
1588
|
-
pdfPage: position.pdfPage,
|
|
1589
|
-
printedPage,
|
|
1590
|
-
pageNumberMode: typeof printedPage === 'number' ? 'printed' : 'pdf',
|
|
1591
|
-
...(trail.length === 0 ? {} : { section: trail[trail.length - 1], sectionTrail: trail.map((entry) => entry.title) }),
|
|
1592
|
-
updatedAt: position.updatedAt,
|
|
1593
|
-
},
|
|
1594
|
-
}
|
|
1595
|
-
},
|
|
1596
|
-
presentCall: () => ({
|
|
1597
|
-
card: 'generic',
|
|
1598
|
-
title: 'Current PDF page',
|
|
1599
|
-
kind: 'read',
|
|
1600
|
-
}),
|
|
1601
|
-
}),
|
|
1602
1517
|
]
|
|
1603
1518
|
}
|
|
1604
1519
|
|
|
@@ -1910,7 +1825,6 @@ export function apply(ctx, rawConfig) {
|
|
|
1910
1825
|
config,
|
|
1911
1826
|
core,
|
|
1912
1827
|
infoCache,
|
|
1913
|
-
positions,
|
|
1914
1828
|
statFile: stat,
|
|
1915
1829
|
})) {
|
|
1916
1830
|
ctx.tools.register(tool)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-book-reader",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Read books and papers in DSH:
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Read books and papers in DSH: a reading toolbar on the official PDF preview, plus PDF tools for the model (page map, full-text search, page text and images).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
7
7
|
"dsh-plugin",
|
|
@@ -17,11 +17,11 @@
|
|
|
17
17
|
"author": "strepsiades",
|
|
18
18
|
"repository": {
|
|
19
19
|
"type": "git",
|
|
20
|
-
"url": "git+https://github.com/
|
|
20
|
+
"url": "git+https://github.com/Strepsiades2/dsh-book-reader.git"
|
|
21
21
|
},
|
|
22
|
-
"homepage": "https://github.com/
|
|
22
|
+
"homepage": "https://github.com/Strepsiades2/dsh-book-reader#readme",
|
|
23
23
|
"bugs": {
|
|
24
|
-
"url": "https://github.com/
|
|
24
|
+
"url": "https://github.com/Strepsiades2/dsh-book-reader/issues"
|
|
25
25
|
},
|
|
26
26
|
"engines": {
|
|
27
27
|
"node": ">=20"
|