@khorsheed/dsh-reader 0.2.0 → 0.2.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 ADDED
@@ -0,0 +1,9 @@
1
+ # 变更记录
2
+
3
+ ## 0.2.1(2026-09-27)
4
+
5
+ 无功能变更。README 截图整理(删除一张从未存在的引用图,全部图片引用可解析);随宿主 0.1.7-rc.2 基线发布波重发(全量构建+测试在 rc.2 基线通过)。
6
+
7
+ ## 0.2.0(2026-09-26)
8
+
9
+ 首个公开发布。
package/README.en.md CHANGED
@@ -1,19 +1,70 @@
1
1
  # @khorsheed/dsh-reader
2
2
 
3
- The **inspiration space**: one **page-type tab** in the right sidebar that gathers RSS/Atom subscriptions and pasted article links onto a single wall. Clicking a card reads the body inside dsh (selectable, so it can be quoted); when the host's fetch cap truncated the body, the page says so in one line and offers a button to the original.
3
+ English | [中文](README.md)
4
4
 
5
- > The package name, tab kind and Remote namespace stay `reader` — they are stable identifiers (renaming them would change a deployed loader row and the on-disk state root). "Inspiration" is the name the reader SEES.
5
+ RSS subscriptions and pasted article links on one wall — click a card and read it right inside dsh, select any passage and quote it into the conversation.
6
+
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
+
9
+ <img src="https://raw.githubusercontent.com/Khorsheed/dsh-web-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
+
11
+ ## Features
12
+
13
+ - **One wall for feeds and links** — RSS/Atom subscriptions and single pasted articles side by side; text search ranked by relevance (exact title → title contains → other fields); the filter (unread / by source / by kind / by tag) shares the search box's local predicates and costs no request; **dedupe is on by default** (three grouping tiers — same entry id, normalized link, folded title plus the same published day — any hit folds), the surviving card carrying a 「N 个重复来源」 badge; the reader's own tags, creatable inline and deletable from the filter popover.
14
+ - **Read the body inside dsh** — the body is extracted as DOM text (exactly what makes passage quoting work): repeated masthead titles are dropped, lazy-loaded images are recovered (`data-src` / `srcset` / `<picture>` / `<noscript>`) and real base64-inline figures are kept, formulas render as native MathML, and arXiv's LaTeXML vector figures survive. A figure the page draws with its own scripts cannot be fetched — its caption stays in the body with one line of explanation. A body the host's fetch cap truncated says 「受限篇幅,内容未完整呈现」 with a 「阅读原文」 button.
15
+ - **On-device translation, no tokens spent** — the browser's own Translator API (Chrome 138+ / Edge 148+, desktop): the model never leaves the device and needs no network. The unit is a SENTENCE — click one sentence to reveal that one sentence's original; three views (translation only / side by side / original only). The detail view and the wall each carry their own globe switch, and the wall decides per FIELD — Chinese is never translated into Chinese. Without that API, with an unavailable pair, or on a body already in Chinese, the globe is simply not rendered.
16
+ - **Translations persist; a reload is free** — the derived sentence pairs are written to the host side (a global sentence memory plus per-entry segment maps; one shared 64 MB budget, LRU, no TTL), so after a reload one click on the globe re-paints the whole article from disk without a single model request.
17
+ - **Fetching runs on the host** — every card carries a state pill (grey = not fetched / blue = fetching / green = full text cached / red = failed); press it and leave the page immediately — the raw page lands on the host's disk first and the browser half extracts it on next open. Full text is **backfilled automatically** (at most 8 entries per refresh, two requests at a time, a failed entry left alone for 6 hours); an entry whose feed publishes full text is cached on its first open with zero requests.
18
+ - **Paste it, it knows what it is** — 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; a DOI link is resolved to its arXiv version first (Crossref + arXiv title search behind a two-factor gate); an OpenReview link is saved link-only with the reason on the card; and a URL that cannot be read is still saved, its cause classified (bot wall / login wall / not a web page / cross-site hop / unreachable / HTTP error).
19
+ - **Quote into the conversation** — the body is an app-level selection, so `@khorsheed/dsh-quote`'s overlay menu already covers it; the detail view also carries **Quote** (selection plus a source label, merged into the current conversation draft through the official input machine — read-merged, never overwritten, never sent) and **Send to side chat** (shown only when the composition really has a `sideChat` service).
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
+ - **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
+
23
+ <img src="https://raw.githubusercontent.com/Khorsheed/dsh-web-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
+
25
+ ## Install
6
26
 
7
- Fetching happens on the **host half** through the sanctioned `ctx.web` egress seam; the browser half only parses and renders. The subscription list and the most recent raw payloads are persisted to `$DSH_HOME/state/dsh-reader/state.json` with atomic `node:fs` writes — **not** through `ctx.fs`, which is the policy-fenced sandboxed filesystem (see Compatibility) — and a daily refresh is armed by a plugin-owned `setTimeout`, since no host scheduler service exists to ride.
27
+ ```sh
28
+ dsh plugin --profile web add @khorsheed/dsh-reader
29
+ ```
8
30
 
9
- ## The three surfaces
31
+ Restart the web instance to activate. Uninstall:
32
+
33
+ ```sh
34
+ dsh plugin --profile web remove @khorsheed/dsh-reader
35
+ ```
36
+
37
+ Exactly one directory is written: `$DSH_HOME/state/dsh-reader/` — `state.json` (the subscription list, the latest payloads, the reader's own tags and the recently-read records) and `bodies/` (one file per body over 256 KiB; the global translation memory is its `translation-memory.json`, and a large per-entry segment map gets its own file). Uninstalling leaves the directory behind — delete the whole `dsh-reader/` for a clean removal.
38
+
39
+ ## Compatibility
40
+
41
+ - npm release line (`@deepseek-ai/dsh@0.1.5-rc.1`): ✅ works, one designed degrade — the `sidebar.right.pane.tab` seat and the `ctx.web.fetch` egress seam are complete on 0.1.5-rc.1; the in-app Sidebar Browser exits (the script-figure notice, a bot-wall card's action) need the `browser` right-Sidebar tab kind from 0.1.6-alpha.2 and fall back to the ordinary external link on a 0.1.5 host. minHost `0.1.5-rc.1`; this package does no preset self-hide (no `pluginInventory` needed).
42
+ - source line (deepseek-harness master): ✅ (verifiedHost: 0.1.5-rc.1)
43
+
44
+ Note: the host fetch seam truncates a decoded body at 100,000 characters and does so **silently** — that is a deployment-configurable cap (see the patch overlay under 「How it works · the egress seam and storage」), not this package's body budget.
45
+
46
+ ## Known Limitations
47
+
48
+ - **Subscribe, do not crawl** — adding a subscription fetches the feed itself and never pre-fetches every entry's body (that would blow up storage and amount to running a crawler); full text arrives a slice per refresh via the backfill, or on the spot when you open an entry.
49
+ - **A PDF (or any non-web file) has no body** — the egress seam accepts only `text/html`, `text/*`, `application/json` and `application/xml`, so `application/pdf` is refused before extraction even starts — and extraction is DOM-based, which a PDF does not have. Such a link is a 「仅链接」 card plus 「阅读原文」; this plugin is not a PDF reader.
50
+ - **A cross-site hop is not followed** — the seam reports the target origin only, never the path, so shorteners and institutional proxies are reported as "this address moves to another site" rather than guessed at.
51
+ - **The unread dot is not persisted** — read state is session state; a reload or restart returns every entry to unread.
52
+ - **Translation needs Chrome 138+ / Edge 148+ on desktop** — Safari, Firefox and mobile have no Translator API, so the globe stays hidden there while everything else keeps working. The trade is deliberate: one 50,000-character article through a host LLM costs tens of thousands of tokens, tens of seconds, and a second copy of the article to the model provider; on-device translation is free, seconds, and stays on the machine (page-translation-grade NMT, not LLM-grade).
53
+ - **Oversized articles cannot be fully extracted** — the seam's default 100,000-character cap (measured: an Anthropic research page ~266k, theverge.com ~899k, newyorker.com ~1.97M characters) leaves the detail view showing the truncation notice plus 「阅读原文」; raising the cap is a deployment decision, and the plugin never works around the host's egress policy itself.
54
+
55
+ ## How it works
56
+
57
+ <details>
58
+ <summary>The surfaces (wall / detail / translation / add / subscriptions / recently read — click to expand)</summary>
59
+
60
+ > The package name, tab kind and Remote namespace stay `reader` — they are stable identifiers (renaming them would change a deployed loader row and the on-disk state root). "Inspiration" is the name the reader SEES.
10
61
 
11
62
  | Surface | Contents |
12
63
  | --- | --- |
13
- | 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 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 |
14
- | 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|
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 |
15
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 |
16
- | 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. **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 |
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 |
17
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 |
18
69
  | Recently read (page) | The header's **clock** opens the **recently read page**: the entries you opened, **newest first** (title, source, "how long ago"), where clicking one **reads it again**; a bin in the page bar **clears the whole list**. It records "what was I just reading", so it is **persisted** (`state.json`'s `recent`, **at most 100 rows**; reopening an entry moves it back to the top instead of duplicating it) and survives a reload and a restart — unlike the pane's own memory (the open article, the reading position, the translation), which is shared **across dsh sessions but not across a reload** (it is per page — see above); `recent` is deployment-level and outlives a restart. **A row stores only the entry id, source id, title, URL and time** — never the body — so the page can still reopen an entry that has **rolled out of its feed's window** (as long as the source is still there). An entry whose source was deleted is still listed but cannot be opened, and says so |
19
70
 
@@ -23,38 +74,37 @@ Fetching happens on the **host half** through the sanctioned `ctx.web` egress se
23
74
 
24
75
  **Click semantics (settled as option B)**: a card click opens the detail view. Most reading happens in dsh; when the body is too long or the reader wants the original, the detail view's own button goes to the browser. The trailing chevron is a cue that the card leads somewhere, not a control.
25
76
 
26
- ## Quoting
27
-
28
- The body is an app-level selection, so `@khorsheed/dsh-quote`'s overlay menu **already covers it** — this package builds no selection menu of its own. The detail view also carries:
77
+ **Quoting**: the body is an app-level selection, so `@khorsheed/dsh-quote`'s overlay menu **already covers it** — this package builds no selection menu of its own. The detail view also carries:
29
78
 
30
79
  - **Quote**: the selection (the whole entry when there is none) plus a source label, merged into the current conversation draft through the official input machine (the live draft is read and merged, never overwritten, never sent);
31
80
  - **Sent to side chat**: shown only when the composition really has a `sideChat` service (the capability handshake decides), hidden otherwise.
32
81
 
33
- `reader` ships its own thin typert Remote (namespace `reader`) with the verbs `capabilities` / `listSources` / `addSource` / `updateSource` / `removeSource` / `refresh` / `getBodies` / `quoteToSideChat`. The host half **never parses** a feed — parsing lives in the browser half, and `getBodies` only hands the raw payload across.
82
+ </details>
34
83
 
35
- ## Install
84
+ <details>
85
+ <summary>Internals (architecture, the egress seam, translation, storage — click to expand)</summary>
36
86
 
37
- ```sh
38
- dsh plugin --profile web add @khorsheed/dsh-reader
39
- # uninstall:
40
- dsh plugin --profile web remove @khorsheed/dsh-reader
41
- ```
87
+ **Architecture.** Fetching happens on the **host half** through the sanctioned `ctx.web` egress seam; the browser half only parses and renders. The host half provides the `reader` service core (`ctx.provide('reader')`) and mounts its thin Typert Remote face (namespace `reader`); the subscription list and the most recent raw payloads are persisted to `$DSH_HOME/state/dsh-reader/state.json` with atomic `node:fs` writes (not through `ctx.fs` — see below), and the daily refresh is armed by a plugin-owned `setTimeout`, since no host scheduler service exists to ride. The browser half registers the right sidebar in two stages: first its own minted tab type `reader` (implementation id `@khorsheed/dsh-reader`, with a guide card) into the `sidebarRightTabs` registry, then the pane body into the keyed `sidebar.right.pane.tab` seat via `slots.inject`; the zh/en dictionaries register with the plugin, and the Remote is mounted by the client itself via `ctx.remote.$mount`. Feed parsing has to live in the browser — the host has no XML/DOM parser (`globalThis.DOMParser === false`) — so `getBodies` only hands the raw payload across. There is no `static inject`: every optional capability (`fs`, `web`, `sideChat`, the `browser` tab kind, the capture Remote) is probed at the point of use and degrades, never throws.
42
88
 
43
- Restart the host afterwards. Exactly one directory is written: `$DSH_HOME/state/dsh-reader/` — `state.json` (the subscription list, the latest payloads, the reader's own tags and the 「最近阅读」 records) and `bodies/` (one file per body over 256 KiB; the global translation memory is its `translation-memory.json`, and a large per-entry segment map gets its own file). Uninstalling leaves the directory behind — delete the whole `dsh-reader/` for a clean removal.
89
+ **Why state does not go through `ctx.fs`**: `ctx.fs` is the **sandboxed** filesystem — every mutation is fenced by the FILE POLICY OF THE CALLING SESSION, so a workspace-write session is refused a write under `$DSH_HOME/state` (`FS_SANDBOX_DENIED`, observed on the acceptance instance). The fence is right and stays; the mistake was aiming a session-scoped capability at deployment-owned state. This package therefore writes with `node:fs` (atomic temp-file plus rename), the way this repo's other host-state packages do (`packages/lab/src/state.ts`), and degrades to memory-only when the root is unwritable.
44
90
 
45
- ## Compatibility
91
+ **The Remote face.** Namespace `reader`, verbs by group: sources — `capabilities` / `listSources` / `addSource` / `updateSource` / `removeSource` / `refresh`; bodies — `getBodies` / `getEntryBody` / `fetchEntryBody` / `storeEntryBody` / `getRawBody` / `entryFetchStates` / `listBackfillCandidates`; translations — `getEntryTranslation` / `getSentenceTranslations` / `rememberSentences` / `clearTranslations`; tags — `entryTags` / `listTags` / `createTag` / `tagEntry` / `renameTag` / `deleteTag` / `pruneTags`; reading records — `recordRead` / `listRecent` / `clearRecent`; cache policy and readouts — `getCachePolicy` / `setCachePolicy` / `getStorageStats`; plus `quoteToSideChat`. The host half **never parses** a feed — parsing lives in the browser half.
92
+
93
+ **Sources are named by the feed's own title**: the host half cannot parse a feed (it has no XML parser), so a new source starts with a URL-derived label — the `raw.githubusercontent.com/...` you see in the strip. Once the browser half has parsed `<channel><title>`, that real title replaces the display label, so cards read "Anthropic Research"; it falls back to the URL when parsing fails. The `{count} d ago` placeholder the list showed for an errored source was the other face of the same class of bug (an uninterpolated parameter) and is fixed.
94
+
95
+ **Translation uses the browser's own on-device model, not the host's**: `self.Translator` (Chrome 138+ / Edge 148+, desktop only) translates inside the page's own process, so **the model never leaves the device, spends no tokens and needs no network** (the first use downloads a small language pack per pair, and `Translator.create()` requires user activation — the click on the globe is exactly that). **The derived sentence pairs are written to the host side** (the global sentence memory and the per-entry maps under `$DSH_HOME/state/dsh-reader`, a shared 64 MB budget, LRU, no TTL; derived content of pages the reader read, the same sensitivity class as the cached originals beside them — the privacy invariant was always about the network, not the disk).
96
+
97
+ **The translation unit is a sentence — not a paragraph, not a text node**: sentences are cut BEFORE the request, so the pairing is true by construction rather than guessed afterwards — a paragraph-level translation comes back as one block, the model merges and splits sentences, and re-aligning the two sides afterwards can show the wrong original. Batches separate sentences with a sentinel and, when the returned part count does not match, that batch is re-sent one unit per request: **granularity is what the fallback gives up, never correctness**. A sentence that crosses an inline element (a link inside it) becomes two units and the link itself is never touched; nothing inside `<pre>`/`<code>` is translated.
98
+
99
+ **A translator that cannot be built is reported, never faked**: `Translator.availability()` is a HINT, not a promise — on Chrome a pair has been observed answering "available" and then rejecting `create()` with `NotSupportedError` ("Unable to create translator for the given source and target language"; Chromium has the same failure recorded for macOS builds). So the plugin (1) asks the browser's `LanguageDetector` for the source language first — a wrong source is one way a pair cannot be built, and that error names NEITHER language; (2) actually tries the target in both accepted spellings, `zh` then `zh-Hans`; and (3) when every attempt is rejected as unsupported, HIDES the globe and says which pairs it tried while pointing at `chrome://on-device-internals` ("Broker State"). Non-permanent failures (download, quota) keep the globe so the reader can retry, and report the attempted pairs plus the reason.
100
+
101
+ **Restoring the body is byte-exact**: translation only rewrites text nodes (the originals stay in memory), so "original only" and cancel put back exactly the HTML the host sent. That is also why a translation cannot lose links, images or tables — they are never touched.
102
+
103
+ **Some sites refuse automatic fetches, and the button says so honestly**: measured, `openai.com` answers **403 to every non-browser request** (with a proxy, without one, and with a browser User-Agent — it returns a bot-challenge page). That is not an extraction failure and no retry fixes it, so for a 401/403 the detail view says "this publisher refuses automatic fetches" instead of "could not extract". **The status code is not the only signal**: an `openreview.net` PDF link was measured answering `302 → /challenge?redirect=…` and then a **200** challenge page — trusting the status code stores that page as the article, which is exactly how "I added it and there is no content" happened. So the decision is a combination: whether the final URL moved, whether the visible text is too short to be a page, and the words a bot or login page leaves behind; any hit means the payload is NOT stored as a body and the link is saved as 「仅链接」. Newsletter platforms and paywalls behave the same way.
104
+
105
+ **Some feeds publish full text for some posts and only a summary for others**: measured on the OpenAI alignment feed, whose 27 entries run from **155 characters** (a one-line summary) to **62,044** (full text). Nothing is truncated; that is how it is published. **A summary is no longer mistaken for the article**: the feed's `<description>` used to be promoted into `contentHtml`, so the entry looked complete — the backfill skipped it, opening it fetched nothing, and the detail view rendered those 167 characters as the whole paper. Such entries are now flagged `summaryOnly`: opening one fetches the real article on the spot (the detail view says 「这条订阅源只发布了摘要 —— 全文在原文页面上」 while it does, with the original-page link, and the line disappears when the text arrives) and the automatic backfill counts them as text it still owes. **That fetch now happens only while the entry has no cached body**: `summaryOnly` is a property of the FEED entry and is true forever, so this used to re-fetch an already-fetched article on EVERY open — replacing its DOM, and its translation, with the same bytes. The fetch is now gated on the text on screen being the feed's own (`fromFeed`).
46
106
 
47
- - npm release line (`@deepseek-ai/dsh@0.1.5-rc.1`): ✅ complete — the `sidebar.right.pane.tab` seat, the `ctx.web.fetch` egress seam, version-guarded `ctx.fs` writes; no `pluginInventory` needed (this package does no preset self-hide).
48
- - Source line (deepseek-harness master): ✅ (verifiedHost: 0.1.5-rc.1)
49
- - **Why state does not go through `ctx.fs`**: `ctx.fs` is this repo's **sandboxed** filesystem — every mutation is fenced by the FILE POLICY OF THE CALLING SESSION, so a workspace-write session is refused a write under `$DSH_HOME/state` (`FS_SANDBOX_DENIED`, observed on the acceptance instance). The fence is right and stays; the mistake was aiming a session-scoped capability at deployment-owned state. This package therefore writes with `node:fs` (atomic temp-file plus rename), the way this repo's other host-state packages do (`packages/lab/src/state.ts`), and degrades to memory-only when the root is unwritable.
50
- - **Sources are named by the feed's own title**: the host half cannot parse a feed (it has no XML parser), so a new source starts with a URL-derived label — the `raw.githubusercontent.com/...` you see in the strip. Once the browser half has parsed `<channel><title>`, that real title replaces the display label, so cards read "Anthropic Research"; it falls back to the URL when parsing fails. The `{count} d ago` placeholder the list showed for an errored source was the other face of the same class of bug (an uninterpolated parameter) and is fixed.
51
- - **Translation uses the browser's own on-device model, not the host's**: `self.Translator` (Chrome 138+ / Edge 148+, desktop only) translates inside the page's own process, so **the model never leaves the device, spends no tokens and needs no network** (the first use downloads a small language pack per pair, and `Translator.create()` requires user activation — the click on the globe is exactly that). **The derived sentence pairs are written to the host side** (the global sentence memory and the per-entry maps under `$DSH_HOME/state/dsh-reader`, a shared 64 MB budget, LRU, no TTL; derived content of pages the reader read, the same sensitivity class as the cached originals beside them — the privacy invariant was always about the network, not the disk). The price is availability: Safari, Firefox and mobile have no such API, and the plugin then hides the globe while everything else keeps working. That trade is deliberate: sending one 50,000-character article through a host LLM costs tens of thousands of tokens, tens of seconds, and a second copy of the article to the model provider, while on-device translation is free, seconds and stays on the machine (its quality is page-translation-grade NMT, not LLM-grade).
52
- - **The translation unit is a sentence — not a paragraph, not a text node**: sentences are cut BEFORE the request, so the pairing is true by construction rather than guessed afterwards — a paragraph-level translation comes back as one block, the model merges and splits sentences, and re-aligning the two sides afterwards can show the wrong original. Batches separate sentences with a sentinel and, when the returned part count does not match, that batch is re-sent one unit per request: **granularity is what the fallback gives up, never correctness**. A sentence that crosses an inline element (a link inside it) becomes two units and the link itself is never touched; nothing inside `<pre>`/`<code>` is translated.
53
- - **A translator that cannot be built is reported, never faked**: `Translator.availability()` is a HINT, not a promise — on Chrome a pair has been observed answering "available" and then rejecting `create()` with `NotSupportedError` ("Unable to create translator for the given source and target language"; Chromium has the same failure recorded for macOS builds). So the plugin (1) asks the browser's `LanguageDetector` for the source language first — a wrong source is one way a pair cannot be built, and that error names NEITHER language; (2) actually tries the target in both accepted spellings, `zh` then `zh-Hans`; and (3) when every attempt is rejected as unsupported, HIDES the globe and says which pairs it tried while pointing at `chrome://on-device-internals` ("Broker State"). Non-permanent failures (download, quota) keep the globe so the reader can retry, and report the attempted pairs plus the reason.
54
- - **Restoring the body is byte-exact**: translation only rewrites text nodes (the originals stay in memory), so "original only" and cancel put back exactly the HTML the host sent. That is also why a translation cannot lose links, images or tables — they are never touched.
55
- - **Some sites refuse automatic fetches, and the button says so honestly**: measured, `openai.com` answers **403 to every non-browser request** (with a proxy, without one, and with a browser User-Agent — it returns a bot-challenge page). That is not an extraction failure and no retry fixes it, so for a 401/403 the detail view says "this publisher refuses automatic fetches" instead of "could not extract". **The status code is not the only signal**: an `openreview.net` PDF link was measured answering `302 → /challenge?redirect=…` and then a **200** challenge page — trusting the status code stores that page as the article, which is exactly how "I added it and there is no content" happened. So the decision is a combination: whether the final URL moved, whether the visible text is too short to be a page, and the words a bot or login page leaves behind; any hit means the payload is NOT stored as a body and the link is saved as 「仅链接」. Newsletter platforms and paywalls behave the same way. It is also why browser-rendered fetching is out of scope here: that needs the browser pane, which is still `planned`.
56
- - **Some feeds publish full text for some posts and only a summary for others**: measured on the OpenAI alignment feed, whose 27 entries run from **155 characters** (a one-line summary) to **62,044** (full text). Nothing is truncated; that is how it is published. **A summary is no longer mistaken for the article**: the feed's `<description>` used to be promoted into `contentHtml`, so the entry looked complete — the backfill skipped it, opening it fetched nothing, and the detail view rendered those 167 characters as the whole paper. Such entries are now flagged `summaryOnly`: opening one fetches the real article on the spot (the detail view says 「这条订阅源只发布了摘要 —— 全文在原文页面上」 while it does, with the original-page link, and the line disappears when the text arrives) and the automatic backfill counts them as text it still owes. **That fetch now happens only while the entry has no cached body**: `summaryOnly` is a property of the FEED entry and is true forever, so this used to re-fetch an already-fetched article on EVERY open — replacing its DOM, and its translation, with the same bytes. The fetch is now gated on the text on screen being the feed's own (`fromFeed`).
57
- - **The host hands over at most 100,000 characters per payload, so an oversized feed can only be read in part**: this is the egress seam's default (`web-fetch-http`'s `maxBodyChars`), NOT this package's body budget (256 KB per source, 2 MiB total). The acceptance instance's feed is 646,905 characters; the host supplies the first 100,000, so only the two entries falling inside that prefix are readable. The plugin **cannot** work around it: `ctx.web.fetch` accepts `{ url }` only — no Range, no custom headers, no pagination — and vendoring `fetch` to bypass it would bypass the host's egress policy, which is not an option. Reading such a feed whole needs **no host code change**: the official patch overlay adds config to that row (`--patch <file>`, or the machine-level `$DSH_HOME/cordis.patch.yml` which applies to every profile):
107
+ **The host hands over at most 100,000 characters per payload, so an oversized feed can only be read in part**: this is the egress seam's default (`web-fetch-http`'s `maxBodyChars`), NOT this package's body budget (2 MiB per source, 12 MiB total — see `MAX_BODY_CHARS_PER_SOURCE`). The acceptance instance's feed is 646,905 characters; the host supplies the first 100,000, so only the two entries falling inside that prefix are readable. The plugin **cannot** work around it: `ctx.web.fetch` accepts `{ url }` only — no Range, no custom headers, no pagination — and vendoring `fetch` to bypass it would bypass the host's egress policy, which is not an option. Reading such a feed whole needs **no host code change**: the official patch overlay adds config to that row (`--patch <file>`, or the machine-level `$DSH_HOME/cordis.patch.yml` which applies to every profile):
58
108
 
