dsh-pet-panel
A dual-face plugin for DeepSeek Harness Web UI: a browser half (desktop pet, session dashboard, and four self-service capability panels) plus a host half (skill / MCP / A2A / team gateways, model-facing A2A tools, and an inbound A2A endpoint), with per-profile data isolation.
Features
Browser (client)
- Desktop pet (
PetView) — a global floating pet above every column, independent of the active session. Draggable, skinnable (five SVG species with eye/mouth emotes), resizable, persisted tolocalStorage. 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. - Session dashboard (
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 Forge (技能工坊) — list / read / write / delete
SKILL.mdfiles, and generate a new skill from a natural-language description via the default model. - Tool Integrations (工具集成) — list / add / edit / delete MCP servers (stdio or streamable-http).
- A2A Management (A2A 管理) — configure this plugin's own Agent Card (with an AI "智能生成" button that drafts the persona from name/description/capabilities via the default model), register external A2A agents (name / URL / description / capabilities / keywords / examples), show live online/offline/latency status per registered agent, and show the generated agent-card URL and
message/sendendpoint for copy. - Team (团队) — create teams from yourself ("me") plus registered external A2A agents; group chat and 1:1 chat with @mention routing (
@namedirected,@allbroadcast, no@= broadcast), multi-turn memory via per-agent A2A contextId, live member online/offline status, and a "对方回复中" typing indicator while waiting for a reply. - File attachment (附件) — the conversation composer and team chat accept file attachments. Text files are read inline and Excel files (
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. - Image attachment (图片) — team chat supports image uploads, canvas-compressed to ≤700KB so they stay under the A2A gateway body limit. When sent to external agents, the image is first described into a text caption by the default vision model (A2A is text-only); "me" reads the image directly through the multimodal prompt path.
- Voice input (语音输入) — both the conversation composer and team chat have a mic button backed by the browser's Web Speech API (Chrome/Edge). Click to record; speech is transcribed live (
zh-CN) and appended to the draft/input. Mic-denied and no-speech surface as a toast, and the button hides itself on unsupported browsers (Firefox/Safari). - Background switcher — four Papergames official wallpapers with a brightness/dim control, persisted to
localStorage.
Host (Node service)
SkillForgeGateway,ToolIntegrationsGateway,A2AConfigGateway,TeamGateway— Typert remotes backing the panels above.- A2A outbound tools —
a2a_list_agents/a2a_calllet the model discover and call registered external A2A agents. - A2A inbound endpoint — serves
/.well-known/agent-card.jsonand a JSON-RPCmessage/sendhandler at/a2a, driving the same agent runtime as the WebUI (identical model, tools, skills, MCP, and multi-turn memory viacontextId↔sessionId). The agent card advertises A2A protocol v1.0 (supportedInterfaceswithprotocolBinding: JSONRPC). - Unified self persona — a single
deployment:personasection 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-createsetupinjection. - Session grouping — inbound A2A and team "me" sessions each resolve to their own dedicated workspace group (
A2A会话/团队会话) so they show up grouped in the session dashboard instead of cluttering the default workspace. - Per-profile isolation — skills, MCP config, A2A config, and teams all resolve to the active profile (
~/.dsh/profiles/<name>/), so each profile sees only its own skills, tools, and teams. - Feishu OAuth login gate — optionally require Feishu (Lark) login before the WebUI opens: unauthenticated visitors are 302-redirected to Feishu's consent page before the index renders. After login a signed cookie stores
open_id/name/avatar, surfaced as a sidebar-footer logout entry (avatar + name) beside Settings. Enabled only whenFEISHU_APP_ID/FEISHU_APP_SECRETare set; unset → the WebUI stays open (safe default). See "Feishu login (optional)". - Image-to-text description —
SelfAgentService.describeImagesuploads 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).
Requirements
- Node.js
^22.19.0 || >=24.0.0(seepackage.jsonengines). - pnpm on your PATH —
dsh pluginis a thin forwarder that spawnspnpminside the profile directory, sopnpm not foundis a hard blocker. - DeepSeek Harness
>=0.1.1-rc.1(seepackage.jsondsh.engines).
Feishu login (optional)
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:
- This uses Feishu's 企业自建应用 (internal app) OAuth endpoints (
open.feishu.cn/open-apis/authen/...), not the passport (网页应用) endpoints. - In the Feishu developer console, register the callback URL under 安全设置 → 重定向 URL — it must match exactly (
127.0.0.1≠localhost). Either fixFEISHU_REDIRECT_URIto one value, or register every port you start with. - Register only ONE redirect URL. Keeping several URLs under 安全设置 → 重定向 URL trips Feishu error 20029 (重定向 URL 有误) at the consent page — keep exactly the one you use, e.g.
http://127.0.0.1:8801/auth/feishu/callback. - Production apps need a version publish + admin approval before a redirect-URL change takes effect. During development, use 开发配置 → 测试企业和人员 → 关联应用 to get a test version whose config applies instantly, with no approval.
- If the variables are unset, the plugin logs a warning and the WebUI stays open (no login) — the safe default, so a normal install is unaffected.
Launch steps by environment
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.
Linux / macOS (bash, zsh)
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
Windows PowerShell
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
Windows cmd (Command Prompt)
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=
Install
From a git repository (recommended for consumers)
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>
From a local checkout (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.
Update
git-source installs
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: installs
Re-run the add (triggers re-copy + prepare), or copy the build output directly:
cp -rf /path/to/dsh-pet-panel/lib/. ~/.dsh/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).
Uninstall / remove
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.
Troubleshooting
1. ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED
pnpm ≥ 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/profiles/<name>/pnpm-workspace.yaml under allowBuilds, drop stale entries, re-run the add.
2. 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.
3. ERR_PNPM_IGNORED_BUILDS pointing at the OLD commit
The 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.
4. pnpm not found on PATH (exit 127)
dsh plugin forwards to pnpm. Install pnpm first (npm i -g pnpm or corepack enable), then re-run.
5. Rebuild done but the change isn't visible (stale bundle / wrong profile)
Symptom: you edit source, pnpm run build exits 0, but the running web UI still shows the old behaviour. Do not assume the logic is wrong — 90% of the time the server is serving a stale bundle, or you patched a profile that isn't the one on the port. Diagnose in this order:
Find which profile owns the port. The UI URL (e.g.
:8801) does not tell you the profile name.netstat -ano | grep ':8801' | grep LISTEN # -> PID wmic process where processid=<PID> get CommandLine # -> --profile <name> --port <n>A
--profile pet-test --port 8801on the process means every edit must land inpet-test, notweb— even if the user calls it "the web page".Confirm the profile's copy is stale (the host reads from the profile's
node_modules, not your checkout):grep -c "<a-new-marker-string>" ~/.dsh/profiles/<name>/node_modules/dsh-pet-panel/lib/client.js # 0 = stale; >0 = currentCopy + restart:
cp -rf /path/to/dsh-pet-panel/lib/. ~/.dsh/profiles/<name>/node_modules/dsh-pet-panel/lib/ taskkill /F /PID <pid> cd ~/.dsh/profiles/<name> && dsh --profile <name> --port <n>(
taskkillneeds single-slash/Fon MSYS/Git-Bash —//Fis not recognized.)Verify the served bytes, not just the file on disk. The web app serves the client bundle at
/plugins/??<comma-joined list>&rev=<hash>— the list is read from the page's<script src>; a bare/plugins/??dsh-pet-panel/client.jsreturns 404. Fetch the full URL and grep for a marker, and hard-refresh (Ctrl+Shift+R) in the browser to clear the cached bundle.
6. EADDRINUSE after a restart
dsh --profile <p> --no-open run in the background forks a child process; killing the parent leaves the child holding the port. Find the leftover listener and kill it:
netstat -ano | grep ':<port>' | grep LISTEN
taskkill /F /PID <pid>
7. ERR_MODULE_NOT_FOUND: Cannot find package 'lightningcss' (developers only)
This fires during pnpm install (the prepare build) when building from source without lightningcss. It is already a devDependency of this repo, so it only bites if you are porting the build tooling — add lightningcss to devDependencies.
Development
Keep this repo and DeepSeek Harness as siblings so the type gate can resolve the harness checkout:
../
├── deepseek-harness # your DeepSeek Harness checkout
└── dsh-pet-panel # this repo
pnpm install
pnpm run build # tsc -b && tsdown && postbuild — emits lib/index.js + lib/client.js + types
pnpm run typecheck # the type gate (needs the sibling harness checkout)
From a local checkout, build and install a file: package:
pnpm run build
dsh plugin --profile <name> add file:.
dsh --profile <name>
How it works
The package declares two manifests in package.json:
dsh.bundle.patch→cordis.patch.yml, which inserts the plugin row into the profile. Installing the package applies the patch layer automatically.dsh.client(platform: web,inject: [...]) → the web module table scans this package into the browser roster and serveslib/client.js.
The client bundle is built by build/tsdown.client.ts — a self-contained port of DeepSeek Harness's own client-bundle preset. It wraps the plugin in window.__ModuleLoader__.load({ id, factory }), compiles CSS Modules through lightningcss, and resolves @deepseek-ai/* + react through the shell's frozen module table (no globals, no import map).
The host half exposes its services as Typert remotes: src/client/remote.ts holds the hand-written TYPERT_REMOTE manifest describing each method's wire codec (skillForge, toolIntegrations, a2aConfig, team), and the client mounts the namespaces onto the Typert client remote.
The inbound A2A endpoint (/a2a) drives the real agent loop via ctx.agents.create / resume + followup + whenIdle, so it shares the WebUI's model, tools, skills, and MCP. Because there is no interactive approval channel over A2A, approval is set to a fail-closed never policy: tools that would require approval are deterministically rejected, and the response carries a metadata.approvalsBlocked array (tool + reason) instead of failing silently.
Documentation
Project docs are auto-generated by OpenWiki into openwiki/ (plus the AGENTS.md / CLAUDE.md entry snippets). A scheduled GitHub Action (.github/workflows/openwiki-update.yml) keeps them in sync on a cron and opens a PR on each update; it reads OPENAI_COMPATIBLE_API_KEY / OPENWIKI_LANGSMITH_API_KEY from repo secrets, so set those before enabling the workflow.
License
MIT
还没有评论,来写第一条。