dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
A desktop pet plus a conversation overview panel for the DeepSeek Harness Web UI — self-contained client plugin (no host service)
A dual-face plugin for DeepSeek Harness Web UI: a browser half (desktop pet, session dashboard, and five self-service capability panels — Skill Forge, Tool Integrations, A2A, Team, Scheduled Tasks) plus a host half (skill / MCP / A2A / team / scheduler gateways, model-facing A2A tools, and an inbound A2A endpoint), with per-profile data isolation.
PetView) — a global floating pet above every column, independent of the active session. Draggable, skinnable (five SVG species with eye/mouth emotes), resizable, persisted to localStorage. Reacts to session lifecycle (running → busy, pending → waiting, finishing → celebration) plus manual feed/play/sleep controls. Toggle it from the chat with /pet on / /pet off / /pet.DashboardView) — a conversation-view tab (会话仪表盘 / Dashboard) with two sections: 概览 (live context occupancy, session totals, 7-day activity trend, archived/forked session lists) and 用量分析 (token-spend ranking, detailed context analysis of the active session). Renders derived data only.SKILL.md files, and generate a new skill from a natural-language description via the default model.message/send endpoint for copy. The version your card currently advertises is displayed next to that URL, read back from the live endpoint with cache: 'no-store' — so a lib/ / package.json mismatch in the deployed copy is visible at a glance instead of only in the raw JSON. When a registered agent is online but its description or capabilities were left blank here, its own card's description and skills are shown instead.@name directed, @all broadcast, no @ = broadcast), live member online/offline status, and a "对方回复中" typing indicator while waiting for a reply. Conversation memory is local, never delegated: the thread's own JSONL is the single source of truth and every turn rebuilds the context window from it (last 20 messages, ≤8000 chars, each line prefixed [sender] text), so a peer can restart, change model, or be swapped out without breaking continuity, and a member added later immediately sees the existing history. No A2A contextId is sent. Group turns also prepend a short [场景] note naming the participants and explaining the [name] prefixes — an A2A Message carries only ROLE_USER/ROLE_AGENT, so without it the peer reads other members' lines as things the user said. Messages carry a timestamp (time-only for today, M/D HH:MM for older ones), artifacts render as image thumbnails or name+size chips (a chip whose bytes were cleared degrades to a 已清空 placeholder instead of a broken image), and the chat header shows that thread's stored artifact bytes behind a two-step confirmation — the same in-panel modal that guards team deletion, since clearing cannot be undone.Asia/Shanghai by default) that run a prompt through the real agent loop on every fire, each with its own optional provider/model override so a cheap model can handle the routine jobs. The panel lists every task with its next/last run time and the full text of the last result — including the failure reason, so a broken run is diagnosable from the UI alone. Per task, results can push to a Feishu (Lark) group webhook; WeChat push is not wired up yet.xlsx/xls/xlsm) are parsed to a table via SheetJS; both insert as a reference chip (Doubao/ChatGPT-style) showing the filename and serializing to the file content on submit. Binary files are rejected with a toast instead of turning into mojibake.MediaRecorder (webm/opus — Chromium records no other container) and posts the audio to a Whisper-compatible /audio/transcriptions endpoint through the host half (VoiceAsrGateway), appending the transcript to the draft/input. Endpoint, model, language, hot-words and a max-recording length live in Settings → Plugins → Plugin configuration → 语音识别 / Speech recognition, with the API key held in the harness credential store rather than the settings file, plus a "test connection" button. Every failure surfaces as visible copy. (The earlier Web Speech API implementation was replaced because it silently swallowed network errors — inside Electron the button merely looked dead: Electron's Chromium ships no Google speech credentials, so the cloud path ends in error(network) after ~1.4 s with zero requests sent, and the on-device path in error(language-not-supported).)localStorage.SkillForgeGateway, ToolIntegrationsGateway, A2AConfigGateway, TeamGateway, ScheduledTasksGateway, VoiceAsrGateway — Typert remotes backing the panels above. SchedulerService owns the cron loop and fires each task by creating a real agent session (its own prompt, tools, skills, and MCP), so a scheduled run is the same agent you get in the chat. VoiceAsrGateway additionally registers the pet-panel-voice settings section (ctx.settings.installSection), which is what puts the "语音识别" card on the plugin-configuration page and routes its configuration into the harness settings.yaml.a2a_list_agents / a2a_call let the model discover and call registered external A2A agents./.well-known/agent-card.json and a JSON-RPC message/send handler at /a2a, driving the same agent runtime as the WebUI (identical model, tools, skills, MCP, and multi-turn memory via contextId ↔ sessionId). The agent card advertises A2A protocol v1.0 (supportedInterfaces with protocolBinding: JSONRPC), and its version is read from the plugin's own package.json so it tracks the real build instead of a hard-coded string. The card is served with Cache-Control: public, max-age=86400 and a content-derived ETag — deliberately not derived from the card version, since editing the description without bumping the version would otherwise leave clients on a stale copy — and answers a matching If-None-Match with 304. Inbound message/send accepts both the v0.2 method name and the v1.0 SendMessage, and reads contextId from message.contextId first, falling back to params.contextId.deployment:persona section is injected into every non-subagent agent (WebUI-hosted, inbound /a2a, and team "me") from the one Agent Card soul, so all three entry points share the same persona rather than relying on per-create setup injection.A2A会话 / 团队会话) so they show up grouped in the session dashboard instead of cluttering the default workspace.$DSH_HOME/profiles/<name>/), so each profile sees only its own skills, tools, and teams.open_id/name/avatar, surfaced as a sidebar-footer logout entry (avatar + name) beside Settings. Enabled only when FEISHU_APP_ID / FEISHU_APP_SECRET are set; unset → the WebUI stays open (safe default). See "Feishu login (optional)".SelfAgentService.describeImages uploads images to the attachment store and runs them through the default vision model, returning a text caption that rides the A2A channel to external agents (which are text-only).^22.19.0 || >=24.0.0 (see package.json engines).dsh plugin is a thin forwarder that spawns pnpm inside the profile directory, so pnpm not found is a hard blocker.>=0.1.1-rc.1 (see package.json dsh.engines). For development, keep it on the same version as the @deepseek-ai/* packages you compile against — see "Dependency versions".The plugin is compiled against the @deepseek-ai/* packages that DeepSeek Harness itself ships at runtime, so the declared versions and the harness you actually run need to line up.
| Package | Declared here | Where it comes from at runtime |
|---|---|---|
@deepseek-ai/dsh-client-{locale,ui-conversation,ui-layout,ui-sidebar,ui-slots,connection}, dsh-home-paths, dsh-skill-filesystem, dsh-token-meter, dsh-tools, dsh-typert-protocol | devDependencies / peerDependencies: ^0.1.5-rc.2 | shipped inside the harness (@deepseek-ai/dsh/node_modules/@deepseek-ai/…, observed at 0.1.5-rc.2), and resolvable from the registry at the same version |
@deepseek-ai/dsh-client-runtime | devDependencies: ^0.1.1-rc.2 | not bundled by the harness; the registry tops out at 0.1.1-rc.2 — nothing to bump |
@deepseek-ai/cordis | devDependencies: ^4.0.2 | ordinary semver, no prerelease trap — the harness ships 4.0.2, so pnpm update moves it normally |
@deepseek-ai/schemastery | dependencies: ^3.18.2 | ordinary semver; already present inside the harness. A plugin that registers a settings section declares its Config with this schema library — not zod, which this package uses only for the Typert wire codecs |
@deepseek-ai/dsh-settings | devDependencies: 0.1.5-rc.2 (pinned exactly) | harness-provided; supplies ctx.settings.installSection, the host-side half of a plugin-configuration card |
@deepseek-ai/dsh-client-ui-settings, …-settings-plugins | devDependencies: 0.1.5-rc.2 (pinned exactly) | harness-provided; settingsScope (the per-namespace scope service the card binds to) and the settings.plugin.item slot declaration. Both are already in the client roster via dsh-web-app, so they are not listed in dsh.client.inject |
@deepseek-ai/dsh-client-store | devDependencies: 0.1.5-rc.2 (pinned exactly) | not imported directly — it is the real home of SnapshotSelectorHook, which dsh-client-ui-slots/lib/types/store.d.ts re-exports. Omitting it is a silent failure: with skipLibCheck: true the resulting TS2307 is swallowed, and every props.useSessions(state => …) / useInput(s => …) callback parameter degrades to implicit any while the type name still prints correctly in an editor |
The prerelease range trap. ^0.1.1-rc.2 reads as >=0.1.1-rc.2 <0.2.0, but semver only lets a prerelease satisfy a range when its major.minor.patch equals the range floor's. So ^0.1.1-rc.2 matches 0.1.1-rc.3 and never 0.1.2-rc.1 or 0.1.5-rc.2: pnpm update cannot move it. The declared types then drift behind the harness without any error — types from 0.1.1-rc.2 while the harness runs 0.1.5-rc.2. Older signatures keep resolving, so the drift is invisible rather than loud: the type gate checks a surface the runtime has already moved past.
What actually changed, 0.1.1-rc.2 → 0.1.5-rc.2 (measured 2026-09-12). The contract signatures this plugin uses held verbatim: notify(level: 'info' | 'error', text: string) sits at client/contract/input.d.ts:195 in 0.1.5-rc.2 and at client/input/contract.d.ts:50 in 0.1.1-rc.2 — identical. What moved is the internal type layout: client/input/contract.d.ts became client/contract/input.d.ts, client/chat/ and client/conversation-nodes/ gave way to client/conversation/, and contract/ gained composer-blocks, context-provenance, conversation, records, request-inspection, snapshot and system-prompt. Realigning therefore compiles clean (tsc -b reports no errors). Do not read a clean compile as proof of runtime compatibility: the harness can change behaviour without touching a single type, which is exactly how the scheduled-task {{cwd}} breakage shipped — the harness moved on while this repo kept compiling against the older surface (see Troubleshooting).
To realign, pin the exact version instead of relying on a range:
pnpm add -D @deepseek-ai/dsh-client-connection@0.1.5-rc.2 @deepseek-ai/dsh-client-locale@0.1.5-rc.2 \
@deepseek-ai/dsh-client-ui-conversation@0.1.5-rc.2 @deepseek-ai/dsh-client-ui-layout@0.1.5-rc.2 \
@deepseek-ai/dsh-client-ui-sidebar@0.1.5-rc.2 @deepseek-ai/dsh-client-ui-slots@0.1.5-rc.2 \
@deepseek-ai/dsh-home-paths@0.1.5-rc.2 @deepseek-ai/dsh-skill-filesystem@0.1.5-rc.2 \
@deepseek-ai/dsh-token-meter@0.1.5-rc.2 @deepseek-ai/dsh-tools@0.1.5-rc.2 \
@deepseek-ai/dsh-typert-protocol@0.1.5-rc.2
Then bump the devDependencies / peerDependencies ranges to match, pnpm run build, and re-run the feature checks — the compile covers types only, never runtime behaviour. A range like ^0.1.5-rc.2 still cannot cross to 0.1.6-rc.1, so repeat this alignment every time the harness moves. This repo is on ^0.1.5-rc.2 for everything except dsh-client-runtime (stuck at ^0.1.1-rc.2, see above) as of 2026-09-15. The four settings / store packages added that day are deliberately pinned exactly at 0.1.5-rc.2: a caret on a prerelease cannot move anyway (the trap above), so pinning just makes the intent explicit and keeps a future pnpm update from silently shifting the surface under settingsScope.
To require Feishu login before the WebUI opens, set these environment variables before starting the profile:
FEISHU_APP_ID=<your_app_id>
FEISHU_APP_SECRET=<your_app_secret>
# optional (defaults to <request-host>/auth/feishu/callback when unset):
FEISHU_REDIRECT_URI=http://127.0.0.1:<port>/auth/feishu/callback
FEISHU_OAUTH_SCOPE=
Details:
open.feishu.cn/open-apis/authen/...), not the passport (网页应用) endpoints.127.0.0.1 ≠ localhost). Either fix FEISHU_REDIRECT_URI to one value, or register every port you start with.http://127.0.0.1:8801/auth/feishu/callback.Pick your shell. The <port> in FEISHU_REDIRECT_URI must match the port dsh actually listens on (--port), and that exact URL must be registered in the Feishu console. The examples below use port 8801 — change it to your own.
Without login:
dsh --profile <name>
With Feishu login (inline env vars):
FEISHU_APP_ID=<app_id> \
FEISHU_APP_SECRET=<secret> \
FEISHU_REDIRECT_URI=http://127.0.0.1:8801/auth/feishu/callback \
dsh --profile <name> --port 8801
Or export once, then start:
export FEISHU_APP_ID=<app_id>
export FEISHU_APP_SECRET=<secret>
export FEISHU_REDIRECT_URI=http://127.0.0.1:8801/auth/feishu/callback
dsh --profile <name> --port 8801
Without login:
dsh --profile <name>
With Feishu login:
$env:FEISHU_APP_ID = "<app_id>"
$env:FEISHU_APP_SECRET = "<secret>"
$env:FEISHU_REDIRECT_URI = "http://127.0.0.1:8801/auth/feishu/callback"
dsh --profile <name> --port 8801
Clear them afterwards (current session only):
Remove-Item Env:FEISHU_APP_ID, Env:FEISHU_APP_SECRET, Env:FEISHU_REDIRECT_URI
Without login:
dsh --profile <name>
With Feishu login (no spaces around = — set VAR = x sets a key with a trailing space):
set FEISHU_APP_ID=<app_id>
set FEISHU_APP_SECRET=<secret>
set FEISHU_REDIRECT_URI=http://127.0.0.1:8801/auth/feishu/callback
dsh --profile <name> --port 8801
Clear them afterwards:
set FEISHU_APP_ID=
set FEISHU_APP_SECRET=
set FEISHU_REDIRECT_URI=
The packaged app never inherits your shell environment, and it asks dsh for a
dynamic port (--port 0 in desktop/src/sidecar.ts). The plugin derives the
redirect_uri from the request host when FEISHU_REDIRECT_URI is unset, so the
registered URL and the actual port could never match — login could not complete in
the Electron build. The shell now solves this with a fixed-port loopback proxy:
// %APPDATA%\DSH Pet Desktop\feishu.json (created empty on first launch)
{
"appId": "<your_app_id>",
"appSecret": "<your_app_secret>",
"callbackPort": 8912
}
Then register exactly http://127.0.0.1:8912/auth/feishu/callback under
安全设置 → 重定向 URL and restart the app.
What the shell does with it:
FEISHU_APP_ID, FEISHU_APP_SECRET and
FEISHU_REDIRECT_URI=http://127.0.0.1:<callbackPort>/auth/feishu/callback
into the dsh child process — the plugin obeys that value verbatim;callbackPort and 302s the callback to the port dsh really bound this
run, query string intact. The extra hop is safe: dsh's cookie is host-only (host,
not port) and dsh validates its own state cookie.Notes:
appId/appSecret = no gate (the app opens straight in), which is what an
untouched install gets.FEISHU_* environment variables still win over the file, and an explicit
FEISHU_REDIRECT_URI is honoured verbatim (so CLI dev is unchanged).callbackPort must be free. A second desktop instance cannot bind it and logs
feishu login NOT ready — it opens without the gate rather than crashing.dsh plugin --profile <name> add github:zw11591-sketch/dsh-pet-panel
--profileis required — a baredsh plugin add ...errors withrequired option '--profile <name>' not specified.
This is a git-source install, so pnpm ≥ 10 blocks the prepare build step until you allow it. pnpm prints the exact package key to copy. For the first install the key is just the package name — add it to the profile's pnpm-workspace.yaml:
allowBuilds:
dsh-pet-panel: true
Then re-run the add command. The first install builds lib/index.js + lib/client.js from source via the prepare script (no type checking — that is what CI does).
Finally start the profile:
dsh --profile <name>
file:)cd dsh-pet-panel
pnpm install
pnpm run build
dsh plugin --profile <name> add file:.
dsh --profile <name>
file:installs are copy-not-symlink on Windows: rebuilding the checkout does NOT propagate into the profile. See "Rebuild done but the change isn't visible" below.
Pitfall: pnpm update will NOT fetch a new commit. A github:user/repo spec only re-fetches when the spec string changes (no version bump → no re-fetch). To actually pull a new commit, use one of:
# A. remove + re-add (simplest, deterministic)
dsh plugin --profile <name> remove dsh-pet-panel
dsh plugin --profile <name> add github:zw11591-sketch/dsh-pet-panel
# B. pin a specific ref, then update
dsh plugin --profile <name> add "github:zw11591-sketch/dsh-pet-panel#<branch-or-commit>"
# C. break the semver range and go newest (only relevant for npm-published plugins)
dsh plugin --profile <name> update --latest dsh-pet-panel
After a git re-add, pnpm will again block the new commit's prepare build until you allow it. The allowBuilds key is commit-specific — update pnpm-workspace.yaml to the exact key pnpm prints:
allowBuilds:
dsh-pet-panel@https://codeload.github.com/zw11591-sketch/dsh-pet-panel/tar.gz/<NEW-COMMIT>: true
Drop the stale <old-commit> entries, then re-run the add.
file: installsRe-run the add (triggers re-copy + prepare), or copy the build output directly:
cp -rf /path/to/dsh-pet-panel/lib/. $DSH_HOME/profiles/<name>/node_modules/dsh-pet-panel/lib/
Then restart the profile (see "Rebuild done but the change isn't visible" for the full restart + verify loop).
dsh plugin --profile <name> remove dsh-pet-panel
This is equivalent to pnpm remove inside the profile directory: it deletes the dependency entry, uninstalls from node_modules, and a post-run reconcile step prunes the plugin from dsh.profile.bundles. Restart the profile; it now boots without the plugin.
Do not hand-edit
dsh.profile.bundlesinpackage.json—dsh plugin add/removekeeps it correct automatically.
ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWEDpnpm ≥ 10 refuses to run the prepare build for a git dependency until its key is allow-listed. The message shows tar.gz/<COMMIT> — that means pnpm did resolve to the right commit; it just won't build.
Fix: copy the exact key pnpm printed into $DSH_HOME/profiles/<name>/pnpm-workspace.yaml under allowBuilds, drop stale entries, re-run the add.
ERR_PNPM_CANNOT_REMOVE_MISSING_DEPS"project has no dependencies of any kind". The profile's package.json has no dependencies field, so pnpm remove has nothing to delete — the plugin survives only as node_modules + lockfile residue (a desync state, typically from a prior hand-edit or an aborted operation).
Fix: skip the remove entirely and add directly — it re-declares the dependency and overwrites node_modules.
ERR_PNPM_IGNORED_BUILDS pointing at the OLD commitThe package actually downloaded, built ("Build complete"), and linked — but pnpm also reports the old commit as an ignored build (stale node_modules/.modules.yaml → ignoredBuilds), exits non-zero, so dsh's reconcile step never runs and bundles is not updated.
Fix: run a plain install, which recomputes ignoredBuilds (clears to []) and lets reconcile append the plugin to bundles:
dsh plugin --profile <name> install
Verify afterwards: ignoredBuilds is [] in .modules.yaml, the lockfile has only the new commit, and bundles lists dsh-pet-panel.
pnpm not found on PATH (exit 127)dsh plugin forwards to pnpm. Install pnpm first (`n