dsh-lan-share
Share text and files between DeepSeek Harness devices on the same local network — from a 局域网服务 tab in the right sidebar, or by asking the agent.
简体中文说明见 README.zh.md.
What it does
- Discovers devices that also run this plugin: UDP multicast plus broadcast,
with a hand-added
IPpath for networks that filter both. - Rename devices — your own name (what the whole LAN sees) or a local alias for someone else's machine.
- Sends text or a file to one chosen device.
- Receives text and files, with the file placed in a directory you choose (the DSH working directory, or a path you set).
- Asks or accepts automatically: an "全部默认接收" master switch, a fallback policy for unknown devices, and a per-device override on top. A size ceiling and an extension filter decide which files may skip the question.
- Notifies on arrival: unread count on the tab, a page-wide prompt for a file that needs a decision, and a receive history with the local path.
- Shows progress for every send and receive: bytes, rate, ETA, cancel.
- Resumes an interrupted transfer from the byte the receiver actually kept.
- Visibility switch: hide this device from the LAN entirely, or require a shared code before another device may connect.
- Seven model tools (
lan_share_devices,lan_share_send_text,lan_share_send_file,lan_share_inbox,lan_share_respond,lan_share_rename_device,lan_share_settings) so the agent can do all of the above in a conversation.
Install
dsh plugin --profile web add dsh-lan-share
DSH Desktop uses the web profile; restart the app afterwards, because the
host half only loads at boot.
If dsh is not on PATH, use the binary shipped with the desktop app:
$app = "<INSTALL_DIR>\DSH Desktop\resources\app"
node "$app\node_modules\@deepseek-ai\dsh\lib\bin.js" plugin --profile web add dsh-lan-share
Verify
profiles\web\package.jsonlistsdsh-lan-shareindependenciesand indsh.profile.bundles.profiles\web\node_modules\dsh-lan-share\lib\containsindex.jsandclient.js.- After a restart, a conversation's right sidebar "add tab" guide offers 局域网服务, and a 局域网服务 button appears in the conversation header.
Install on another machine
A LAN plugin is installed once per machine — it is not "install on one, the whole network has it". Two routes:
1. Ship the tarball (no registry, no toolchain on the target)
On the development machine:
npm run check # make sure the artifacts are current and the gates pass
npm pack # writes dsh-lan-share-<version>.tgz
Copy the .tgz to the target (USB, scp, or send it with this very plugin), then
on the target machine:
# The dsh bundled with the desktop app; DSH Desktop uses the `web` profile
$app = "<INSTALL_DIR>\DSH Desktop\resources\app"
& "$app\node_modules\node\bin\node.exe" "$app\node_modules\@deepseek-ai\dsh\lib\bin.js" `
plugin --profile web add "C:\path\to\dsh-lan-share-0.1.0.tgz"
Then quit and restart that machine's harness (tray → Quit for the desktop app,
Ctrl+C and re-run for dsh web).
lib/ is built into the tarball, so the target needs no Node toolchain, no
npm install, and no network. There is no src/ and no devDependency inside it.
2. Publish to npm
npm publish --access public
Every machine is then one command, with nothing to copy:
dsh plugin --profile web add dsh-lan-share
Verifying on the target
# 1) the manifest
Select-String dsh-lan-share "$env:APPDATA\dsh-desktop\harness\profiles\web\package.json"
# 2) the built halves shipped with it
Test-Path "$env:APPDATA\dsh-desktop\harness\profiles\web\node_modules\dsh-lan-share\lib\client.js"
# 3) the LAN plane (no login needed; the port is in the startup log or the panel header)
curl.exe -s "http://127.0.0.1:48901/lan-share/v1/info"
# 4) the panel plane: 401 without a cookie, 200 with one
curl.exe -s -o NUL -w '%{http_code}' "http://127.0.0.1:43129/lan-share/api/state"
That 401/200 pair is the cheapest proof the trust gate is really working, and
it is worth re-running on every deployment.
Two timings that look like failures
- The panel routes register a beat after the listening port. Probing
/lan-share/api/statethe instant the port opens answers404; a second later it answers401. Do not read that as "the plugin did not install". - The browser boot graph is scanned once, at harness start. After installing
— or after rebuilding
lib/client.js— that harness must be restarted, because the page keeps the old graph. The host half hot-mounts; the browser half does not.
Use
Open the tab from the sidebar's add-tab guide, or ask the agent to send something.
- Same network, nothing else to do. On a normal home or office LAN the two devices find each other within a few seconds. If they do not, use 手动添加设备 with the other machine's IP — many guest Wi-Fi networks and managed switches drop multicast.
- First time on a trusted network: leave 共享口令 empty, or set the same code on both machines. With a code set, a device that does not know it cannot connect at all.
- Send: pick the device, then 发文字 or 发文件. The file box takes a local absolute path — a browser cannot read a real path from a file picker, so the path is what the panel can actually send.
- Receive: with the default policy the panel asks before any byte moves. Turn on 全部默认接收 to skip the question, and use a device's own policy selector when one machine should stay manual.
Where received files go
The 设置 tab chooses between 当前工作区目录 (the DSH process working
directory, or a path you set) and a 指定目录. The resolved absolute path is
shown at the top of the panel, so there is never a guess about where a file
landed. Partially received transfers live in <save dir>/.dsh-lan-share-incoming
and are removed once the file is complete.
Ports and firewall
| Port | Protocol | Purpose |
|---|---|---|
| 48900 | UDP | Beacons: multicast to 239.255.77.88, plus broadcast to 255.255.255.255 |
| 48901 | TCP | Transfer plane, bound to 0.0.0.0 |
Both are configurable in the composition patch row:
- insert:
- id: lan-share
name: 'dsh-lan-share'
config:
discoveryPort: 48900
transferPort: 48901
multicastGroup: '239.255.77.88'
checksumMaxBytes: 268435456
maxConcurrentInbound: 4
registerTools: true
If the port is taken (a second instance on the same machine), the plugin binds a free port instead, says so in the panel, and hand-added devices still work.
The transfer plane is not the DSH web server. The web server is
loopback-only and gated by a browser cookie; this plugin runs its own listener on
0.0.0.0 so a peer on the LAN can reach it, and gates every route with the
bearer token from the handshake. The panel, in contrast, uses same-origin routes
on the DSH web server and is gated by the host's own trust fence.
Security model
- Beacons are unauthenticated advertisements. They can only make a device appear; they never authorise anything.
- Every data request carries a token proving the sender completed a handshake. With 共享口令 set, that requires knowing the code, and changing the code invalidates every token already issued.
- The sender's name comes from the receiver's own device table, never from the request body — one device cannot label its messages as another's.
- File names are sanitized to a single path segment, and the resolved path is re-checked against the save directory before every write.
- Executables are refused by default (
exe, bat, cmd, com, scr, msi, ps1, vbs, js, jar), whatever the policy says. - A file is only renamed into the save directory after its declared size — and its digest, when one was offered — has been verified.
An open LAN (no share code) means any device running this plugin on the same segment can send you files. That is the convenient default for a home network; set a code on an untrusted one.
Development
npm install
npm test # tsc + tests/*.ts
npm run build # host half → lib/, browser half → lib/client.js
npm run typecheck
npm run check # tests + build + the two publish gates
Tests have no framework: each file under tests/ is a plain Node script that
prints N/M checks passed. They cover four layers — pure rules, two real nodes
over loopback sockets, UDP discovery, the assembled service twice over, and the
panel routes' trust gate. See AGENTS.md for the platform contracts
worth knowing before changing anything.
License
MIT
No comments yet. Be the first to write one.