59
109
  ```yaml
60
110
  - id: web-fetch-http
@@ -62,29 +112,33 @@ Restart the host afterwards. Exactly one directory is written: `$DSH_HOME/state/
62
112
  maxBodyChars: 2000000 # default 100000
63
113
  ```
64
114
 
65
- Verified with `dsh --profile web --patch <file> --dump-config` (the composed tree carries the value), and then applied on the acceptance instance: the same feed went from "100,000 characters, truncated, 2 entries" to "**646,905 characters, untruncated, 21 complete entries**". **Raised again on 2026-09-19**: research articles are large in their own right — the transformer-circuits.pub emotions paper is a **41.8 MB** HTML document (60 of its figures are inline base64), and the seam's BYTE cap (`maxResponseBytes`) defaults to 5 MB, so the whole fetch fails (`response exceeds the maximum of 5000000 bytes`) with `maxBodyChars` waiting behind it. The acceptance instance's deployment config now raises **both** to 64 MB and the fetch **timeout from its 30 s default to 180 s** (that page takes 19.5 s on a direct connection, so a busy machine used to fail it with `web fetch timed out`; same patch row, `$DSH_HOME/profiles/web/cordis.patch.yml`). The price is machine-wide: peak memory and transfer per fetch go up with it. Note that this package's OWN per-source budget must not sit below the host's cap, or storing the payload re-cuts it — it is 2 MiB per source, 12 MiB total. The boundary that remains is ownership — a plugin patch mounts its OWN row only (the repo forbids one package mounting or editing another's row), so this override is a **deployment configuration** decision, and it raises the cap for **every web fetch on the machine**.
66
- - **Full text is filled in automatically — there is no button to press**: after each load the host reports which entries still lack text (at most 8 per run; a failed entry is left alone for 6 hours), and the browser half fetches + extracts them into a 24-hour cache (configurable on the subscription page), **two requests at a time**. An entry being filled keeps showing the feed's own text, the footer reports "Filling in 3/8…", and a completed card carries a "Full text" badge (meaning: the card still shows the feed's own summary, and the full text is cached — open it to read). **Do not want to wait for the slice? Open that entry**: if no body is held locally, opening it fetches once right there and says 「正在抓取正文…」 instead of waiting for the backfill's turn.
67
- - Why not fetch them all at once: that would crawl every article of every subscription. This takes a slice per refresh and moves forward.
68
- - **What arrives is what is shown**: a publisher refusing us (401/403), an unreachable host, and an unreadable page are three different sentences, and the body area always shows the feed's own content plus "read the original" — never a blank page.
69
- - **A feed the host truncates is salvaged, not discarded**: the egress seam caps a payload by CHARACTER COUNT, and a cap that lands inside a tag makes the whole document malformed — which used to mean **no entries at all**. The parser now keeps every complete `<item>`/`<entry>` (re-wrapping each fragment with the document root's namespace declarations, without which a fragment carrying `content:encoded` is itself malformed), repairs the cut-off block best-effort and flags it **partial** so its arrived text still renders, and the wall shows an "incomplete" line with the original-page link. The shape is pinned by a committed fixture (`tests/fixtures/feed-truncated.xml`).
70
- Measured: the acceptance instance's 646 KB feed (of which the host hands over 100,000 characters) now yields **2 cards** — one complete 42 KB body and one partial — where before this fix it yielded **0**.
71
- - **The default filter is "all", not "today"**: a feed's newest entry is often not from today (the acceptance instance's feed was 8 days old), and a default `today` filter made a freshly subscribed wall look empty — which reads as "the subscribe did not work". There is **no today filter at all** now: the four ways to narrow the wall (unread / by source / by kind / by tag) live in the filter popover, and the button's own pressed state plus the `#sourceId` / `#rss` / `#link` / `@tagId` query it writes into the search box keep "what is hidden" visible.
72
- - **A body over 256 KiB is stored as its own file**: `state.json` keeps only its metadata (file name and character count) and the markup itself lives in `$DSH_HOME/state/dsh-reader/bodies/<hash>.html`. The article-retention setting (12/24/168 hours or forever) governs both kinds identically, and eviction deletes the file with the metadata. The reason is that `state.json` is read and rewritten as a whole: a paper whose figures are inlined as base64 extracts to tens of megabytes, and inlining those would make every reader operation pay a multi-megabyte round trip.
73
- - **A repeated title at the top of the body is dropped**: the detail view already renders the entry title, and the article's own HTML often carries it again (measured: TWO `<h1>`s, preceded by the site name and a logo link). Extraction removes top-level headings that repeat the document title, plus the leading masthead and blank lines, so the body starts at real content ("Authors").
74
- - **The body has no reading measure**: paragraphs and lists fill the width the sidebar is given — no `max-width` (the earlier paragraph-only cap left a ragged band of empty space on the right that read as a broken layout). Want a shorter line? Narrow the sidebar.
75
- - **The host fetch cap is a known limit**: `ctx.web.fetch` truncates a decoded body at 100,000 characters and does so **silently**. Articles past that cannot be fully extracted (measured: an Anthropic research page ~266k, theverge.com ~899k, newyorker.com ~1.97M characters); a summary page's title, author and description still arrive. The detail view therefore shows 「受限篇幅,内容未完整呈现」 with a 「阅读原文」 button. RSS feeds are mostly safe (measured: HN 11KB, BBC 25KB, 阮一峰 69KB, Simon Willison 83KB — 83KB already sits against the cap).
76
- - **Degrade matrix**: no `fs` → state is memory-only and lost on restart (the handshake reports `hasFs: false` rather than pretending persistence exists); no `web` → a pasted link is still saved, marked 「仅链接 (unreachable)」 with 「阅读原文」, and no exception is thrown and no URL is lost; no `sideChat` → the side-chat action is hidden; no `quote` → the selection overlay is simply absent, while the entry-level Quote button keeps working. A host below 0.1.6-alpha.2 (no `browser` right-Sidebar tab kind) → the spots that want a real browser (the script-figure notice, the bot-wall card action) offer the ordinary external link instead, without an error.
77
- - **A cross-origin redirect is followed only when it can be reconstructed**: the host seam refuses one (`WEB_REDIRECT_BLOCKED`) and reports the target **origin only — never the path**. So this service re-enters the seam only for a same-HOST scheme/port change (`http://host/feed` → `https://host/feed`, path and query preserved) and otherwise reports that the address moves to another site instead of following it — fetching the bare origin would return a different page and file it under the reader's URL. The earlier text here claimed "up to 3 hops"; that follow path was dead code, because it looked for the text `WEB_REDIRECT_BLOCKED` in an error MESSAGE that never contains it (the host puts it in `error.code`), so every cross-origin redirect surfaced as the seam's raw sentence. It now reads the code, with a regression test.
78
- - **Refusals become 「仅链接」 cards**: a fetched page with almost no readable text (under 200 visible characters after script/style are stripped) that also carries bot-wall or login-wall markers is never stored as an article — **a challenge page never becomes a card's body** — and `application/pdf` and friends are refused outright by the egress seam (`WEB_UNSUPPORTED_CONTENT_TYPE`: only html/text/json/xml). Those cards stay on the wall, take tags, copy their link and can be deleted; opening one shows the reason and 「阅读原文」.
79
- - **A permanent refusal is not retried automatically**: bot walls, login walls, PDFs, HTTP errors and empty pages are recorded as non-retryable (`isRetryablePreviewFailure` lets only `unreachable` through), so the automatic backfill stops knocking; the subscription page's per-source Refresh still retries by hand and clears the record on success. That is what keeps the plugin from tapping a site that will always say no every six hours.
80
- - **No conditional requests**: the host seam exposes no custom-request-header entry point, so there is no ETag/If-None-Match and every refresh is a full fetch.
81
- - **Nothing behind a login wall**: there is no credential-injection surface.
115
+ Verified with `dsh --profile web --patch <file> --dump-config` (the composed tree carries the value), and then applied on the acceptance instance: the same feed went from "100,000 characters, truncated, 2 entries" to "**646,905 characters, untruncated, 21 complete entries**". **Raised again on 2026-09-19**: research articles are large in their own right — the transformer-circuits.pub emotions paper is a **41.8 MB** HTML document (60 of its figures are inline base64), and the seam's BYTE cap (`maxResponseBytes`) defaults to 5 MB, so the whole fetch fails (`response exceeds the maximum of 5000000 bytes`) with `maxBodyChars` waiting behind it. The acceptance instance's deployment config now raises **both** to 64 MB and the fetch **timeout from its 30 s default to 180 s** (that page takes 19.5 s on a direct connection, so a busy machine used to fail it with `web fetch timed out`). The price is machine-wide: peak memory and transfer per fetch go up with it. The boundary that remains is ownership — a plugin patch mounts its OWN row only (the repo forbids one package mounting or editing another's row), so this override is a **deployment configuration** decision, and it raises the cap for **every web fetch on the machine**.
82
116
 
83
- ## Known Limitations
117
+ **Full text is filled in automatically — there is no button to press**: after each load the host reports which entries still lack text (at most 8 per run; a failed entry is left alone for 6 hours), and the browser half fetches + extracts them into a 24-hour cache (configurable on the subscription page), **two requests at a time**. An entry being filled keeps showing the feed's own text, the footer reports "Filling in 3/8…", and a completed card carries a "Full text" badge (meaning: the card still shows the feed's own summary, and the full text is cached — open it to read). **Do not want to wait for the slice? Open that entry**: if no body is held locally, opening it fetches once right there and says 「正在抓取正文…」 instead of waiting for the backfill's turn.
118
+
119
+ - Why not fetch them all at once: that would crawl every article of every subscription. This takes a slice per refresh and moves forward.
120
+ - **What arrives is what is shown**: a publisher refusing us (401/403), an unreachable host, and an unreadable page are three different sentences, and the body area always shows the feed's own content plus "read the original" — never a blank page.
121
+
122
+ **A feed the host truncates is salvaged, not discarded**: the egress seam caps a payload by CHARACTER COUNT, and a cap that lands inside a tag makes the whole document malformed — which used to mean **no entries at all**. The parser now keeps every complete `<item>`/`<entry>` (re-wrapping each fragment with the document root's namespace declarations, without which a fragment carrying `content:encoded` is itself malformed), repairs the cut-off block best-effort and flags it **partial** so its arrived text still renders, and the wall shows an "incomplete" line with the original-page link. The shape is pinned by a committed fixture (`tests/fixtures/feed-truncated.xml`). Measured: the acceptance instance's 646 KB feed (of which the host hands over 100,000 characters) now yields **2 cards** — one complete 42 KB body and one partial — where before this fix it yielded **0**.
123
+
124
+ **Refusals become 「仅链接」 cards**: a fetched page with almost no readable text (under 200 visible characters after script/style are stripped) that also carries bot-wall or login-wall markers is never stored as an article — **a challenge page never becomes a card's body** — and `application/pdf` and friends are refused outright by the egress seam (`WEB_UNSUPPORTED_CONTENT_TYPE`: only html/text/json/xml). Those cards stay on the wall, take tags, copy their link and can be deleted; opening one shows the reason and 「阅读原文」.
125
+
126
+ **A permanent refusal is not retried automatically**: bot walls, login walls, PDFs, HTTP errors and empty pages are recorded as non-retryable (`isRetryablePreviewFailure` lets only `unreachable` through), so the automatic backfill stops knocking; the subscription page's per-source Refresh still retries by hand and clears the record on success. That is what keeps the plugin from tapping a site that will always say no every six hours.
127
+
128
+ **A cross-origin redirect is followed only when it can be reconstructed**: the host seam refuses one (`WEB_REDIRECT_BLOCKED`) and reports the target **origin only — never the path**. So this service re-enters the seam only for a same-HOST scheme/port change (`http://host/feed` → `https://host/feed`, path and query preserved) and otherwise reports that the address moves to another site instead of following it — fetching the bare origin would return a different page and file it under the reader's URL. The decision reads `error.code`, not the error message text (the host puts `WEB_REDIRECT_BLOCKED` on `error.code`), with a regression test.
129
+
130
+ **No conditional requests**: the host seam exposes no custom-request-header entry point, so there is no ETag/If-None-Match and every refresh is a full fetch. **Nothing behind a login wall**: there is no credential-injection surface.
131
+
132
+ **A body over 256 KiB is stored as its own file**: `state.json` keeps only its metadata (file name and character count) and the markup itself lives in `$DSH_HOME/state/dsh-reader/bodies/<hash>.html`. The article-retention setting (12/24/168 hours or forever) governs both kinds identically, and eviction deletes the file with the metadata. The reason is that `state.json` is read and rewritten as a whole: a paper whose figures are inlined as base64 extracts to tens of megabytes, and inlining those would make every reader operation pay a multi-megabyte round trip.
133
+
134
+ **A repeated title at the top of the body is dropped**: the detail view already renders the entry title, and the article's own HTML often carries it again (measured: TWO `<h1>`s, preceded by the site name and a logo link). Extraction removes top-level headings that repeat the document title, plus the leading masthead and blank lines, so the body starts at real content ("Authors").
135
+
136
+ **The default filter is "all", not "today"**: a feed's newest entry is often not from today (the acceptance instance's feed was 8 days old), and a default `today` filter made a freshly subscribed wall look empty — which reads as "the subscribe did not work". There is **no today filter at all** now: the four ways to narrow the wall (unread / by source / by kind / by tag) live in the filter popover, and the button's own pressed state plus the `#sourceId` / `#rss` / `#link` / `@tagId` query it writes into the search box keep "what is hidden" visible.
137
+
138
+ **Degrade matrix**: no `fs` → state is memory-only and lost on restart (the handshake reports `hasFs: false` rather than pretending persistence exists); no `web` → a pasted link is still saved, marked 「仅链接 (unreachable)」 with 「阅读原文」, and no exception is thrown and no URL is lost; no `sideChat` → the side-chat action is hidden; no `quote` → the selection overlay is simply absent, while the entry-level Quote button keeps working; no capture package → the 「渲染抓取」 action on the script-figure notice does not render; a host below 0.1.6-alpha.2 (no `browser` right-Sidebar tab kind) → the spots that want a real browser (the script-figure notice, the bot-wall card action) offer the ordinary external link instead, without an error.
139
+
140
+ </details>
141
+
142
+ ## Development
84
143
 
