@khorsheed/dsh-reader 0.2.1 → 0.3.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.
- package/CHANGELOG.md +5 -0
- package/README.en.md +3 -3
- package/README.i18n.yaml +2 -2
- package/README.md +3 -3
- package/lib/client.js +2684 -1352
- package/lib/index.js +486 -0
- package/lib/typert.host.js +91 -31
- package/lib/typert.remote-client.d.ts +3 -1
- package/lib/typert.remote-client.js +69 -31
- package/lib/types/client/ReaderPane.js +46 -1
- package/lib/types/client/contract.d.ts +9 -1
- package/lib/types/client/index.js +1 -0
- package/lib/types/image-fetch.d.ts +81 -0
- package/lib/types/image-fetch.js +385 -0
- package/lib/types/remote.d.ts +9 -1
- package/lib/types/remote.js +11 -0
- package/lib/types/service.d.ts +30 -1
- package/lib/types/service.js +40 -0
- package/lib/types/types.d.ts +19 -0
- package/package.json +10 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# 变更记录
|
|
2
2
|
|
|
3
|
+
## 0.3.0(2026-09-30)
|
|
4
|
+
|
|
5
|
+
- **CORP 拦截图救回**:以 `cross-origin-resource-policy: same-origin` 供图的站点(如 claude.dev)会让浏览器跨域 `<img>` 必挂,只有宿主进程抓取能绕过。新增 `fetchImage` Remote——只重抓已经加载失败的图片(公网地址校验钉在 DNS 答案上、逐跳重定向再校验、限 `image/*` + 8MiB + 15s、32MiB LRU),阅读面板把这些图改指 data: URI。健康图片从不经过代理;旧版宿主下图片维持 broken
|
|
6
|
+
- 加宽 `@deepseek-ai/dsh-*` peer 区间以覆盖宿主 0.2.0
|
|
7
|
+
|
|
3
8
|
## 0.2.1(2026-09-27)
|
|
4
9
|
|
|
5
10
|
无功能变更。README 截图整理(删除一张从未存在的引用图,全部图片引用可解析);随宿主 0.1.7-rc.2 基线发布波重发(全量构建+测试在 rc.2 基线通过)。
|
package/README.en.md
CHANGED
|
@@ -6,7 +6,7 @@ RSS subscriptions and pasted article links on one wall — click a card and read
|
|
|
6
6
|
|
|
7
7
|
Interesting papers and articles pile up as browser tabs, and feeding one to the agent means copy-pasting. This plugin adds a page-type tab to the right sidebar — the inspiration space: feeds refresh on a daily schedule, a pasted link becomes a card, and the body renders as readable text inside dsh (selectable for quoting, translatable on-device by the browser's own model). A body the host could not fetch whole, or a site that refuses bots, says so honestly — the wall never looks fine while being hollow.
|
|
8
8
|
|
|
9
|
-
<img src="https://raw.githubusercontent.com/Khorsheed/dsh-
|
|
9
|
+
<img src="https://raw.githubusercontent.com/Khorsheed/dsh-basic/main/docs/screenshots/reader-1.png" width="640" alt="the inspiration space wall: a card feed of subscriptions and saved links, with filter, sort, wall translation, refresh, settings and add in the header row and the search field on its own row">
|
|
10
10
|
|
|
11
11
|
## Features
|
|
12
12
|
|
|
@@ -20,7 +20,7 @@ Interesting papers and articles pile up as browser tabs, and feeding one to the
|
|
|
20
20
|
- **Subscription page and recently read** — one row per source with in-place name and address editing, per-source refresh, pause/resume and remove, plus the cache policy (body retention, translation budget, clear translation cache) and storage readouts on the same page; **recently read** persists 100 rows, survives a restart, and can reopen an entry that has rolled out of its feed's window.
|
|
21
21
|
- **Leave and carry on** — the open article, the reading position anchored by content (the Nth block, this character of its text), the wall's narrowing and the translation are remembered by the whole page across dsh sessions; the place is also written to `sessionStorage`, so a reloaded page finds the reader where they were.
|
|
22
22
|
|
|
23
|
-
<img src="https://raw.githubusercontent.com/Khorsheed/dsh-
|
|
23
|
+
<img src="https://raw.githubusercontent.com/Khorsheed/dsh-basic/main/docs/screenshots/reader-2.png" width="640" alt="the detail view: the body rendered as DOM text, the globe lit with sentence-aligned translation, clicking one sentence revealing its original">
|
|
24
24
|
|
|
25
25
|
## Install
|
|
26
26
|
|
|
@@ -62,7 +62,7 @@ Note: the host fetch seam truncates a decoded body at 100,000 characters and doe
|
|
|
62
62
|
| Surface | Contents |
|
|
63
63
|
| --- | --- |
|
|
64
64
|
| The wall | **Content first**: the entry cards own the first screen. **The search field owns its own full-width row**, and the filter, sort and wall-translation controls moved up into the pane's header (with refresh, settings and add) — a field wedged between three buttons is a field the reader cannot see what they typed in on a narrow sidebar; every popover hangs under **the button that opened it** and cannot cover the other one. The filter is a **two-page popover** narrowing the wall four ways — read state (unread), by source, by KIND (a feed or a saved link) and by tag: the root page holds the two read-state rows (unread, and **Hide duplicates**), a by-source drill row that carries the **current value** (so no narrowing is ever hidden) and the three kind rows. **Dedupe is on by default**: an article republished by several feeds folds into one card (three grouping tiers, any hit folds: same entry id — a shared guid shares fetch/read/translation state by design; normalized link — scheme/www/utm-style tracking parameters stripped; folded title plus the same published day; never fuzzy), the surviving card carries a 「N 个重复来源」 badge (hover names the other sources), reading any copy reads the group, the footer notes how many folded, and the backfill never spends a fetch on a hidden copy, and drilling in opens the source page, which owns the panel's height, its own scroll and — past six sources — its own search box, so **however many subscriptions accumulate, the panel's first page stays a fixed handful of rows**. It is the same local predicate as the search box (D14), so filtering costs no round trip, and its value is visible in the search box (`#sourceId` / `#rss` / `#link` / `@tagId`), which is also how it is cleared — and **a field with text in it grows an × at its right edge**, one click away from clearing both the typed term and the filter value. A **text** search is ranked by relevance — exact title, then title contains, then the other fields, with the chosen sort deciding inside each tier (`#sourceId` / `@tagId` selectors are not ranked) — and the footer says how many entries matched (`N matching “X”`), so a narrowed wall can never be mistaken for the whole wall. **The wall translates too**: the globe at the right of the toolbar (its own switch, separate from the detail view's, sharing one translation memory). It decides **per FIELD**: a Chinese title or summary is left exactly as it is, a mixed card has only its English half translated, and Chinese is never translated into Chinese. Only cards that have **entered the viewport** are queued (IntersectionObserver feeds a queue, six cards per pass, grouped by detected language, through the article's own sentinel-batch contract), so scrolling back costs nothing and opening an entry does not re-translate its title. Two views: translation only (**hovering the translated TEXT** swaps just that FIELD back to its original — the title for the title, the summary for the summary; the card's blank space, tile and chevron change nothing) and side by side (each field's original stays under its translation and keeps that FIELD's own typography — a title's original is title-sized, told apart by ink and weight rather than shrunk to caption size). **The text boxes are fixed** — title and summary each reserve two clamped lines — so swapping languages on hover cannot resize a card or shift the grid; only the side-by-side view makes the grid taller, and that is a mode the reader picks. Entry cards are **cards** — a fill, a hairline border, a 12px radius — where hover only lifts the tone (never the geometry) and a keyboard focus draws a ring; unread entries ink their title and keep the **dot** (7px) on the tile's corner — session-only, never persisted — while read entries stay put and go quieter; the feed's own category and the reader's own tag share one pill metric (the reader's tag is outlined, and the row wraps), and tagging opens from the card's `+` into a panel where you tick the vocabulary or **type a name and press Enter to create it** (the native `datalist` is gone, replaced by stylable rows). **Tags are deletable from the filter popover's `By tag` section** (a quiet × at the end of each row removes it from the vocabulary and from every entry, and clears the narrowing when it was the active one) — a vocabulary you can only add to is a one-way door. A saved link whose body could not be read carries a quiet dashed 「仅链接」 badge, and hover and detail both say why. **A saved link's card upgrades to the article's own title once the body parses** (the body's first h1, else the document title, plus the first substantial paragraph as the excerpt) and keeps it in the host annotation — the card still reads that memory after the body's budget eviction, so a wall of papers is scannable without opening each one. A saved link without `publishedAt` sorts by **its source's added time**, so a freshly added link sits at the top instead of sinking. A freshness line sits at the bottom — "Refreshed 3 h ago · daily 10:00" — so a reader can tell whether the snapshot is worth re-fetching |
|
|
65
|
-
| Detail | Where a card leads. The body renders as DOM text — **that is exactly what makes passage quoting work**, and why this surface never hosts an iframe. The body has **no reading measure**: it fills the width the sidebar is given, and paragraphs and lists share one right edge. Toolbar: back, **translate (globe + view menu)**, copy link, open the original in the browser, and — for a single saved link — **a delete button** (a feed entry gets none: the next refresh would bring it back, and a delete that undoes itself is a lie); the **tag row under the title also carries 「重新抓取」** (the same host action as the card's pill, and asking for a fresh copy drops that entry's translation record — the body is about to be a different DOM). Below the body, the other entries "also from this source". **A figure the page draws with its own scripts cannot be fetched**: such a `<figure>` carries nothing but an empty div and a caption in the served HTML (measured: 84 of them on the transformer-circuits.pub paper), and a fetch runs no scripts — so **the caption stays in the body** (it is text the page published, and it says what the picture shows) while one line above the body says 「这一页有 N 张插图由页面自己的脚本绘制,抓取时拿不到画面(图注保留在正文里)」 with the original-page link (opened in the in-app Sidebar Browser when the host has one; and when a future capture package is mounted, a 「渲染抓取」 action appears here — unprobed, it does not render). **And a capture-rendered page keeps its figures**: script-drawn figures arrive as inline SVG with inlined styles and survive through a safe subset (shapes/text/defs/gradients/clips/symbol reuse; `script`/`foreignObject`/SMIL/events/external references never do, and `url(` may only point at a same-document fragment) — instead of leaving only a caption. **Composite figures keep their structure too**: a capture-rendered composite (a diagram plus property cards built from styled boxes) keeps its div/span containers inside the figure with an allowlisted style set (layout/paint properties; `position:fixed`, over-bound `z-index`, external `url()`s and script tricks all stripped), and every unwrapped block container leaves a word boundary — sibling texts can never concatenate. **A picture whose URL IS in the static markup is recovered, though**: lazy-loading sites keep the real URL in a `data-src`-style attribute, a `srcset`, a `<picture>`'s `<source>`, or the `<noscript>` no-JS fallback (the `src` holds a 1px placeholder), and the extractor resolves the candidates in that order, taking the largest srcset one; **the converse shape is kept too — pages that inline their real figures as base64 data URIs (transformer-circuits.pub style)**: placeholder and picture are split by payload size (a 512-char threshold; a 1px gif is ~70, a real chart runs to the thousands), the small one still counts as absent and yields to the lazy attributes, the large one is kept as-is; every emitted `<img>` carries `referrerpolicy="no-referrer"`, because anti-hotlink CDNs answer 403 otherwise. **Formulas render as native MathML**: an arXiv HTML paper's `<math alttext="…">` passes through a safe presentation subset (`annotation-xml` and any script never do — the former is an HTML-injection vector), and display math scrolls sideways instead of breaking the layout. **LaTeXML's vector figures are kept too**: arXiv HTML embeds them as `<object type="image/svg+xml" data="…">`, which the normalizer rewrites as an `<img>` carrying the original width/height; a non-image object (a PDF, a movie) is still dropped whole |
|
|
65
|
+
| Detail | Where a card leads. The body renders as DOM text — **that is exactly what makes passage quoting work**, and why this surface never hosts an iframe. The body has **no reading measure**: it fills the width the sidebar is given, and paragraphs and lists share one right edge. Toolbar: back, **translate (globe + view menu)**, copy link, open the original in the browser, and — for a single saved link — **a delete button** (a feed entry gets none: the next refresh would bring it back, and a delete that undoes itself is a lie); the **tag row under the title also carries 「重新抓取」** (the same host action as the card's pill, and asking for a fresh copy drops that entry's translation record — the body is about to be a different DOM). Below the body, the other entries "also from this source". **A figure the page draws with its own scripts cannot be fetched**: such a `<figure>` carries nothing but an empty div and a caption in the served HTML (measured: 84 of them on the transformer-circuits.pub paper), and a fetch runs no scripts — so **the caption stays in the body** (it is text the page published, and it says what the picture shows) while one line above the body says 「这一页有 N 张插图由页面自己的脚本绘制,抓取时拿不到画面(图注保留在正文里)」 with the original-page link (opened in the in-app Sidebar Browser when the host has one; and when a future capture package is mounted, a 「渲染抓取」 action appears here — unprobed, it does not render). **And a capture-rendered page keeps its figures**: script-drawn figures arrive as inline SVG with inlined styles and survive through a safe subset (shapes/text/defs/gradients/clips/symbol reuse; `script`/`foreignObject`/SMIL/events/external references never do, and `url(` may only point at a same-document fragment) — instead of leaving only a caption. **Composite figures keep their structure too**: a capture-rendered composite (a diagram plus property cards built from styled boxes) keeps its div/span containers inside the figure with an allowlisted style set (layout/paint properties; `position:fixed`, over-bound `z-index`, external `url()`s and script tricks all stripped), and every unwrapped block container leaves a word boundary — sibling texts can never concatenate. **A picture whose URL IS in the static markup is recovered, though**: lazy-loading sites keep the real URL in a `data-src`-style attribute, a `srcset`, a `<picture>`'s `<source>`, or the `<noscript>` no-JS fallback (the `src` holds a 1px placeholder), and the extractor resolves the candidates in that order, taking the largest srcset one; **the converse shape is kept too — pages that inline their real figures as base64 data URIs (transformer-circuits.pub style)**: placeholder and picture are split by payload size (a 512-char threshold; a 1px gif is ~70, a real chart runs to the thousands), the small one still counts as absent and yields to the lazy attributes, the large one is kept as-is; every emitted `<img>` carries `referrerpolicy="no-referrer"`, because anti-hotlink CDNs answer 403 otherwise. **There is a second refusal a referrer policy cannot fix**: an origin answering `Cross-Origin-Resource-Policy: same-origin` (measured: every `/media` figure on claude.dev) withholds the pixels from a cross-origin page even though a direct fetch gets a 200 — the pane showed the broken-image icon over the full alt text. Those images are fetched host-side instead (public-address validation with the connection pinned to the validated answers, per-hop re-validation, an 8 MiB cap, and an `image/*` content-type gate — the host's own `ctx.web` seam decodes text only and cannot carry bytes): an `<img>` that already failed is re-pointed at the fetched bytes (a `data:` URI), a healthy image never pays the proxy, and one the host cannot have either stays broken (「阅读原文」 remains the way out). **Formulas render as native MathML**: an arXiv HTML paper's `<math alttext="…">` passes through a safe presentation subset (`annotation-xml` and any script never do — the former is an HTML-injection vector), and display math scrolls sideways instead of breaking the layout. **LaTeXML's vector figures are kept too**: arXiv HTML embeds them as `<object type="image/svg+xml" data="…">`, which the normalizer rewrites as an `<img>` carrying the original width/height; a non-image object (a PDF, a movie) is still dropped whole |
|
|
66
66
|
| Translation (on-device) | The toolbar's **globe** is a switch: lit means this body is showing a translation. Its caret holds three views — **translation only** (the default; a one-time line above the body teaches "click any sentence to see its original, click again to hide it"), **side by side** (every paragraph carries its original underneath, foldable per paragraph), and **original only**. The unit is a SENTENCE: clicking one sentence reveals that ONE sentence's original under its paragraph (hairline plus a highlight), and several can be open at once; when the sentence contains an inline link and was cut into several text runs, the reveal still shows the COMPLETE sentence rather than the fragments, the original line itself is NOT a control (it is text meant to be selected for quoting — clicking it does nothing, and a click that ends a selection is ignored, so quoting never folds the sentence away). The English original is set at BODY size (13.5px, the secondary ink, 1.62 leading, 3px between lines) and is spaced by WHERE its container sits: beside a paragraph it hugs that paragraph (4px above, 14px before the next block), while inside a list item, quote or table cell it sits 5px under the translation instead of being pulled up into it by a negative margin. An EXPANDED sentence keeps one quiet marker — a hairline underline, with no standing fill or bar (that is the "this sentence is bound to the line below" cue). The pairing highlight itself is HOVER-ONLY: pointing at a Chinese sentence lights that sentence (including the fragments a link split it into) together with its one original line, and pointing at the original line does the reverse. Nothing carries a standing colour or bar by default — the earlier always-on marks turned a skimmed article into confetti. All of it runs **in the browser** (the `Translator` API in Chrome 138+ / Edge 148+, desktop): the model never leaves the device and spends no tokens, but **the derived sentence pairs do persist** (below), while the globe record and translator sessions never do. Without that API, with an unavailable pair, or on a body that is already Chinese, the globe is **not rendered at all**. **The translations persist in two tiers** (under `$DSH_HOME/state/dsh-reader`, the same directory, sensitivity class and uninstall story as the cached originals): a global **sentence memory** (`<src>→<tgt>:<sentenceHash>` → source + translation, 50k entries) and a **per-entry segment map** (hash → translation, keyed to the body's hash, so a minor refetch still reuses every unchanged sentence). One shared budget — 64 MB by default, tunable through the same cache-policy verb as the body retention — evicts least-recently-used across both tiers, with **no TTL** (a translation costs a gesture plus per-sentence model work to rebuild, so it must not expire by the clock). The net effect: after a reload the globe still waits for its gesture, and after it the whole article re-paints from disk with the model untouched; both tiers carry a schema version that reads as a miss and evicts lazily, so a browser-model upgrade never passes old translations off as the new model's |
|
|
67
67
|
| Add (dialog) | The header's plus, the empty-state card and the dashed card at the end of the wall all open the same **dialog** (the members tab's invite-dialog idiom: transparent overlay plus centered card, Esc and overlay-click close), because adding is a momentary act and the wall should not be replaced by it. **What comes back decides how it is stored** (D15): a feed becomes a subscription, a web page is saved as a single item. **An arXiv abs/pdf link is upgraded to the paper's official HTML version** (`arxiv.org/html/<id>` — LaTeXML-built semantic markup: real tables, real formulas); when the HTML version does not exist (404) the pasted address falls back to the ordinary path. **A DOI link is resolved to its arXiv version first** (Crossref for the title and authors → an arXiv title search → a two-factor gate of title similarity plus author family-name overlap; no match means the ordinary path, never a guess), and a landed card marks the DOI it came from; **an OpenReview link is saved link-only without a fetch** (measured unreadable server-side: the API sits behind a Cloudflare challenge, the forum is a JS SPA), with the reason and the suggestion on the card (paste the arXiv version if one exists). **A URL that cannot be read is still saved**: the verdict changed from "succeeded / failed" to "saved, plus whether it can be previewed", the reason is classified (bot wall / login wall / not a web page / cross-site hop / unreachable / HTTP error) and the card is marked 「仅链接」 — dropping the URL the reader pasted is worse than a card that can only be opened in a browser |
|
|
68
68
|
| Subscriptions (page) | The header's settings button opens the **subscription page**: one **compressed settings row** at the top (daily refresh time and article retention side by side, their long explanations moved into hover titles), a **storage row** (the article cache's readout — "N cached · size", the translation cache's readout — "M maps + S sentences · size" — the **translation budget** input in MB, committed on blur/Enter, and a two-step **clear translation cache** button; the readouts are aggregated on the host, the tables never cross the wire) plus two labelled **chip groups** — `By type` (all / feeds / saved links, each with its count) and `Sort` (added / name / last fetch, added-time descending by default) — then one row per source where **both the name and the feed address are editable in place** (a changed address is a different document, so it refetches on save; clearing the name falls back to the feed's own title), plus its kind, item count, last fetch, failure reason (classified, not the seam's raw sentence), per-source refresh, pause/resume, remove, and the **daily refresh time setting**. Those questions are about SOURCES rather than about content, so they do not live on the wall |
|
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# last confirmed-consistent state. Both languages carry equal authority; after
|
|
3
3
|
# editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/dsh-reader/README.en.md
|
|
5
|
-
packages/dsh-reader/README.en.md:
|
|
6
|
-
packages/dsh-reader/README.md:
|
|
5
|
+
packages/dsh-reader/README.en.md: 8ce556c7a4154e7946365b6104d8a7655e5de25b
|
|
6
|
+
packages/dsh-reader/README.md: b10d1ec3d89de6125f94b53e0b71febdebaccb81
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ RSS 订阅和随手粘来的文章链接收在同一面墙上——点开就在
|
|
|
6
6
|
|
|
7
7
|
刷到的论文和文章散落在浏览器标签页里,想喂给 agent 还得复制粘贴。这个插件在右侧栏加一个页面型 tab「灵感空间」:订阅源每天按点自动刷新,链接粘进来就存成卡片;正文在 dsh 里直接渲染成可读文本(能划选引用、能用浏览器自己的端上模型翻成中文),抓不全或站点拒抓都如实标明,绝不给一堵看起来正常、实则空心的墙。
|
|
8
8
|
|
|
9
|
-
<img src="https://raw.githubusercontent.com/Khorsheed/dsh-
|
|
9
|
+
<img src="https://raw.githubusercontent.com/Khorsheed/dsh-basic/main/docs/screenshots/reader-1.png" width="640" alt="灵感空间的墙:订阅与保存链接的卡片流,表头一行是筛选、排序、整墙翻译、刷新、设置与新增,搜索框独占一行">
|
|
10
10
|
|
|
11
11
|
## 特性
|
|
12
12
|
|
|
@@ -20,7 +20,7 @@ RSS 订阅和随手粘来的文章链接收在同一面墙上——点开就在
|
|
|
20
20
|
- **订阅管理与最近阅读**——管理页里每个源一行:名称与订阅地址就地编辑、单独刷新、暂停/恢复、删除,缓存策略(正文保留时长、译文预算、清空译文缓存)与存储读数同页;「最近阅读」落盘 100 条、重启还在,滚出 feed 窗口的条目也能接着读。
|
|
21
21
|
- **离开再回来接着读**——打开的文章、按内容锚定的阅读位置(第几个块、块内第几个字符)、墙上的筛选与译文,跨 dsh 会话由整个页面记住;位置另写 `sessionStorage`,页面重启后仍在原处。
|
|
22
22
|
|
|
23
|
-
<img src="https://raw.githubusercontent.com/Khorsheed/dsh-
|
|
23
|
+
<img src="https://raw.githubusercontent.com/Khorsheed/dsh-basic/main/docs/screenshots/reader-2.png" width="640" alt="详情页:正文以 DOM 文本渲染,地球开关点亮后按句对照翻译,点一句展开该句原文">
|
|
24
24
|
|
|
25
25
|
## 安装
|
|
26
26
|
|
|
@@ -62,7 +62,7 @@ dsh plugin --profile web remove @khorsheed/dsh-reader
|
|
|
62
62
|
| 面 | 内容 |
|
|
63
63
|
| --- | --- |
|
|
64
64
|
| 灵感墙 | **内容优先**:第一屏就是条目卡片流。**搜索框独占一行、铺满整行宽度**,筛选 / 排序 / 整墙翻译三个控件都移到**表头那一行**(和刷新、设置、加号在一起)——搜索框夹在三个按钮之间时,窄侧栏里连自己打了什么都看不全;每个弹层仍然挂在**打开它的那个按钮**下面,不会盖住另一个按钮。**文字搜索按相关性排**:标题完全命中 → 标题包含 → 其它字段命中,同一档里才用你选的排序(`#源id` / `@标签id` 这类选择器不参与排序),所以「搜一个标题却先出来别的」不再发生;搜索生效时底部还会写明**匹配了多少条**(`匹配「X」N 条`),列表有没有被筛过一眼可见。筛选是一个**两级弹层**,按「阅读状态」(未读)、「按来源」、「**按类型**」(订阅源 / 保存的链接)、「按标签」四种方式收窄墙:根页是阅读状态两行(只看未读、**隐藏重复**)、`按来源` 钻取行(那一行带着**当前值**,所以不存在「被藏起来的状态」)和类型三行。**隐藏重复默认开**:同一篇文章被多个源转载时折成一张卡(三层归并,任一命中即同组:同条目 id——共享 guid 按设计共享抓取/已读/译文;链接归一化——剥协议/www/utm 类追踪参数;折叠标题 + 同一发布日;绝不模糊匹配),幸存卡带「N 个重复来源」徽章(悬停列出其它来源),读过一个副本整组即已读,底部注明隐藏了几条,自动补抓也不为被隐藏的副本花请求,点进去是来源页——独占弹层高度、自己滚动、来源超过 6 个时出现自己的搜索框;**所以订阅再多,弹层第一页也恒定是几行**。筛选用的是和搜索同一套本地谓词(D14),所以不花请求,值就写在搜索框里(`#源id` / `#rss` / `#link` / `@标签id`)、看得见也能清掉——**搜索框里有内容时框内右侧出现一个 ×**,点它一次清空(筛选值和手输的关键词一起清)。**墙上也能整墙翻译**:工具条最右的地球(和详情页那个各自独立,共享同一份翻译记忆)。开启后**逐字段判语言**——中文标题/摘要原样保留、混合卡片只翻英文那一半,绝不中译中;只翻**进入过视口**的卡片(IntersectionObserver 喂队列、每批 6 张、按语言分组、沿用正文那套哨兵批量+校验+逐条兜底),滚回去不重复请求,进详情也不再翻一次标题。下拉两档:只看译文(**鼠标悬停在被翻译的文字上**时只把**那个字段**换回原文——标题换标题、摘要换摘要,卡片空白处/瓷贴/箭头一律不动,便于核对又不神经质)/ 双语对照(每个字段的原文常驻在译文下方,且**沿用该字段自身的排版**——标题原文就是标题字号与行高,靠墨色和字重与译文区分,不缩成小注)。**卡片文字盒是固定的**:标题与摘要各按两行预留高度并双向 clamp,所以译文/原文互换(hover 或对照)都不会改变卡片高度、网格不跳动;只有「双语对照」是显式切换、会让网格整体变高一次。条目卡片是**有底、有描边、12px 圆角的卡**:悬停只提亮不位移,键盘聚焦有环;未读条目标题上墨、瓷砖角上一个**圆点**(7px,会话级、不落盘),已读条目留在原位、只是安静一档;feed 自己的分类与读者自建标签共用同一套药丸尺寸(自建标签带描边、可折行),打标签按卡片上的「+」打开面板——里面勾选已存在的标签,或**输入名字回车新建**(原生 datalist 已换成可样式化的行);**标签在筛选弹层的「按标签」区可以删除**(每行右侧一个安静的 ×,删掉即从词表和所有条目上移除,正在用它筛选时收窄也一并清掉)。**保存的链接如果读不到正文,卡片上有一个「仅链接」小徽章**(虚线、安静),悬停与详情会说明原因(见下方「只能存链接的链接」)。**链接卡片的标题在正文解析出来后升级成文章自己的**(正文第一个 h1,否则文档标题;另带第一段实质文字当摘要),随正文写进宿主标注——正文被预算淘汰后卡片仍读这份记忆,一墙论文不用一篇篇点开。没有 `publishedAt` 的保存链接按**它的源加入时间**参与时间排序,所以刚加的那条在最上面,不会沉底。底部一行**新鲜度**:「刷出于 3 小时前 · 每日 10:00」,让你决定要不要按刷新 |
|
|
65
|
-
| 详情 | 点卡片进来。正文以 DOM 文本渲染——**这正是划选引用能工作的原因**,也是这个面不放 iframe 的原因。正文**没有阅读量度**:侧栏给多宽就铺多宽,段落和列表共用同一条右缘。工具条:返回、**翻译(地球 + 视图下拉)**、复制链接、在浏览器打开原文,**单条保存的链接另有一个删除按钮**(订阅源的条目没有:下次刷新它还会回来,给「删除」是假承诺);标题下的**标签行里还有「重新抓取」**(和卡片药丸同一个宿主动作,用它换一份新副本时会清掉这篇的译文记录——正文要换成新的 DOM)。正文下面列"同一来源"的其他条目。**页面用脚本画的插图抓不到**:这类 `<figure>` 在服务器 HTML 里只有一个空 div 和一句图注(实测 transformer-circuits.pub 这篇有 84 张),抓取不执行脚本,所以**图注保留在正文里**(它是页面发布的文字,写明了画的是什么),正文上方另给一行「这一页有 N 张插图由页面自己的脚本绘制,抓取时拿不到画面(图注保留在正文里)—— 阅读原文」(宿主有 Sidebar Browser 时这个出口开在应用内;未来 capture 包在场时这里还会出现「渲染抓取」——探测不到就不渲染)。**capture 渲染回来的图能进正文**:页面脚本绘制的图以「内联 SVG + 内联样式」到达,归一化按安全子集保留(形状/文字/defs/渐变/裁剪/symbol 复用;`script`/`foreignObject`/SMIL/事件/外部引用永不放行,`url(` 只许本文档片段)——而不是只剩图注。**组合图的结构同样保**:capture 渲染的组合图(图 + 样式盒搭的属性卡)在 figure 里保留 div/span 容器与白名单样式(布局/绘制属性;`position:fixed`、`z-index` 超限、外部 `url()`、脚本把戏全剥),而且任何被解包的块级容器都留下词边界——相邻文字绝不粘连。**但静态标记里就有 URL 的图会回收**:懒加载站点把真 URL 放在 `data-src` 等属性、`srcset`、`<picture>` 的 `<source>` 或 `<noscript>` 的 no-JS 兜底里(`src` 里只是 1px 占位图),抽取按这个顺序解析候选、取 srcset 里最大的一张;**反过来,把真图直接内联成 base64 data URI 的页面(transformer-circuits 一类)也保**——占位图与真图按载荷大小分道(阈值 512 字符,1px gif 约 70、真图表数千起),小的照旧当占位、穿透到懒加载属性,大的原样保留;正文输出的每个 `<img>` 都带 `referrerpolicy="no-referrer"`(防盗链的 CDN 否则直接 403
|
|
65
|
+
| 详情 | 点卡片进来。正文以 DOM 文本渲染——**这正是划选引用能工作的原因**,也是这个面不放 iframe 的原因。正文**没有阅读量度**:侧栏给多宽就铺多宽,段落和列表共用同一条右缘。工具条:返回、**翻译(地球 + 视图下拉)**、复制链接、在浏览器打开原文,**单条保存的链接另有一个删除按钮**(订阅源的条目没有:下次刷新它还会回来,给「删除」是假承诺);标题下的**标签行里还有「重新抓取」**(和卡片药丸同一个宿主动作,用它换一份新副本时会清掉这篇的译文记录——正文要换成新的 DOM)。正文下面列"同一来源"的其他条目。**页面用脚本画的插图抓不到**:这类 `<figure>` 在服务器 HTML 里只有一个空 div 和一句图注(实测 transformer-circuits.pub 这篇有 84 张),抓取不执行脚本,所以**图注保留在正文里**(它是页面发布的文字,写明了画的是什么),正文上方另给一行「这一页有 N 张插图由页面自己的脚本绘制,抓取时拿不到画面(图注保留在正文里)—— 阅读原文」(宿主有 Sidebar Browser 时这个出口开在应用内;未来 capture 包在场时这里还会出现「渲染抓取」——探测不到就不渲染)。**capture 渲染回来的图能进正文**:页面脚本绘制的图以「内联 SVG + 内联样式」到达,归一化按安全子集保留(形状/文字/defs/渐变/裁剪/symbol 复用;`script`/`foreignObject`/SMIL/事件/外部引用永不放行,`url(` 只许本文档片段)——而不是只剩图注。**组合图的结构同样保**:capture 渲染的组合图(图 + 样式盒搭的属性卡)在 figure 里保留 div/span 容器与白名单样式(布局/绘制属性;`position:fixed`、`z-index` 超限、外部 `url()`、脚本把戏全剥),而且任何被解包的块级容器都留下词边界——相邻文字绝不粘连。**但静态标记里就有 URL 的图会回收**:懒加载站点把真 URL 放在 `data-src` 等属性、`srcset`、`<picture>` 的 `<source>` 或 `<noscript>` 的 no-JS 兜底里(`src` 里只是 1px 占位图),抽取按这个顺序解析候选、取 srcset 里最大的一张;**反过来,把真图直接内联成 base64 data URI 的页面(transformer-circuits 一类)也保**——占位图与真图按载荷大小分道(阈值 512 字符,1px gif 约 70、真图表数千起),小的照旧当占位、穿透到懒加载属性,大的原样保留;正文输出的每个 `<img>` 都带 `referrerpolicy="no-referrer"`(防盗链的 CDN 否则直接 403)。**还有 403 之外的一种拦法**:源站回 `Cross-Origin-Resource-Policy: same-origin`(实测 claude.dev 的全部 `/media` 插图)时,浏览器跨源拿不到像素——直接抓明明 200,页面上却只有裂图加整句 alt。这种图改由宿主侧代抓(公共地址校验、连接钉死在已校验地址、每跳重验、8 MiB 上限、`image/*` 类型闸——宿主自己的 `ctx.web` 缝只给文本体,给不了二进制):正文里加载失败的 `<img>` 会被改指到宿主取回的字节(data: URI),加载正常的图永不走这条代理,宿主也取不回来的保持裂图原样(「阅读原文」仍是出口)。**LaTeXML 的矢量图也保**:arxiv HTML 把图嵌成 `<object type="image/svg+xml" data="…">`,归一化把它改写成带原始宽高的 `<img>`;非图片的 object(PDF、视频)照旧整棵丢弃。**公式是原生渲染的 MathML**:arxiv HTML 版的 `<math alttext="…">` 按安全子集放行(`annotation-xml` 与任何脚本永不放行——前者是 HTML 注射通道),块级公式横向滚动而不是顶破排版 |
|
|
66
66
|
| 翻译(浏览器本地) | 工具条上的**地球**是开关:点亮 = 正文显示译文。它右边的下拉三档:**只看译文**(默认,正文上方一条一次性提示说明「点任意一句可看原文,再点收起」)、**双语对照**(每段下方常驻原文小字,可整段折起)、**只看原文**。翻译单元是**句子**:点一句只展开那**一句**的原文(段落下、左侧细线 + 高亮),再点收起,可同时展开多句;**即使这句话中间夹着链接、被切成几段文本,展开的仍是完整的那一句**(不会显示成碎片),**原文那一行不是控件**(它是给选中引用用的文本,点它不收起;点句子才收起,而且**正在选区里的点击会被忽略**,所以拖选原文或译文都不会把展开的原文收掉)。英文原文按**正文级**排版(13.5px、secondary 墨色、行距 1.62、行间 3px);它**按容器位置分别排边距**(段落旁:贴住所属段落,上 4px / 下 14px;列表项、引用、表格单元格里则在译文下方 5px,**不会被负边距拽上去贴住译文**),**已展开的那一句保留一条很细的下划线**(只有下划线,没有常驻底色/竖条——那是「这句绑定了下面那行原文」的安静标记);配对高亮**只在 hover 时**出现:鼠标移到某句中文上,这一句(含被链接切开的各段)和它对应那一行原文一起高亮;移到原文行上同理反向。**默认不画任何底色或竖条** —— 试过的常驻标记会把整篇点成花的。**全程在浏览器里跑**(Chrome 138+/Edge 148+ 桌面版的 `Translator` API),模型不出设备、不花 token;**译文的句子对会落盘**(见下),地球开关与翻译会话不落——没有这个 API、语言对不可用、或正文本来就是中文时,地球**不出现**。**译文两层落盘**(`$DSH_HOME/state/dsh-reader`,与缓存正文同一目录、同一敏感级、同一个卸载故事):全局**句子记忆**(按 `<源>→<目标>:<句子哈希>` 存原文+译文,5 万句上限)+ **按条目的段落映射**(哈希→译文,键在正文哈希上,正文小幅重抓后逐句复用仍在);合计预算默认 64 MB(订阅管理页的缓存策略同一个动词可调),按「最近使用」跨两层淘汰,**没有 TTL**(重建一份译文要一次手势加逐句模型,不该按小时过期)。效果是:刷新后地球照样等那一下手势,手势之后整篇从磁盘重绘、模型不沾;句子在两份 schema 版本失配时读作 miss、惰性淘汰(浏览器模型升级后不会把旧译文当成新模型的产出) |
|
|
67
67
|
| 新增(弹窗) | 表头加号、空态卡片、墙上尾部虚线卡片三处都能打开同一个**弹窗**(成员 tab 的邀请弹窗同款做法:透明遮罩 + 居中卡片,Esc / 点遮罩关闭),因为「新增」是个瞬时动作,墙没必要被它替换掉。粘进去的地址**存成什么由抓回来的内容决定**(D15):是 feed 就建订阅,是网页就只存这一篇。**arxiv 的 abs/pdf 链接自动升级到官方 HTML 版**(`arxiv.org/html/<id>`,LaTeXML 生成的语义标记:真表格、真公式),HTML 版不存在(404)时回退到粘进来的地址照常走。**DOI 链接先解析成 arXiv 版再落地**(Crossref 取标题与作者 → arXiv 标题搜索 → 标题相似度 + 作者姓重合的双因子闸;配不上就回普通路径,绝不猜),命中时卡片标注来源 DOI;**OpenReview 链接不抓、直接存仅链接**(实测服务器端读不到:API 在 Cloudflare 挑战后、论坛是 JS SPA),原因和建议写在卡片上(有 arxiv 版就粘 arxiv 链接)。**抓不到也一定存下来**:判定从「成功 / 失败」改成「已保存 + 能不能预览」,原因分档写在弹窗里(反爬 / 需要登录 / 不是网页 / 跨站跳转 / 连不上 / HTTP 错误),对应的卡片标成「仅链接」——把读者粘的 URL 丢掉,比留一张只能打开原文的卡更坏 |
|
|
68
68
|
| 订阅管理(页面) | 表头齿轮进入**订阅管理页**:顶部是一行**压缩设置**(每日刷新时间 + 正文保留并排,长说明收进悬停提示)与一行**存储区**(原文缓存读数「N 篇 · 体量」、译文缓存读数「M 篇映射 + S 句 · 体量」、**译文预算**输入(MB,blur/回车提交)、两步的**清空译文缓存**——读数是宿主聚合的,表本身不过线)与两排带小标题的**筛选片**(`按类型`:全部 / 订阅源 / 保存的链接,各带条数;`排序`:按添加时间 / 按名称 / 按抓取时间,默认添加时间倒序),下面每个源一行,**名称与订阅地址都可以就地编辑**(改地址 = 换一份文档,保存后立刻重抓,名称留空则回退成 feed 自己的标题)、类型、条目数、上次抓取时间、失败原因(按分类说人话,不是宿主缝原话)、单独刷新、暂停/恢复、删除,以及**每日刷新时间的配置**。这些问题是关于「源」而不是关于「内容」的,所以它们不在墙上 |
|