Ckarefulon/dsh-deepseek-webchat ↗★ 0
@ckarefulon/dsh-deepseek-webchat
在侧边栏嵌入官方网页版并绑定会话上下文 适合想免API Key直接在侧边栏使用官方网页版并推送上下文的用户。
安装
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:Ckarefulon/dsh-deepseek-webchat说明文档
阅读完整 README ↗dsh-deepseek-webchat
A DSH sidebar plugin that shows the real chat.deepseek.com web app next to your session, binds one DeepSeek conversation to each DSH Session, and lets you push selected messages into the DeepSeek composer as a blockquote — which you then send yourself.
No API key, no proxy, no scraping. It embeds the actual website through the desktop shell's native browser guest, the same channel DSH's built-in side-card browser uses.
What it does
| Real web app | chat.deepseek.com itself, in a sidebar tab. Not an iframe, not the API. |
| Login persists | Sign in once; the plugin restores your session on later launches. |
| One conversation per DSH Session | Each DSH Session gets its own DeepSeek conversation, remembered by id. |
| Follows the session you switch to | Switch DSH Sessions and the sidebar follows to that Session's conversation. |
| Manual context push | Tick rows, add a question, preview it, then fill and send. |
| Questions included | The agent's questions to you, their options, and your answers are quotable too. |
| Todo list included | The agent's todo list is quotable without turning anything on. |
| One click to send | 「填入并发送」 fills the composer and submits it; 「仅填入」 stops there. |
| Bulk selection | Select all, clear, or take the last 5/10/20/50 rows in one click. |
| Advanced opt-in | A switch reveals thinking, tool calls and results — off by default. |
| Explained context | An optional note tells DeepSeek what the quotes are and where they came from. |
| No back-flow | DeepSeek's answers are never written back into your DSH conversation. |
Install
Requires DSH Desktop 0.2.0-rc.1 or newer, and a profile that is already
initialised (the desktop profile is created by the app on first run).
dsh plugin --profile desktop add github:Ckarefulon/dsh-deepseek-webchat
Then restart DSH Desktop. The tab appears in the sidebar column's guide, and a chat icon in the conversation header opens it directly.
lib/ is committed to this repository on purpose, and there is no prepare
script, so the install needs no build step and no pnpm allowBuilds
approval.
Installing from a local clone instead
git clone https://github.com/Ckarefulon/dsh-deepseek-webchat
dsh plugin --profile desktop add /absolute/path/to/dsh-deepseek-webchat
Uninstalling
dsh plugin --profile desktop remove @ckarefulon/dsh-deepseek-webchat
Your bindings and saved login live in ~/.dsh/dsh-deepseek-webchat/ and are
left in place; delete that directory to remove them too.
Troubleshooting: DSH will not start after installing
DSH shows "The application could not start or stopped unexpectedly" and
refuses to open. The crash log (the path is shown in the dialog, under
%APPDATA%\@deepseek-ai\dsh-desktop\logs\) contains
client-modules: client bundle not found.
That means the installed copy's exports["./client"] does not point at a real
file. Fix it by reinstalling the plugin from a commit that has the flat layout:
dsh plugin --profile desktop remove @ckarefulon/dsh-deepseek-webchat
dsh plugin --profile desktop add github:Ckarefulon/dsh-deepseek-webchat
To confirm the layout before restarting, from the installed package directory:
node -e "const p=require('./package.json');const r=p.exports['./client'].default;console.log(r, require('fs').existsSync(r))"
It must print ./lib/client.js true. If it prints false, do not restart DSH —
the boot will abort again.
The dialog's "Disable third-party plugins, back up profile patch, and
restart" button also works: it starts DSH with third-party plugins unmounted
and saves your profile patch alongside as cordis.patch.yml.bak-.
That backup is your configuration, not the plugin, so keep it.
Using it
- Open the tab. Click the chat icon in the conversation header, or pick 「DeepSeek 网页版」 from the sidebar column's guide.
- Sign in to DeepSeek the first time. It is a normal browser login, inside the panel.
- Send one message in the DeepSeek page. DeepSeek creates a conversation lazily, so this is the moment its id exists — the plugin notices and binds it to the current DSH Session. The toolbar dot turns green.
- Quote context. Press 「引用上下文」. The list opens scrolled to the newest rows. Tick what you want — or use 「全选」, 「清除」, or the number box plus 「选末尾」 to take the last few in one go — optionally type a question or instruction, and check the preview. The list holds your turns, the assistant's replies, the agent's todo list, the questions it asked you (with the options it offered), and your answers to them.
- Press 「填入并发送」. The text goes into DeepSeek's composer and is submitted for you; the panel returns to the page and the selection clears, so the next push starts from a clean slate. Use 「仅填入」 instead if you would rather read it over in the composer first.
- That is the send. The plugin watches the composer to confirm it went out, and says so — or tells you plainly that it did not, so you can press Enter there yourself.
The toolbar also has 「刷新」 (reload the page) and 「解除绑定」 (forget this Session's conversation, so the next visit starts a new one).
Switching to a different DSH Session moves the panel to that Session's conversation automatically.
What gets sent, and how
Every push is still something you asked for: the plugin fills the composer and submits it only when you click 「填入并发送」. There is no timer, no background upload, and nothing is sent merely because the panel is open.
Sending goes through DeepSeek's own composer handler rather than a synthetic
shortcut invented here. Its composer runs an onKeyDown that, for a plain
Enter, calls the same function its send button calls — so the plugin dispatches
a bubbling Enter at the textarea and lets DeepSeek do the rest. (Its button's
class name is a build-time hash, so guessing at it would break on any rebuild.)
Because a synthetic key press could in principle be ignored, the plugin does not assume it worked. DeepSeek clears its textarea once a send is accepted, so the plugin reads the composer back: cleared means sent. If it still holds text, the plugin falls back to clicking DeepSeek's own send button, and if that fails too it says so instead of pretending — leaving the text sitting in the composer for you to send by hand.
「附上说明」 (on by default) puts a short note in front of the quoted rows:
以下是我在另一个 AI 助手(DSH)里的对话片段,引用给你作为背景参考。以「> 」 开头的行是原文,【】里标出的是这段内容属于谁…
Without it, DeepSeek receives a wall of quotes and 【】 labels with no explanation of what they are. Turn it off if you would rather send the raw quotes.
The advanced switch
By default the picker lists the conversation as a person reads it, plus the agent's todo list. Tick 「高级:包含思考过程与工具调用」 to also list the machinery behind it: the model's reasoning blocks, its tool calls, the tool results, slash commands, and compaction summaries — including tool output that may contain file contents. It is off on every open, and the host does the filtering, so an advanced row is never even sent to the browser unless you asked for it.
Each row is marked with an icon from the shell's own icon set plus a short label, so a quoted answer and a quoted tool result stay distinguishable in the blockquote:
> 【提问】Confirm:Which layout?
> · Sidebar — right column
> · Center
> 【回答】Sidebar, please
> 【待办】[x] write the picker
> [~] test it
Privacy
This plugin is built so that nothing leaves your machine unless you press a button, and so that the things you would least want to leak are never even offered.
What is sent, and when. Only the rows you tick, and only when you click 「填入并发送」 (or 「仅填入」, which stops at the composer). Both are explicit actions; the plugin has no timer and no background upload.
What is never offered. The host half filters the session log before it reaches the picker, dropping in both modes:
system/messageanddeveloper/messageevents,- injected
user/messageevents — agent instructions, skill catalogs, goal rounds, time context, compaction checkpoints, team and webhook deliveries, which DSH records as user messages but which you never typed. Only events whosesource.kindisuser(your turns) oruser-question-reply(your answers to the agent) are treated as yours, - approval records, request headers, model and sandbox selections.
What is off by default. The model's private reasoning blocks, tool calls and tool results are not offered until you turn on the advanced switch. When you do, remember that a tool result can contain file contents the agent read — that is exactly the kind of thing the default set keeps out, and the switch is your explicit say-so. A todo list is the exception: it is the agent's own plan, which you are already reading on screen, so it is offered by default.
So a system prompt, an API key in your environment, or a file the agent read cannot be quoted by accident.
What is stored locally. Two JSON files in ~/.dsh/dsh-deepseek-webchat/:
bindings.json— the DSH Session id → DeepSeek conversation id map. No message content, ever.session.json— the guest's DeepSeek login state (its site storage and non-HttpOnly cookie string), so you stay signed in. This is a credential; treat the file as you would a browser profile. It is written with the same permissions as any other file your user creates.
No telemetry. The plugin makes no network request of its own. The only traffic is the DeepSeek web app talking to DeepSeek, inside its own guest.
The page runs sandboxed. The desktop shell approves the guest with
nodeIntegration: false, contextIsolation: true, sandbox: true and
webSecurity: true, and refuses every permission request. The page cannot reach
your files or your DSH process.
Known limitations
These are real, and worth knowing before you rely on the plugin.
HttpOnly cookies cannot be restored. The DSH Host runs as a separate Node
child process, not inside Electron, so the plugin cannot reach Electron's cookie
store. It can only read document.cookie from inside the guest, and HttpOnly
cookies are invisible there by design. In practice DeepSeek keeps its credential
in localStorage.userToken and sends it as a bearer token, so replaying site
storage is what restores your login — but if DeepSeek ever moves its credential
to an HttpOnly cookie, this plugin will start asking you to sign in again after
each restart.
The guest partition is per-run. The shell hands out a process-lifetime session partition and the plugin cannot ask for a different one. Login survival is therefore the snapshot-and-replay above, not a persistent browser profile.
One DeepSeek login, shared. Every DSH Session shares a single DeepSeek account and browser session; the per-Session split is the conversation, not the login. Separate DSH Sessions do not get separate DeepSeek accounts.
Binding happens on the first message. DeepSeek creates a conversation only when you send something, so a DSH Session stays unbound until you send at least one message in the page. Until then the toolbar dot is hollow.
DeepSeek can change its page. The composer fill targets
textarea.ds-textarea__textarea, and the conversation id is read from DeepSeek's
/a//s/ route. If DeepSeek redesigns either, the fill or the binding
stops working until this plugin is updated. The rest of the plugin — embedding,
login, per-Session tabs — is unaffected.
Desktop only. The embedded page needs the desktop browser guest channel. In
a plain dsh web profile the panel says so instead of falling back to an iframe
(which DeepSeek would refuse anyway, via frame-ancestors 'none').
One DeepSeek tab per DSH Session. The tab kind is single-instance, so you cannot open two DeepSeek panels side by side for the same Session.
How it works
Architecture
Two halves, as DSH plugins are:
Host half (lib/index.js, exports .) runs in the DSH Host process and
serves five routes under /api/dsh-deepseek-webchat/:
| Route | Purpose |
|---|---|
state | What the panel needs to describe itself. |
messages?sessionId=[&advanced=1] | The Session's quotable rows, filtered. |
binding | Read/write/forget the DSH→DeepSeek conversation map. |
session/restore | Hand back the saved login state. |
session/save | Store the guest's login state. |
It reads messages through ctx.sessionQuery.readSession(), which is
live-preferred, and reduces the raw event log with the same text-extraction
rules the harness itself uses.
The advanced flag is honoured host-side, so the default response never
contains a reasoning or tool row at all — the browser half cannot accidentally
reveal one it was never given. The picker re-requests the list when the switch
flips rather than filtering locally.
Rows carry a kind alongside role: user and assistant for dialogue,
question and answer for the agent's questions and your replies (default),
then reasoning, tool-call, tool-result, todo, command and summary
(advanced). A question row is read from the tool/call event whose
name === 'ask_user_question', parsed with the same questionsOf shape the
harness's own user-questions service uses; the answer arrives as an ordinary
user/message tagged source.kind === 'user-question-reply', which the plain
human filter would otherwise drop.
Browser half (lib/client.js, exports ./client) registers the sidebar tab
type, its body, and a header door.
The guest, and why it is not an iframe. chat.deepseek.com sends
Content-Security-Policy: frame-ancestors 'none', so an is refused by the page. Instead the bundle asks `globalThis.dshDesktop.browser.acquire(...)` for a reservation and attaches a whose src is about:blank#
and whose partition is the issued one. The shell's will-attach-webview
matches the lease, applies hardened preferences, and lets the navigation
through.
Why the guest lives outside React. A `` is destroyed when it leaves
the document, so a slot-owned guest would reload on every tab switch — losing a
half-typed question. The element instead lives in a container the plugin owns at
the document root, and the panel body only measures its area; the guest is
positioned onto that rectangle while the tab shows and hidden when it does not.
The tab type declares keepMounted: true so the body survives switches.
Why the conversation URL works. DeepSeek's own routes are
/a/:agentId/s/:sessionId, and the default agent is chat, so a conversation is
addressable as https://chat.deepseek.com/a/chat/s/ — no in-page JavaScript
is needed to reopen one.
Development
Plain JavaScript ESM. No TypeScript, no bundler, no dependencies.
npm run build # copy src/ -> lib/
npm test # node --test
lib/ is a byte-for-byte copy of src/ and is committed, because a
git-hosted plugin must not need a build step on install. npm test asserts the
two still match, so they cannot drift.
After editing src/, run npm run build and commit both trees.
src/index.js host half -> lib/index.js
src/client/index.js browser half -> lib/client.js (note: flat, not lib/client/index.js)
scripts/build.mjs the copy
test/index.test.js host: u