dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
面向 DeepSeek Harness 的文献检索:pubmed_search、pubmed_paper、pubmed_related 走官方 NCBI E-utilities(含 elink 相关文献排序),scholar_search、scholar_cite 走 Google Scholar(MLA/APA/Chicago/Harvard/Vancouver/BibTeX,需 SerpApi 后端)。两个后端统一为同一种 paper 结构,设置卡片可配置 API key、Scholar 后端与速率/条数上限。
English · 中文
The npm package is scoped:
@astervolans/dsh-literature-search. GitHub Packages only accepts scoped names, and the same scoped name is used on npmjs.org so there is one install command for both registries.The plugin's runtime identity is mostly unscoped — the exported
name(literature-search), the settings namespace, the/plugin/literature-searchroutes and thedsh.bundle.patchrowidare all unchanged, so an existing profile keeps its configuration. Two identifiers are the scoped package name, because DSH resolves both through the profile directory, where only the scoped package exists:
- the row's
name— the module specifier the cordis loader imports. If it drifts frompackage.json,dsh webaborts at startup withCannot find package;test/config.test.mjsfails the build if they drift.- the client bundle's registration id in
lib/client.js— the DSH client loader keys a boot-graph row by the package name it resolved, so a bundle filed under any other id leaves the row unarrived, gets retried, and aborts the page withduplicate factory registration(that is 0.3.0's startup defect, fixed in 0.3.1);test/client.test.mjsfails the build if they drift.
A DeepSeek Harness (dsh) plugin that searches the literature through the
official PubMed NCBI E-utilities API and Google Scholar, and returns
both backends as one shared paper shape.
esearch (find PMIDs) → esummary
(metadata) → efetch (abstract / MeSH / keywords) → elink (related work).
Free, no key required, documented, and stable over the long run.google_scholar engine; without one it parses scholar.google.com HTML
directly. Google throttles and blocks that, so the plugin fails loudly
instead of quietly returning an empty result.fetch /
AbortSignal. The @deepseek-ai/* packages come from the DSH runtime, so
there is nothing to build and nothing extra to install.@astervolans/dsh-literature-search and the page is at the top,
above its rows — split into three collapsible sections (PubMed / Google Scholar
/ General) for the NCBI and SerpApi keys, the Scholar backend, rate and result
limits, and a one-click connectivity test.The API research behind these choices — parameters, rate limits, response shapes, measured evidence and rejected alternatives — is written up in
docs/API-RESEARCH.md(Chinese).
| Tool | Upstream | What it does |
|---|---|---|
pubmed_search | E-utilities esearch + esummary + efetch | Field-tagged queries with sort, publication-date windows and offset paging. Returns PMID, title, authors, journal, volume/issue/pages, DOI, PMC id, abstract, MeSH and keywords. |
pubmed_paper | efetch + esummary | The full record for one PMID, including abstract, MeSH, keywords, publication types and comment/reference lines. |
pubmed_related | elink pubmed_pubmed | Ranks articles related to a given one using NCBI's related-article algorithm. |
scholar_search | SerpApi / HTML | Google Scholar search: title, authors, venue, year, cited-by count, snippet and PDF link. Supports a year window and paging. |
scholar_cite | SerpApi google_scholar_cite | Ready-to-paste citations in MLA, APA, Chicago, Harvard, Vancouver and BibTeX (SerpApi backend only). |
All tool names are distinct from the existing dsh-ai4scholar plugin
(search_pubmed / search_google_scholar), so the two can coexist.
dsh plugin --profile desktop add @astervolans/dsh-literature-search
dsh plugin forwards to pnpm inside the profile directory and then reconciles
the profile's bundle list: any installed dependency whose manifest declares
dsh.bundle joins the layer stack automatically. Replace desktop with web
if you run the dsh web CLI. Restart DSH afterwards so the new tools load.
Remove it with:
dsh plugin --profile desktop remove @astervolans/dsh-literature-search
Upgrading from 0.2.x? The package was renamed from the unscoped
dsh-literature-search, so remove the old one and add the scoped one. The plugin
row id, the settings namespace and your saved keys are unaffected — the runtime
identity did not change (see the note at the top). If you installed 0.3.0,
upgrade to 0.3.1 before restarting: 0.3.0's client bundle still registered the
pre-rename id, so the configuration page failed to load and the desktop host
disabled the bundle.
Each release also publishes to GitHub Packages
(https://npm.pkg.github.com). That registry requires authentication even for
public packages, so add a .npmrc — project-level, or ~/.npmrc — with a
personal access token that has at least read:packages:
@astervolans:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=YOUR_GITHUB_PAT
dsh plugin --profile desktop add @astervolans/dsh-literature-search
Two things to know before choosing this route: the package is private by default on GitHub even though the repository is public (flip it once at package settings → Danger Zone → Change visibility), and installation needs a token. npmjs.org needs neither, so GitHub Packages is a mirror rather than the friendlier path.
powershell -ExecutionPolicy Bypass -File install.ps1 -Profile desktop
-Profile is the target profile (desktop for the desktop app, web for
the dsh web CLI).$DSH_HOME\profiles\<profile>\node_modules\@astervolans\dsh-literature-search,
and it merges the literature-search config row into that profile's
cordis.patch.yml (idempotent — safe to re-run). If the managed row is still
the pre-0.3.0 unscoped one, the script rewrites its module specifier in place,
because the loader resolves that string from the profile directory.dsh web) to load the new tools.powershell -ExecutionPolicy Bypass -File uninstall.ps1 -Profile desktop.You can also skip the profile entirely and verify retrieval first with the CLI:
node cli.mjs pubmed "CRISPR base editing" --max 3
node cli.mjs paper 33301246
node cli.mjs scholar "base editing" --max 5 # HTML backend without a key; may be blocked
| Environment variable / credential | Effect | If unset |
|---|---|---|
NCBI_API_KEY | Raises the E-utilities quota from 3 to 10 requests/second | Still works; the plugin throttles itself to 350 ms per request |
SERPAPI_API_KEY | Routes Google Scholar through SerpApi (stable, includes cited-by counts and citation formats) | Falls back to HTML scraping, which Google may answer with HTTP 429 |
NCBI_EMAIL | CLI only; sent as the E-utilities email parameter | NCBI recommends setting it so they can contact you about unusual traffic |
Three ways to configure them. The plugin resolves in this order: inline config → credential service → environment variable.
$DSH_HOME\.credentials.yaml (the DSH credential service), written by the
settings page, which stores NCBI_API_KEY / SERPAPI_API_KEY.cordis.patch.yml via pubmedApiKey / scholarSerpApiKey —
not recommended, because it is easy to commit by accident.The client plugin (lib/client.js) registers this bundle's configuration into
the Plugins panel, on the row cordis.patch.yml declares, and the server
side exposes /plugin/literature-search to back it:
| Route | Purpose |
|---|---|
GET /plugin/literature-search/config | Returns the (redacted) settings namespace, the revision, and status facts |
POST /plugin/literature-search/config | Patch write fenced by expectedRevision; a conflict returns 409 |
POST /plugin/literature-search/credential | Writes or clears a key for a slot (pubmed / scholar) |
POST /plugin/literature-search/test | Actually runs a PubMed esearch and a Scholar search, and reports latency and outcome |
@deepseek-ai/dsh-client-ui-plugin-manager owns the sidebar's 插件 / Plugins
panel and declares one slot per plugin-configuration cell. This bundle uses
plugins.bundle.config, a keyed slot whose key is the bundle's package
name — the page belongs to the package, not to one of its rows:
ctx.slots.inject('plugins.bundle.config', () =>
ctx.slots.register({ name: 'plugins.bundle.config', key: CONFIG_KEY }, LiteratureSearchConfig)
);
with CONFIG_KEY === '@astervolans/dsh-literature-search' (package.json#name).
The panel renders it in PackageDetail between the package's one-liner and its
row list, gated on ledger.bundles.has(pkg.name) — so 插件 →
@astervolans/dsh-literature-search shows the configuration at the top of the
page, above the literature-search row.
Change the key and nothing errors: the section simply does not render. That is
why test/client.test.mjs pins it against package.json#name.
0.3.3 first put this page on the row (
plugins.row.config, keyed`${package name}#${row id}`, reached by clicking the row). 0.4.0 moved it one level up. Unlike the row cell,plugins.bundle.configasks forview: "page"only — it never requests thesummaryone-liner — but the component keeps answering both, because the siblingplugins.itemcell hands the same props shape to cards that must.
Settings → 内置插件 is not used: that section is the read-only inventory of what the deployment ships, and the panel's own copy points plugin configuration at the Plugins panel ("在这里配置官方插件,安装和管理其他插件。内置插件列表及运行状态可在 「设置 → 内置插件」中查看").
PubMed, Google Scholar and 通用 are three collapsible sections built by
ConfigSection: a <button> header (so the disclosure is keyboard-reachable and
the whole strip is the hit target) plus a body that is absent while collapsed.
The open state lives in the page as one openSections object rather than in each
section, for two reasons: a section owning its own useState could not be driven
as a group, and it would add one hook per section to a render path whose hook
order must stay fixed. A section is open unless it was explicitly collapsed, so a
partial state object renders expanded rather than blank.
All three start collapsed: the page reads as a table of contents first — three
headers carrying their status badges say what the plugin is doing — and you open
only the group you came to change. The badge stays in the header while collapsed,
so a folded section still tells you whether its key is configured. To default them
open instead, seed useState({ pubmed: true, scholar: true, general: true }).
DSH 2.x removed ctx.settings.register: a plugin exports its Config and the
host derives the page from it, keyed by the plugin's profile entry id. Only the
fields carrying meta.volatile are offered, so every field here is marked live
through a guarded live() helper — .volatile() needs schemastery ≥ 3.18.4, and
a profile can hoist an older copy above the installation's one, in which case the
helper falls back to .extra('volatile', true) (same flag, any version).
Live fields arrive in apply(ctx, config) as cosmokit volatile references rather
than plain values, so the runtime re-reads them on every access and drops its
cached clients when the projection changes. If GET /config answers
503 settings namespace … is not registered, the page was not generated: the
entry is not active, or no field is volatile. The response carries a hint with
exactly that diagnosis.
What the page gives you:
$DSH_HOME/.credentials.yaml, never into the
settings document. The page shows the source (environment = read-only,
saved = editable) and the configuration state.scholarProvider is a three-way choice (auto / serpapi /
html) and takes effect immediately on save; the client cache is
invalidated and rebuilt.enabled, pubmedEnabled, scholarEnabled,
promptGuidance and promptOrder are registration-time switches; changing
them requires restarting DSH. The page says so.Implementation notes: lib/web.js owns the routes and fact collection;
lib/client.js is a hand-written module-loader bundle
(window.__ModuleLoader__.load, with require("react") resolved by the DSH
client runtime). There is no build step.
Each key slot is judged by independent probes, and a non-sensitive
diagnostic line (ref= / describe= / resolve= / settings-secret= /
error=) is printed under the badge:
ctx.credentials, writing
$DSH_HOME/.credentials.yaml) — preferred;pubmedApiKey / scholarSerpApiKey,
marked role('secret') and redacted on read) — used automatically when the
current composition has no credential service. resolveSecret prefers
it too.So whether or not a credential service is present, a key lands somewhere the
plugin genuinely reads. If the credential probes disagree — describe says
unconfigured while resolve returns a value — the page shows "configured" and
keeps both raw conclusions in the diagnostic line.
The host's describe({ redactSecrets: true }) reports secrets[].set: true for
a role('secret') field whenever it is merely present in the projected value,
and this namespace declares default('') — so an empty key reads as "set" too.
That flag is therefore printed for diagnosis (settings-secret=set /
settings-secret=declared/empty) but never decides the badge on its own; only a
non-empty inline value counts, which is what resolveSecret reads. Treating it
as configured is what used to leave a cleared slot claiming
已配置(保存在设置中)— "configured, saved in settings". Clearing a slot empties
both layers: the credential store and the inline field, which outranks it.
The server also writes the verdict, without any key values, to:
$DSH_HOME/.dsh-literature-search/diagnostics.json
GET /config, POST /credential and POST /test each refresh it. It holds
the raw describe/resolve conclusions, error text, DSH_HOME and cwd, and
booleans for whether a same-named environment variable exists. If you saved a
key and the page still says unconfigured, read this file first.
Both the server and the tool layer read settings / credentials from the
injected context (ctx.inject(['credentials'], …)) rather than
ctx.get('credentials') — the latter can miss a sibling plugin's service in
some compositions, which is the usual reason a key "saves but never resolves".
The literature-search row in cordis.patch.yml:
| Key | Default | Meaning |
|---|---|---|
enabled | true | Whether the plugin registers its tools |
pubmedEnabled | true | Whether the three pubmed_* tools are registered |
pubmedBaseUrl | https://eutils.ncbi.nlm.nih.gov/entrez/eutils | E-utilities root |
pubmedTool / pubmedEmail | dsh-literature-search / empty | The tool / email parameters sent on every request (required by NCBI's usage policy) |
pubmedApiKeyEnv / pubmedApiKey | NCBI_API_KEY / empty | Key reference / inline key |
pubmedRateLimitMs | 0 | Minimum interval between requests; 0 = 350 ms without a key, 110 ms with one |
scholarEnabled | true | Whether the two scholar_* tools are registered |
scholarProvider | auto | auto (SerpApi when a key exists, else HTML) / serpapi / html |
scholarBaseUrl / scholarSerpApiBaseUrl | Official Google / SerpApi URLs | Can point at a self-hosted proxy |
scholarSerpApiKeyEnv / scholarSerpApiKey | SERPAPI_API_KEY / empty | Key reference / inline key |
scholarHl | en | Google Scholar interface language |
scholarRateLimitMs | 0 | 0 = 2500 ms for HTML, 250 ms for SerpApi |
defaultMaxResults / maxResultsCap | 10 / 50 | Count when the model does not specify / the maximum requestable |
abstractMaxChars | 600 | Per-paper abstract or snippet limit; 0 = omit abstracts |
requestTimeoutMs / maxRetries / retryBackoffMs | 30000 / 3 / 1000 | Per-request timeout, retry count and backoff base (429/5xx are retried, honouring Retry-After) |
toolTimeoutMs | 120000 | Budget for one tool call |
userAgent | empty (built-in Chrome UA) | Overrides the default UA |
promptGuidance / promptOrder | true / 155 | Whether the system-prompt paragraph is registered, and its order |
On the model side, plain language is enough:
Use pubmed_search for "base editing" reviews published after 2020, take 5, then use pubmed_related on the first one to find 5 related papers.
Use scholar_search to find the 10 most-cited "CRISPR base editing" papers, then pass the first resultId to scholar_cite for BibTeX.
Example output (pubmed_paper 33301246):
PubMed record: Safety and Efficacy of the BNT162b2 mRNA Covid-19 Vaccine.
Safety and Efficacy of the BNT162b2 mRNA Covid-19 Vaccine.
2020 · The New England journal of medicine · vol 383 · no 27 · pp 2603-2615
Polack, Fernando P, Thomas, Stephen J, Kitchin, Nicholas et al. (29 authors)
DOI: 10.1056/NEJMoa2034577 · PMID: 33301246 · PMC: PMC7745181
https://pubmed.ncbi.nlm.nih.gov/33301246/
Types: Clinical Trial, Phase III, Journal Article, Randomized Controlled Trial
MeSH: BNT162 Vaccine, COVID-19, SARS-CoV-2, ...
Abstract: BACKGROUND: ...
The offline suite needs two DSH runtime packages (@deepseek-ai/dsh-tools and
@deepseek-ai/schemastery). Prepare them either way:
# Route A: extract them from a local DSH install (no network)
powershell -ExecutionPolicy Bypass -File setup-dev-links.ps1 # first run only
# Route B: install them from npm (what CI uses; works without DSH installed)
node tools/fetch-dev-deps.mjs
# Offline: parsers + plugin contract + settings routes + client bundle + config drift
node test/run-all.mjs
# Live: real upstreams, needs access to the corresponding domains
node test/live-pubmed.mjs # eutils.ncbi.nlm.nih.gov
node test/live-scholar.mjs # scholar.google.com (a refusal is recorded as SKIP)
node test/probe-scholar.mjs # connectivity diagnosis: status / result blocks / anti-bot
setup-dev-links.ps1 calls tools/extract-dev-deps.mjs, which extracts just
the DSH runtime closure the tests need (@deepseek-ai/dsh-tools,
schemastery, yaml and their transitive deps) out of
D:\DSH Desktop\resources\app.asar into .dev-deps/, then junctions it into
node_modules/. DSH upgrades and profile layout changes do not affect it.
The plugin itself has zero npm dependencies, and neither .dev-deps/ nor
node_modules/ is ever copied into a profile.tools/fetch-dev-deps.mjs takes the other route: it installs the same
packages from npm at the versions pinned in .dev-deps/package.json, then
symlinks them into node_modules/. This is what CI uses, on machines with no
DSH installed (.github/workflows/ci.yml runs the full offline suite on
Ubuntu and Windows across Node 22 and 24).fetch and fake services — no network.window.__ModuleLoader__ plus a minimal
React shim to actually execute and walk the render tree, which catches typos
and null dereferences in the configuration page without a browser. It has already
paid for itself by finding a state.drafts null crash. Since 0.2.2 it expands
function components for a deep render, so nodes owned by child components —
such as the credential fields' buttons — are reachable from a test, and it
guards the theme regressions directly: primary buttons must use paired
tokens, and --dsw-alias-brand-primary must never be a fill colour. Since
0.3.1 it also pins the registration contract that broke 0.3.0: the bundle must
file exactly one factory under package.json#name, and a second execution of
the same bundle must be the duplicate-registration crash rather than a
silent retry. Since 0.3.3 it pins the Plugins-panel cell as well — the slot
name, the key, and the summary/page split — because a key the manager never
dispatches costs the page its section without raising anything. Since 0.4.0 it
also drives the three-section disclosure: the header is a button carrying
aria-expanded, a collapsed section renders no fields but keeps its status
badge, and the page seeds all three collapsed.dsh-literature-search/
├── lib/
│ ├── index.js # Plugin entry: Config / apply / runtime / settings namespace / prompt paragraph
│ ├── web.js # /plugin/literature-search routes: config, credentials, connectivity test
│ ├── client.js # Plugins-panel configuration page (window.__ModuleLoader__, no build step)
│ ├── http.js # Rate gate, timeout, retries, Retry-After, JSON/text decoding
│ ├── paper.js # Shared paper shape, output schema, rendering
│ ├── medline.js # MEDLINE text parsing (efetch rettype=medline)
│ ├── pubmed.js # E-utilities client + the three pubmed_* tools
│ └── scholar.js # Google Scholar (SerpApi / HTML) + the two scholar_* tools
├── tools/
│ ├── extract-dev-deps.mjs # Extract the test runtime closure from app.asar
│ └── fetch-dev-deps.mjs # Ins