85
- - **Subscribe, do not crawl**: adding a subscription fetches the feed itself and never pre-fetches every entry's body (that would blow up storage and amount to running a crawler). Opening one entry pays for one fetch.
86
- - **A PDF (or any non-web file) has no body**: the egress seam accepts only `text/html`, `text/*`, `application/json` and `application/xml`, so `application/pdf` is refused before extraction even starts — and extraction is DOM-based, which a PDF does not have. Such a link is a 「仅链接」 card plus 「阅读原文」; this plugin is not a PDF reader.
87
- - **A cross-site hop is not followed**: the seam reports the target origin only, so the path cannot be reconstructed. Shorteners and institutional proxies are therefore reported as "this address moves to another site" rather than guessed at.
88
- - **Read state and bodies are not persisted**: they are session state. Persisting a per-entry read cursor would need a second, write-heavy document, and "what is new since I last looked" is the whole value of this plugin.
89
- - **XML parsing lives in the browser**: the host has no XML/DOM parser (`globalThis.DOMParser === false`), so feed parsing has to happen client-side.
90
- - **No browser-pane dependency**: `proposals/active/2026-09-13-browser-pane.md` is still `planned`, and canvas CDP frames cannot host selection quoting.
144
+ Part of the [dsh-plugins](https://github.com/Khorsheed/dsh-plugins) monorepo (`packages/dsh-reader`). Issues and contributions welcome there.
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: eb4fd3ca3636e3329283829f1734f6708699aa78
6
- packages/dsh-reader/README.md: 212a805160058c7cfbdccded74890ca3543f0f0b
5
+ packages/dsh-reader/README.en.md: 8a9acc5ce76c0279eb43531930e13f0091f1620f
6
+ packages/dsh-reader/README.md: 800838d550cd359e8fe4740914f6086b50f3160f
package/README.md CHANGED
@@ -1,12 +1,63 @@
1
1
  # @khorsheed/dsh-reader
2
2
 
3
- **灵感空间**:右侧栏一个**页面型 tab**,把 RSS/Atom 订阅和随手粘贴的文章链接收在同一面「灵感墙」上。点卡片在 dsh 里直接读正文(可划选引用),正文受宿主抓取上限截断时明说「内容未完整呈现」并给一个去原文的按钮。
3
+ [English](README.en.md) | 中文
4
4
 
5
- > 包名、tab kind、Remote namespace 都还是 `reader` —— 它们是稳定标识(改了等于改部署的 loader 行和落盘目录),「灵感空间」是**用户看到的名字**。
5
+ RSS 订阅和随手粘来的文章链接收在同一面墙上——点开就在 dsh 里读,划选就能引用进对话。
6
+
7
+ 刷到的论文和文章散落在浏览器标签页里,想喂给 agent 还得复制粘贴。这个插件在右侧栏加一个页面型 tab「灵感空间」:订阅源每天按点自动刷新,链接粘进来就存成卡片;正文在 dsh 里直接渲染成可读文本(能划选引用、能用浏览器自己的端上模型翻成中文),抓不全或站点拒抓都如实标明,绝不给一堵看起来正常、实则空心的墙。
8
+
9
+ <img src="https://raw.githubusercontent.com/Khorsheed/dsh-web-basic/main/docs/screenshots/reader-1.png" width="640" alt="灵感空间的墙:订阅与保存链接的卡片流,表头一行是筛选、排序、整墙翻译、刷新、设置与新增,搜索框独占一行">
10
+
11
+ ## 特性
12
+
13
+ - **一面墙收齐订阅和链接**——RSS/Atom 订阅与粘贴的单篇文章同墙排列;文字搜索按相关性排(标题完全命中 → 标题包含 → 其它字段命中),筛选(未读 / 按来源 / 按类型 / 按标签)与搜索共用同一套本地谓词、不花请求;**转载去重默认开**(同条目 id / 归一化链接 / 折叠标题 + 同一发布日三层归并,任一命中即同组),幸存卡带「N 个重复来源」徽章;读者自建标签随打随用,在筛选弹层里也能删。
14
+ - **点卡片就在 dsh 里读正文**——正文抽取为 DOM 文本(这正是划选引用能工作的原因):去掉与标题重复的站头,还原懒加载图片(`data-src` / `srcset` / `<picture>` / `<noscript>`)与 base64 内联真图,公式以原生 MathML 渲染,arXiv 的 LaTeXML 矢量图保留;页面脚本画的图抓不到画面,但图注保留在正文里并另给一行说明。被宿主抓取上限截断时显示「受限篇幅,内容未完整呈现」+「阅读原文」。
15
+ - **端上翻译,模型不出设备**——用浏览器自己的 Translator API(Chrome 138+ / Edge 148+ 桌面版),不花 token、不联网;翻译单元是**句子**,点一句只展开那一句的原文;三档视图(只看译文 / 双语对照 / 只看原文)。详情页与整面墙各有一个翻译开关,墙上逐字段判语言、绝不中译中。没有这个 API、语言对不可用、或正文本来就是中文时,地球按钮干脆不出现。
16
+ - **译文落盘,重开零成本**——句子对写在宿主侧(全局句子记忆 + 按条目的段落映射,共享 64 MB 预算、按最近使用淘汰、无 TTL),刷新后点一下地球,整篇从磁盘重绘,模型一个请求都不发。
17
+ - **抓取跑在宿主上**——卡片上的状态药丸(灰 = 未抓 / 蓝 = 抓取中 / 绿 = 已有全文 / 红 = 失败)点一下就开始,随后可以立刻离开页面:原始网页先落宿主磁盘,下次打开由浏览器半抽出正文。全文**自动补齐**(每次刷新最多 8 条、同时最多 2 个请求、失败 6 小时内不重试);feed 自己发布全文的条目第一次打开零请求缓存。
18
+ - **粘进来就知道是什么**——是 feed 就建订阅、是网页就存单篇;arxiv 的 abs/pdf 链接自动升级官方 HTML 版;DOI 链接先解析成 arXiv 版再落地;OpenReview 直接存「仅链接」并写明原因;抓不到的地址也一定存下来,原因分档写在卡片上(反爬 / 需要登录 / 不是网页 / 跨站跳转 / 连不上 / HTTP 错误)。
19
+ - **引用进对话**——正文是应用级选区,`@khorsheed/dsh-quote` 的浮层菜单天然覆盖它;详情页另有「引用」(选区 + 来源标签经官方输入机合入当前会话草稿,读现有草稿合并、绝不覆盖、绝不发送)与「引用到侧边对话」(组合里真有 `sideChat` 服务时才显示)。
20
+ - **订阅管理与最近阅读**——管理页里每个源一行:名称与订阅地址就地编辑、单独刷新、暂停/恢复、删除,缓存策略(正文保留时长、译文预算、清空译文缓存)与存储读数同页;「最近阅读」落盘 100 条、重启还在,滚出 feed 窗口的条目也能接着读。
21
+ - **离开再回来接着读**——打开的文章、按内容锚定的阅读位置(第几个块、块内第几个字符)、墙上的筛选与译文,跨 dsh 会话由整个页面记住;位置另写 `sessionStorage`,页面重启后仍在原处。
22
+
23
+ <img src="https://raw.githubusercontent.com/Khorsheed/dsh-web-basic/main/docs/screenshots/reader-2.png" width="640" alt="详情页:正文以 DOM 文本渲染,地球开关点亮后按句对照翻译,点一句展开该句原文">
24
+
25
+ ## 安装
6
26
 
7
- 抓取在**宿主半**完成(走 `ctx.web` 这条官方出网缝),浏览器半只做解析和渲染;订阅清单与最近一次抓回的原始 payload 落在 `$DSH_HOME/state/dsh-reader/state.json`(用 `node:fs` 原子写,**不走 `ctx.fs`** —— 那是按会话策略上锁的沙箱文件系统,见下方 Compatibility),每日按点自动刷新(插件自带 `setTimeout`,不是宿主服务)。
27
+ ```sh
28
+ dsh plugin --profile web add @khorsheed/dsh-reader
29
+ ```
8
30
 
9
- ## 三个面
31
+ 装完重启 web 实例后生效。卸载:
32
+
33
+ ```sh
34
+ dsh plugin --profile web remove @khorsheed/dsh-reader
35
+ ```
36
+
37
+ 落盘只有一处目录:`$DSH_HOME/state/dsh-reader/`——`state.json`(订阅清单 + 最近 payload + 读者自己的标签与「最近阅读」记录)与 `bodies/`(超过 256 KiB 的正文各占一个文件;全局翻译记忆是其中的 `translation-memory.json`,超过阈值的条目译文映射也各占一个文件)。卸载后这个目录会留下——想彻底清干净就删掉整个 `dsh-reader/`。
38
+
39
+ ## Compatibility
40
+
41
+ - npm 发布线(`@deepseek-ai/dsh@0.1.5-rc.1`):✅ 可用,一处设计内降级——`sidebar.right.pane.tab` 座位、`ctx.web.fetch` 出网缝在 0.1.5-rc.1 上完整;应用内 Sidebar Browser 出口(脚本插图提示、反爬卡片的动作)需要 0.1.6-alpha.2+ 的 `browser` 右栏 tab 类型,0.1.5 宿主上退化为普通外部链接。minHost `0.1.5-rc.1`;本插件不做 preset 自隐(不需要 `pluginInventory`)。
42
+ - 源码线(deepseek-harness master):✅(verifiedHost: 0.1.5-rc.1)。
43
+
44
+ 注意:宿主抓取缝默认把解码后的正文截到 100,000 字符且**静默截断**——这是部署配置可以抬升的上限(见「实现原理 · 抓取缝与存储」里的 patch 叠加层写法),不是插件的正文预算。
45
+
46
+ ## 已知限制
47
+
48
+ - **只订阅、不爬全文**——新增订阅只抓 feed 本身,不会为每条目预抓正文(存储会爆,而且那是在当爬虫);全文由自动补齐每次刷新补一片,或点开某条时就地抓一次。
49
+ - **PDF 与其它非网页文件没有正文**——宿主的出网缝只认 `text/html`、`text/*`、`application/json`、`application/xml`,`application/pdf` 会被直接拒绝;而正文提取本来就是 DOM 的,PDF 没有 DOM。所以 PDF 链接只能存成「仅链接」的卡片 + 「阅读原文」,本插件不是 PDF 阅读器。
50
+ - **跨站跳转不跟随**——宿主缝只报目标 origin、不报路径,所以换了主机的跳转(短链、ezproxy 之类的代理)一律如实报「跳到另一个站点」,不会跟过去猜。
51
+ - **未读圆点不落盘**——未读标记是会话级状态,刷新或重启后全部回到未读。
52
+ - **翻译只在 Chrome 138+ / Edge 148+ 桌面版可用**——Safari、Firefox、移动端没有 Translator API,这些浏览器里地球按钮不出现,其余功能不受影响(这是刻意的选择:一篇 5 万字符的长文走宿主 LLM 是几万 token + 十几秒 + 把文章再发给模型服务一次;端上翻译零成本、秒级、不出设备,质量是网页翻译级的 NMT)。
53
+ - **超大文章抽不全**——宿主缝默认 100,000 字符上限(实测:Anthropic 研究页 ~266k、theverge.com ~899k、newyorker.com ~1.97M 字符),超限文章显示「受限篇幅」提示 +「阅读原文」;需要全文时由部署方抬升宿主缝上限,插件自己不会绕开宿主的出网策略。
54
+
55
+ ## 实现原理
56
+
57
+ <details>
58
+ <summary>各个面(灵感墙 / 详情 / 翻译 / 新增 / 订阅管理 / 最近阅读,点击展开)</summary>
59
+
60
+ > 包名、tab kind、Remote namespace 都还是 `reader` —— 它们是稳定标识(改了等于改部署的 loader 行和落盘目录),「灵感空间」是**用户看到的名字**。
10
61
 
11
62
  | 面 | 内容 |
12
63
  | --- | --- |
@@ -23,38 +74,37 @@
23
74
 
24
75
  **点击语义(定案方案 B)**:点卡片 = 进详情页。大多数阅读在 dsh 里完成;正文太长或想直接看原文时,详情页自己的按钮会跳浏览器。卡片尾部的箭头是"这里能进"的提示,不是控件。
25
76
 
26
- ## 引用
27
-
28
- 正文是应用级选区,所以 `@khorsheed/dsh-quote` 的浮层菜单**本来就覆盖它**——本插件不自己造选区菜单。详情页底部另有:
77
+ **引用**:正文是应用级选区,所以 `@khorsheed/dsh-quote` 的浮层菜单**本来就覆盖它**——本插件不自己造选区菜单。详情页底部另有:
29
78
 
30
79
  - **引用**:选区(没选区就是整条)+ 来源标签,经官方输入机合成进当前会话草稿(读现有草稿合并,绝不覆盖、绝不发送);
31
80
  - **引用到侧边对话**:只有组合里真有 `sideChat` 服务时才显示(能力握手说了算),否则这一项隐藏。
32
81
 
33
- `reader` 自带一个薄 typert Remote(namespace `reader`),verb:`capabilities` / `listSources` / `addSource` / `updateSource` / `removeSource` / `refresh` / `getBodies` / `quoteToSideChat`。宿主半**不解析** feed——解析在浏览器半,`getBodies` 只把原始 payload 交过去。
82
+ </details>
34
83
 
35
- ## 安装
84
+ <details>
85
+ <summary>内部机制(架构、抓取缝、翻译、存储,点击展开)</summary>
36
86
 
37
- ```sh
38
- dsh plugin --profile web add @khorsheed/dsh-reader
39
- # 卸载:
40
- dsh plugin --profile web remove @khorsheed/dsh-reader
41
- ```
87
+ **架构。** 抓取在**宿主半**完成(走 `ctx.web` 这条官方出网缝),浏览器半只做解析和渲染:宿主半提供 `reader` 服务核心(`ctx.provide('reader')`)并挂出薄 Typert Remote 面(namespace `reader`),订阅清单与最近一次抓回的原始 payload 落在 `$DSH_HOME/state/dsh-reader/state.json`(用 `node:fs` 原子写,不走 `ctx.fs`,见下),每日按点自动刷新由插件自带的 `setTimeout` 驱动(宿主没有调度服务可骑)。浏览器半两段式注册右侧栏:先把自己铸造的 tab 类型 `reader`(实现 id `@khorsheed/dsh-reader`,带 guide 卡片)注册进 `sidebarRightTabs` 注册表,再经 `slots.inject('sidebar.right.pane.tab', …)` 把面板本体挂到 keyed 座位;中英文字典随插件注册;Remote 经 `ctx.remote.$mount` 自行挂载。feed 解析必然在浏览器半——宿主侧没有 XML/DOM 解析器(`globalThis.DOMParser === false`),`getBodies` 只把原始 payload 交过去。没有 `static inject`:每个可选能力(`fs`、`web`、`sideChat`、`browser` tab 类型、capture Remote)都在使用时探测,缺席即降级,绝不抛错。
42
88
 
43
- 装完重启宿主。落盘只有一处目录:`$DSH_HOME/state/dsh-reader/`——`state.json`(订阅清单 + 最近 payload + 读者自己的标签与「最近阅读」记录)与 `bodies/`(超过 256 KiB 的正文各占一个文件;全局翻译记忆是其中的 `translation-memory.json`,超过阈值的条目译文映射也各占一个文件)。卸载后这个目录会留下——想彻底清干净就删掉整个 `dsh-reader/`。
89
+ **状态为什么不走 `ctx.fs`**:`ctx.fs` 是**沙箱**文件系统,每一次写都由**调用会话**的文件策略把关;workspace-write 会话写 `$DSH_HOME/state` 会被拒(`FS_SANDBOX_DENIED`,验收实例上真实发生过)。这道栅栏本身是对的、要留着;错的是把「属于部署」的状态交给一个「属于会话」的能力。所以本包按仓库里其它宿主态包的写法(`packages/lab/src/state.ts`)用 `node:fs` 原子写(临时文件 + rename),目录不可写时降级为仅内存。
44
90
 
45
- ## Compatibility
91
+ **Remote 面。** namespace `reader`,verb 分组:源管理 `capabilities` / `listSources` / `addSource` / `updateSource` / `removeSource` / `refresh`;正文 `getBodies` / `getEntryBody` / `fetchEntryBody` / `storeEntryBody` / `getRawBody` / `entryFetchStates` / `listBackfillCandidates`;译文 `getEntryTranslation` / `getSentenceTranslations` / `rememberSentences` / `clearTranslations`;标签 `entryTags` / `listTags` / `createTag` / `tagEntry` / `renameTag` / `deleteTag` / `pruneTags`;阅读记录 `recordRead` / `listRecent` / `clearRecent`;缓存策略与读数 `getCachePolicy` / `setCachePolicy` / `getStorageStats`;以及 `quoteToSideChat`。宿主半**不解析** feed——解析在浏览器半。
92
+
93
+ **来源名用 feed 自己声明的标题**:宿主半不解析 feed(它没有 XML 解析器),所以新建时只能给一个从 URL 推出来的名字(就是你看到的那串 `raw.githubusercontent.com/...`)。浏览器半解析出 `<channel><title>` 后会用真实标题替换显示名,所以卡片上会出现「Anthropic Research」这样的名字,失败时回退成 URL。列表里报错时显示「{count} 天前」这种占位符也是同一类问题的另一面(参数没被插值),已修。
46
94
 
47
- - npm 发布线(`@deepseek-ai/dsh@0.1.5-rc.1`):✅ 完整——`sidebar.right.pane.tab` 座位、`ctx.web.fetch` 出网缝、`ctx.fs` 版本守卫写、`pluginInventory` 都不需要(本插件不做 preset 自隐)。
48
- - 源码线(deepseek-harness master):✅(verifiedHost: 0.1.5-rc.1)
49
- - **状态为什么不用 `ctx.fs`**:`ctx.fs` 是本仓库的**沙箱**文件系统,每一次写都由**调用会话**的文件策略把关;workspace-write 会话写 `$DSH_HOME/state` 会被拒(`FS_SANDBOX_DENIED`,验收实例上真实发生过)。这道栅栏本身是对的、要留着;错的是把「属于部署」的状态交给一个「属于会话」的能力。所以本包按仓库里其它宿主态包的写法(`packages/lab/src/state.ts`)用 `node:fs` 原子写(临时文件 + rename),目录不可写时降级为仅内存。
50
- - **来源名用 feed 自己声明的标题**:宿主半不解析 feed(它没有 XML 解析器),所以新建时只能给一个从 URL 推出来的名字(就是你看到的那串 `raw.githubusercontent.com/...`)。浏览器半解析出 `<channel><title>` 后会用真实标题替换显示名,所以卡片上会出现「Anthropic Research」这样的名字,失败时回退成 URL。列表里报错时显示「{count} 天前」这种占位符也是同一类问题的另一面(参数没被插值),已修。
51
- - **翻译用的是浏览器自己的端上模型,不是宿主的模型**:`self.Translator`(Chrome 138+/Edge 148+,仅桌面版)在本机进程内翻译,所以**模型不出设备、不花任何 token、不联网**(首次按语言对下一次小语言包,`Translator.create()` 需要用户手势——点地球那一下正好是)。**译文的派生句子对会写到宿主侧**(`$DSH_HOME/state/dsh-reader` 下的全局句子记忆 + 按条目映射,默认 64 MB 预算按最近使用淘汰,无 TTL;敏感级与旁边缓存的正文相同,隐私论据从来是关于网络而不是磁盘)。代价是**只在 Chrome/Edge 桌面存在**:Safari、Firefox、移动端没有这个 API,插件就把地球藏起来,其余功能不受影响。这是刻意的选择:一篇 5 万字符的长文如果走宿主 LLM,是几万 token + 十几秒 + 把文章再发给模型服务一次;端上翻译是零成本、秒级、不出设备(质量是网页翻译级的 NMT,不是 LLM 级)。
52
- - **翻译单元是句子,不是段落,也不是文本节点**:先按句切好再发出去,配对是**构造出来的真**,不是事后猜的——段落级翻译拿回来是一整段,模型会合句拆句,再对齐就可能给你看错原文。批量发送时句子之间插哨兵,返回的段数对不上就把那一批退回「一句一请求」,**代价是粒度,永远不是正确性**。跨行内元素的句子(例如句中有链接)会变成两个单元,链接本身不动;`<pre>`/`<code>` 里的内容从不翻译。
53
- - **翻译器建不起来时不会假装**:`Translator.availability()` 只是**提示**,不是承诺 —— Chrome 上确实出现过「availability 说能用、`create()` 却抛 `NotSupportedError`("Unable to create translator for the given source and target language")」的情况(macOS 构建有同类记录)。所以插件会:① 源语言先问浏览器的 `LanguageDetector`(猜错源语言正会让某些语言对建不起来,而那条报错**不说是哪个语言对**);② 目标语言按 `zh` → `zh-Hans` 两种写法依次真建一次;③ 全都以 `NotSupportedError` 拒绝时,**地球自行消失**并明说「这个浏览器无法翻译 en → zh / en → zh-Hans」,同时指向 `chrome://on-device-internals` 的 Broker State 自查;网络/配额这类**非永久**失败则保留地球以便重试,并报出尝试过的语言对与原因(浏览器原文里两个语言都不出现)。
54
- - **正文还原是逐字节的**:翻译只改文本节点(原文在内存里留着),「只看原文」和取消都能把正文还原成宿主发来的那份 HTML 原样。这也是为什么译文不会丢掉链接、图片、表格——它们根本没被碰过。
55
- - **有些站点拒绝自动抓取,界面会诚实地告诉你**:实测 `openai.com` 对**任何非浏览器请求都返回 403**(带代理、不带代理、换浏览器 UA 都一样,返回的是一个反机器人挑战页)。这不是抽取失败,也不是能靠重试解决的问题,所以详情页对 401/403 显示「这个站点拒绝自动抓取(原文地址对人以外的请求返回验证页)」而不是「无法提取正文」。**状态码不是唯一线索**:实测 `openreview.net` 的 PDF 链接是 `302 → /challenge?redirect=…` 然后 **200** 的挑战页——只看状态码会把挑战页当成文章存下来(这正是「加进去了但点开没内容」的根因)。所以判定是组合的:最终 URL 与请求 URL 是否变了、可见文本是否少到不像一页、以及反爬/登录页的痕迹词;命中任一条就不再存正文,改存「仅链接」。同类站点(newsletter 平台、付费墙)会有同样表现。这也是为什么浏览器渲染抓取不在本插件范围内:那条路需要 browser-pane,而它目前仍是 planned。
56
- - **有的 feed 只对部分文章给全文,其余只给摘要**:实测 OpenAI alignment feed 的 27 条正文从 **155 字符**(一句摘要)到 **62,044 字符**(全文)不等——这不是截断,是发布方就这么发的。**摘要不再被当成全文**:以前 feed 的 `<description>` 会被塞进 `contentHtml`,于是条目看起来「已经有正文」——自动补齐跳过它、点开也不再抓,详情页就把那 167 字的摘要当成整篇文章显示。现在解析器给这种条目打上 `summaryOnly`,**打开时照样就地抓一次真文章**(抓取期间正文上方写明「这条订阅源只发布了摘要 —— 全文在原文页面上」,抓到之后这句自动消失),自动补齐也把它列进待补清单。**重抓只在这篇还没有缓存正文时发生**:`summaryOnly` 是 feed 条目的属性、一旦为真就永远为真,所以以前这里会让已经抓过的文章**每次打开都重抓一次**(用同一份正文把 DOM 换掉,译文也就跟着没了);现在只有屏上那段文字是 feed 自己给的(`fromFeed`)才触发抓取。
57
- - **宿主一次只交接 100,000 字符,所以超大 feed 只能读到一部分**:这是宿主出网缝的默认上限(`web-fetch-http` 的 `maxBodyChars`),**不是插件的正文预算**(插件自己的预算是单源 256 KB / 总量 2 MiB)。你那条 feed 有 646,905 字符,宿主只给前 100,000,所以 21 条里只有落在前面那部分的两条能读到。插件**无法**绕过它:`ctx.web.fetch` 只接受 `{ url }`,没有 Range、没有自定义请求头、没有分页,插件也不会为了绕过它去 vendor `fetch`(那等于绕开宿主的出网策略)。要让 646 KB 的 feed 完整可读,**不需要改宿主代码**:用官方的 patch 叠加层给那一行加 config 即可(`--patch <file>` 或机器级的 `$DSH_HOME/cordis.patch.yml`,后者对所有 profile 生效):
95
+ **翻译用的是浏览器自己的端上模型,不是宿主的模型**:`self.Translator`(Chrome 138+/Edge 148+,仅桌面版)在本机进程内翻译,所以**模型不出设备、不花任何 token、不联网**(首次按语言对下一次小语言包,`Translator.create()` 需要用户手势——点地球那一下正好是)。**译文的派生句子对会写到宿主侧**(`$DSH_HOME/state/dsh-reader` 下的全局句子记忆 + 按条目映射,默认 64 MB 预算按最近使用淘汰,无 TTL;敏感级与旁边缓存的正文相同,隐私论据从来是关于网络而不是磁盘)。
96
+
97
+ **翻译单元是句子,不是段落,也不是文本节点**:先按句切好再发出去,配对是**构造出来的真**,不是事后猜的——段落级翻译拿回来是一整段,模型会合句拆句,再对齐就可能给你看错原文。批量发送时句子之间插哨兵,返回的段数对不上就把那一批退回「一句一请求」,**代价是粒度,永远不是正确性**。跨行内元素的句子(例如句中有链接)会变成两个单元,链接本身不动;`<pre>`/`<code>` 里的内容从不翻译。
98
+
99
+ **翻译器建不起来时不会假装**:`Translator.availability()` 只是**提示**,不是承诺 —— Chrome 上确实出现过「availability 说能用、`create()` 却抛 `NotSupportedError`("Unable to create translator for the given source and target language")」的情况(macOS 构建有同类记录)。所以插件会:① 源语言先问浏览器的 `LanguageDetector`(猜错源语言正会让某些语言对建不起来,而那条报错**不说是哪个语言对**);② 目标语言按 `zh` → `zh-Hans` 两种写法依次真建一次;③ 全都以 `NotSupportedError` 拒绝时,**地球自行消失**并明说「这个浏览器无法翻译 en → zh / en → zh-Hans」,同时指向 `chrome://on-device-internals` 的 Broker State 自查;网络/配额这类**非永久**失败则保留地球以便重试,并报出尝试过的语言对与原因(浏览器原文里两个语言都不出现)。
100
+
101
+ **正文还原是逐字节的**:翻译只改文本节点(原文在内存里留着),「只看原文」和取消都能把正文还原成宿主发来的那份 HTML 原样。这也是为什么译文不会丢掉链接、图片、表格——它们根本没被碰过。
102
+
103
+ **有些站点拒绝自动抓取,界面会诚实地告诉你**:实测 `openai.com` 对**任何非浏览器请求都返回 403**(带代理、不带代理、换浏览器 UA 都一样,返回的是一个反机器人挑战页)。这不是抽取失败,也不是能靠重试解决的问题,所以详情页对 401/403 显示「这个站点拒绝自动抓取(原文地址对人以外的请求返回验证页)」而不是「无法提取正文」。**状态码不是唯一线索**:实测 `openreview.net` 的 PDF 链接是 `302 → /challenge?redirect=…` 然后 **200** 的挑战页——只看状态码会把挑战页当成文章存下来(这正是「加进去了但点开没内容」的根因)。所以判定是组合的:最终 URL 与请求 URL 是否变了、可见文本是否少到不像一页、以及反爬/登录页的痕迹词;命中任一条就不再存正文,改存「仅链接」。同类站点(newsletter 平台、付费墙)会有同样表现。
104
+
105
+ **有的 feed 只对部分文章给全文,其余只给摘要**:实测 OpenAI alignment feed 的 27 条正文从 **155 字符**(一句摘要)到 **62,044 字符**(全文)不等——这不是截断,是发布方就这么发的。**摘要不再被当成全文**:以前 feed 的 `<description>` 会被塞进 `contentHtml`,于是条目看起来「已经有正文」——自动补齐跳过它、点开也不再抓,详情页就把那 167 字的摘要当成整篇文章显示。现在解析器给这种条目打上 `summaryOnly`,**打开时照样就地抓一次真文章**(抓取期间正文上方写明「这条订阅源只发布了摘要 —— 全文在原文页面上」,抓到之后这句自动消失),自动补齐也把它列进待补清单。**重抓只在这篇还没有缓存正文时发生**:`summaryOnly` 是 feed 条目的属性、一旦为真就永远为真,所以以前这里会让已经抓过的文章**每次打开都重抓一次**(用同一份正文把 DOM 换掉,译文也就跟着没了);现在只有屏上那段文字是 feed 自己给的(`fromFeed`)才触发抓取。
106
+
107
+ **宿主一次只交接 100,000 字符,所以超大 feed 只能读到一部分**:这是宿主出网缝的默认上限(`web-fetch-http` 的 `maxBodyChars`),**不是插件的正文预算**(插件自己的预算是单源 2 MiB / 总量 12 MiB,见 `MAX_BODY_CHARS_PER_SOURCE`)。你那条 feed 有 646,905 字符,宿主只给前 100,000,所以 21 条里只有落在前面那部分的两条能读到。插件**无法**绕过它:`ctx.web.fetch` 只接受 `{ url }`,没有 Range、没有自定义请求头、没有分页,插件也不会为了绕过它去 vendor `fetch`(那等于绕开宿主的出网策略)。要让 646 KB 的 feed 完整可读,**不需要改宿主代码**:用官方的 patch 叠加层给那一行加 config 即可(`--patch <file>` 或机器级的 `$DSH_HOME/cordis.patch.yml`,后者对所有 profile 生效):
58
108
 
59
109
  ```yaml
60
110
  - id: web-fetch-http
@@ -62,29 +112,33 @@ dsh plugin --profile web remove @khorsheed/dsh-reader
62
112
  maxBodyChars: 2000000 # 默认 100000
63
113
  ```
64
114
 
65
- 已用 `dsh --profile web --patch <file> --dump-config` 实测确认合成结果里带着这个值;3080 上已按此加上覆盖并实测:同一条 feed 由「100,000 字符 / 截断 / 2 条」变成「**646,905 字符 / 未截断 / 21 条完整**」。**2026-09-19 又抬了一档**:研究类文章本身就很大——实测 `transformer-circuits.pub` 的 emotions 那篇 HTML 有 **41.8 MB**(60 张插图是内联 base64),而宿主缝的**字节上限** `maxResponseBytes` 默认只有 5 MB,整次抓取会直接失败(`response exceeds the maximum of 5000000 bytes`),`maxBodyChars` 也挡在前面。现在 3080 的部署配置把**两个上限一起抬到 64 MB**,并把**抓取超时从默认 30 秒抬到 180 秒**(同一篇实测直连 19.5 秒,机器一忙以前就会以 `web fetch timed out` 失败;见 `$DSH_HOME/profiles/web/cordis.patch.yml`)。代价是**全机**每次抓取的峰值内存与传输量都随之上升。注意**插件自己的单源预算必须 ≥ 宿主上限**,否则落盘时会再切一刀——本包的预算是 2 MiB/源、12 MiB/总量(见 `MAX_BODY_CHARS_PER_SOURCE`)。注意边界:插件的 patch 只能挂**自己那一行**(仓库禁止插件去挂/改别人的行),所以这条覆盖属于**部署配置**,由部署方决定;而且它抬高的是**全机所有网页抓取**的上限。
66
- - **全文是自动补齐的,不需要你点**:每次加载后,宿主给出"还缺全文"的条目清单(一次最多 8 条,失败的条目 6 小时内不再重试),浏览器半条一条抓取+抽取并写入缓存(24 小时,管理页可改),**同时最多 2 个请求**。补齐中的条目仍在墙上显示 feed 自己给的内容,底部一行进度「正在补齐全文 3/8…」;抓到的卡片标一个「已抓全文」(徽章的含义是:卡片上仍是 feed 自己的摘要,全文已经缓存好了,点开就能读)。**不确定要不要等?直接点开那一条**:打开时若本地还没有正文,它会就地抓一次并显示「正在抓取正文…」,不必等补齐轮到它。
67
- - 为什么不是"全部一次抓完":那等于把每个订阅源的所有文章都爬一遍。这里是**每次刷新补一片**,滚动前进。
68
- - **抓不到就展示已有的**:站点拒绝(401/403)、连不上、抽取失败,三种情况分开说明,正文位置永远显示 feed 自己发布的内容 + 「阅读原文」,绝不空白。
69
- - **被宿主截断的 feed 会抢救,而不是整份丢掉**:宿主缝按字符数截断,截在标签中间会让整份 XML 不合法——而"整份不合法"曾经等于**一条都没有**。现在解析器会:完整闭合的 `<item>`/`<entry>` 全部保留(用文档根上的命名空间声明重新包装,否则带 `content:encoded` 前缀的片段自己也是坏 XML);被截断的那条尽最大努力修复并标成**部分**,正文照常显示已经拿到的部分;墙上给一行「未完整」说明 + 阅读原文。配一个提交在仓库里的 fixture(`tests/fixtures/feed-truncated.xml`)锁住这个形状。
70
- 实测:验收实例那条 646 KB 的 feed(宿主只给 100,000 字符)现在能出 **2 张卡**——一张完整的 42 KB 正文,一张部分正文——而在这次修复前是 **0 张**。
71
- - **默认过滤是「全部」而不是「今日」**:RSS 源的最新条目经常不是今天的(验收实例那条 feed 最新一篇是 8 天前),默认「今日」会让刚订阅完的墙看起来像空的 —— 也就是「订阅成功了但什么都没有」。现在**没有「今日」这个过滤**:收窄墙的四种方式(未读 / 按来源 / 按类型 / 按标签)都在筛选弹层里,按钮自己的按下态、以及它写进搜索框的 `#源id` / `#rss` / `#link` / `@标签id`,让「被隐藏了什么」始终看得见。
72
- - **正文超过 256 KiB 会存成单独文件**:`state.json` 只留元数据(文件名 + 字符数),正文本体落在 `$DSH_HOME/state/dsh-reader/bodies/<hash>.html`。【正文保留】设置的 12/24/168 小时 / 永久对**两种正文一视同仁**,淘汰(超出「保留条数」上限或 TTL 到期后重抓)时会连文件一起删掉。这样做是因为 `state.json` 是一份整体读写的 JSON:一篇插图内联成 base64 的论文抽出来几十 MB,直接塞进去会让之后每一次读者操作都付出几 MB 的往返。
73
- - **正文顶部的重复标题会被去掉**:详情页已经渲染了条目标题,而文章自己的 HTML 里常常再来一两遍(实测那篇有**两个** `<h1>`,前面还跟着站名和 logo 链接)。抽取时会去掉与文档标题重复的一二级标题、开头的站头chaff 与空行,正文从「Authors」这类真实内容开始。
74
- - **正文没有阅读量度**:段落和列表都铺满侧栏给的宽度,没有 `max-width`(早先只给段落限宽,右侧留出一条参差的空白带,看起来像布局坏了)。想要更短的行,把侧栏收窄即可。
75
- - **宿主抓取上限是已知限制**:`ctx.web.fetch` 把解码后的正文截到 100,000 字符且**静默截断**。超限的文章抽不全(实测:Anthropic 研究页 ~266k、theverge.com ~899k、newyorker.com ~1.97M 字符);摘要页里的文章标题、作者、摘要仍然在。详情页会因此显示「受限篇幅,内容未完整呈现」+「阅读原文」。RSS 源基本安全(实测:HN 11KB、BBC 25KB、阮一峰 69KB、Simon Willison 83KB——83KB 已经贴着上限)。
76
- - **降级矩阵**:无 `fs` → 状态只在内存里、重启即丢(握手会 `hasFs: false`,不假装有持久化);无 `web` → 新增的链接照样存下来并标成「仅链接(连不上)」+「阅读原文」,不抛异常、更不丢 URL;无 `sideChat` → 侧边对话那一项隐藏;无 `quote` → 选区浮层缺席,但条目级「引用」按钮照常工作。宿主低于 0.1.6-alpha.2(没有 `browser` 这个右栏 tab 类型)→ 需要真浏览器的地方(脚本插图提示、反爬卡片的动作)给的是普通外部链接,不报错。
77
- - **跨源重定向只跟随能还原的那一跳**:宿主缝会拒跨源跳转(`WEB_REDIRECT_BLOCKED`),而且**只报目标 origin、不报路径**。所以插件只在**同一个主机换 scheme/端口**时重新进缝(`http://host/feed` → `https://host/feed`,路径与查询原样保留);**换了主机就如实说「这个地址会跳到另一个站点」,不再跟过去**——跟着 origin 根路径抓回来的是另一个页面,然后还会被当成读者粘的那个地址存下来。这条以前写的是「最多跟 3 跳」,但那段判定读的是错误**消息文本**里根本没有的 `WEB_REDIRECT_BLOCKED`(宿主把它放在 `error.code` 上),所以跟随从来没生效过、每次都是把宿主缝的原话甩给用户;现在按 `code` 判定,并补了回归测试。
78
- - **站点拒绝 / 登录墙 / PDF 都只会变成「仅链接」**:抓回来的页面如果**几乎没有可读文本**(`< 200` 字符可见文本,去掉 script/style 后计)并且带着反爬或登录页的痕迹,就不当作文章存下来——**挑战页本身绝不落成卡片正文**。`application/pdf` 之类更是宿主的出网缝直接拒绝的类型(`WEB_UNSUPPORTED_CONTENT_TYPE`,只认 html/text/json/xml)。这些卡片照常出现在墙上、能打标签、能复制链接、能删除,点开只有原因 + 「阅读原文」。
79
- - **不可重试的原因不会被自动重试**:反爬、登录墙、PDF、HTTP 错误、空页面都记录成不可重试(`isRetryablePreviewFailure` 只放行 `unreachable`),自动补齐不会再敲它们;订阅管理页那一行的「刷新」仍可手动重试,成功后清掉记录。这样插件不会对一个永远拒绝它的站点每 6 小时敲一次。
80
- - **不做条件请求**:宿主缝没有自定义请求头入口,所以没有 ETag/If-None-Match,每次刷新都是完整抓取。
81
- - **订阅登录墙后面的内容:不做**。没有凭证注入面。
82
-
83
- ## Known Limitations
84
-
85
- - **只订阅、不抓文**:新增订阅时只抓 feed 本身,不会为每条目预抓正文(存储会爆,而且那是在当爬虫)。想看某条正文时点开它,付一次抓取。
86
- - **PDF 与其它非网页文件没有正文**:宿主的出网缝只认 `text/html`、`text/*`、`application/json`、`application/xml`,`application/pdf` 会被直接拒绝;而正文提取本来就是 DOM 的,PDF 没有 DOM。所以 PDF 链接只能存成「仅链接」的卡片 + 「阅读原文」,插件不是 PDF 阅读器。
87
- - **跨站跳转不跟随**:宿主缝只报目标 origin,路径无法还原,所以换了主机的跳转(短链、ezproxy 之类的代理)一律如实报「跳到另一个站点」,不会跟过去猜。
88
- - **未读与正文不落盘**:会话级状态。持久化逐条已读游标需要第二份写密集文档,而"自上次看过以来有什么新的"本身就是这个插件的价值所在。
89
- - **XML 解析在浏览器**:宿主侧没有 XML/DOM 解析器(`globalThis.DOMParser === false`),所以 feed 解析必然在客户端。
90
- - **不依赖 browser-pane**:`proposals/active/2026-09-13-browser-pane.md` 还是 planned,而且 canvas 的 CDP 帧里承载不了划选引用。
115
+ 已用 `dsh --profile web --patch <file> --dump-config` 实测确认合成结果里带着这个值;验收实例上已按此加上覆盖并实测:同一条 feed 由「100,000 字符 / 截断 / 2 条」变成「**646,905 字符 / 未截断 / 21 条完整**」。**2026-09-19 又抬了一档**:研究类文章本身就很大——实测 `transformer-circuits.pub` 的 emotions 那篇 HTML 有 **41.8 MB**(60 张插图是内联 base64),而宿主缝的**字节上限** `maxResponseBytes` 默认只有 5 MB,整次抓取会直接失败(`response exceeds the maximum of 5000000 bytes`),`maxBodyChars` 也挡在前面。现在验收实例的部署配置把**两个上限一起抬到 64 MB**,并把**抓取超时从默认 30 秒抬到 180 秒**(同一篇实测直连 19.5 秒,机器一忙以前就会以 `web fetch timed out` 失败)。代价是**全机**每次抓取的峰值内存与传输量都随之上升。注意边界:插件的 patch 只能挂**自己那一行**(仓库禁止插件去挂/改别人的行),所以这条覆盖属于**部署配置**,由部署方决定;而且它抬高的是**全机所有网页抓取**的上限。
116
+
117
+ **全文是自动补齐的,不需要你点**:每次加载后,宿主给出"还缺全文"的条目清单(一次最多 8 条,失败的条目 6 小时内不再重试),浏览器半一条一条抓取+抽取并写入缓存(24 小时,管理页可改),**同时最多 2 个请求**。补齐中的条目仍在墙上显示 feed 自己给的内容,底部一行进度「正在补齐全文 3/8…」;抓到的卡片标一个「已抓全文」(徽章的含义是:卡片上仍是 feed 自己的摘要,全文已经缓存好了,点开就能读)。**不确定要不要等?直接点开那一条**:打开时若本地还没有正文,它会就地抓一次并显示「正在抓取正文…」,不必等补齐轮到它。
118
+
119
+ - 为什么不是"全部一次抓完":那等于把每个订阅源的所有文章都爬一遍。这里是**每次刷新补一片**,滚动前进。
120
+ - **抓不到就展示已有的**:站点拒绝(401/403)、连不上、抽取失败,三种情况分开说明,正文位置永远显示 feed 自己发布的内容 + 「阅读原文」,绝不空白。
121
+
122
+ **被宿主截断的 feed 会抢救,而不是整份丢掉**:宿主缝按字符数截断,截在标签中间会让整份 XML 不合法——而"整份不合法"曾经等于**一条都没有**。现在解析器会:完整闭合的 `<item>`/`<entry>` 全部保留(用文档根上的命名空间声明重新包装,否则带 `content:encoded` 前缀的片段自己也是坏 XML);被截断的那条尽最大努力修复并标成**部分**,正文照常显示已经拿到的部分;墙上给一行「未完整」说明 + 阅读原文。配一个提交在仓库里的 fixture(`tests/fixtures/feed-truncated.xml`)锁住这个形状。实测:验收实例那条 646 KB 的 feed(宿主只给 100,000 字符)现在能出 **2 张卡**——一张完整的 42 KB 正文,一张部分正文——而在这次修复前是 **0 张**。
123
+
124
+ **站点拒绝 / 登录墙 / PDF 都只会变成「仅链接」**:抓回来的页面如果**几乎没有可读文本**(`< 200` 字符可见文本,去掉 script/style 后计)并且带着反爬或登录页的痕迹,就不当作文章存下来——**挑战页本身绝不落成卡片正文**。`application/pdf` 之类更是宿主的出网缝直接拒绝的类型(`WEB_UNSUPPORTED_CONTENT_TYPE`,只认 html/text/json/xml)。这些卡片照常出现在墙上、能打标签、能复制链接、能删除,点开只有原因 + 「阅读原文」。
125
+
126
+ **不可重试的原因不会被自动重试**:反爬、登录墙、PDF、HTTP 错误、空页面都记录成不可重试(`isRetryablePreviewFailure` 只放行 `unreachable`),自动补齐不会再敲它们;订阅管理页那一行的「刷新」仍可手动重试,成功后清掉记录。这样插件不会对一个永远拒绝它的站点每 6 小时敲一次。
127
+
128
+ **跨源重定向只跟随能还原的那一跳**:宿主缝会拒跨源跳转(`WEB_REDIRECT_BLOCKED`),而且**只报目标 origin、不报路径**。所以插件只在**同一个主机换 scheme/端口**时重新进缝(`http://host/feed` → `https://host/feed`,路径与查询原样保留);**换了主机就如实说「这个地址会跳到另一个站点」,不再跟过去**——跟着 origin 根路径抓回来的是另一个页面,然后还会被当成读者粘的那个地址存下来。判定按 `error.code` 而不是错误消息文本(宿主把 `WEB_REDIRECT_BLOCKED` 放在 `error.code` 上),有回归测试。
129
+
130
+ **不做条件请求**:宿主缝没有自定义请求头入口,所以没有 ETag/If-None-Match,每次刷新都是完整抓取。**订阅登录墙后面的内容:不做**——没有凭证注入面。
131
+
132
+ **正文超过 256 KiB 会存成单独文件**:`state.json` 只留元数据(文件名 + 字符数),正文本体落在 `$DSH_HOME/state/dsh-reader/bodies/<hash>.html`。【正文保留】设置的 12/24/168 小时 / 永久对**两种正文一视同仁**,淘汰(超出「保留条数」上限或 TTL 到期后重抓)时会连文件一起删掉。这样做是因为 `state.json` 是一份整体读写的 JSON:一篇插图内联成 base64 的论文抽出来几十 MB,直接塞进去会让之后每一次读者操作都付出几 MB 的往返。
133
+
134
+ **正文顶部的重复标题会被去掉**:详情页已经渲染了条目标题,而文章自己的 HTML 里常常再来一两遍(实测那篇有**两个** `<h1>`,前面还跟着站名和 logo 链接)。抽取时会去掉与文档标题重复的一二级标题、开头的站头 chaff 与空行,正文从「Authors」这类真实内容开始。
135
+
136
+ **默认过滤是「全部」而不是「今日」**:RSS 源的最新条目经常不是今天的(验收实例那条 feed 最新一篇是 8 天前),默认「今日」会让刚订阅完的墙看起来像空的 —— 也就是「订阅成功了但什么都没有」。现在**没有「今日」这个过滤**:收窄墙的四种方式(未读 / 按来源 / 按类型 / 按标签)都在筛选弹层里,按钮自己的按下态、以及它写进搜索框的 `#源id` / `#rss` / `#link` / `@标签id`,让「被隐藏了什么」始终看得见。
137
+
138
+ **降级矩阵**:无 `fs` → 状态只在内存里、重启即丢(握手会 `hasFs: false`,不假装有持久化);无 `web` → 新增的链接照样存下来并标成「仅链接(连不上)」+「阅读原文」,不抛异常、更不丢 URL;无 `sideChat` → 侧边对话那一项隐藏;无 `quote` → 选区浮层缺席,但条目级「引用」按钮照常工作;无 capture 包 → 脚本插图提示处不出现「渲染抓取」;宿主低于 0.1.6-alpha.2(没有 `browser` 这个右栏 tab 类型)→ 需要真浏览器的地方(脚本插图提示、反爬卡片的动作)给的是普通外部链接,不报错。
139
+
140
+ </details>
141
+
142
+ ## 开发
143
+
144
+ 隶属 [dsh-plugins](https://github.com/Khorsheed/dsh-plugins) monorepo(`packages/dsh-reader`)。问题与贡献请移步该仓库。
package/lib/client.js CHANGED
@@ -8814,7 +8814,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8814
8814
  }
8815
8815
  //#endregion
8816
8816
  //#region \0dsh-css:src/client/ReaderPane.module.css.mjs
8817
- const css = ".Qor37a_root{--reader-gutter:12px;height:100%;min-height:0;color:var(--dsw-alias-label-primary);flex-direction:column;display:flex;position:relative;overflow:hidden}.Qor37a_head{border-bottom:1px solid var(--dsw-alias-border-l2);flex:none;align-items:center;gap:2px;min-width:0;padding:8px 8px 8px 10px;display:flex}.Qor37a_headTitle{flex:1;align-items:center;gap:6px;min-width:0;font-size:12.5px;font-weight:600;display:flex}.Qor37a_count{color:var(--dsw-alias-label-tertiary);font-size:10.5px;font-weight:400}.Qor37a_tool{width:26px;height:26px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:50%;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.Qor37a_tool:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.Qor37a_toolOn{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary)}.Qor37a_toolDanger:hover{background:var(--dsw-alias-interactive-bg-hover-danger,var(--dsw-alias-interactive-bg-hover));color:var(--dsw-static-red-600,#d54941)}.Qor37a_tools{flex:none;align-items:center;gap:4px;min-width:0;padding:8px 10px 6px;display:flex}.Qor37a_toolWrap{flex:none;display:inline-flex;position:relative}.Qor37a_search{flex:1 1 0;min-width:44px;position:relative}.Qor37a_search svg{color:var(--dsw-alias-label-tertiary);pointer-events:none;position:absolute;top:50%;left:7px;transform:translateY(-50%)}.Qor37a_search input{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);width:100%;min-width:0;height:26px;color:var(--dsw-alias-label-primary);border-radius:7px;outline:none;padding:0 28px 0 26px;font-family:inherit;font-size:11.5px}.Qor37a_searchClear{width:22px;height:22px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:50%;justify-content:center;align-items:center;padding:0;display:inline-flex;position:absolute;top:50%;right:3px;transform:translateY(-50%)}.Qor37a_searchClear:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.Qor37a_search input::placeholder{color:var(--dsw-alias-label-tertiary)}.Qor37a_search input:focus{border-color:var(--dsw-static-deepseek-500)}.Qor37a_scroll{flex:1;min-height:0;padding:2px 8px 10px;overflow-y:auto}.Qor37a_list{grid-template-columns:repeat(auto-fill,minmax(264px,1fr));gap:8px;display:grid}.Qor37a_card{--reader-card-fill:var(--dsw-static-neutral-50,#fafafa);--reader-card-hover:var(--dsw-static-neutral-100,#f5f5f5);border:1px solid var(--dsw-alias-border-l1);background:var(--reader-card-fill);text-align:left;cursor:pointer;border-radius:12px;grid-template-columns:auto minmax(0,1fr) auto;align-items:start;gap:2px 9px;padding:9px 9px 9px 10px;transition:background-color .12s,border-color .12s;display:grid;position:relative}body[data-ds-dark-theme] .Qor37a_card{--reader-card-fill:var(--dsw-static-neutral-850,#212123);--reader-card-hover:var(--dsw-static-neutral-800,#292929)}.Qor37a_card:hover{border-color:var(--dsw-alias-border-l2);background:var(--reader-card-hover)}.Qor37a_card:active{border-color:var(--dsw-alias-border-l3);background:var(--reader-card-hover)}.Qor37a_card:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.Qor37a_tileWrap{grid-area:1/1/span 3;width:24px;height:24px;position:relative}.Qor37a_tile{width:24px;height:24px;color:var(--dsw-static-neutral-00);border-radius:6px;place-items:center;font-size:10.5px;font-weight:700;display:grid}.Qor37a_unread{background:var(--dsw-static-deepseek-500);width:7px;height:7px;box-shadow:0 0 0 1.5px var(--dsw-alias-bg-base);border-radius:50%;position:absolute;bottom:-1px;right:-1px}.Qor37a_meta{grid-column:2;align-items:baseline;gap:6px;min-width:0;display:flex}.Qor37a_source{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;font-size:10.5px;overflow:hidden}.Qor37a_sep{color:var(--dsw-alias-label-tertiary);opacity:.6;flex:none;font-size:10.5px}.Qor37a_when{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;flex:none;font-size:10.5px}.Qor37a_title{-webkit-line-clamp:2;overflow-wrap:anywhere;min-width:0;min-height:34px;color:var(--dsw-alias-label-secondary);-webkit-box-orient:vertical;grid-column:2/-1;font-size:12.5px;font-weight:600;line-height:17px;display:-webkit-box;overflow:hidden}.Qor37a_card[data-unread] .Qor37a_title{color:var(--dsw-alias-label-primary);font-weight:650}.Qor37a_summary{-webkit-line-clamp:2;overflow-wrap:anywhere;min-width:0;min-height:32px;color:var(--dsw-alias-label-secondary);-webkit-box-orient:vertical;grid-column:2/-1;margin-top:3px;font-size:11.5px;line-height:16px;display:-webkit-box;overflow:hidden}.Qor37a_cardOrigTitle{-webkit-line-clamp:2;overflow-wrap:anywhere;min-width:0;color:var(--dsw-alias-label-tertiary);-webkit-box-orient:vertical;grid-column:2/-1;margin-top:2px;font-size:12.5px;font-weight:400;line-height:17px;display:-webkit-box;overflow:hidden}.Qor37a_cardOrigSummary{-webkit-line-clamp:2;overflow-wrap:anywhere;min-width:0;color:var(--dsw-alias-label-tertiary);-webkit-box-orient:vertical;grid-column:2/-1;margin-top:2px;font-size:11.5px;line-height:16px;display:-webkit-box;overflow:hidden}.Qor37a_tags{flex-wrap:wrap;grid-column:2/-1;align-items:center;gap:5px;min-width:0;margin-top:6px;display:flex}.Qor37a_tag,.Qor37a_tagOwned{border-radius:999px;flex:none;align-items:center;height:18px;padding:0 7px;font-size:10.5px;line-height:1;display:inline-flex}.Qor37a_tag{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-tertiary)}.Qor37a_tagOwned{border:1px solid var(--dsw-static-deepseek-500);color:var(--dsw-alias-label-primary)}.Qor37a_tagLinkOnly{border:1px dashed var(--dsw-alias-border-l3,var(--dsw-alias-border-l2));height:18px;color:var(--dsw-alias-label-tertiary);border-radius:999px;flex:none;align-items:center;padding:0 7px;font-size:10.5px;line-height:1;display:inline-flex}.Qor37a_author{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;font-size:10px;overflow:hidden}.Qor37a_chevron{width:22px;height:22px;color:var(--dsw-alias-label-tertiary);border-radius:6px;grid-area:1/3;justify-content:center;align-items:center;margin-top:2px;display:inline-flex}.Qor37a_card:hover .Qor37a_chevron{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-secondary)}.Qor37a_metaBar{border-top:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-tertiary);flex:none;align-items:center;gap:5px;padding:5px 12px 7px;font-size:10.5px;line-height:15px;display:flex}.Qor37a_state{color:var(--dsw-alias-label-tertiary);padding:26px 14px;font-size:11.5px;line-height:18px}.Qor37a_state b{color:var(--dsw-alias-label-primary);margin-bottom:4px;font-weight:600;display:block}.Qor37a_add{flex-direction:column;flex:1;min-height:0;padding:12px 12px 0;display:flex}.Qor37a_add h2{margin:0 0 4px;font-size:13.5px;font-weight:700}.Qor37a_add p{color:var(--dsw-alias-label-tertiary);margin:0 0 12px;font-size:11px;line-height:17px}.Qor37a_add input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);width:100%;height:30px;color:var(--dsw-alias-label-primary);border-radius:8px;outline:none;margin-bottom:8px;padding:0 9px;font-family:inherit;font-size:12px}.Qor37a_add input:focus{border-color:var(--dsw-static-deepseek-500)}.Qor37a_submit{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-base);cursor:pointer;border:none;border-radius:8px;align-self:flex-start;padding:7px 13px;font-family:inherit;font-size:11.5px}.Qor37a_submit:hover{opacity:.88}.Qor37a_verdict{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);border-radius:9px;margin-top:12px;padding:9px 11px;font-size:11px;line-height:17px}.Qor37a_verdict b{color:var(--dsw-alias-label-primary)}.Qor37a_verdictReason{word-break:break-word;opacity:.9;margin-top:4px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10px;line-height:15px;display:block}.Qor37a_detail{flex-direction:column;flex:1;min-height:0;display:flex}.Qor37a_bar{--reader-bar-pad:calc(var(--reader-gutter) - 10px);min-width:0;padding:6px var(--reader-bar-pad);border-bottom:1px solid var(--dsw-alias-border-l2);flex:none;align-items:center;gap:2px;display:flex}.Qor37a_barLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-primary);flex:1;padding:0 4px;font-size:12.5px;font-weight:600;overflow:hidden}.Qor37a_spacer{flex:1}.Qor37a_detailBody{min-height:0;padding:0 var(--reader-gutter) 22px;flex:1;overflow-y:auto}.Qor37a_kicker{color:var(--dsw-alias-label-tertiary);align-items:center;gap:6px;margin:14px 0 8px;font-size:10.5px;display:flex}.Qor37a_kicker .Qor37a_tile{border-radius:4px;width:16px;height:16px;font-size:9px}.Qor37a_detailTitle{letter-spacing:-.005em;margin:0 0 9px;font-size:17px;font-weight:700;line-height:1.4}.Qor37a_tagRow{flex-wrap:wrap;align-items:center;gap:4px;margin-bottom:14px;display:flex}.Qor37a_refetch{border:1px solid var(--dsw-alias-border-l2);height:18px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border-radius:999px;flex:none;align-items:center;gap:3px;padding:0 7px;font-family:inherit;font-size:10.5px;line-height:1;display:inline-flex}.Qor37a_refetch:hover:not(:disabled){border-color:var(--dsw-static-deepseek-500);color:var(--dsw-alias-label-primary)}.Qor37a_refetch:disabled{cursor:default;opacity:.6}.Qor37a_rule{background:var(--dsw-alias-border-l2);height:1px;margin:0 0 16px}.Qor37a_article{color:var(--dsw-alias-label-secondary)}.Qor37a_articleZh{letter-spacing:.005em;font-size:13.5px;line-height:1.78}.Qor37a_articleEn{letter-spacing:-.003em;font-size:13.5px;line-height:1.66}.Qor37a_article p{margin:0 0 12px}.Qor37a_article h1,.Qor37a_article h2{color:var(--dsw-alias-label-primary);margin:24px 0 9px;font-size:15px;font-weight:700;line-height:1.45}.Qor37a_article h3,.Qor37a_article h4{color:var(--dsw-alias-label-primary);margin:19px 0 7px;font-size:13.5px;font-weight:700;line-height:1.45}.Qor37a_article ul,.Qor37a_article ol{margin:0 0 12px;padding-left:19px}.Qor37a_article li{margin:0 0 5px}.Qor37a_article blockquote{border-left:3px solid var(--dsw-alias-border-l3);margin:0 0 12px;padding:2px 0 2px 11px}.Qor37a_article code{background:var(--dsw-alias-bg-layer-1);font-family:var(--dsw-font-mono,ui-monospace, monospace);border-radius:4px;padding:1px 5px;font-size:.92em}.Qor37a_article pre{background:var(--dsw-alias-bg-layer-1);border-radius:8px;margin:0 0 12px;padding:10px 12px;overflow-x:auto}.Qor37a_article pre code{background:0 0;padding:0;font-size:12px}.Qor37a_article a{color:var(--dsw-alias-link);text-decoration:none}.Qor37a_article a:hover{text-underline-offset:2px;text-decoration:underline}.Qor37a_article img{border-radius:8px;max-width:100%;height:auto;margin:4px 0 12px;display:block}.Qor37a_article strong{color:var(--dsw-alias-label-primary);font-weight:650}.Qor37a_article table{border-collapse:collapse;width:100%;margin:0 0 12px;font-size:12.5px}.Qor37a_article th,.Qor37a_article td{border:1px solid var(--dsw-alias-border-l2);text-align:left;padding:5px 8px}.Qor37a_article math{font-size:1.02em}.Qor37a_article math[display=block]{max-width:100%;margin:0 0 12px;padding:6px 0;display:block;overflow:auto hidden}.Qor37a_incomplete{border-top:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);margin:18px 0 0;padding-top:12px;font-size:11px;line-height:17px}.Qor37a_incompleteLink{font-family:inherit;font-size:inherit;color:var(--dsw-alias-link);cursor:pointer;background:0 0;border:none;padding:0}.Qor37a_incompleteLink:hover{text-underline-offset:2px;text-decoration:underline}.Qor37a_alsoFrom{margin-top:22px}.Qor37a_alsoFromLabel{letter-spacing:.04em;color:var(--dsw-alias-label-tertiary);margin-bottom:6px;font-size:10.5px;font-weight:600}.Qor37a_alsoFromRow{border:none;border-top:1px solid var(--dsw-alias-border-l1);width:100%;color:var(--dsw-alias-label-secondary);text-align:left;cursor:pointer;background:0 0;align-items:baseline;gap:8px;padding:7px 0;font-family:inherit;font-size:11.5px;display:flex}.Qor37a_alsoFromRow:hover{color:var(--dsw-alias-label-primary)}.Qor37a_alsoFromTitle{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;overflow:hidden}.Qor37a_composer{border-top:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);flex:none;padding:7px 10px}.Qor37a_composerLabel{color:var(--dsw-alias-label-tertiary);margin-bottom:4px;font-size:10px}.Qor37a_composerText{max-height:52px;color:var(--dsw-alias-label-secondary);white-space:pre-wrap;word-break:break-word;font-size:11px;line-height:16px;overflow-y:auto}.Qor37a_composerEmpty{color:var(--dsw-alias-label-tertiary)}.Qor37a_foot{border-top:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-tertiary);flex:none;align-items:center;gap:6px;padding:6px 10px;font-size:10.5px;display:flex}.Qor37a_menu{z-index:20;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);min-width:150px;box-shadow:var(--dsw-shadow-lv3,0 12px 40px #0000003d);border-radius:10px;padding:4px;position:absolute;top:calc(100% + 4px);right:0}.Qor37a_menu button{width:100%;color:var(--dsw-alias-label-primary);text-align:left;cursor:pointer;background:0 0;border:none;border-radius:6px;justify-content:space-between;align-items:center;gap:10px;padding:6px 8px;font-family:inherit;font-size:11.5px;display:flex}.Qor37a_menu button:hover{background:var(--dsw-alias-interactive-bg-hover)}.Qor37a_check{color:var(--dsw-static-deepseek-500)}.Qor37a_sourceGrid{grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:8px;padding:4px 2px 10px;display:grid}.Qor37a_sourceCard{border:1px solid var(--dsw-alias-border-l2-darkmode-thin,#0000001a);background:var(--dsw-alias-bg-layer-1);min-width:0;color:inherit;font:inherit;text-align:left;cursor:pointer;border-radius:12px;align-items:center;gap:9px;padding:9px 10px;display:flex}.Qor37a_sourceCard:hover{background:var(--dsw-alias-interactive-bg-hover)}.Qor37a_addCard{border:1px dashed var(--dsw-alias-border-l2,#7f7f7f66);min-height:58px;color:var(--dsw-alias-label-tertiary,#7f8287);font:inherit;cursor:pointer;background:0 0;border-radius:12px;justify-content:center;align-items:center;gap:6px;padding:9px 10px;font-size:12.5px;display:flex}.Qor37a_addCard:hover{background:var(--dsw-alias-interactive-bg-hover,#7f7f7f24);color:var(--dsw-alias-label-primary)}.Qor37a_emptyCard{border:1px dashed var(--dsw-alias-border-l2,#7f7f7f66);min-height:132px;color:var(--dsw-alias-label-secondary);font:inherit;text-align:left;cursor:pointer;background:0 0;border-radius:14px;flex-direction:column;grid-column:1/-1;align-items:flex-start;gap:6px;padding:18px 20px;display:flex}.Qor37a_emptyCard:hover{background:var(--dsw-alias-interactive-bg-hover,#7f7f7f24)}.Qor37a_emptyTitle{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:600}.Qor37a_emptyBody{color:var(--dsw-alias-label-tertiary);font-size:12.5px;line-height:19px}.Qor37a_sourceInfo{flex-direction:column;gap:1px;min-width:0;display:flex}.Qor37a_sourceName{color:var(--dsw-alias-label-primary);white-space:nowrap;text-overflow:ellipsis;font-size:12.5px;font-weight:600;overflow:hidden}.Qor37a_sourceMeta{color:var(--dsw-alias-label-tertiary);white-space:nowrap;text-overflow:ellipsis;font-size:10.5px;line-height:15px;overflow:hidden}.Qor37a_sourceFailed{color:var(--dsw-static-red-600,#d54941)}.Qor37a_sourceError{color:var(--dsw-static-red-600,#d54941);word-break:break-word;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10px;line-height:14px}.Qor37a_overlay{z-index:100;text-align:left;background:0 0;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.Qor37a_dialog{border:1px solid var(--dsw-alias-border-l1,#7f7f7f29);background:var(--dsw-alias-bg-layer-1);width:520px;max-width:calc(100vw - 48px);max-height:calc(100vh - 96px);box-shadow:var(--dsw-shadow-lv3,0 12px 40px #0000003d);border-radius:18px;flex-direction:column;gap:12px;padding:20px 22px;display:flex;overflow-y:auto}.Qor37a_dialog h2{color:var(--dsw-alias-label-primary);margin:0;font-size:15px;font-weight:600}.Qor37a_dialog p{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:19px}.Qor37a_dialog input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font:inherit;border-radius:9px;padding:9px 11px;font-size:12.5px}.Qor37a_dialog input:focus{border-color:var(--dsw-static-deepseek-500);outline:none}.Qor37a_dialogActions{justify-content:flex-end;align-items:center;gap:8px;margin-top:2px;display:flex}.Qor37a_ghost{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;background:0 0;border-radius:9px;padding:7px 13px;font-size:12px}.Qor37a_ghost:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.Qor37a_paneBody{min-height:0;padding:12px var(--reader-gutter);flex:1;overflow-y:auto}.Qor37a_help{color:var(--dsw-alias-label-tertiary);margin:0 0 8px;font-size:11.5px;line-height:18px}.Qor37a_settingsRow{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:10px;flex-wrap:wrap;align-items:center;gap:8px 16px;margin:0 0 10px;padding:6px 10px;display:flex}.Qor37a_setting{cursor:pointer;align-items:center;gap:6px;display:inline-flex}.Qor37a_settingLabel{color:var(--dsw-alias-label-tertiary);font-size:11.5px}.Qor37a_settingControl{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font:inherit;border-radius:7px;padding:3px 6px;font-size:11.5px}.Qor37a_settingReadout{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;font-size:11.5px}.Qor37a_manageTools{flex-wrap:wrap;align-items:center;gap:4px 8px;margin-bottom:8px;display:flex}.Qor37a_chipGroupLabel{color:var(--dsw-alias-label-tertiary);flex:none;font-size:10px}.Qor37a_chipRow{flex-wrap:wrap;gap:4px;display:flex}.Qor37a_chip{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;background:0 0;border-radius:999px;align-items:center;gap:4px;padding:2px 7px;font-size:10.5px;display:inline-flex}.Qor37a_chip:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.Qor37a_chipOn{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary);border-color:#0000}.Qor37a_chipCount{color:var(--dsw-alias-label-tertiary);font-size:10px}.Qor37a_sourceList,.Qor37a_recentList{flex-direction:column;gap:6px;display:flex}.Qor37a_recentRow{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);text-align:left;cursor:pointer;border-radius:10px;flex-direction:column;gap:3px;width:100%;padding:9px 10px;font-family:inherit;display:flex}.Qor37a_recentRow:hover:not(:disabled){border-color:var(--dsw-alias-border-l2);background:var(--dsw-alias-interactive-bg-hover)}.Qor37a_recentRow:disabled{cursor:default;opacity:.55}.Qor37a_recentTitle{color:var(--dsw-alias-label-primary);text-overflow:ellipsis;white-space:nowrap;font-size:12.5px;line-height:17px;overflow:hidden}.Qor37a_recentMeta{color:var(--dsw-alias-label-tertiary);align-items:center;gap:5px;font-size:10.5px;display:flex}.Qor37a_recentSource{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.Qor37a_sourceRow{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:10px;align-items:flex-start;gap:9px;padding:9px 10px;display:flex}.Qor37a_sourceRow .Qor37a_sourceInfo{flex:1}.Qor37a_rowAction{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;background:0 0;border-radius:7px;flex:none;margin-top:2px;padding:4px 9px;font-size:11px}.Qor37a_rowAction:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.Qor37a_rowActionDanger{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-static-red-600,#d54941);font:inherit;cursor:pointer;background:0 0;border-radius:7px;flex:none;padding:4px 9px;font-size:11px}.Qor37a_rowActionDanger:hover{background:var(--dsw-alias-interactive-bg-hover-danger,var(--dsw-alias-interactive-bg-hover))}.Qor37a_toolSpinning svg{animation:.9s linear infinite Qor37a_reader-spin}@media (prefers-reduced-motion:reduce){.Qor37a_toolSpinning svg{animation:none}}@keyframes Qor37a_reader-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.Qor37a_tool:disabled{opacity:.6;cursor:default}.Qor37a_filterPanel{z-index:20;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);min-width:200px;max-width:280px;max-height:min(62vh,440px);box-shadow:var(--dsw-shadow-lv3,0 12px 40px #0000003d);border-radius:10px;flex-direction:column;padding:6px;display:flex;position:absolute;top:calc(100% + 4px);right:0}.Qor37a_filterPanelHead{flex:none;align-items:center;gap:6px;padding:2px 4px 6px 2px;display:flex}.Qor37a_filterBack{width:22px;height:22px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:0;border-radius:6px;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.Qor37a_filterBack:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.Qor37a_filterPanelTitle{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:11px;font-weight:600}.Qor37a_filterValue{min-width:0;max-width:55%;color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;flex:0 auto;font-size:11px;overflow:hidden}.Qor37a_filterChevron{color:var(--dsw-alias-label-tertiary);flex:none;align-items:center;display:inline-flex}.Qor37a_filterSearch{flex:auto;min-width:0;margin:0 2px 4px;position:relative}.Qor37a_filterSearch svg{color:var(--dsw-alias-label-tertiary);pointer-events:none;position:absolute;top:50%;left:6px;transform:translateY(-50%)}.Qor37a_filterSearch input{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);width:100%;min-width:0;height:24px;color:var(--dsw-alias-label-primary);border-radius:7px;outline:none;padding:0 28px 0 21px;font-family:inherit;font-size:11.5px}.Qor37a_filterSearch input:focus{border-color:var(--dsw-alias-state-business-primary)}.Qor37a_filterList{flex:1;min-height:0;overflow-y:auto}.Qor37a_filterEmpty{color:var(--dsw-alias-label-tertiary);padding:8px;font-size:11px;line-height:16px}.Qor37a_filterSection{letter-spacing:.02em;color:var(--dsw-alias-label-tertiary);text-transform:uppercase;padding:6px 8px 2px;font-size:10px;font-weight:600}.Qor37a_filterRow{width:100%;color:var(--dsw-alias-label-primary);font:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:7px;align-items:center;gap:6px;padding:5px 8px;font-size:12px;display:flex}.Qor37a_filterRow:hover{background:var(--dsw-alias-interactive-bg-hover)}.Qor37a_filterCheck{width:12px;color:var(--dsw-static-deepseek-500);flex:none;font-size:11px}.Qor37a_filterLabel{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;overflow:hidden}.Qor37a_filterCount{color:var(--dsw-alias-label-tertiary);flex:none;font-size:10.5px}.Qor37a_filterTagRow{align-items:center;gap:2px;display:flex}.Qor37a_filterTagRow .Qor37a_filterRow{flex:1;min-width:0}.Qor37a_filterTagDelete{width:20px;height:20px;color:var(--dsw-alias-label-tertiary);font:inherit;cursor:pointer;background:0 0;border:none;border-radius:6px;flex:none;padding:0;font-size:13px;line-height:1}.Qor37a_filterTagDelete:hover{background:var(--dsw-alias-interactive-bg-hover-danger,var(--dsw-alias-interactive-bg-hover));color:var(--dsw-static-red-600,#d54941)}.Qor37a_chipBroken{background:var(--dsw-static-amber-500,#d9a441);color:#1f2329;border-radius:50%;flex:none;place-items:center;width:14px;height:14px;font-size:10px;font-weight:700;display:grid}.Qor37a_notice{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:9px;flex-direction:column;gap:4px;margin:0 10px 8px;padding:8px 10px;display:flex}.Qor37a_noticeRow{justify-content:space-between;align-items:baseline;gap:10px;display:flex}.Qor37a_noticeText{color:var(--dsw-alias-label-secondary);font-size:11px;line-height:17px}.Qor37a_noticeLink{color:var(--dsw-static-deepseek-500);font:inherit;cursor:pointer;background:0 0;border:0;flex:none;padding:0;font-size:11px}.Qor37a_sourceNameInput,.Qor37a_sourceUrlInput{width:100%;color:var(--dsw-alias-label-primary);font:inherit;background:0 0;border:1px solid #0000;border-radius:6px;min-width:0;padding:2px 4px}.Qor37a_sourceNameInput{font-size:12.5px;font-weight:600}.Qor37a_sourceUrlInput{color:var(--dsw-alias-label-tertiary);font-size:10.5px}.Qor37a_sourceNameInput:hover,.Qor37a_sourceUrlInput:hover{border-color:var(--dsw-alias-border-l1)}.Qor37a_sourceNameInput:focus,.Qor37a_sourceUrlInput:focus{border-color:var(--dsw-static-deepseek-500);background:var(--dsw-alias-bg-base);outline:none}.Qor37a_tagEdit{flex-wrap:wrap;align-items:center;gap:6px;margin:10px 0 0;display:flex}.Qor37a_tagEditLabel{color:var(--dsw-alias-label-tertiary);font-size:11px}.Qor37a_tagChip{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border-radius:999px;align-items:center;gap:4px;padding:2px 7px;font-size:11px;display:inline-flex}.Qor37a_tagChipRemove{color:var(--dsw-alias-label-tertiary);font:inherit;cursor:pointer;background:0 0;border:0;padding:0;font-size:13px;line-height:1}.Qor37a_tagChipRemove:hover{color:var(--dsw-static-red-600,#d54941)}.Qor37a_tagAdd{border:1px dashed var(--dsw-alias-border-l2);width:22px;height:22px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border-radius:999px;justify-content:center;align-items:center;padding:0;line-height:0;display:inline-flex}.Qor37a_tagAdd:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.Qor37a_tagInput{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);width:100%;color:var(--dsw-alias-label-primary);font:inherit;border-radius:8px;outline:none;padding:5px 9px;font-size:11px}.Qor37a_tagInput:focus{border-color:var(--dsw-alias-state-business-primary)}.Qor37a_cardMenu{z-index:40;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);min-width:150px;box-shadow:var(--dsw-shadow-lv3,0 12px 40px #0000003d);border-radius:9px;padding:4px;position:fixed}.Qor37a_cardMenu button{width:100%;color:var(--dsw-alias-label-primary);font:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:7px;padding:5px 9px;font-size:12px;display:block}.Qor37a_cardMenu button:hover{background:var(--dsw-alias-interactive-bg-hover)}.Qor37a_cardMenu .Qor37a_cardMenuDanger{color:var(--dsw-static-red-600,#d54941)}.Qor37a_fetchPill{border:1px solid var(--dsw-alias-border-l2);height:18px;color:var(--dsw-alias-label-tertiary);cursor:pointer;border-radius:999px;flex:none;align-items:center;gap:4px;padding:0 7px;font-size:10.5px;line-height:1;display:inline-flex}.Qor37a_fetchPill:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.Qor37a_fetchPill_fetching{border-color:var(--dsw-alias-state-business-primary,#4a7cf6);color:var(--dsw-alias-state-business-primary,#4a7cf6)}.Qor37a_fetchPill_ready{border-color:var(--dsw-static-green-600,#2f9e44);color:var(--dsw-static-green-600,#2f9e44)}.Qor37a_fetchPill_failed{border-color:var(--dsw-static-red-600,#d54941);color:var(--dsw-static-red-600,#d54941)}.Qor37a_pillPop{z-index:40;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);max-width:248px;box-shadow:var(--dsw-shadow-lv3,0 12px 40px #0000003d);color:var(--dsw-alias-label-secondary);border-radius:9px;padding:8px 10px;font-size:11px;line-height:17px;position:fixed}.Qor37a_pillPopReason{margin:0}.Qor37a_pillPopActions{gap:12px;margin-top:6px;display:flex}.Qor37a_tagOnCard{border:1px dashed var(--dsw-alias-border-l2);width:18px;height:18px;color:var(--dsw-alias-label-tertiary);opacity:0;cursor:pointer;border-radius:999px;justify-content:center;align-items:center;padding:0;line-height:0;display:inline-flex}.Qor37a_card:hover .Qor37a_tagOnCard,.Qor37a_tagOnCard:focus-visible{opacity:1}.Qor37a_tagOnCard:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.Qor37a_cardTagPanel{z-index:40;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);width:236px;max-height:296px;box-shadow:var(--dsw-shadow-lv3,0 12px 40px #0000003d);border-radius:12px;flex-direction:column;padding:6px;display:flex;position:fixed}.Qor37a_tagPanelHead{flex:none;justify-content:space-between;align-items:baseline;gap:8px;padding:4px 8px 6px;display:flex}.Qor37a_tagPanelTitle{color:var(--dsw-alias-label-primary);font-size:11px;font-weight:600}.Qor37a_tagPanelHint{color:var(--dsw-alias-label-tertiary);font-size:10px}.Qor37a_tagPanelList{flex:1;min-height:0;overflow-y:auto}.Qor37a_tagPanelEmpty{color:var(--dsw-alias-label-tertiary);padding:4px 8px 8px;font-size:10.5px;line-height:15px}.Qor37a_tagRow,.Qor37a_tagCreate{width:100%;color:var(--dsw-alias-label-primary);font:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:8px;align-items:center;gap:6px;padding:5px 8px;font-size:11.5px;display:flex}.Qor37a_tagRow:hover,.Qor37a_tagCreate:hover{background:var(--dsw-alias-interactive-bg-hover)}.Qor37a_tagRow[data-on]{color:var(--dsw-alias-label-primary);font-weight:600}.Qor37a_tagRowCheck{width:12px;color:var(--dsw-static-deepseek-500);flex:none;font-size:11px}.Qor37a_tagRowName{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;overflow:hidden}.Qor37a_tagCreate{color:var(--dsw-static-deepseek-500);margin-top:1px}.Qor37a_tagPanelField{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-tertiary);border-radius:8px;flex:none;align-items:center;gap:6px;margin-top:6px;padding:0 8px;display:flex}.Qor37a_tagPanelField:focus-within{border-color:var(--dsw-alias-state-business-primary)}.Qor37a_tagEditField{flex-direction:column;gap:4px;min-width:170px;display:flex}.Qor37a_tagSuggest{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);box-shadow:var(--dsw-shadow-lv3,0 12px 40px #0000003d);border-radius:10px;flex-direction:column;gap:1px;padding:4px;display:flex}.Qor37a_cardTagPanel .Qor37a_tagInput{background:0 0;border:0;border-radius:0;width:100%;padding:6px 0;font-size:11.5px}.Qor37a_cardTagPanel .Qor37a_tagInput:focus{border:0}.Qor37a_unit{-webkit-box-decoration-break:clone;box-decoration-break:clone;cursor:pointer;border-radius:3px;transition:background-color .1s}.Qor37a_unit:hover{background:var(--dsw-alias-interactive-bg-hover)}.Qor37a_unit[data-open]{box-shadow:inset 0 -1px 0 var(--dsw-static-deepseek-500)}.Qor37a_unit:hover,.Qor37a_unit[data-hover]{background:var(--dsw-alias-interactive-bg-hover);box-shadow:inset 0 -2px 0 var(--dsw-static-deepseek-500)}.Qor37a_article[data-reader-translated=orig] .Qor37a_unit{cursor:text}.Qor37a_article[data-reader-translated=orig] .Qor37a_unit:hover{background:0 0}.Qor37a_article[data-reader-translated=trans],.Qor37a_article[data-reader-translated=both]{letter-spacing:.005em;font-size:13.5px;line-height:1.78}.Qor37a_reveal{border-left:2px solid var(--dsw-alias-border-l2);padding:1px 0 1px 10px}.Qor37a_reveal[data-placement=sibling]{margin:-8px 0 14px}.Qor37a_reveal[data-placement=inside]{margin:5px 0 4px}.Qor37a_reveal[data-placement=inline]{margin:5px 0 10px}.Qor37a_revealLine{letter-spacing:-.003em;color:var(--dsw-alias-label-secondary);cursor:text;border-radius:4px;margin:0 0 3px;padding-left:7px;font-size:13.5px;line-height:1.62;transition:background-color .1s}.Qor37a_revealLine:last-child{margin-bottom:0}.Qor37a_revealLine:hover{background:var(--dsw-alias-interactive-bg-hover)}.Qor37a_revealLine[data-hover]{background:var(--dsw-alias-interactive-bg-active);box-shadow:inset 2px 0 0 var(--dsw-static-deepseek-500);color:var(--dsw-alias-label-primary)}.Qor37a_translateWrap{flex:none;align-items:center;display:inline-flex;position:relative}.Qor37a_translateCaret{width:15px;height:26px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:0;border-radius:5px;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.Qor37a_translateCaret:hover,.Qor37a_translateCaret[aria-expanded=true]{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.Qor37a_translateMenu{z-index:30;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);width:232px;box-shadow:var(--dsw-shadow-lv3,0 12px 40px #0000003d);border-radius:11px;padding:5px;position:absolute;top:calc(100% + 6px);right:-30px}.Qor37a_translateMenuHead{border-bottom:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-tertiary);margin-bottom:4px;padding:5px 8px 6px;font-size:10px}.Qor37a_translateMenuItem{width:100%;color:var(--dsw-alias-label-primary);font:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:8px;align-items:center;gap:8px;padding:6px 8px;font-size:12px;display:flex}.Qor37a_translateMenuItem:hover{background:var(--dsw-alias-interactive-bg-hover)}.Qor37a_translateCheck{width:12px;color:var(--dsw-static-deepseek-500);flex:none;font-size:11px}.Qor37a_translateMenuLabel{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;overflow:hidden}.Qor37a_translateStatus{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-secondary);border-radius:8px;align-items:center;gap:8px;margin:0 0 10px;padding:5px 9px;font-size:10.5px;display:flex}.Qor37a_translateStatus b{color:var(--dsw-alias-label-primary);font-weight:600}.Qor37a_translateBar{background:var(--dsw-alias-border-l1);border-radius:2px;flex:1;height:4px;overflow:hidden}.Qor37a_translateBar i{background:var(--dsw-static-deepseek-500);border-radius:2px;height:100%;transition:width .2s;display:block}.Qor37a_translateTip{border:1px dashed var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);border-radius:8px;align-items:center;gap:8px;margin:0 0 10px;padding:5px 9px;font-size:10.5px;display:flex}.Qor37a_translateTip button{color:var(--dsw-alias-label-tertiary);font:inherit;cursor:pointer;background:0 0;border:0;margin-left:auto;padding:0 2px;font-size:13px;line-height:1}.Qor37a_translateTip button:hover{color:var(--dsw-alias-label-primary)}";
8817
+ const css = ".TLqg8W_root{--reader-gutter:12px;height:100%;min-height:0;color:var(--dsw-alias-label-primary);flex-direction:column;display:flex;position:relative;overflow:hidden}.TLqg8W_head{border-bottom:1px solid var(--dsw-alias-border-l2);flex:none;align-items:center;gap:2px;min-width:0;padding:8px 8px 8px 10px;display:flex}.TLqg8W_headTitle{flex:1;align-items:center;gap:6px;min-width:0;font-size:12.5px;font-weight:600;display:flex}.TLqg8W_count{color:var(--dsw-alias-label-tertiary);font-size:10.5px;font-weight:400}.TLqg8W_tool{width:26px;height:26px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:50%;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.TLqg8W_tool:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.TLqg8W_toolOn{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary)}.TLqg8W_toolDanger:hover{background:var(--dsw-alias-interactive-bg-hover-danger,var(--dsw-alias-interactive-bg-hover));color:var(--dsw-static-red-600,#d54941)}.TLqg8W_tools{flex:none;align-items:center;gap:4px;min-width:0;padding:8px 10px 6px;display:flex}.TLqg8W_toolWrap{flex:none;display:inline-flex;position:relative}.TLqg8W_search{flex:1 1 0;min-width:44px;position:relative}.TLqg8W_search svg{color:var(--dsw-alias-label-tertiary);pointer-events:none;position:absolute;top:50%;left:7px;transform:translateY(-50%)}.TLqg8W_search input{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);width:100%;min-width:0;height:26px;color:var(--dsw-alias-label-primary);border-radius:7px;outline:none;padding:0 28px 0 26px;font-family:inherit;font-size:11.5px}.TLqg8W_searchClear{width:22px;height:22px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:50%;justify-content:center;align-items:center;padding:0;display:inline-flex;position:absolute;top:50%;right:3px;transform:translateY(-50%)}.TLqg8W_searchClear:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.TLqg8W_search input::placeholder{color:var(--dsw-alias-label-tertiary)}.TLqg8W_search input:focus{border-color:var(--dsw-static-deepseek-500)}.TLqg8W_scroll{flex:1;min-height:0;padding:2px 8px 10px;overflow-y:auto}.TLqg8W_list{grid-template-columns:repeat(auto-fill,minmax(264px,1fr));gap:8px;display:grid}.TLqg8W_card{--reader-card-fill:var(--dsw-static-neutral-50,#fafafa);--reader-card-hover:var(--dsw-static-neutral-100,#f5f5f5);border:1px solid var(--dsw-alias-border-l1);background:var(--reader-card-fill);text-align:left;cursor:pointer;border-radius:12px;grid-template-columns:auto minmax(0,1fr) auto;align-items:start;gap:2px 9px;padding:9px 9px 9px 10px;transition:background-color .12s,border-color .12s;display:grid;position:relative}body[data-ds-dark-theme] .TLqg8W_card{--reader-card-fill:var(--dsw-static-neutral-850,#212123);--reader-card-hover:var(--dsw-static-neutral-800,#292929)}.TLqg8W_card:hover{border-color:var(--dsw-alias-border-l2);background:var(--reader-card-hover)}.TLqg8W_card:active{border-color:var(--dsw-alias-border-l3);background:var(--reader-card-hover)}.TLqg8W_card:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}.TLqg8W_tileWrap{grid-area:1/1/span 3;width:24px;height:24px;position:relative}.TLqg8W_tile{width:24px;height:24px;color:var(--dsw-static-neutral-00);border-radius:6px;place-items:center;font-size:10.5px;font-weight:700;display:grid}.TLqg8W_unread{background:var(--dsw-static-deepseek-500);width:7px;height:7px;box-shadow:0 0 0 1.5px var(--dsw-alias-bg-base);border-radius:50%;position:absolute;bottom:-1px;right:-1px}.TLqg8W_meta{grid-column:2;align-items:baseline;gap:6px;min-width:0;display:flex}.TLqg8W_source{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;font-size:10.5px;overflow:hidden}.TLqg8W_sep{color:var(--dsw-alias-label-tertiary);opacity:.6;flex:none;font-size:10.5px}.TLqg8W_when{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;flex:none;font-size:10.5px}.TLqg8W_title{-webkit-line-clamp:2;overflow-wrap:anywhere;min-width:0;min-height:34px;color:var(--dsw-alias-label-secondary);-webkit-box-orient:vertical;grid-column:2/-1;font-size:12.5px;font-weight:600;line-height:17px;display:-webkit-box;overflow:hidden}.TLqg8W_card[data-unread] .TLqg8W_title{color:var(--dsw-alias-label-primary);font-weight:650}.TLqg8W_summary{-webkit-line-clamp:2;overflow-wrap:anywhere;min-width:0;min-height:32px;color:var(--dsw-alias-label-secondary);-webkit-box-orient:vertical;grid-column:2/-1;margin-top:3px;font-size:11.5px;line-height:16px;display:-webkit-box;overflow:hidden}.TLqg8W_cardOrigTitle{-webkit-line-clamp:2;overflow-wrap:anywhere;min-width:0;color:var(--dsw-alias-label-tertiary);-webkit-box-orient:vertical;grid-column:2/-1;margin-top:2px;font-size:12.5px;font-weight:400;line-height:17px;display:-webkit-box;overflow:hidden}.TLqg8W_cardOrigSummary{-webkit-line-clamp:2;overflow-wrap:anywhere;min-width:0;color:var(--dsw-alias-label-tertiary);-webkit-box-orient:vertical;grid-column:2/-1;margin-top:2px;font-size:11.5px;line-height:16px;display:-webkit-box;overflow:hidden}.TLqg8W_tags{flex-wrap:wrap;grid-column:2/-1;align-items:center;gap:5px;min-width:0;margin-top:6px;display:flex}.TLqg8W_tag,.TLqg8W_tagOwned{border-radius:999px;flex:none;align-items:center;height:18px;padding:0 7px;font-size:10.5px;line-height:1;display:inline-flex}.TLqg8W_tag{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-tertiary)}.TLqg8W_tagOwned{border:1px solid var(--dsw-static-deepseek-500);color:var(--dsw-alias-label-primary)}.TLqg8W_tagLinkOnly{border:1px dashed var(--dsw-alias-border-l3,var(--dsw-alias-border-l2));height:18px;color:var(--dsw-alias-label-tertiary);border-radius:999px;flex:none;align-items:center;padding:0 7px;font-size:10.5px;line-height:1;display:inline-flex}.TLqg8W_author{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;font-size:10px;overflow:hidden}.TLqg8W_chevron{width:22px;height:22px;color:var(--dsw-alias-label-tertiary);border-radius:6px;grid-area:1/3;justify-content:center;align-items:center;margin-top:2px;display:inline-flex}.TLqg8W_card:hover .TLqg8W_chevron{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-secondary)}.TLqg8W_metaBar{border-top:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-tertiary);flex:none;align-items:center;gap:5px;padding:5px 12px 7px;font-size:10.5px;line-height:15px;display:flex}.TLqg8W_state{color:var(--dsw-alias-label-tertiary);padding:26px 14px;font-size:11.5px;line-height:18px}.TLqg8W_state b{color:var(--dsw-alias-label-primary);margin-bottom:4px;font-weight:600;display:block}.TLqg8W_add{flex-direction:column;flex:1;min-height:0;padding:12px 12px 0;display:flex}.TLqg8W_add h2{margin:0 0 4px;font-size:13.5px;font-weight:700}.TLqg8W_add p{color:var(--dsw-alias-label-tertiary);margin:0 0 12px;font-size:11px;line-height:17px}.TLqg8W_add input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);width:100%;height:30px;color:var(--dsw-alias-label-primary);border-radius:8px;outline:none;margin-bottom:8px;padding:0 9px;font-family:inherit;font-size:12px}.TLqg8W_add input:focus{border-color:var(--dsw-static-deepseek-500)}.TLqg8W_submit{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-base);cursor:pointer;border:none;border-radius:8px;align-self:flex-start;padding:7px 13px;font-family:inherit;font-size:11.5px}.TLqg8W_submit:hover{opacity:.88}.TLqg8W_verdict{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);border-radius:9px;margin-top:12px;padding:9px 11px;font-size:11px;line-height:17px}.TLqg8W_verdict b{color:var(--dsw-alias-label-primary)}.TLqg8W_verdictReason{word-break:break-word;opacity:.9;margin-top:4px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10px;line-height:15px;display:block}.TLqg8W_detail{flex-direction:column;flex:1;min-height:0;display:flex}.TLqg8W_bar{--reader-bar-pad:calc(var(--reader-gutter) - 10px);min-width:0;padding:6px var(--reader-bar-pad);border-bottom:1px solid var(--dsw-alias-border-l2);flex:none;align-items:center;gap:2px;display:flex}.TLqg8W_barLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-primary);flex:1;padding:0 4px;font-size:12.5px;font-weight:600;overflow:hidden}.TLqg8W_spacer{flex:1}.TLqg8W_detailBody{min-height:0;padding:0 var(--reader-gutter) 22px;flex:1;overflow-y:auto}.TLqg8W_kicker{color:var(--dsw-alias-label-tertiary);align-items:center;gap:6px;margin:14px 0 8px;font-size:10.5px;display:flex}.TLqg8W_kicker .TLqg8W_tile{border-radius:4px;width:16px;height:16px;font-size:9px}.TLqg8W_detailTitle{letter-spacing:-.005em;margin:0 0 9px;font-size:17px;font-weight:700;line-height:1.4}.TLqg8W_tagRow{flex-wrap:wrap;align-items:center;gap:4px;margin-bottom:14px;display:flex}.TLqg8W_refetch{border:1px solid var(--dsw-alias-border-l2);height:18px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border-radius:999px;flex:none;align-items:center;gap:3px;padding:0 7px;font-family:inherit;font-size:10.5px;line-height:1;display:inline-flex}.TLqg8W_refetch:hover:not(:disabled){border-color:var(--dsw-static-deepseek-500);color:var(--dsw-alias-label-primary)}.TLqg8W_refetch:disabled{cursor:default;opacity:.6}.TLqg8W_rule{background:var(--dsw-alias-border-l2);height:1px;margin:0 0 16px}.TLqg8W_article{color:var(--dsw-alias-label-secondary)}.TLqg8W_articleZh{letter-spacing:.005em;font-size:13.5px;line-height:1.78}.TLqg8W_articleEn{letter-spacing:-.003em;font-size:13.5px;line-height:1.66}.TLqg8W_article p{margin:0 0 12px}.TLqg8W_article h1,.TLqg8W_article h2{color:var(--dsw-alias-label-primary);margin:24px 0 9px;font-size:15px;font-weight:700;line-height:1.45}.TLqg8W_article h3,.TLqg8W_article h4{color:var(--dsw-alias-label-primary);margin:19px 0 7px;font-size:13.5px;font-weight:700;line-height:1.45}.TLqg8W_article ul,.TLqg8W_article ol{margin:0 0 12px;padding-left:19px}.TLqg8W_article li{margin:0 0 5px}.TLqg8W_article blockquote{border-left:3px solid var(--dsw-alias-border-l3);margin:0 0 12px;padding:2px 0 2px 11px}.TLqg8W_article code{background:var(--dsw-alias-bg-layer-1);font-family:var(--dsw-font-mono,ui-monospace, monospace);border-radius:4px;padding:1px 5px;font-size:.92em}.TLqg8W_article pre{background:var(--dsw-alias-bg-layer-1);border-radius:8px;margin:0 0 12px;padding:10px 12px;overflow-x:auto}.TLqg8W_article pre code{background:0 0;padding:0;font-size:12px}.TLqg8W_article a{color:var(--dsw-alias-link);text-decoration:none}.TLqg8W_article a:hover{text-underline-offset:2px;text-decoration:underline}.TLqg8W_article img{border-radius:8px;max-width:100%;height:auto;margin:4px 0 12px;display:block}.TLqg8W_article strong{color:var(--dsw-alias-label-primary);font-weight:650}.TLqg8W_article table{border-collapse:collapse;width:100%;margin:0 0 12px;font-size:12.5px}.TLqg8W_article th,.TLqg8W_article td{border:1px solid var(--dsw-alias-border-l2);text-align:left;padding:5px 8px}.TLqg8W_article math{font-size:1.02em}.TLqg8W_article math[display=block]{max-width:100%;margin:0 0 12px;padding:6px 0;display:block;overflow:auto hidden}.TLqg8W_incomplete{border-top:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);margin:18px 0 0;padding-top:12px;font-size:11px;line-height:17px}.TLqg8W_incompleteLink{font-family:inherit;font-size:inherit;color:var(--dsw-alias-link);cursor:pointer;background:0 0;border:none;padding:0}.TLqg8W_incompleteLink:hover{text-underline-offset:2px;text-decoration:underline}.TLqg8W_alsoFrom{margin-top:22px}.TLqg8W_alsoFromLabel{letter-spacing:.04em;color:var(--dsw-alias-label-tertiary);margin-bottom:6px;font-size:10.5px;font-weight:600}.TLqg8W_alsoFromRow{border:none;border-top:1px solid var(--dsw-alias-border-l1);width:100%;color:var(--dsw-alias-label-secondary);text-align:left;cursor:pointer;background:0 0;align-items:baseline;gap:8px;padding:7px 0;font-family:inherit;font-size:11.5px;display:flex}.TLqg8W_alsoFromRow:hover{color:var(--dsw-alias-label-primary)}.TLqg8W_alsoFromTitle{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;overflow:hidden}.TLqg8W_composer{border-top:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);flex:none;padding:7px 10px}.TLqg8W_composerLabel{color:var(--dsw-alias-label-tertiary);margin-bottom:4px;font-size:10px}.TLqg8W_composerText{max-height:52px;color:var(--dsw-alias-label-secondary);white-space:pre-wrap;word-break:break-word;font-size:11px;line-height:16px;overflow-y:auto}.TLqg8W_composerEmpty{color:var(--dsw-alias-label-tertiary)}.TLqg8W_foot{border-top:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-tertiary);flex:none;align-items:center;gap:6px;padding:6px 10px;font-size:10.5px;display:flex}.TLqg8W_menu{z-index:20;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);min-width:150px;box-shadow:var(--dsw-shadow-lv3,0 12px 40px #0000003d);border-radius:10px;padding:4px;position:absolute;top:calc(100% + 4px);right:0}.TLqg8W_menu button{width:100%;color:var(--dsw-alias-label-primary);text-align:left;cursor:pointer;background:0 0;border:none;border-radius:6px;justify-content:space-between;align-items:center;gap:10px;padding:6px 8px;font-family:inherit;font-size:11.5px;display:flex}.TLqg8W_menu button:hover{background:var(--dsw-alias-interactive-bg-hover)}.TLqg8W_check{color:var(--dsw-static-deepseek-500)}.TLqg8W_sourceGrid{grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:8px;padding:4px 2px 10px;display:grid}.TLqg8W_sourceCard{border:1px solid var(--dsw-alias-border-l2-darkmode-thin,#0000001a);background:var(--dsw-alias-bg-layer-1);min-width:0;color:inherit;font:inherit;text-align:left;cursor:pointer;border-radius:12px;align-items:center;gap:9px;padding:9px 10px;display:flex}.TLqg8W_sourceCard:hover{background:var(--dsw-alias-interactive-bg-hover)}.TLqg8W_addCard{border:1px dashed var(--dsw-alias-border-l2,#7f7f7f66);min-height:58px;color:var(--dsw-alias-label-tertiary,#7f8287);font:inherit;cursor:pointer;background:0 0;border-radius:12px;justify-content:center;align-items:center;gap:6px;padding:9px 10px;font-size:12.5px;display:flex}.TLqg8W_addCard:hover{background:var(--dsw-alias-interactive-bg-hover,#7f7f7f24);color:var(--dsw-alias-label-primary)}.TLqg8W_emptyCard{border:1px dashed var(--dsw-alias-border-l2,#7f7f7f66);min-height:132px;color:var(--dsw-alias-label-secondary);font:inherit;text-align:left;cursor:pointer;background:0 0;border-radius:14px;flex-direction:column;grid-column:1/-1;align-items:flex-start;gap:6px;padding:18px 20px;display:flex}.TLqg8W_emptyCard:hover{background:var(--dsw-alias-interactive-bg-hover,#7f7f7f24)}.TLqg8W_emptyTitle{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:600}.TLqg8W_emptyBody{color:var(--dsw-alias-label-tertiary);font-size:12.5px;line-height:19px}.TLqg8W_sourceInfo{flex-direction:column;gap:1px;min-width:0;display:flex}.TLqg8W_sourceName{color:var(--dsw-alias-label-primary);white-space:nowrap;text-overflow:ellipsis;font-size:12.5px;font-weight:600;overflow:hidden}.TLqg8W_sourceMeta{color:var(--dsw-alias-label-tertiary);white-space:nowrap;text-overflow:ellipsis;font-size:10.5px;line-height:15px;overflow:hidden}.TLqg8W_sourceFailed{color:var(--dsw-static-red-600,#d54941)}.TLqg8W_sourceError{color:var(--dsw-static-red-600,#d54941);word-break:break-word;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10px;line-height:14px}.TLqg8W_overlay{z-index:100;text-align:left;background:0 0;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.TLqg8W_dialog{border:1px solid var(--dsw-alias-border-l1,#7f7f7f29);background:var(--dsw-alias-bg-layer-1);width:520px;max-width:calc(100vw - 48px);max-height:calc(100vh - 96px);box-shadow:var(--dsw-shadow-lv3,0 12px 40px #0000003d);border-radius:18px;flex-direction:column;gap:12px;padding:20px 22px;display:flex;overflow-y:auto}.TLqg8W_dialog h2{color:var(--dsw-alias-label-primary);margin:0;font-size:15px;font-weight:600}.TLqg8W_dialog p{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:19px}.TLqg8W_dialog input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font:inherit;border-radius:9px;padding:9px 11px;font-size:12.5px}.TLqg8W_dialog input:focus{border-color:var(--dsw-static-deepseek-500);outline:none}.TLqg8W_dialogActions{justify-content:flex-end;align-items:center;gap:8px;margin-top:2px;display:flex}.TLqg8W_ghost{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;background:0 0;border-radius:9px;padding:7px 13px;font-size:12px}.TLqg8W_ghost:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.TLqg8W_paneBody{min-height:0;padding:12px var(--reader-gutter);flex:1;overflow-y:auto}.TLqg8W_help{color:var(--dsw-alias-label-tertiary);margin:0 0 8px;font-size:11.5px;line-height:18px}.TLqg8W_settingsRow{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:10px;flex-wrap:wrap;align-items:center;gap:8px 16px;margin:0 0 10px;padding:6px 10px;display:flex}.TLqg8W_setting{cursor:pointer;align-items:center;gap:6px;display:inline-flex}.TLqg8W_settingLabel{color:var(--dsw-alias-label-tertiary);font-size:11.5px}.TLqg8W_settingControl{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font:inherit;border-radius:7px;padding:3px 6px;font-size:11.5px}.TLqg8W_settingReadout{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;font-size:11.5px}.TLqg8W_manageTools{flex-wrap:wrap;align-items:center;gap:4px 8px;margin-bottom:8px;display:flex}.TLqg8W_chipGroupLabel{color:var(--dsw-alias-label-tertiary);flex:none;font-size:10px}.TLqg8W_chipRow{flex-wrap:wrap;gap:4px;display:flex}.TLqg8W_chip{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;background:0 0;border-radius:999px;align-items:center;gap:4px;padding:2px 7px;font-size:10.5px;display:inline-flex}.TLqg8W_chip:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.TLqg8W_chipOn{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary);border-color:#0000}.TLqg8W_chipCount{color:var(--dsw-alias-label-tertiary);font-size:10px}.TLqg8W_sourceList,.TLqg8W_recentList{flex-direction:column;gap:6px;display:flex}.TLqg8W_recentRow{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);text-align:left;cursor:pointer;border-radius:10px;flex-direction:column;gap:3px;width:100%;padding:9px 10px;font-family:inherit;display:flex}.TLqg8W_recentRow:hover:not(:disabled){border-color:var(--dsw-alias-border-l2);background:var(--dsw-alias-interactive-bg-hover)}.TLqg8W_recentRow:disabled{cursor:default;opacity:.55}.TLqg8W_recentTitle{color:var(--dsw-alias-label-primary);text-overflow:ellipsis;white-space:nowrap;font-size:12.5px;line-height:17px;overflow:hidden}.TLqg8W_recentMeta{color:var(--dsw-alias-label-tertiary);align-items:center;gap:5px;font-size:10.5px;display:flex}.TLqg8W_recentSource{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.TLqg8W_sourceRow{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:10px;align-items:flex-start;gap:9px;padding:9px 10px;display:flex}.TLqg8W_sourceRow .TLqg8W_sourceInfo{flex:1}.TLqg8W_rowAction{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;background:0 0;border-radius:7px;flex:none;margin-top:2px;padding:4px 9px;font-size:11px}.TLqg8W_rowAction:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.TLqg8W_rowActionDanger{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-static-red-600,#d54941);font:inherit;cursor:pointer;background:0 0;border-radius:7px;flex:none;padding:4px 9px;font-size:11px}.TLqg8W_rowActionDanger:hover{background:var(--dsw-alias-interactive-bg-hover-danger,var(--dsw-alias-interactive-bg-hover))}.TLqg8W_toolSpinning svg{animation:.9s linear infinite TLqg8W_reader-spin}@media (prefers-reduced-motion:reduce){.TLqg8W_toolSpinning svg{animation:none}}@keyframes TLqg8W_reader-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.TLqg8W_tool:disabled{opacity:.6;cursor:default}.TLqg8W_filterPanel{z-index:20;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);min-width:200px;max-width:280px;max-height:min(62vh,440px);box-shadow:var(--dsw-shadow-lv3,0 12px 40px #0000003d);border-radius:10px;flex-direction:column;padding:6px;display:flex;position:absolute;top:calc(100% + 4px);right:0}.TLqg8W_filterPanelHead{flex:none;align-items:center;gap:6px;padding:2px 4px 6px 2px;display:flex}.TLqg8W_filterBack{width:22px;height:22px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:0;border-radius:6px;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.TLqg8W_filterBack:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.TLqg8W_filterPanelTitle{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:11px;font-weight:600}.TLqg8W_filterValue{min-width:0;max-width:55%;color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;flex:0 auto;font-size:11px;overflow:hidden}.TLqg8W_filterChevron{color:var(--dsw-alias-label-tertiary);flex:none;align-items:center;display:inline-flex}.TLqg8W_filterSearch{flex:auto;min-width:0;margin:0 2px 4px;position:relative}.TLqg8W_filterSearch svg{color:var(--dsw-alias-label-tertiary);pointer-events:none;position:absolute;top:50%;left:6px;transform:translateY(-50%)}.TLqg8W_filterSearch input{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);width:100%;min-width:0;height:24px;color:var(--dsw-alias-label-primary);border-radius:7px;outline:none;padding:0 28px 0 21px;font-family:inherit;font-size:11.5px}.TLqg8W_filterSearch input:focus{border-color:var(--dsw-alias-state-business-primary)}.TLqg8W_filterList{flex:1;min-height:0;overflow-y:auto}.TLqg8W_filterEmpty{color:var(--dsw-alias-label-tertiary);padding:8px;font-size:11px;line-height:16px}.TLqg8W_filterSection{letter-spacing:.02em;color:var(--dsw-alias-label-tertiary);text-transform:uppercase;padding:6px 8px 2px;font-size:10px;font-weight:600}.TLqg8W_filterRow{width:100%;color:var(--dsw-alias-label-primary);font:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:7px;align-items:center;gap:6px;padding:5px 8px;font-size:12px;display:flex}.TLqg8W_filterRow:hover{background:var(--dsw-alias-interactive-bg-hover)}.TLqg8W_filterCheck{width:12px;color:var(--dsw-static-deepseek-500);flex:none;font-size:11px}.TLqg8W_filterLabel{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;overflow:hidden}.TLqg8W_filterCount{color:var(--dsw-alias-label-tertiary);flex:none;font-size:10.5px}.TLqg8W_filterTagRow{align-items:center;gap:2px;display:flex}.TLqg8W_filterTagRow .TLqg8W_filterRow{flex:1;min-width:0}.TLqg8W_filterTagDelete{width:20px;height:20px;color:var(--dsw-alias-label-tertiary);font:inherit;cursor:pointer;background:0 0;border:none;border-radius:6px;flex:none;padding:0;font-size:13px;line-height:1}.TLqg8W_filterTagDelete:hover{background:var(--dsw-alias-interactive-bg-hover-danger,var(--dsw-alias-interactive-bg-hover));color:var(--dsw-static-red-600,#d54941)}.TLqg8W_chipBroken{background:var(--dsw-static-amber-500,#d9a441);color:#1f2329;border-radius:50%;flex:none;place-items:center;width:14px;height:14px;font-size:10px;font-weight:700;display:grid}.TLqg8W_notice{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:9px;flex-direction:column;gap:4px;margin:0 10px 8px;padding:8px 10px;display:flex}.TLqg8W_noticeRow{justify-content:space-between;align-items:baseline;gap:10px;display:flex}.TLqg8W_noticeText{color:var(--dsw-alias-label-secondary);font-size:11px;line-height:17px}.TLqg8W_noticeLink{color:var(--dsw-static-deepseek-500);font:inherit;cursor:pointer;background:0 0;border:0;flex:none;padding:0;font-size:11px}.TLqg8W_sourceNameInput,.TLqg8W_sourceUrlInput{width:100%;color:var(--dsw-alias-label-primary);font:inherit;background:0 0;border:1px solid #0000;border-radius:6px;min-width:0;padding:2px 4px}.TLqg8W_sourceNameInput{font-size:12.5px;font-weight:600}.TLqg8W_sourceUrlInput{color:var(--dsw-alias-label-tertiary);font-size:10.5px}.TLqg8W_sourceNameInput:hover,.TLqg8W_sourceUrlInput:hover{border-color:var(--dsw-alias-border-l1)}.TLqg8W_sourceNameInput:focus,.TLqg8W_sourceUrlInput:focus{border-color:var(--dsw-static-deepseek-500);background:var(--dsw-alias-bg-base);outline:none}.TLqg8W_tagEdit{flex-wrap:wrap;align-items:center;gap:6px;margin:10px 0 0;display:flex}.TLqg8W_tagEditLabel{color:var(--dsw-alias-label-tertiary);font-size:11px}.TLqg8W_tagChip{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border-radius:999px;align-items:center;gap:4px;padding:2px 7px;font-size:11px;display:inline-flex}.TLqg8W_tagChipRemove{color:var(--dsw-alias-label-tertiary);font:inherit;cursor:pointer;background:0 0;border:0;padding:0;font-size:13px;line-height:1}.TLqg8W_tagChipRemove:hover{color:var(--dsw-static-red-600,#d54941)}.TLqg8W_tagAdd{border:1px dashed var(--dsw-alias-border-l2);width:22px;height:22px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border-radius:999px;justify-content:center;align-items:center;padding:0;line-height:0;display:inline-flex}.TLqg8W_tagAdd:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.TLqg8W_tagInput{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);width:100%;color:var(--dsw-alias-label-primary);font:inherit;border-radius:8px;outline:none;padding:5px 9px;font-size:11px}.TLqg8W_tagInput:focus{border-color:var(--dsw-alias-state-business-primary)}.TLqg8W_cardMenu{z-index:40;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);min-width:150px;box-shadow:var(--dsw-shadow-lv3,0 12px 40px #0000003d);border-radius:9px;padding:4px;position:fixed}.TLqg8W_cardMenu button{width:100%;color:var(--dsw-alias-label-primary);font:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:7px;padding:5px 9px;font-size:12px;display:block}.TLqg8W_cardMenu button:hover{background:var(--dsw-alias-interactive-bg-hover)}.TLqg8W_cardMenu .TLqg8W_cardMenuDanger{color:var(--dsw-static-red-600,#d54941)}.TLqg8W_fetchPill{border:1px solid var(--dsw-alias-border-l2);height:18px;color:var(--dsw-alias-label-tertiary);cursor:pointer;border-radius:999px;flex:none;align-items:center;gap:4px;padding:0 7px;font-size:10.5px;line-height:1;display:inline-flex}.TLqg8W_fetchPill:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.TLqg8W_fetchPill_fetching{border-color:var(--dsw-alias-state-business-primary,#4a7cf6);color:var(--dsw-alias-state-business-primary,#4a7cf6)}.TLqg8W_fetchPill_ready{border-color:var(--dsw-static-green-600,#2f9e44);color:var(--dsw-static-green-600,#2f9e44)}.TLqg8W_fetchPill_failed{border-color:var(--dsw-static-red-600,#d54941);color:var(--dsw-static-red-600,#d54941)}.TLqg8W_pillPop{z-index:40;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);max-width:248px;box-shadow:var(--dsw-shadow-lv3,0 12px 40px #0000003d);color:var(--dsw-alias-label-secondary);border-radius:9px;padding:8px 10px;font-size:11px;line-height:17px;position:fixed}.TLqg8W_pillPopReason{margin:0}.TLqg8W_pillPopActions{gap:12px;margin-top:6px;display:flex}.TLqg8W_tagOnCard{border:1px dashed var(--dsw-alias-border-l2);width:18px;height:18px;color:var(--dsw-alias-label-tertiary);opacity:0;cursor:pointer;border-radius:999px;justify-content:center;align-items:center;padding:0;line-height:0;display:inline-flex}.TLqg8W_card:hover .TLqg8W_tagOnCard,.TLqg8W_tagOnCard:focus-visible{opacity:1}.TLqg8W_tagOnCard:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.TLqg8W_cardTagPanel{z-index:40;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);width:236px;max-height:296px;box-shadow:var(--dsw-shadow-lv3,0 12px 40px #0000003d);border-radius:12px;flex-direction:column;padding:6px;display:flex;position:fixed}.TLqg8W_tagPanelHead{flex:none;justify-content:space-between;align-items:baseline;gap:8px;padding:4px 8px 6px;display:flex}.TLqg8W_tagPanelTitle{color:var(--dsw-alias-label-primary);font-size:11px;font-weight:600}.TLqg8W_tagPanelHint{color:var(--dsw-alias-label-tertiary);font-size:10px}.TLqg8W_tagPanelList{flex:1;min-height:0;overflow-y:auto}.TLqg8W_tagPanelEmpty{color:var(--dsw-alias-label-tertiary);padding:4px 8px 8px;font-size:10.5px;line-height:15px}.TLqg8W_tagRow,.TLqg8W_tagCreate{width:100%;color:var(--dsw-alias-label-primary);font:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:8px;align-items:center;gap:6px;padding:5px 8px;font-size:11.5px;display:flex}.TLqg8W_tagRow:hover,.TLqg8W_tagCreate:hover{background:var(--dsw-alias-interactive-bg-hover)}.TLqg8W_tagRow[data-on]{color:var(--dsw-alias-label-primary);font-weight:600}.TLqg8W_tagRowCheck{width:12px;color:var(--dsw-static-deepseek-500);flex:none;font-size:11px}.TLqg8W_tagRowName{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;overflow:hidden}.TLqg8W_tagCreate{color:var(--dsw-static-deepseek-500);margin-top:1px}.TLqg8W_tagPanelField{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-tertiary);border-radius:8px;flex:none;align-items:center;gap:6px;margin-top:6px;padding:0 8px;display:flex}.TLqg8W_tagPanelField:focus-within{border-color:var(--dsw-alias-state-business-primary)}.TLqg8W_tagEditField{flex-direction:column;gap:4px;min-width:170px;display:flex}.TLqg8W_tagSuggest{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);box-shadow:var(--dsw-shadow-lv3,0 12px 40px #0000003d);border-radius:10px;flex-direction:column;gap:1px;padding:4px;display:flex}.TLqg8W_cardTagPanel .TLqg8W_tagInput{background:0 0;border:0;border-radius:0;width:100%;padding:6px 0;font-size:11.5px}.TLqg8W_cardTagPanel .TLqg8W_tagInput:focus{border:0}.TLqg8W_unit{-webkit-box-decoration-break:clone;box-decoration-break:clone;cursor:pointer;border-radius:3px;transition:background-color .1s}.TLqg8W_unit:hover{background:var(--dsw-alias-interactive-bg-hover)}.TLqg8W_unit[data-open]{box-shadow:inset 0 -1px 0 var(--dsw-static-deepseek-500)}.TLqg8W_unit:hover,.TLqg8W_unit[data-hover]{background:var(--dsw-alias-interactive-bg-hover);box-shadow:inset 0 -2px 0 var(--dsw-static-deepseek-500)}.TLqg8W_article[data-reader-translated=orig] .TLqg8W_unit{cursor:text}.TLqg8W_article[data-reader-translated=orig] .TLqg8W_unit:hover{background:0 0}.TLqg8W_article[data-reader-translated=trans],.TLqg8W_article[data-reader-translated=both]{letter-spacing:.005em;font-size:13.5px;line-height:1.78}.TLqg8W_reveal{border-left:2px solid var(--dsw-alias-border-l2);padding:1px 0 1px 10px}.TLqg8W_reveal[data-placement=sibling]{margin:-8px 0 14px}.TLqg8W_reveal[data-placement=inside]{margin:5px 0 4px}.TLqg8W_reveal[data-placement=inline]{margin:5px 0 10px}.TLqg8W_revealLine{letter-spacing:-.003em;color:var(--dsw-alias-label-secondary);cursor:text;border-radius:4px;margin:0 0 3px;padding-left:7px;font-size:13.5px;line-height:1.62;transition:background-color .1s}.TLqg8W_revealLine:last-child{margin-bottom:0}.TLqg8W_revealLine:hover{background:var(--dsw-alias-interactive-bg-hover)}.TLqg8W_revealLine[data-hover]{background:var(--dsw-alias-interactive-bg-active);box-shadow:inset 2px 0 0 var(--dsw-static-deepseek-500);color:var(--dsw-alias-label-primary)}.TLqg8W_translateWrap{flex:none;align-items:center;display:inline-flex;position:relative}.TLqg8W_translateCaret{width:15px;height:26px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:0;border-radius:5px;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.TLqg8W_translateCaret:hover,.TLqg8W_translateCaret[aria-expanded=true]{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.TLqg8W_translateMenu{z-index:30;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);width:232px;box-shadow:var(--dsw-shadow-lv3,0 12px 40px #0000003d);border-radius:11px;padding:5px;position:absolute;top:calc(100% + 6px);right:-30px}.TLqg8W_translateMenuHead{border-bottom:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-tertiary);margin-bottom:4px;padding:5px 8px 6px;font-size:10px}.TLqg8W_translateMenuItem{width:100%;color:var(--dsw-alias-label-primary);font:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:8px;align-items:center;gap:8px;padding:6px 8px;font-size:12px;display:flex}.TLqg8W_translateMenuItem:hover{background:var(--dsw-alias-interactive-bg-hover)}.TLqg8W_translateCheck{width:12px;color:var(--dsw-static-deepseek-500);flex:none;font-size:11px}.TLqg8W_translateMenuLabel{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;overflow:hidden}.TLqg8W_translateStatus{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-secondary);border-radius:8px;align-items:center;gap:8px;margin:0 0 10px;padding:5px 9px;font-size:10.5px;display:flex}.TLqg8W_translateStatus b{color:var(--dsw-alias-label-primary);font-weight:600}.TLqg8W_translateBar{background:var(--dsw-alias-border-l1);border-radius:2px;flex:1;height:4px;overflow:hidden}.TLqg8W_translateBar i{background:var(--dsw-static-deepseek-500);border-radius:2px;height:100%;transition:width .2s;display:block}.TLqg8W_translateTip{border:1px dashed var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);border-radius:8px;align-items:center;gap:8px;margin:0 0 10px;padding:5px 9px;font-size:10.5px;display:flex}.TLqg8W_translateTip button{color:var(--dsw-alias-label-tertiary);font:inherit;cursor:pointer;background:0 0;border:0;margin-left:auto;padding:0 2px;font-size:13px;line-height:1}.TLqg8W_translateTip button:hover{color:var(--dsw-alias-label-primary)}";
8818
8818
  const tagId = "@khorsheed/dsh-reader/ReaderPane.module.css";
8819
8819
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
8820
8820
  const tag = document.createElement("style");
@@ -8824,172 +8824,172 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8824
8824
  document.head.appendChild(tag);
8825
8825
  }
