@khorsheed/dsh-reader 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +90 -0
- package/README.i18n.yaml +6 -0
- package/README.md +90 -0
- package/cordis.patch.yml +13 -0
- package/lib/client.js +15119 -0
- package/lib/index.js +3660 -0
- package/lib/invariant.js +38 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +1738 -0
- package/lib/typert.remote-client.d.ts +82 -0
- package/lib/typert.remote-client.js +1115 -0
- package/lib/types/arxiv.d.ts +47 -0
- package/lib/types/arxiv.js +76 -0
- package/lib/types/client/ReaderPane.d.ts +71 -0
- package/lib/types/client/ReaderPane.js +3135 -0
- package/lib/types/client/contract.d.ts +254 -0
- package/lib/types/client/contract.js +1 -0
- package/lib/types/client/definition.d.ts +27 -0
- package/lib/types/client/definition.js +32 -0
- package/lib/types/client/extract-article.d.ts +102 -0
- package/lib/types/client/extract-article.js +1024 -0
- package/lib/types/client/icons.d.ts +27 -0
- package/lib/types/client/icons.js +11 -0
- package/lib/types/client/index.d.ts +34 -0
- package/lib/types/client/index.js +222 -0
- package/lib/types/client/locales.d.ts +391 -0
- package/lib/types/client/locales.js +382 -0
- package/lib/types/client/parse-rss.d.ts +69 -0
- package/lib/types/client/parse-rss.js +656 -0
- package/lib/types/client/quote.d.ts +89 -0
- package/lib/types/client/quote.js +119 -0
- package/lib/types/client/selectors.d.ts +167 -0
- package/lib/types/client/selectors.js +413 -0
- package/lib/types/client/session.d.ts +171 -0
- package/lib/types/client/session.js +223 -0
- package/lib/types/client/store.d.ts +181 -0
- package/lib/types/client/store.js +226 -0
- package/lib/types/client/translate.d.ts +379 -0
- package/lib/types/client/translate.js +873 -0
- package/lib/types/index.d.ts +48 -0
- package/lib/types/index.js +25 -0
- package/lib/types/invariant.d.ts +29 -0
- package/lib/types/invariant.js +38 -0
- package/lib/types/link-resolvers.d.ts +107 -0
- package/lib/types/link-resolvers.js +299 -0
- package/lib/types/remote.d.ts +224 -0
- package/lib/types/remote.js +275 -0
- package/lib/types/schedule.d.ts +78 -0
- package/lib/types/schedule.js +114 -0
- package/lib/types/service.d.ts +633 -0
- package/lib/types/service.js +1697 -0
- package/lib/types/store.d.ts +224 -0
- package/lib/types/store.js +916 -0
- package/lib/types/types.d.ts +710 -0
- package/lib/types/types.js +182 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +128 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared vocabulary of `@khorsheed/dsh-reader`: the persisted state document,
|
|
3
|
+
* the Remote wire payloads, and the pure helpers both faces share.
|
|
4
|
+
*
|
|
5
|
+
* Two responsibilities are deliberately split across the process boundary:
|
|
6
|
+
*
|
|
7
|
+
* - the HOST half owns the network (the sanctioned `ctx.web` fetch seam) and
|
|
8
|
+
* the disk (one version-guarded JSON document over `ctx.fs`). It stores the
|
|
9
|
+
* LAST RAW PAYLOAD per source — feed XML or article HTML — plus fetch
|
|
10
|
+
* metadata, and never parses either.
|
|
11
|
+
* - the BROWSER half owns parsing. The host runtime has no XML/DOM parser at
|
|
12
|
+
* all (Node's `globalThis.DOMParser` is absent), so the single parser lives
|
|
13
|
+
* in the browser where `DOMParser` is native, and its output never needs to
|
|
14
|
+
* be persisted: entries and extracted article bodies are session state.
|
|
15
|
+
*
|
|
16
|
+
* @module @khorsheed/dsh-reader/types
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Whether an automatic retry is worth the request.
|
|
20
|
+
*
|
|
21
|
+
* Only a transport failure is: a bot wall, a login wall, a PDF and a 404 all
|
|
22
|
+
* answer the same way forever, so retrying them on a timer would be a crawler
|
|
23
|
+
* with a grudge rather than a reader. The manual refresh stays available.
|
|
24
|
+
*
|
|
25
|
+
* @param code - the recorded failure.
|
|
26
|
+
* @returns true when the entry may be retried automatically.
|
|
27
|
+
*/
|
|
28
|
+
export function isRetryablePreviewFailure(code) {
|
|
29
|
+
return code === 'unreachable';
|
|
30
|
+
}
|
|
31
|
+
/** The default cache policy: a day, and a few hundred articles. */
|
|
32
|
+
export const DEFAULT_CACHE_POLICY = { ttlHours: 24, maxEntries: 500 };
|
|
33
|
+
/* ---------------------------------------------------- translation memory */
|
|
34
|
+
/**
|
|
35
|
+
* The schema version of the persisted translation tiers (the global sentence
|
|
36
|
+
* memory's table, and each entry's segment map). A browser Translator model
|
|
37
|
+
* upgrade quietly changes what the same sentence translates to, and a
|
|
38
|
+
* segmentation change here changes what a "sentence" is — bumping this turns
|
|
39
|
+
* every old record into a MISS. Records are evicted LAZILY (the next write or
|
|
40
|
+
* budget pass drops them), never wiped eagerly.
|
|
41
|
+
*/
|
|
42
|
+
export const TRANSLATION_STORAGE_VERSION = 1;
|
|
43
|
+
/**
|
|
44
|
+
* The default total budget for both translation tiers, in characters — 64 MB
|
|
45
|
+
* worth of text. Counted in characters, like every other budget in this
|
|
46
|
+
* document (a CJK sentence costs more bytes than that reads; the approximation
|
|
47
|
+
* is the store's existing convention).
|
|
48
|
+
*/
|
|
49
|
+
export const DEFAULT_TRANSLATION_BUDGET_CHARS = 64 * 1024 * 1024;
|
|
50
|
+
/** How many sentences the global memory keeps before its own LRU end is evicted. */
|
|
51
|
+
export const MAX_TRANSLATION_MEMORY_ENTRIES = 50_000;
|
|
52
|
+
/* ------------------------------------------------------------------ helpers */
|
|
53
|
+
/** Default state root segment under `$DSH_HOME/state`. */
|
|
54
|
+
export const STATE_ROOT_SEGMENT = 'dsh-reader';
|
|
55
|
+
/** Default daily refresh time. */
|
|
56
|
+
export const DEFAULT_REFRESH_TIME = '10:00';
|
|
57
|
+
/**
|
|
58
|
+
* Per-source raw-payload budget in characters.
|
|
59
|
+
*
|
|
60
|
+
* Sized to the HOST's egress cap rather than below it: a deployment can raise
|
|
61
|
+
* `web-fetch-http`'s `maxBodyChars` (the acceptance instance runs it at
|
|
62
|
+
* 2,000,000), and a per-source budget smaller than what the seam is willing to
|
|
63
|
+
* deliver would silently re-truncate every large feed here — the reader would
|
|
64
|
+
* pay for the fetch and still get half a document, with the note blaming the
|
|
65
|
+
* fetch. The fetch cap is the outer bound; this one must not be the inner one.
|
|
66
|
+
*/
|
|
67
|
+
export const MAX_BODY_CHARS_PER_SOURCE = 2 * 1024 * 1024;
|
|
68
|
+
/** Total raw-payload budget in characters across all sources (newest kept). */
|
|
69
|
+
export const MAX_TOTAL_BODY_CHARS = 12 * 1024 * 1024;
|
|
70
|
+
/**
|
|
71
|
+
* How large an extracted body may be before it lives in its own file.
|
|
72
|
+
*
|
|
73
|
+
* `state.json` is read and rewritten as a whole on every mutation, so a research
|
|
74
|
+
* paper whose figures are inlined as base64 (transformer-circuits.pub's emotions
|
|
75
|
+
* paper is a 41.8 MB HTML document and extracts to tens of megabytes) would make
|
|
76
|
+
* every reader operation pay a multi-megabyte JSON round trip. Above this
|
|
77
|
+
* threshold the body is written to one file under `bodies/` and the document
|
|
78
|
+
* keeps only its metadata — so the cache policy (`ttlHours`, `maxEntries`) still
|
|
79
|
+
* governs it exactly like an inline body, and eviction deletes the file.
|
|
80
|
+
*/
|
|
81
|
+
export const INLINE_BODY_MAX_CHARS = 256 * 1024;
|
|
82
|
+
/**
|
|
83
|
+
* Build a display label for a source from its URL: the registrable-ish host
|
|
84
|
+
* plus the last meaningful path segment, which is what distinguishes several
|
|
85
|
+
* feeds from one publisher.
|
|
86
|
+
*
|
|
87
|
+
* @param url - the source URL (any string; a malformed one degrades to its own head).
|
|
88
|
+
* @returns a short human label.
|
|
89
|
+
*/
|
|
90
|
+
export function defaultSourceLabel(url) {
|
|
91
|
+
try {
|
|
92
|
+
const parsed = new URL(url);
|
|
93
|
+
const host = parsed.hostname.replace(/^www\./, '');
|
|
94
|
+
const segments = parsed.pathname.split('/').filter(segment => segment.length > 0);
|
|
95
|
+
const last = segments.at(-1) ?? '';
|
|
96
|
+
// Drop a bare feed file name that repeats the host ("example.com/feed.xml"
|
|
97
|
+
// reads better as "example.com/feed"); keep the host alone when nothing
|
|
98
|
+
// useful is left.
|
|
99
|
+
const tail = last.replace(/\.(xml|rss|atom|json)$/i, '');
|
|
100
|
+
return tail.length > 0 ? `${host}/${tail}` : host;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return url.slice(0, 60);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Build a stable entry id. Feeds are inconsistent about identifiers, so the
|
|
108
|
+
* order is: explicit guid/id, then the entry link, then a hash over title and
|
|
109
|
+
* link together (a feed with neither must still deduplicate across refreshes).
|
|
110
|
+
*
|
|
111
|
+
* @param entry - the identifying fields an entry may carry.
|
|
112
|
+
* @returns an id stable across refreshes of the same feed content.
|
|
113
|
+
*/
|
|
114
|
+
export function stableEntryId(entry) {
|
|
115
|
+
const explicit = entry.guid?.trim();
|
|
116
|
+
if (explicit !== undefined && explicit.length > 0)
|
|
117
|
+
return `g:${explicit}`;
|
|
118
|
+
const link = entry.link?.trim();
|
|
119
|
+
if (link !== undefined && link.length > 0)
|
|
120
|
+
return `l:${link}`;
|
|
121
|
+
return `h:${hash32(`${entry.title}\u0000${entry.link ?? ''}`).toString(36)}`;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* The entry id a saved link's single entry carries.
|
|
125
|
+
*
|
|
126
|
+
* A `link` source is exactly one entry, so the host must be able to name that
|
|
127
|
+
* entry when it records a per-entry annotation (the fetch failure the detail
|
|
128
|
+
* view reads). Both halves use this one function: a magic string duplicated
|
|
129
|
+
* across the process boundary is a bug waiting for a rename.
|
|
130
|
+
*
|
|
131
|
+
* @param sourceId - the link source's id.
|
|
132
|
+
* @returns the entry id the browser will build for it.
|
|
133
|
+
*/
|
|
134
|
+
export function linkEntryId(sourceId) {
|
|
135
|
+
return `link:${sourceId}`;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The query that selects every entry of one source KIND.
|
|
139
|
+
*
|
|
140
|
+
* The same local-predicate mechanism as a source or tag filter, so a kind
|
|
141
|
+
* narrowing is visible in the search box and can be cleared there. The prefix
|
|
142
|
+
* cannot collide with a real source id: ids are always `<kind>-<hash>`.
|
|
143
|
+
*
|
|
144
|
+
* @param kind - the source kind to select.
|
|
145
|
+
* @returns the query string.
|
|
146
|
+
*/
|
|
147
|
+
export function kindQuery(kind) {
|
|
148
|
+
return `#${kind}`;
|
|
149
|
+
}
|
|
150
|
+
/** The source kinds a kind query can name, for the predicate that reads one. */
|
|
151
|
+
export const READER_SOURCE_KINDS = ['rss', 'link'];
|
|
152
|
+
/**
|
|
153
|
+
* Shorten a summary for a card, collapsing whitespace so multi-line feed
|
|
154
|
+
* descriptions do not blow up the layout.
|
|
155
|
+
*
|
|
156
|
+
* @param text - the raw summary or content text.
|
|
157
|
+
* @param maxLength - the maximum returned length, ellipsis included.
|
|
158
|
+
* @returns the collapsed, shortened text, or `undefined` when there is none.
|
|
159
|
+
*/
|
|
160
|
+
export function summarize(text, maxLength = 280) {
|
|
161
|
+
if (text === undefined)
|
|
162
|
+
return undefined;
|
|
163
|
+
const collapsed = text.replace(/\s+/g, ' ').trim();
|
|
164
|
+
if (collapsed.length === 0)
|
|
165
|
+
return undefined;
|
|
166
|
+
if (collapsed.length <= maxLength)
|
|
167
|
+
return collapsed;
|
|
168
|
+
return `${collapsed.slice(0, maxLength - 1)}…`;
|
|
169
|
+
}
|
|
170
|
+
/** Coerce an unknown thrown value into a message string. */
|
|
171
|
+
export function errorMessage(error) {
|
|
172
|
+
return error instanceof Error ? error.message : String(error);
|
|
173
|
+
}
|
|
174
|
+
/** 32-bit FNV-1a hash, used only to build stable ids (never for security). */
|
|
175
|
+
function hash32(input) {
|
|
176
|
+
let hash = 0x811c9dc5;
|
|
177
|
+
for (let index = 0; index < input.length; index++) {
|
|
178
|
+
hash ^= input.charCodeAt(index);
|
|
179
|
+
hash = Math.imul(hash, 0x01000193);
|
|
180
|
+
}
|
|
181
|
+
return hash >>> 0;
|
|
182
|
+
}
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@khorsheed/dsh-reader",
|
|
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",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"types": "lib/types/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/types/index.d.ts",
|
|
11
|
+
"default": "./lib/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./invariant": {
|
|
14
|
+
"types": "./lib/types/invariant.d.ts",
|
|
15
|
+
"default": "./lib/invariant.js"
|
|
16
|
+
},
|
|
17
|
+
"./types": {
|
|
18
|
+
"types": "./lib/types/types.d.ts",
|
|
19
|
+
"default": "./lib/types.js"
|
|
20
|
+
},
|
|
21
|
+
"./client": {
|
|
22
|
+
"types": "./lib/types/client/index.d.ts",
|
|
23
|
+
"default": "./lib/client.js"
|
|
24
|
+
},
|
|
25
|
+
"./typert": {
|
|
26
|
+
"types": "./lib/typert.host.d.ts",
|
|
27
|
+
"default": "./lib/typert.host.js"
|
|
28
|
+
},
|
|
29
|
+
"./remote": {
|
|
30
|
+
"types": "./lib/typert.remote-client.d.ts",
|
|
31
|
+
"default": "./lib/typert.remote-client.js"
|
|
32
|
+
},
|
|
33
|
+
"./src/*": "./src/*",
|
|
34
|
+
"./locale/*.json": "./locale/*.json",
|
|
35
|
+
"./package.json": "./package.json"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"lib",
|
|
39
|
+
"lib/typert.host.d.ts",
|
|
40
|
+
"lib/typert.host.js",
|
|
41
|
+
"lib/typert.remote-client.d.ts",
|
|
42
|
+
"lib/typert.remote-client.js",
|
|
43
|
+
"cordis.patch.yml",
|
|
44
|
+
"locale/*.json"
|
|
45
|
+
],
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"zod": "^4.4.3"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
52
|
+
"@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.6",
|
|
53
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
|
|
54
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6",
|
|
55
|
+
"@deepseek-ai/dsh-client-ui-session": "^0.1.0-rc.6",
|
|
56
|
+
"@deepseek-ai/dsh-client-ui-sidebar-right": "^0.1.7-rc.1",
|
|
57
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
|
|
58
|
+
"@deepseek-ai/dsh-invariants": "^0.1.0-rc.6",
|
|
59
|
+
"@deepseek-ai/dsh-session": "^0.1.0-rc.6",
|
|
60
|
+
"@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.6",
|
|
61
|
+
"react": "^18.2.0"
|
|
62
|
+
},
|
|
63
|
+
"peerDependenciesMeta": {
|
|
64
|
+
"@deepseek-ai/dsh-api-remotes": {
|
|
65
|
+
"optional": true
|
|
66
|
+
},
|
|
67
|
+
"@deepseek-ai/dsh-client-locale": {
|
|
68
|
+
"optional": true
|
|
69
|
+
},
|
|
70
|
+
"@deepseek-ai/dsh-client-ui-primitives": {
|
|
71
|
+
"optional": true
|
|
72
|
+
},
|
|
73
|
+
"@deepseek-ai/dsh-client-ui-session": {
|
|
74
|
+
"optional": true
|
|
75
|
+
},
|
|
76
|
+
"@deepseek-ai/dsh-client-ui-sidebar-right": {
|
|
77
|
+
"optional": true
|
|
78
|
+
},
|
|
79
|
+
"@deepseek-ai/dsh-client-ui-slots": {
|
|
80
|
+
"optional": true
|
|
81
|
+
},
|
|
82
|
+
"@deepseek-ai/dsh-invariants": {
|
|
83
|
+
"optional": true
|
|
84
|
+
},
|
|
85
|
+
"@deepseek-ai/dsh-session": {
|
|
86
|
+
"optional": true
|
|
87
|
+
},
|
|
88
|
+
"react": {
|
|
89
|
+
"optional": true
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
"keywords": [
|
|
93
|
+
"dsh",
|
|
94
|
+
"dsh-plugin",
|
|
95
|
+
"cordis",
|
|
96
|
+
"reader",
|
|
97
|
+
"rss",
|
|
98
|
+
"atom",
|
|
99
|
+
"feed",
|
|
100
|
+
"readability",
|
|
101
|
+
"sidebar"
|
|
102
|
+
],
|
|
103
|
+
"dsh": {
|
|
104
|
+
"bundle": {
|
|
105
|
+
"patch": "./cordis.patch.yml"
|
|
106
|
+
},
|
|
107
|
+
"client": {
|
|
108
|
+
"inject": [
|
|
109
|
+
"@deepseek-ai/dsh-api-remotes",
|
|
110
|
+
"@deepseek-ai/dsh-client-locale",
|
|
111
|
+
"@deepseek-ai/dsh-client-ui-primitives",
|
|
112
|
+
"@deepseek-ai/dsh-client-ui-sidebar-right",
|
|
113
|
+
"@deepseek-ai/dsh-client-ui-session",
|
|
114
|
+
"@deepseek-ai/dsh-client-ui-slots"
|
|
115
|
+
],
|
|
116
|
+
"platform": "web"
|
|
117
|
+
},
|
|
118
|
+
"compat": {
|
|
119
|
+
"minHost": "0.1.5-rc.1",
|
|
120
|
+
"notes": "A page-type right-Sidebar tab (minted kind `reader`) over a Typert Remote data face. Fetching runs on the HOST through the sanctioned `ctx.web` seam; state is written with plain `node:fs` (atomic temp+rename) under `$DSH_HOME/state/dsh-reader`, NOT through `ctx.fs` — that service is the session-fenced sandboxed filesystem, so a workspace-write session refuses a write to the state root (observed on the acceptance instance). Every optional capability is probed at apply time and degrades (no `web` → domain refusals; no `sideChat` → the side-chat gesture hides; no `quote` → selection quoting is unavailable while the entry-level quote button still works). KNOWN LIMIT: the host fetch seam caps a decoded body at 100,000 chars and truncates silently, so articles larger than that cannot be extracted — the detail view falls back to a 「受限篇幅,内容未完整呈现」 notice with a 「阅读原文」 button (measured: Anthropic research pages, theverge.com, newyorker.com). On-device translation (0.2.0) runs the page's own Translator API (Chrome 138+/Edge 148+, desktop): the model never leaves the device and no model quota is spent, while the derived sentence pairs persist on the host (a global memory file plus per-entry segment maps under the state root, one shared budget, no TTL) so a reload re-paints instead of re-translating. Where that API is absent or the pair is unavailable, the globe is simply not rendered — the pane keeps working exactly as before. Source bodies that are already Chinese are not offered translation at all. On host 0.1.6-alpha.2+ the spots that want a real browser (the script-figure notice, a bot-wall card's action) open the in-app Sidebar Browser tab, probed through the `browser` tab kind; on older hosts they are the ordinary external link. Pages that inline real figures as base64 data URIs are now kept whole (substantive payloads over a 512-char threshold; 1px placeholders still yield to lazy-loading attributes), so a figure-heavy paper on a cap-raised deployment becomes a multi-MB cached body by design — bodies are bounded by entry count + TTL, not by a char budget.",
|
|
121
|
+
"verifiedHost": "0.1.5-rc.1"
|
|
122
|
+
},
|
|
123
|
+
"references": [
|
|
124
|
+
"@khorsheed/dsh-quote",
|
|
125
|
+
"@khorsheed/dsh-sidechat"
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
}
|