dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
English · 中文
Your real Obsidian vault, in the DSH right Sidebar — browse the vault tree, search every note, read notes in Obsidian's own reading-view typography, follow [[wikilinks]], edit in place, and hand a note to Obsidian itself when you want the app.
obsidian-workbenchBob-Bo1/obsidian-workbench is an Obsidian-flavoured Markdown workbench: hand it Markdown files and it browses, edits, previews, creates, moves and deletes them inside a workspace. This plugin starts from the other end — it attaches to the vault folder Obsidian itself uses, auto-detected from Obsidian's registry or chosen once by hand, and works in vault units: [[wikilinks]] resolve against the whole vault, note pages use Obsidian's reading-view numbers, and a note can be handed to Obsidian to open. If your notes already live in an Obsidian vault and you want to read and touch *that* vault from DSH, this is the one that knows what a vault is. If you want a general Markdown file manager for a workspace, the workbench is the better fit.
git clone https://github.com/DedsecLemon/dsh-obsidian.git D:/skill/dsh-obsidian
# in ~/.dsh/profiles/<your profile>/package.json add: "dsh-obsidian-panel": "link:D:/skill/dsh-obsidian"
# put it under the nested dsh.profile.bundles (not a top-level "bundles"), then run pnpm install thereThe full copy-paste profile file, the restart rules and troubleshooting are in INSTALL.md.
Requirements: DSH ^0.2.0-rc.2 on a web profile, Node >=22.12.0. If the host has no sessions/workspaces service, the tree, search, note pages and outline still work — you just do not get the 对话 (conversation) entry.
An Obsidian vault inside the DSH right Sidebar. Install steps: INSTALL.md. No build step — index.mjs and client.js are the running source.
git clone https://github.com/DedsecLemon/dsh-obsidian.git D:/skill/dsh-obsidian
# then in ~/.dsh/profiles/<your profile>/package.json:{
"dependencies": { "dsh-obsidian-panel": "link:D:/skill/dsh-obsidian" },
"dsh": { "profile": { "bundles": ["dsh-obsidian-panel"] } }
}The bundle list is `dsh.profile.bundles` — nested, not a top-level bundles. A top-level one installs the dependency and never mounts the bundle, so the plugin is silently absent. INSTALL.md has the full copy-paste file.
The project is dsh-obsidian; the package name is `dsh-obsidian-panel`, and it is not published to npm — install it from this repository. The name is not a choice: npm's dsh-obsidian belongs to a different DSH + Obsidian plugin by another author, so that name could never be published — or installed — without getting that one instead. The plugin's runtime identity is unchanged: routes are /dsh-obsidian/*, slot keys are dsh-obsidian/*, and the app label is 知识库.
· DSH · ^0.2.0-rc.2 — verified on 0.2.0-rc.2 Node.js · >=22.12.0 — only stable node: APIs; tested on 22.12.0 Systems · Windows, macOS, Linux (os). Obsidian itself is found per platform — /Applications and ~/Applications on macOS, AppImage//usr/bin//usr/local/bin/snap/flatpak on Linux, the usual per-user and machine-wide installs on Windows — and DSH_OBSIDIAN_APP overrides every one of them. Vault detection reads Obsidian's own registry where that platform keeps it. test/platform-check.mjs asserts all three lists. Run on Windows so far: the macOS and Linux lists are asserted, and only the Windows list has been exercised against a real Obsidian Profile · web (dsh.client.platform), i.e. any profile with a client half Per-release · dsh.compatibility.dshReleases in package.json. Only 0.2.0-rc.2 is compatible; anything else is unknown until somebody runs it — that is what the field means Install / start / uninstall · verified against the packed tarball in a disposable profile: docs/LIFECYCLE.md
Re-run the compatibility matrix and the lifecycle evidence before claiming a new release:
node test/profile-lifecycle.mjs # install → start → uninstall, in a temp profile
node test/mount-check.cjs # the client half, mounted
node test/render-check.cjs # registration + the guards
node test/host-check.mjs # every HTTP route and the security boundaryA vault panel is not a toy: it reads your notes, writes the one you edit, and needs a place to keep three small state files. So this plugin declares its surfaces instead of leaving them to be discovered:
Signal · What is actually true files · Reads the vault folder you chose (and, only for the first-run guess, Obsidian's own registry %APPDATA%\obsidian\obsidian.json). Writes: the three state files under DSH_HOME/dsh-obsidian/ (vault.json, chat.json, diag.json, ≈0.3 KB) and the one note you press 保存 on. Every read is confined to the vault root — escapes through symlinks or junctions are refused (the host harness proves it) network · No outbound requests at all. The panel fetches the host's own loopback routes (/dsh-obsidian/*), which is why the client half contains fetch. There is no telemetry and no third-party endpoint commands · Opening Obsidian goes through the host's own subprocess service — the plugin never imports node:child_process and never builds a shell string. It happens only when you click 在 Obsidian 中打开 credentials · None. The marketplace scanner flags *any* process.env read as a credentials signal; this plugin reads APPDATA, LOCALAPPDATA, ProgramFiles, ProgramFiles(x86), DSH_HOME and DSH_OBSIDIAN_APP — environment paths. No tokens, no API keys, no cookies, no OAuth dependencies · No runtime dependencies (dependencies is empty). jsdom/react/react-dom are dev-only, for the harnesses external services · The Obsidian desktop app — optional, only for "open in Obsidian" — and the DSH host it runs inside. Opening a note hands Obsidian's obsidian:// URI to the platform's own handler (start on Windows, open on macOS, xdg-open on Linux)
Failure bounds — the plugin degrades in pieces rather than half-registering:
Missing · What the reader sees slots, sidebarRightTabs, sidebarRight · the plugin does not activate at all — no entry in the left Sidebar, and nothing stays half-registered sessions, workspaces · the tree, the note pages and the outline work normally; 对话 says why it cannot open uiWorkspace · no folder picker and no conversation navigation; the vault path can still be set from the panel the host's webServer · no routes register, so the panel has no data to show Obsidian is not installed · only "open in Obsidian" is unavailable (the obsidian:// protocol handler is still tried) Obsidian is somewhere unusual · set DSH_OBSIDIAN_APP to its absolute path — that outranks every discovered location on every platform the vault was moved or deleted · the tree shows the error and asks for a folder again — a failed read is never cached as "empty"
Because files and network are genuinely used, a marketplace that grants automatic installation only to capability-free plugins will keep this one user-reviewed. That is the honest outcome, not a defect to engineer around: the alternative would be a note browser that cannot read notes.
Obsidian's registry is a guess, not an answer. On first run the panel shows a chooser instead of a tree, offering the detected vault and a folder picker; nothing is read until the reader agrees to a folder. The choice is stored under DSH_HOME (dsh-obsidian/vault.json) and from then on outranks the registry for every read. The header keeps a folder button, so changing it later is one click.
GET/POST /dsh-obsidian/vault is that state. The POST validates that the folder exists, is a directory, and looks like a knowledge base — it either carries Obsidian's own .obsidian folder or has at least one .md/.markdown/.canvas/ .txt file within two levels. "Exists and is a directory" was true of every folder on the machine, so aiming the panel at one served it as if it held notes; the refusal names the path and what was missing. The host-check probe backs the file up and restores it — like the chat Session id, it is live state, not a fixture, and a harness that overwrote it would silently change which folder the user's plugin reads.
A DeepSeek Harness bundle that puts an Obsidian vault in the app's right Sidebar: a 知识库 (Knowledge Base) tab with the vault tree and full-vault search, a note page that renders Markdown the way Obsidian's reading view does and lets you edit the file in place, and a conversation with an agent whose Session lives in the vault's own Workspace — opened in the app's own conversation panel, so the sidebar keeps its space.
It writes to the vault, and that is exactly one narrow path. POST /dsh-obsidian/note overwrites one *existing* note, and only when the path resolves inside the vault, the target is a file this plugin already shows as a note, and its current size fits the read cap — so a truncated view can never be saved back over the whole file. Nothing else in this package writes into the vault; the conversation Session id is DSH's own bookkeeping and lives under DSH_HOME.
Surface · Kind · Where Panel body · Client slot · sidebar.right.pane.tab, keyed dsh-obsidian/notes Conversation · Shell panel · opened with uiWorkspace.openSession(vault Session) — 对话 in the tree panel and on the note page; this plugin declares no seat and renders none of it Note page body · Client slot · sidebar.right.pane.tab, keyed dsh-obsidian/note Note outline · Client UI · the note page's 大纲 panel, anchored to the dsh-obsidian-outline-* ids renderMarkdown puts on headings Launcher row · Client slot · sidebar.footer.action (id dsh-obsidian) Tab types · Client service · sidebarRightTabs.register({ id, kind, patterns, canOpen, title }) GET /dsh-obsidian/status · Host route · what the plugin resolved (app, vault, config) POST /dsh-obsidian/open · Host route · open/focus Obsidian, optional { "file": "笔记/x.md" } GET /dsh-obsidian/tree · Host route · one directory level, ?path= ('' = vault root) GET /dsh-obsidian/note · Host route · ?path= → capped text + stat (+ root) GET /dsh-obsidian/search · Host route · ?q=&limit= → matching lines GET /dsh-obsidian/resolve-link · Host route · ?name= → matching note paths GET/POST /dsh-obsidian/chat · Host route · the remembered conversation Session id GET/POST /dsh-obsidian/vault · Host route · the folder the reader agreed to GET/POST /dsh-obsidian/diag · Host route · the client half's own account of its registrations obsidian_open · Agent tool · open/focus Obsidian, optionally on one note
Every route answers GET and POST; any other method is a 405, not a silent GET. There is deliberately no sidebar.panellist or main registration: this plugin used to add a second entry labelled 知识库 to the left sidebar, and the footer row is the only way in.
Three identifiers have to agree, and each fails differently: the tab type's title is the chip text, its kind is what openTab names, and its id is the key the body registers under in sidebar.right.pane.tab.
It does not embed the Obsidian window. Obsidian is a native Electron app with no addressable web surface, so no iframe, `` or browser tab can host it — the shell's own browser tab loads URLs, and Obsidian is not a URL.
The asymmetry is worth stating because the reverse direction *does* work: the dsh-harness Obsidian plugin embeds this app inside Obsidian. That succeeds only because DSH is a web page. The mirror image has no equivalent.
So this plugin brings the vault's content into the right Sidebar, and leaves the real Obsidian one click away through the obsidian_open route/tool.
A real agent conversation is opened in the centre as an ordinary Session whose Workspace is the vault — not a tab of the shell's chat, and not on the "start a new Session" screen. It is one Session that is created on first use and reused afterwards, so a plan survives a reload and can be refined across days.
It has to belong to the vault's Workspace, not merely to its directory. This is the part that is easy to get wrong, and getting it wrong is visible: a Session created with only a cwd belongs to *no* Workspace, so the conversation opens on the blank new-Session screen with a workspace picker, and the whole thing reads as "just another new conversation". resolveVaultChatSession therefore asks ctx.workspaces.create({ path }) first — documented as idempotently resolving an existing path, so the vault's Workspace is never registered twice — and passes the resulting workspaceId (plus cwd) to ctx.sessions.create.
Nothing about the conversation UI is reimplemented, and no seat is occupied. The app already has one good place for a conversation and one panel that renders it: the centre, whose main key conversation hosts the shipped Conversation. 对话 therefore does not mount anything of ours — it selects the vault's Session through uiWorkspace.openSession(sessionId) and the shell draws it:
navigator.openSession(sessionId) // ctx.get('uiWorkspace')That is also why this plugin declares no slot children: an earlier version declared a conversation seat on two of its own surfaces (and, before that, tried to occupy sidebar.chat.conversation, which ui-subagent owns — a second declaration throws inside apply and the shell rolls back every registration the plugin made). Declaring nothing costs nothing and cannot collide.
Handoff is about files, not about the AI's plan. A note is "dropped into" a conversation by inserting its @path mention at the caret — inputActions.captureInsertion() + inputActions.insertText(' @… ', span), the exact mechanism a drag-and-drop would produce. The mention is ABSOLUTE (@D:/知识库/笔记/foo.md) because the receiving conversation's Workspace is not the vault: a relative mention would resolve against the wrong root. The controls live next to the selected note in the tree and in the note page's header.
A note opens in its own tab, not inline: full-height rendered Markdown with its [[wikilink]]s clickable. Each link resolves against the vault via /dsh-obsidian/resolve-link and opens the target as its own page — following a link never loses the page you were reading. The tree stays put in the 知识库 panel; the note does not replace it.
The typography is Obsidian's, transcribed. The numbers in READ are the default theme's own reading-view values — 16px at line-height 1.5, the heading ladder (1.618em/1.462em/1.318em/1.188em/1.076em/1em) with its weights (700 for h1, 600 for the rest) and line heights, 1rem between blocks, 2.5rem above a heading that follows another block, 2.25em of list indent, a 2px accent rule and 24px of padding on a quote, and an unadorned code surface. render-check pins several of them, because drift here is exactly what makes the page stop looking like Obsidian.
A GFM table renders as a table. Before that it rendered as a paragraph of pipes, which is most of what a note with a table looked like.
The outline. 大纲 in the note's header opens a floating list of its headings down the right edge of the pane: clicking one scrolls the reading area to it, and the row for the heading you are currently inside is the one marked. Depth is indentation, so a ### under a ## reads as nested.
It is deliberately a *view over the rendered page*, not a second parse of the note. renderMarkdown writes each heading's anchor (id) and its depth (data-outline) as DOM attributes while it renders, and the panel reads them back with querySelectorAll — one parse, so the list can never describe a page other than the one on screen. That includes the capped case: a note longer than MAX_RENDER_LINES renders only its head, and the outline lists exactly the headings that exist, with nothing trailing off the end pointing at a heading that was never drawn. Anchors are numbered per page, so two note tabs open at once cannot collide.
The panel closes with its own control, with the ×, and on entering edit mode — the rendered page is gone, so there is nothing left to outline. mount-check drives open → pick → jump → close against a note whose heading ladder is one/two/three/two, and asserts the anchors, the indentation and the scroll target.
Editing. 编辑 swaps the rendered view for a plain-text editor over the same file; 保存 posts the whole text to /dsh-obsidian/note. A note whose read was truncated is refused editing outright — the partial view is not a safe base for overwriting the file.
Quoting a note into the conversation. One control, because there is one place the mention can land: 引用到对话 inserts the note's absolute @path at the caret of the conversation the centre is showing — the same thing dragging the file in would do. The path is ABSOLUTE because the receiving conversation's Workspace need not be the vault: a relative mention would resolve against the wrong root. An unknown vault root is a refusal (no-path), never an @undefined mention.
There is no second composer to reach for any more, which is why this is one control and not two: the vault conversation is simply the Session the reader opened from 对话. mount-check asserts the click hands that Session to the shell and that the quote lands in the composer the page was given.
The conversation is an ordinary DSH Session — one whose Workspace is the vault — and 对话 hands it to the app: uiWorkspace.openSession(sessionId) selects it, and the centre shows it in the shell's own conversation panel. This plugin renders none of it, and none of it touches the right Sidebar's layout: the tree and the note stay visible beside it, and the plugin's own chrome is one button.
That is the whole design, and it is the answer to a question this plugin got wrong three times. A right-Sidebar pane is a few hundred pixels of tool space, so a conversation put there has two possible shapes and both are wrong: it either eats the tree's height (the old footer, then the draggable popup) or replaces it (a tab of its own). Either way the plugin has to invent a size for something the reader uses in bursts, next to a column whose whole job is showing the vault. The centre is where the app puts conversations: it is full width, it already has the composer, the transcript, the stop control and the rest, and the Session is host-side — so opening and closing this view costs the conversation nothing.
Two consequences worth stating:
seat of its own on two surfaces, which meant owning a second copy of a conversation's state and keeping it fed. There is nothing of ours to collide with sidebar.chat.conversation (ui-subagent owns that one in this profile) and nothing to keep alive.
inputActions a tab body receives belong to the Session whose Sidebar this is — the one the centre is showing — so quoting a note puts its absolute @path into whatever conversation is open, which is the vault conversation whenever the reader arrived through 对话. Nothing is published module-side any more, because there is no second composer to reach.
The remembered Session is the one holding the history, so "open the vault conversation" should mean that one — which is what the button used to do, silently. That made the other intention (start fresh) impossible without hunting through the workspace, and the opposite mistake (always starting a new one) throws the history away. So 对话 resolves first and *asks* when there is something to come back to:
继续上次对话 回到知识库里的那段历史
新开一个对话 同一工作区,从空白开始No remembered Session means no menu: the first use creates one and opens it. "New" is a real new Session in the vault's Workspace, and the remembered id is replaced only after that Session exists — a failure leaves the history addressable.
The vault's Workspace is registered the moment the reader confirms a folder, not on the first 对话. The Workspace is what makes the folder a place the agent can be sent to and what the sidebar lists, so resolving it lazily meant nothing appeared until the reader went looking for the conversation. workspaces.create is idempotent by contract, so the lazy path in resolveVaultChatTarget still works and costs nothing when the Workspace is already there.
The right Sidebar's tabs are session-scoped, so selecting another Session hands the pane a fresh, empty tab set: the tree — or the note page the reader was on — disappeared with the Session it belonged to. Opening a conversation is not closing the tree, so the plugin remembers which of its surfaces was on screen (rememberSurface) and puts it back after openSession (restoreSurface), retrying briefly because the pane remounts for the new Session. The tree's own browse state — expansion, selection, search text, the levels already fetched — is kept across that remount too, because it is the same vault; it is dropped outright if the vault turns out to be a different one.
编辑 swaps the rendered page for a textarea over the same file, and the two have to be the same box: 16px on 1.5, the same 18px 22px 96px padding, the same typeface — and the same metadata line (size / time / 已截断) above the text. That row existed only in the reading view, so pressing the button moved the body up by its height (~24px). The editor itself used to be 13px monospace with its own padding, so the mode change was the most visible thing about it. mount-check asserts the two boxes against each other: same padding, same font metrics, and the same metadata row on both sides.
sidebar.chat.conversation is a single slot, and the slots core refuses a second declaration of it (dsh-client-ui-slots):
if (options.children)