8826
8826
  var ReaderPane_module_css_default = {
8827
- "filterPanel": "Qor37a_filterPanel",
8828
- "dialogActions": "Qor37a_dialogActions",
8829
- "alsoFromRow": "Qor37a_alsoFromRow",
8830
- "translateMenuHead": "Qor37a_translateMenuHead",
8831
- "filterCount": "Qor37a_filterCount",
8832
- "source": "Qor37a_source",
8833
- "fetchPill_failed": "Qor37a_fetchPill_failed",
8834
- "fetchPill_ready": "Qor37a_fetchPill_ready",
8835
- "toolWrap": "Qor37a_toolWrap",
8836
- "sourceName": "Qor37a_sourceName",
8837
- "cardMenuDanger": "Qor37a_cardMenuDanger",
8838
- "filterTagRow": "Qor37a_filterTagRow",
8839
- "tagCreate": "Qor37a_tagCreate",
8840
- "tagEditField": "Qor37a_tagEditField",
8841
- "translateTip": "Qor37a_translateTip",
8842
- "filterLabel": "Qor37a_filterLabel",
8843
- "tagOwned": "Qor37a_tagOwned",
8844
- "barLabel": "Qor37a_barLabel",
8845
- "author": "Qor37a_author",
8846
- "headTitle": "Qor37a_headTitle",
8847
- "filterPanelTitle": "Qor37a_filterPanelTitle",
8848
- "head": "Qor37a_head",
8849
- "tagEditLabel": "Qor37a_tagEditLabel",
8850
- "tagRowCheck": "Qor37a_tagRowCheck",
8851
- "tagOnCard": "Qor37a_tagOnCard",
8852
- "incompleteLink": "Qor37a_incompleteLink",
8853
- "dialog": "Qor37a_dialog",
8854
- "recentMeta": "Qor37a_recentMeta",
8855
- "tagLinkOnly": "Qor37a_tagLinkOnly",
8856
- "tagPanelList": "Qor37a_tagPanelList",
8857
- "tagSuggest": "Qor37a_tagSuggest",
8858
- "sourceFailed": "Qor37a_sourceFailed",
8859
- "toolSpinning": "Qor37a_toolSpinning",
8860
- "sourceNameInput": "Qor37a_sourceNameInput",
8861
- "translateCaret": "Qor37a_translateCaret",
8862
- "translateCheck": "Qor37a_translateCheck",
8863
- "translateBar": "Qor37a_translateBar",
8864
- "notice": "Qor37a_notice",
8865
- "list": "Qor37a_list",
8866
- "settingReadout": "Qor37a_settingReadout",
8867
- "chipCount": "Qor37a_chipCount",
8868
- "cardMenu": "Qor37a_cardMenu",
8869
- "root": "Qor37a_root",
8870
- "searchClear": "Qor37a_searchClear",
8871
- "article": "Qor37a_article",
8872
- "recentTitle": "Qor37a_recentTitle",
8873
- "foot": "Qor37a_foot",
8874
- "toolOn": "Qor37a_toolOn",
8875
- "when": "Qor37a_when",
8876
- "sourceGrid": "Qor37a_sourceGrid",
8877
- "detailBody": "Qor37a_detailBody",
8878
- "composerLabel": "Qor37a_composerLabel",
8879
- "sourceInfo": "Qor37a_sourceInfo",
8880
- "chipGroupLabel": "Qor37a_chipGroupLabel",
8881
- "chipOn": "Qor37a_chipOn",
8882
- "tagInput": "Qor37a_tagInput",
8883
- "submit": "Qor37a_submit",
8884
- "translateMenuItem": "Qor37a_translateMenuItem",
8885
- "reveal": "Qor37a_reveal",
8886
- "filterPanelHead": "Qor37a_filterPanelHead",
8887
- "tileWrap": "Qor37a_tileWrap",
8888
- "search": "Qor37a_search",
8889
- "alsoFromLabel": "Qor37a_alsoFromLabel",
8890
- "paneBody": "Qor37a_paneBody",
8891
- "filterValue": "Qor37a_filterValue",
8892
- "filterSearch": "Qor37a_filterSearch",
8893
- "tile": "Qor37a_tile",
8894
- "state": "Qor37a_state",
8895
- "tagChipRemove": "Qor37a_tagChipRemove",
8896
- "tag": "Qor37a_tag",
8897
- "tagPanelField": "Qor37a_tagPanelField",
8898
- "cardOrigSummary": "Qor37a_cardOrigSummary",
8899
- "tools": "Qor37a_tools",
8900
- "scroll": "Qor37a_scroll",
8901
- "articleEn": "Qor37a_articleEn",
8902
- "detail": "Qor37a_detail",
8903
- "recentList": "Qor37a_recentList",
8904
- "translateMenu": "Qor37a_translateMenu",
8905
- "chipRow": "Qor37a_chipRow",
8906
- "translateMenuLabel": "Qor37a_translateMenuLabel",
8907
- "reader-spin": "Qor37a_reader-spin",
8908
- "filterEmpty": "Qor37a_filterEmpty",
8909
- "metaBar": "Qor37a_metaBar",
8910
- "title": "Qor37a_title",
8911
- "sourceCard": "Qor37a_sourceCard",
8912
- "sourceUrlInput": "Qor37a_sourceUrlInput",
8913
- "menu": "Qor37a_menu",
8914
- "count": "Qor37a_count",
8915
- "sep": "Qor37a_sep",
8916
- "addCard": "Qor37a_addCard",
8917
- "tool": "Qor37a_tool",
8918
- "chevron": "Qor37a_chevron",
8919
- "spacer": "Qor37a_spacer",
8920
- "alsoFrom": "Qor37a_alsoFrom",
8921
- "filterBack": "Qor37a_filterBack",
8922
- "tagChip": "Qor37a_tagChip",
8923
- "tagRowName": "Qor37a_tagRowName",
8924
- "settingsRow": "Qor37a_settingsRow",
8925
- "revealLine": "Qor37a_revealLine",
8926
- "composer": "Qor37a_composer",
8927
- "pillPopActions": "Qor37a_pillPopActions",
8928
- "composerEmpty": "Qor37a_composerEmpty",
8929
- "card": "Qor37a_card",
8930
- "emptyTitle": "Qor37a_emptyTitle",
8931
- "tagAdd": "Qor37a_tagAdd",
8932
- "tagEdit": "Qor37a_tagEdit",
8933
- "kicker": "Qor37a_kicker",
8934
- "settingLabel": "Qor37a_settingLabel",
8935
- "tagRow": "Qor37a_tagRow",
8936
- "translateWrap": "Qor37a_translateWrap",
8937
- "fetchPill": "Qor37a_fetchPill",
8938
- "tagPanelHint": "Qor37a_tagPanelHint",
8939
- "recentRow": "Qor37a_recentRow",
8940
- "bar": "Qor37a_bar",
8941
- "articleZh": "Qor37a_articleZh",
8942
- "emptyBody": "Qor37a_emptyBody",
8943
- "noticeLink": "Qor37a_noticeLink",
8944
- "fetchPill_fetching": "Qor37a_fetchPill_fetching",
8945
- "check": "Qor37a_check",
8946
- "translateStatus": "Qor37a_translateStatus",
8947
- "recentSource": "Qor37a_recentSource",
8948
- "cardTagPanel": "Qor37a_cardTagPanel",
8949
- "chip": "Qor37a_chip",
8950
- "unit": "Qor37a_unit",
8951
- "emptyCard": "Qor37a_emptyCard",
8952
- "filterRow": "Qor37a_filterRow",
8953
- "filterList": "Qor37a_filterList",
8954
- "summary": "Qor37a_summary",
8955
- "verdictReason": "Qor37a_verdictReason",
8956
- "overlay": "Qor37a_overlay",
8957
- "toolDanger": "Qor37a_toolDanger",
8958
- "unread": "Qor37a_unread",
8959
- "refetch": "Qor37a_refetch",
8960
- "meta": "Qor37a_meta",
8961
- "filterCheck": "Qor37a_filterCheck",
8962
- "sourceMeta": "Qor37a_sourceMeta",
8963
- "pillPopReason": "Qor37a_pillPopReason",
8964
- "tagPanelTitle": "Qor37a_tagPanelTitle",
8965
- "chipBroken": "Qor37a_chipBroken",
8966
- "composerText": "Qor37a_composerText",
8967
- "pillPop": "Qor37a_pillPop",
8968
- "tagPanelHead": "Qor37a_tagPanelHead",
8969
- "verdict": "Qor37a_verdict",
8970
- "ghost": "Qor37a_ghost",
8971
- "sourceList": "Qor37a_sourceList",
8972
- "detailTitle": "Qor37a_detailTitle",
8973
- "help": "Qor37a_help",
8974
- "noticeText": "Qor37a_noticeText",
8975
- "tagPanelEmpty": "Qor37a_tagPanelEmpty",
8976
- "filterChevron": "Qor37a_filterChevron",
8977
- "add": "Qor37a_add",
8978
- "filterSection": "Qor37a_filterSection",
8979
- "alsoFromTitle": "Qor37a_alsoFromTitle",
8980
- "rule": "Qor37a_rule",
8981
- "settingControl": "Qor37a_settingControl",
8982
- "filterTagDelete": "Qor37a_filterTagDelete",
8983
- "manageTools": "Qor37a_manageTools",
8984
- "incomplete": "Qor37a_incomplete",
8985
- "cardOrigTitle": "Qor37a_cardOrigTitle",
8986
- "rowAction": "Qor37a_rowAction",
8987
- "tags": "Qor37a_tags",
8988
- "setting": "Qor37a_setting",
8989
- "sourceRow": "Qor37a_sourceRow",
8990
- "noticeRow": "Qor37a_noticeRow",
8991
- "rowActionDanger": "Qor37a_rowActionDanger",
8992
- "sourceError": "Qor37a_sourceError"
8827
+ "tagEditLabel": "TLqg8W_tagEditLabel",
8828
+ "translateMenuLabel": "TLqg8W_translateMenuLabel",
8829
+ "tagRowCheck": "TLqg8W_tagRowCheck",
8830
+ "tagOwned": "TLqg8W_tagOwned",
8831
+ "tagChipRemove": "TLqg8W_tagChipRemove",
8832
+ "translateCheck": "TLqg8W_translateCheck",
8833
+ "kicker": "TLqg8W_kicker",
8834
+ "filterChevron": "TLqg8W_filterChevron",
8835
+ "source": "TLqg8W_source",
8836
+ "tagEditField": "TLqg8W_tagEditField",
8837
+ "addCard": "TLqg8W_addCard",
8838
+ "filterPanelHead": "TLqg8W_filterPanelHead",
8839
+ "filterRow": "TLqg8W_filterRow",
8840
+ "sourceGrid": "TLqg8W_sourceGrid",
8841
+ "filterLabel": "TLqg8W_filterLabel",
8842
+ "filterTagDelete": "TLqg8W_filterTagDelete",
8843
+ "tagAdd": "TLqg8W_tagAdd",
8844
+ "recentList": "TLqg8W_recentList",
8845
+ "unit": "TLqg8W_unit",
8846
+ "toolWrap": "TLqg8W_toolWrap",
8847
+ "chevron": "TLqg8W_chevron",
8848
+ "submit": "TLqg8W_submit",
8849
+ "emptyCard": "TLqg8W_emptyCard",
8850
+ "paneBody": "TLqg8W_paneBody",
8851
+ "settingLabel": "TLqg8W_settingLabel",
8852
+ "translateMenuItem": "TLqg8W_translateMenuItem",
8853
+ "verdict": "TLqg8W_verdict",
8854
+ "filterList": "TLqg8W_filterList",
8855
+ "dialogActions": "TLqg8W_dialogActions",
8856
+ "tagEdit": "TLqg8W_tagEdit",
8857
+ "chipOn": "TLqg8W_chipOn",
8858
+ "detail": "TLqg8W_detail",
8859
+ "tagRow": "TLqg8W_tagRow",
8860
+ "emptyTitle": "TLqg8W_emptyTitle",
8861
+ "emptyBody": "TLqg8W_emptyBody",
8862
+ "dialog": "TLqg8W_dialog",
8863
+ "sourceName": "TLqg8W_sourceName",
8864
+ "meta": "TLqg8W_meta",
8865
+ "help": "TLqg8W_help",
8866
+ "summary": "TLqg8W_summary",
8867
+ "filterValue": "TLqg8W_filterValue",
8868
+ "filterSearch": "TLqg8W_filterSearch",
8869
+ "cardOrigTitle": "TLqg8W_cardOrigTitle",
8870
+ "scroll": "TLqg8W_scroll",
8871
+ "settingReadout": "TLqg8W_settingReadout",
8872
+ "cardMenu": "TLqg8W_cardMenu",
8873
+ "pillPop": "TLqg8W_pillPop",
8874
+ "translateStatus": "TLqg8W_translateStatus",
8875
+ "translateBar": "TLqg8W_translateBar",
8876
+ "spacer": "TLqg8W_spacer",
8877
+ "recentMeta": "TLqg8W_recentMeta",
8878
+ "searchClear": "TLqg8W_searchClear",
8879
+ "filterSection": "TLqg8W_filterSection",
8880
+ "translateMenu": "TLqg8W_translateMenu",
8881
+ "verdictReason": "TLqg8W_verdictReason",
8882
+ "manageTools": "TLqg8W_manageTools",
8883
+ "refetch": "TLqg8W_refetch",
8884
+ "recentRow": "TLqg8W_recentRow",
8885
+ "filterTagRow": "TLqg8W_filterTagRow",
8886
+ "composer": "TLqg8W_composer",
8887
+ "sourceRow": "TLqg8W_sourceRow",
8888
+ "tagPanelList": "TLqg8W_tagPanelList",
8889
+ "pillPopActions": "TLqg8W_pillPopActions",
8890
+ "add": "TLqg8W_add",
8891
+ "filterPanel": "TLqg8W_filterPanel",
8892
+ "composerEmpty": "TLqg8W_composerEmpty",
8893
+ "cardTagPanel": "TLqg8W_cardTagPanel",
8894
+ "tagRowName": "TLqg8W_tagRowName",
8895
+ "notice": "TLqg8W_notice",
8896
+ "bar": "TLqg8W_bar",
8897
+ "tag": "TLqg8W_tag",
8898
+ "root": "TLqg8W_root",
8899
+ "composerText": "TLqg8W_composerText",
8900
+ "sourceMeta": "TLqg8W_sourceMeta",
8901
+ "fetchPill": "TLqg8W_fetchPill",
8902
+ "when": "TLqg8W_when",
8903
+ "translateCaret": "TLqg8W_translateCaret",
8904
+ "translateMenuHead": "TLqg8W_translateMenuHead",
8905
+ "tile": "TLqg8W_tile",
8906
+ "metaBar": "TLqg8W_metaBar",
8907
+ "sourceList": "TLqg8W_sourceList",
8908
+ "tagPanelHead": "TLqg8W_tagPanelHead",
8909
+ "article": "TLqg8W_article",
8910
+ "chipGroupLabel": "TLqg8W_chipGroupLabel",
8911
+ "alsoFromTitle": "TLqg8W_alsoFromTitle",
8912
+ "headTitle": "TLqg8W_headTitle",
8913
+ "toolOn": "TLqg8W_toolOn",
8914
+ "filterEmpty": "TLqg8W_filterEmpty",
8915
+ "tagPanelField": "TLqg8W_tagPanelField",
8916
+ "recentTitle": "TLqg8W_recentTitle",
8917
+ "incomplete": "TLqg8W_incomplete",
8918
+ "search": "TLqg8W_search",
8919
+ "tags": "TLqg8W_tags",
8920
+ "barLabel": "TLqg8W_barLabel",
8921
+ "detailTitle": "TLqg8W_detailTitle",
8922
+ "incompleteLink": "TLqg8W_incompleteLink",
8923
+ "sourceInfo": "TLqg8W_sourceInfo",
8924
+ "head": "TLqg8W_head",
8925
+ "chipBroken": "TLqg8W_chipBroken",
8926
+ "cardOrigSummary": "TLqg8W_cardOrigSummary",
8927
+ "detailBody": "TLqg8W_detailBody",
8928
+ "rule": "TLqg8W_rule",
8929
+ "sourceFailed": "TLqg8W_sourceFailed",
8930
+ "chipCount": "TLqg8W_chipCount",
8931
+ "reader-spin": "TLqg8W_reader-spin",
8932
+ "recentSource": "TLqg8W_recentSource",
8933
+ "fetchPill_ready": "TLqg8W_fetchPill_ready",
8934
+ "cardMenuDanger": "TLqg8W_cardMenuDanger",
8935
+ "setting": "TLqg8W_setting",
8936
+ "tagSuggest": "TLqg8W_tagSuggest",
8937
+ "author": "TLqg8W_author",
8938
+ "reveal": "TLqg8W_reveal",
8939
+ "alsoFromRow": "TLqg8W_alsoFromRow",
8940
+ "state": "TLqg8W_state",
8941
+ "filterCount": "TLqg8W_filterCount",
8942
+ "foot": "TLqg8W_foot",
8943
+ "toolDanger": "TLqg8W_toolDanger",
8944
+ "menu": "TLqg8W_menu",
8945
+ "rowActionDanger": "TLqg8W_rowActionDanger",
8946
+ "tools": "TLqg8W_tools",
8947
+ "sep": "TLqg8W_sep",
8948
+ "title": "TLqg8W_title",
8949
+ "chip": "TLqg8W_chip",
8950
+ "tagOnCard": "TLqg8W_tagOnCard",
8951
+ "tileWrap": "TLqg8W_tileWrap",
8952
+ "sourceError": "TLqg8W_sourceError",
8953
+ "sourceUrlInput": "TLqg8W_sourceUrlInput",
8954
+ "tool": "TLqg8W_tool",
8955
+ "card": "TLqg8W_card",
8956
+ "settingsRow": "TLqg8W_settingsRow",
8957
+ "alsoFrom": "TLqg8W_alsoFrom",
8958
+ "noticeRow": "TLqg8W_noticeRow",
8959
+ "articleZh": "TLqg8W_articleZh",
8960
+ "tagPanelTitle": "TLqg8W_tagPanelTitle",
8961
+ "sourceCard": "TLqg8W_sourceCard",
8962
+ "translateTip": "TLqg8W_translateTip",
8963
+ "list": "TLqg8W_list",
8964
+ "check": "TLqg8W_check",
8965
+ "tagLinkOnly": "TLqg8W_tagLinkOnly",
8966
+ "filterPanelTitle": "TLqg8W_filterPanelTitle",
8967
+ "composerLabel": "TLqg8W_composerLabel",
8968
+ "rowAction": "TLqg8W_rowAction",
8969
+ "sourceNameInput": "TLqg8W_sourceNameInput",
8970
+ "noticeLink": "TLqg8W_noticeLink",
8971
+ "revealLine": "TLqg8W_revealLine",
8972
+ "tagPanelHint": "TLqg8W_tagPanelHint",
8973
+ "unread": "TLqg8W_unread",
8974
+ "count": "TLqg8W_count",
8975
+ "overlay": "TLqg8W_overlay",
8976
+ "ghost": "TLqg8W_ghost",
8977
+ "chipRow": "TLqg8W_chipRow",
8978
+ "tagChip": "TLqg8W_tagChip",
8979
+ "tagInput": "TLqg8W_tagInput",
8980
+ "fetchPill_fetching": "TLqg8W_fetchPill_fetching",
8981
+ "pillPopReason": "TLqg8W_pillPopReason",
8982
+ "tagPanelEmpty": "TLqg8W_tagPanelEmpty",
8983
+ "tagCreate": "TLqg8W_tagCreate",
8984
+ "filterBack": "TLqg8W_filterBack",
8985
+ "noticeText": "TLqg8W_noticeText",
8986
+ "translateWrap": "TLqg8W_translateWrap",
8987
+ "toolSpinning": "TLqg8W_toolSpinning",
8988
+ "settingControl": "TLqg8W_settingControl",
8989
+ "articleEn": "TLqg8W_articleEn",
8990
+ "alsoFromLabel": "TLqg8W_alsoFromLabel",
8991
+ "filterCheck": "TLqg8W_filterCheck",
8992
+ "fetchPill_failed": "TLqg8W_fetchPill_failed"
8993
8993
  };
