DedsecLemon/dsh-obsidian ↗★ 1

dsh-obsidian-panel

在右侧栏集成Obsidian知识库及搜索大纲功能。 适合Obsidian重度用户,便于在对话时同屏浏览和检索本地笔记。

套件
dsh-obsidian-panel
相容性
待驗證
版本
1.2.0
授權
MIT
最近更新
2026年10月2日

安裝

$npx -p @deepseek-ai/dsh dsh plugin --profile web add github:DedsecLemon/dsh-obsidian

English · 中文

dsh-obsidian

CI License: MIT

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//package.json
"dsh-obsidian-panel": "link:D:/skill/dsh-obsidian"   # dependencies
"bundles": ["dsh-obsidian-panel"]                    # dsh.profile.bundles

Naming

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 知识库.

Compatibility

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
SystemsWindows, 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
Profileweb (dsh.client.platform), i.e. any profile with a client half
Per-releasedsh.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 / uninstallverified 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 boundary

What it touches, and what breaks when it cannot

A 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:

SignalWhat is actually true
filesReads 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)
networkNo 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
commandsOpening 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 中打开
credentialsNone. 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
dependenciesNo runtime dependencies (dependencies is empty). jsdom/react/react-dom are dev-only, for the harnesses
external servicesThe 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:

MissingWhat the reader sees
slots, sidebarRightTabs, sidebarRightthe plugin does not activate at all — no entry in the left Sidebar, and nothing stays half-registered
sessions, workspacesthe tree, the note pages and the outline work normally; 对话 says why it cannot open
uiWorkspaceno folder picker and no conversation navigation; the vault path can still be set from the panel
the host's webServerno routes register, so the panel has no data to show
Obsidian is not installedonly "open in Obsidian" is unavailable (the obsidian:// protocol handler is still tried)
Obsidian is somewhere unusualset DSH_OBSIDIAN_APP to its absolute path — that outranks every discovered location on every platform
the vault was moved or deletedthe 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.

Which folder is the vault

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.

What it contributes

SurfaceKindWhere
Panel bodyClient slotsidebar.right.pane.tab, keyed dsh-obsidian/notes
ConversationShell panelopened with uiWorkspace.openSession(vault Session) — 对话 in the tree panel and on the note page
Conversation seat occupantClient slotdsh-obsidian/panel.conversation, and dsh-obsidian/note.conversation on the note page
Note page bodyClient slotsidebar.right.pane.tab, keyed dsh-obsidian/note — also declares its own conversation child
Note outlineClient UIthe note page's 大纲 panel, anchored to the dsh-obsidian-outline-* ids renderMarkdown puts on headings
Launcher rowClient slotsidebar.footer.action (id dsh-obsidian)
Tab typesClient servicesidebarRightTabs.register({ id, kind, patterns, canOpen, title })
GET /dsh-obsidian/statusHost routewhat the plugin resolved (app, vault, config)
POST /dsh-obsidian/openHost routeopen/focus Obsidian, optional { "file": "笔记/x.md" }
GET /dsh-obsidian/treeHost routeone directory level, ?path= ('' = vault root)
GET /dsh-obsidian/noteHost route?path= → capped text + stat (+ root)
GET /dsh-obsidian/searchHost route?q=&limit= → matching lines
GET /dsh-obsidian/resolve-linkHost route?name= → matching note paths
GET/POST /dsh-obsidian/chatHost routethe remembered conversation Session id
GET/POST /dsh-obsidian/vaultHost routethe folder the reader agreed to
GET/POST /dsh-obsidian/diagHost routethe client half's own account of its registrations
obsidian_openAgent toolopen/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.

What this deliberately does not do

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.

The vault conversation

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.

The note page

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 s