8994
8994
  //#endregion
8995
8995
  //#region src/client/ReaderPane.tsx
@@ -14076,7 +14076,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
14076
14076
  }
14077
14077
  var produce = new Immer2().produce;
14078
14078
  //#endregion
14079
- //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-client-store@0.1.7-rc.1_@deepseek-ai+cordis@4.0.4/node_modules/@deepseek-ai/dsh-client-store/lib/index.js
14079
+ //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-client-store@0.1.7-rc.2_@deepseek-ai+cordis@4.0.4/node_modules/@deepseek-ai/dsh-client-store/lib/index.js
14080
14080
  /**
14081
14081
  * React-free snapshot store engine (zustand vanilla + immer + subscribeWithSelector +
14082
14082
  * rafFlush middleware + opt-in persist + dev freeze) plus the declarative
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@khorsheed/dsh-reader",
3
3
  "description": "Link reader tab: RSS/Atom subscriptions plus pasted article links, a card feed with a readable detail view, host-side fetch and daily refresh over a Typert Remote data face",
4
- "version": "0.2.0",
4
+ "version": "0.2.1",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/index.d.ts",
@@ -41,7 +41,8 @@
41
41
  "lib/typert.remote-client.d.ts",
42
42
  "lib/typert.remote-client.js",
43
43
  "cordis.patch.yml",
44
- "locale/*.json"
44
+ "locale/*.json",
45
+ "CHANGELOG.md"
45
46
  ],
46
47
  "license": "MIT",
47
48
  "dependencies": {