DeepSeek Harness Hub
← 返回列表

桌面宠物会话面板zw11591-sketch/dsh-pet-panel

DeepSeek Harnessspec-screened在 GitHub 查看 ↗
需源码安装

一个用于 DeepSeek Harness Web UI…

暂不能直接安装(需源码编译或环境不满足):仓库缺少 package.json,无法用 dsh 插件安装命令安装。 · 最近上游提交 2026/9/17 · 已提供中文文档

一个桌面宠物,外加一个用于 DeepSeek Harness Web UI 的对话概览面板——自包含的客户端插件(无需宿主服务)

综合分
30.7
GitHub 分
30.7
用户评分
★ Stars
1
周下载量
安装插件(需先安装 dsh CLI 引擎:npm install -g @deepseek-ai/dsh)
dsh plugin --profile web add zw11591-sketch/dsh-pet-panel
仓库缺少 package.json,无法用 dsh 插件安装命令安装,改用 GitHub 源安装
数据截至 2026/9/18(元数据每日更新 · 实装验证按队列轮转,单条结论的验证时间见上方)
安装兼容性检查需源码安装

以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。

npm 包dsh-pet-panel(未发布到 npm,仅可源码安装)
Node 引擎未声明 engines.node
dsh CLI 依赖未声明 dsh 版本约束
入口文件缺少入口声明

仓库缺少 package.json,无法用 dsh 插件安装命令安装

验证方式:npm registry 存在性 + package.json 静态校验 · 最后验证 2026/9/19 10:27:25

依赖的 DSH / Cordis 模块
@deepseek-ai/schemastery@deepseek-ai/cordis@deepseek-ai/dsh-client-locale@deepseek-ai/dsh-client-runtime@deepseek-ai/dsh-client-store@deepseek-ai/dsh-client-ui-conversation@deepseek-ai/dsh-client-ui-layout@deepseek-ai/dsh-client-ui-settings@deepseek-ai/dsh-client-ui-settings-plugins@deepseek-ai/dsh-client-ui-sidebar@deepseek-ai/dsh-client-ui-slots@deepseek-ai/dsh-home-paths
用户评分
还没有人投票,来当第一个
订阅周报,不错过优质插件更新
每周一封 · 高评分插件 + 新用户活动

README

dsh-pet-panel

一个用于 DeepSeek Harness Web UI 的双面插件:浏览器端(桌面宠物、会话仪表盘,以及五个自助能力面板——技能工坊、工具集成、A2A、团队、定时任务)加上宿主端(技能 / MCP / A2A / 团队 / 调度器网关、面向模型的 A2A 工具,以及一个入站 A2A 端点),并具备按 profile 的数据隔离。

功能特性

浏览器(客户端)

- 桌面宠物(PetView)——一个悬浮于所有列之上的全局宠物,独立于当前活动会话。可拖动、可换肤(五种 SVG 物种,带眼睛/嘴巴表情)、可调整大小,并持久化到 localStorage。它会响应会话生命周期(运行中 → 忙碌,等待中 → 等待,即将结束 → 庆祝),并支持手动喂食/玩耍/睡觉控制。可在聊天中通过 /pet on / /pet off / /pet 切换。
- 会话仪表盘(DashboardView)——一个会话视图标签页(会话仪表盘 / Dashboard),包含两个区块:概览(实时上下文占用、会话总计、7 天活动趋势、已归档/已分叉会话列表)和用量分析(token 消耗排名、当前会话的详细上下文分析)。仅渲染派生数据。
- 技能工坊(技能工坊)——列出 / 读取 / 写入 / 删除 SKILL.md 文件,并通过默认模型根据自然语言描述生成新技能。
- 工具集成(工具集成)——列出 / 添加 / 编辑 / 删除 MCP 服务器(stdio 或 streamable-http)。
- A2A 管理(A2A 管理)——配置此插件自身的 Agent Card(带有一个 AI“智能生成”按钮,可通过默认模型根据名称/描述/能力起草人设),注册外部 A2A 代理(名称 / URL / 描述 / 能力 / 关键词 / 示例),为每个已注册代理显示实时在线/离线/延迟状态,并显示生成的 agent-card URL 和 message/send 端点以供复制。你的 card 当前对外声明的版本会显示在该 URL 旁边,并通过 cache: 'no-store' 从实时端点读回——因此部署副本中的 lib/ / package.json 不匹配问题可一眼看出,而不只是出现在原始 JSON 中。当已注册代理在线,但其描述或能力在此处留空时,将改为显示其自身 card 的描述和技能。
- 团队(Team)—— 由你自己(“我”)加上已注册的外部 A2A agent 创建团队;支持群聊和 1:1 聊天,并通过 @mention 路由(@name 定向、@all 广播、无 @ = 广播),实时显示成员在线/离线状态,并在等待回复时显示“对方回复中”的输入指示器。对话记忆是本地保存的,绝不委托出去: 该会话自己的 JSONL 是唯一事实来源,每一轮都会从中重建上下文窗口(最近 20 条消息,≤8000 字符,每行前缀为 [sender] text),因此对端可以重启、更换模型或被替换而不会破坏连续性,后加入的成员也能立即看到已有历史。不会发送 A2A contextId。群组轮次还会在前面加上一段简短的 [场景] 说明,列出参与者并解释 [name] 前缀——A2A Message 只携带 ROLE_USER/ROLE_AGENT,所以如果没有这段说明,对端会把其他成员的行读成用户说过的话。消息带有时间戳(今天的只显示时间,更早的显示 M/D HH:MM),artifact 会渲染为图片缩略图或名称+大小 chip(字节已被清除的 chip 会降级为 已清空 占位符,而不是损坏的图片),聊天头部会在两步确认后显示该会话存储的 artifact 字节——与保护团队删除的是同一个面板内模态框,因为清除无法撤销。
- 定时任务(Scheduled tasks)—— 创建 cron 任务(5 字段 cron + IANA 时区,默认 Asia/Shanghai),每次触发时都会通过真实 agent 循环运行一个 prompt,每个任务都可以有自己的可选 provider/model 覆盖,这样便宜模型就能处理例行任务。面板会列出每个任务及其下次/上次运行时间,以及上次结果的完整文本——包括失败原因,因此仅凭 UI 就能诊断出运行失败。每个任务的结果都可以推送到飞书(Lark)群 webhook;微信推送尚未接入。
- 文件附件(File attachment)—— 对话编辑器和团队聊天都接受文件附件。文本文件会内联读取,Excel 文件(xlsx/xls/xlsm)会通过 SheetJS 解析为表格;两者都会作为引用 chip(Doubao/ChatGPT 风格)插入,显示文件名,并在提交时序列化为文件内容。二进制文件会被拒绝并弹出 toast,而不是变成乱码。
- 图片附件(Image attachment)—— 团队聊天支持图片上传,并通过 canvas 压缩到 ≤700KB,以保持在 A2A 网关请求体限制之下。发送给外部 agent 时,图片会先由默认视觉模型描述为文本说明(A2A 仅支持文本);“我”则通过多模态 prompt 路径直接读取图片。
- 语音输入 — 对话输入框和团队聊天都有一个麦克风按钮,使用 MediaRecorder 进行录音(webm/opus — Chromium 不录制其他容器格式),并通过宿主端(VoiceAsrGateway)将音频发送到兼容 Whisper 的 /audio/transcriptions 端点,将转录文本追加到草稿/输入框中。端点、模型、语言、热词和最大录音时长位于 设置 → 插件 → 插件配置 → 语音识别 / Speech recognition,API 密钥保存在 harness 凭据存储中而非设置文件中,还有一个“测试连接”按钮。每次失败都会以可见文案呈现。(此前的 Web Speech API 实现已被替换,因为它会静默吞掉 network 错误 — 在 Electron 内部,按钮看起来只是失效了:Electron 的 Chromium 不附带 Google 语音凭据,因此云端路径在约 1.4 秒后以 error(network) 结束,且未发送任何请求,而设备端路径则以 error(language-not-supported) 结束。)
- 背景切换器 — 四张 Papergames 官方壁纸,带有亮度/暗度控制,持久化到 localStorage。

宿主(Node 服务)

- SkillForgeGateway、ToolIntegrationsGateway、A2AConfigGateway、TeamGateway、ScheduledTasksGateway、VoiceAsrGateway — 支撑上述面板的 Typert 远程对象。SchedulerService 拥有 cron 循环,并通过创建真实的 agent 会话来触发每个任务(拥有自己的提示词、工具、技能和 MCP),因此定时运行与你从聊天中得到的 agent 完全相同。VoiceAsrGateway 还注册了 pet-panel-voice 设置区段(ctx.settings.installSection),正是它将“语音识别”卡片放到插件配置页上,并将其配置路由到 harness 的 settings.yaml。
- A2A 出站工具 — a2a_list_agents / a2a_call 让模型可以发现并调用已注册的外部 A2A agent。
- A2A 入站端点 — 在 /.well-known/agent-card.json 提供 agent 卡片,并在 /a2a 提供 JSON-RPC message/send 处理器,驱动与 WebUI 相同的 agent 运行时(相同的模型、工具、技能、MCP,以及通过 contextId ↔ sessionId 实现的多轮记忆)。agent 卡片声明支持 A2A 协议 v1.0(supportedInterfaces 带有 protocolBinding: JSONRPC),其 version 从插件自身的 package.json 读取,因此它跟踪真实构建而非硬编码字符串。卡片以 Cache-Control: public, max-age=86400 和基于内容派生的 ETag 提供 — 刻意不从卡片版本派生,因为否则在不提升版本的情况下编辑描述会让客户端停留在过期副本上 — 并对匹配的 If-None-Match 返回 304。入站 message/send 同时接受 v0.2 方法名和 v1.0 SendMessage,并优先从 message.contextId 读取 contextId,回退到 params.contextId。
- 统一自身人格 — 单个 deployment:persona 区段会从同一个 Agent Card 灵魂注入到每个非子代理的代理中(WebUI 托管的、入站 /a2a 以及团队“我”),因此这三个入口共享同一人格,而不再依赖每次创建时的 setup 注入。
- 会话分组 — 入站 A2A 和团队“我”的会话各自解析到专属的工作区分组(A2A会话 / 团队会话),因此它们会在会话仪表板中分组显示,而不会杂乱地堆在默认工作区中。
- 按配置文件隔离 — 技能、MCP 配置、A2A 配置和团队都解析到当前活动的配置文件($DSH_HOME/profiles//),因此每个配置文件只能看到自己的技能、工具和团队。
- 飞书 OAuth 登录门禁 — 可选择要求 WebUI 打开前先进行飞书(Lark)登录:未认证的访客会在索引渲染前被 302 重定向到飞书的授权页面。登录后,一个签名 cookie 会存储 open_id/姓名/头像,并在“设置”旁边的侧边栏页脚显示为登出条目(头像 + 姓名)。仅在设置了 FEISHU_APP_ID / FEISHU_APP_SECRET 时启用;未设置 → WebUI 保持开放(安全默认值)。参见“飞书登录(可选)”。
- 图像转文本描述 — SelfAgentService.describeImages 将图像上传到附件存储,并通过默认视觉模型运行它们,返回一个文本说明,该说明会经由 A2A 通道传递给外部代理(这些代理仅支持文本)。

要求

- Node.js ^22.19.0 || >=24.0.0(参见 package.json 的 engines)。
- pnpm 位于你的 PATH 中 — dsh plugin 是一个轻量转发器,会在配置文件目录内启动 pnpm,因此 pnpm not found 是硬性阻塞问题。
- DeepSeek Harness >=0.1.1-rc.1(参见 package.json 的 dsh.engines)。开发时,请让它与你编译所针对的 @deepseek-ai/ 包保持相同版本 — 参见“依赖版本”。

依赖版本

该插件是针对 DeepSeek Harness 自身在运行时随附的 @deepseek-ai/ 包编译的,因此声明的版本和你实际运行的 harness 需要保持一致。

| 包 | 此处声明 | 运行时来源 |
| --- | --- | --- |
| @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 | 随附于 harness 内(@deepseek-ai/dsh/node_modules/@deepseek-ai/…,观察到为 0.1.5-rc.2),并且可从 registry 以相同版本解析 |
| @deepseek-ai/dsh-client-runtime | devDependencies:^0.1.1-rc.2 | 不由 harness 捆绑;registry 最高为 0.1.1-rc.2 — 无需升级 |
| @deepseek-ai/cordis | devDependencies:^4.0.2 | 普通 semver,没有预发布陷阱 — harness 随附 4.0.2,因此 pnpm update 会正常升级它 |
| @deepseek-ai/schemastery | dependencies:^3.18.2 | 普通 semver;已存在于 harness 中。注册设置分区的插件用这个 schema 库声明其 Config——而不是 zod,本包仅将 zod 用于 Typert 传输编解码器 |
| @deepseek-ai/dsh-settings | devDependencies:0.1.5-rc.2(精确锁定) | 由 harness 提供;提供 ctx.settings.installSection,即插件配置卡片的宿主侧部分 |
| @deepseek-ai/dsh-client-ui-settings、…-settings-plugins | devDependencies:0.1.5-rc.2(精确锁定) | 由 harness 提供;settingsScope(卡片绑定到的按命名空间作用域服务)和 settings.plugin.item 插槽声明。两者都已通过 dsh-web-app 存在于客户端名册中,因此它们不列在 dsh.client.inject 中 |
| @deepseek-ai/dsh-client-store | devDependencies:0.1.5-rc.2(精确锁定) | 不直接导入——它是 SnapshotSelectorHook 的真正归属,而 dsh-client-ui-slots/lib/types/store.d.ts 重新导出了它。省略它会导致静默失败:在 skipLibCheck: true 下,由此产生的 TS2307 被吞掉,每个 props.useSessions(state => …) / useInput(s => …) 回调参数都退化为隐式 any,而类型名称在编辑器中仍能正确显示 |

预发布范围陷阱。 ^0.1.1-rc.2 读作 >=0.1.1-rc.2
FEISHU_APP_SECRET=
optional (defaults to /auth/feishu/callback when unset):
FEISHU_REDIRECT_URI=http://127.0.0.1:/auth/feishu/callback
FEISHU_OAUTH_SCOPE=

详情:

- 这里使用的是飞书的企业自建应用 (internal app) OAuth 端点(open.feishu.cn/open-apis/authen/...),而非 passport(网页应用)端点。
- 在飞书开发者控制台中,在 安全设置 → 重定向 URL 下注册回调 URL——它必须完全匹配(127.0.0.1 ≠ localhost)。要么将 FEISHU_REDIRECT_URI 固定为一个值,要么注册你启动时用到的每一个端口。
- 只注册一个重定向 URL。 在 安全设置 → 重定向 URL 下保留多个 URL 会在授权页面触发飞书错误 20029(重定向 URL 有误)——只保留你实际使用的那一个,例如 http://127.0.0.1:8801/auth/feishu/callback。
- 生产应用需要版本发布 + 管理员审批,重定向 URL 的更改才会生效。开发期间,使用 开发配置 → 测试企业和人员 → 关联应用 获取一个测试版本,其配置会立即生效,无需审批。
- 如果这些变量未设置,插件会记录一条警告,WebUI 保持打开(无登录)——这是安全的默认行为,因此正常安装不受影响。

按环境划分的启动步骤

选择你的 shell。FEISHU_REDIRECT_URI 中的  必须与 dsh 实际监听的端口(--port)一致,并且该确切 URL 必须在飞书控制台中注册。下面的示例使用端口 8801——请改成你自己的端口。

Linux / macOS(bash、zsh)

不启用登录:
sh
dsh --profile

启用飞书登录(内联环境变量):
sh
FEISHU_APP_ID= \
FEISHU_APP_SECRET= \
FEISHU_REDIRECT_URI=http://127.0.0.1:8801/auth/feishu/callback \
dsh --profile  --port 8801

或者先导出一次,然后启动:
sh
export FEISHU_APP_ID=
export FEISHU_APP_SECRET=
export FEISHU_REDIRECT_URI=http://127.0.0.1:8801/auth/feishu/callback
dsh --profile  --port 8801

Windows PowerShell

不登录:
powershell
dsh --profile

使用飞书登录:
powershell
$env:FEISHU_APP_ID = ""
$env:FEISHU_APP_SECRET = ""
$env:FEISHU_REDIRECT_URI = "http://127.0.0.1:8801/auth/feishu/callback"
dsh --profile  --port 8801

之后清除它们(仅当前会话):
powershell
Remove-Item Env:FEISHU_APP_ID, Env:FEISHU_APP_SECRET, Env:FEISHU_REDIRECT_URI

Windows cmd(命令提示符)

不登录:
bat
dsh --profile

使用飞书登录(= 两侧不要有空格——set VAR = x 会设置一个带尾随空格的键):
bat
set FEISHU_APP_ID=
set FEISHU_APP_SECRET=
set FEISHU_REDIRECT_URI=http://127.0.0.1:8801/auth/feishu/callback
dsh --profile  --port 8801

之后清除它们:
bat
set FEISHU_APP_ID=
set FEISHU_APP_SECRET=
set FEISHU_REDIRECT_URI=

桌面外壳(DSH Pet Desktop)

打包后的应用永远不会继承你的 shell 环境,而且它会向 dsh 请求一个
动态端口(desktop/src/sidecar.ts 中的 --port 0)。当
FEISHU_REDIRECT_URI 未设置时,插件会根据请求主机推导出
redirect_uri,因此注册的 URL 和实际端口永远无法匹配——在
Electron 构建中登录无法完成。现在外壳通过一个固定端口的回环代理解决了这个问题:
json
// %APPDATA%\DSH Pet Desktop\feishu.json  (首次启动时创建为空文件)
{
"appId": "",
"appSecret": "",
"callbackPort": 8912
}

然后在
安全设置 → 重定向 URL 下注册完全一致的 http://127.0.0.1:8912/auth/feishu/callback,并重启应用。

外壳对此的处理方式:

- 将 FEISHU_APP_ID、FEISHU_APP_SECRET 和
FEISHU_REDIRECT_URI=http://127.0.0.1:/auth/feishu/callback
注入到 dsh 子进程中——插件会原样遵循该值;
- 监听 callbackPort,并将回调以 302 重定向到本次运行 dsh 实际绑定的端口,
查询字符串保持不变。这一额外跳转是安全的:dsh 的 cookie 是仅限主机的(主机,
而非端口),并且 dsh 会校验自己的 state cookie。

注意事项:

- appId/appSecret 为空 = 无门禁(应用直接打开),这也是
未做任何改动的安装所得到的结果。
- 真实的 FEISHU_ 环境变量仍然优先于该文件,并且显式的
FEISHU_REDIRECT_URI 会被原样遵循(因此 CLI 开发不受影响)。
- callbackPort 必须空闲。第二个桌面实例无法绑定它,并会记录
feishu login NOT ready——它会以无门禁的方式打开,而不是崩溃。
- 再次出现错误 20029:请只保留一个注册的重定向 URL。

安装

从 git 仓库安装(推荐给使用者)
sh
dsh plugin --profile  add github:zw11591-sketch/dsh-pet-panel

--profile 是必填项——直接运行 dsh plugin add ... 会报错
required option '--profile ' not specified。

这是一次 git 源安装,因此在你允许之前,pnpm ≥ 10 会阻止 prepare 构建步骤。pnpm 会打印出需要复制的确切包键。对于首次安装,该键就是包名——将其添加到该 profile 的 pnpm-workspace.yaml 中:

allowBuilds:
dsh-pet-panel: true

然后重新运行 add 命令。首次安装会通过 prepare 脚本从源码构建 lib/index.js + lib/client.js(不做类型检查——那是 CI 的工作)。

最后启动该 profile:

dsh --profile

从本地检出安装(file:)

cd dsh-pet-panel
pnpm install
pnpm run build
dsh plugin --profile  add file:.
dsh --profile

在 Windows 上,file: 安装是复制而非符号链接:重新构建检出目录
并不会传播到 profile 中。请参阅下文“重新构建完成但更改不可见”。

更新

git 源安装

陷阱:pnpm update 不会拉取新提交。 github:user/repo 这样的 spec 只有在 spec 字符串发生变化时才会重新拉取(没有版本号变更 → 不会重新拉取)。要真正拉取新提交,请使用以下方法之一:

A. 移除 + 重新添加(最简单、确定性最强)
dsh plugin --profile  remove dsh-pet-panel
dsh plugin --profile  add github:zw11591-sketch/dsh-pet-panel

B. 固定到特定 ref,然后更新
dsh plugin --profile  add "github:zw11591-sketch/dsh-pet-panel#"

C. 打破 semver 范围并获取最新版(仅对 npm 发布的插件有意义)
dsh plugin --profile  update --latest dsh-pet-panel

在 git 重新添加之后,在你允许之前,pnpm 会再次阻止新提交的 prepare 构建。allowBuilds 的键是特定于提交的——请将 pnpm-workspace.yaml 更新为 pnpm 打印出的确切键:

allowBuilds:
dsh-pet-panel@https://codeload.github.com/zw11591-sketch/dsh-pet-panel/tar.gz/: true

删除过时的  条目,然后重新运行 add。

file: 安装

重新运行 add(触发重新复制 + prepare),或直接复制构建输出:

cp -rf /path/to/dsh-pet-panel/lib/. $DSH_HOME/profiles//node_modules/dsh-pet-panel/lib/

然后重启该 profile(完整的重启 + 验证流程请参阅“重新构建完成但更改不可见”)。

卸载 / 移除

dsh plugin --profile  remove dsh-pet-panel

这等同于在 profile 目录内执行 pnpm remove:它会删除依赖条目、从 node_modules 中卸载,并且运行后的 reconcile 步骤会将该插件从 dsh.profile.bundles 中清除。重启该 profile;它现在会在没有该插件的情况下启动。

不要手动编辑 package.json 中的 dsh.profile.bundles——dsh plugin add/remove
会自动保持其正确。

故障排除

1. ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED
pnpm ≥ 10 会拒绝为 git 依赖运行 prepare 构建,直到其 key 被加入允许列表。提示信息显示 tar.gz/ —— 这意味着 pnpm 确实解析到了正确的 commit;它只是不愿构建。

修复: 将 pnpm 打印出的确切 key 复制到 $DSH_HOME/profiles//pnpm-workspace.yaml 中的 allowBuilds 下,删除过期的条目,重新运行 add。

2. ERR_PNPM_CANNOT_REMOVE_MISSING_DEPS

“project has no dependencies of any kind”。该 profile 的 package.json 没有 dependencies 字段,因此 pnpm remove 没有任何东西可删除 —— 插件仅以 node_modules + lockfile 残留的形式存在(一种失同步状态,通常来自先前的手工编辑或中断的操作)。

修复: 完全跳过 remove,直接 add —— 它会重新声明依赖并覆盖 node_modules。

3. ERR_PNPM_IGNORED_BUILDS 指向旧的 commit

该包实际上已下载、构建(“Build complete”)并链接 —— 但 pnpm 还将旧* commit 报告为被忽略的构建(过期的 node_modules/.modules.yaml → ignoredBuilds),以非零状态退出,因此 dsh 的 reconcile 步骤永远不会运行,bundles 也不会更新。

修复: 运行一次普通安装,它会重新计算 ignoredBuilds(清空为 [])并让 reconcile 将插件追加到 bundles:

dsh plugin --profile  install

之后验证:.modules.yaml 中的 ignoredBuilds 为 [],lockfile 中只有新的 commit,且 bundles 列出了 dsh-pet-panel。

4. pnpm not found on PATH(退出码 127)

dsh plugin 会转发给 pnpm。请先安装 pnpm(npm i -g pnpm 或 corepack enable),然后重新运行。

5. 重建已完成但更改不可见(过期的 bundle / 错误的 profile)

症状:你编辑了源码,pnpm run build 以 0 退出,但正在运行的 Web UI 仍显示旧行为。不要假设逻辑有误 —— 90% 的情况下是服务器在提供过期的 bundle,或者你修补的 profile 并不是占用该端口的那个。按以下顺序诊断:

1. 找出哪个 profile 占用了该端口。 UI URL(例如 :8801)不会告诉你 profile 名称。

netstat -ano | grep ':8801' | grep LISTEN          # -> PID
wmic process where processid= get CommandLine   # -> --profile  --port

进程上的 --profile pet-test --port 8801 意味着每次编辑都必须落在 pet-test 中,而不是 web —— 即使用户称它为“网页”。

2. 确认该 profile 的副本已过期(宿主从 profile 的 node_modules 读取,而不是你的检出目录):

grep -c "" $DSH_HOME/profiles//node_modules/dsh-pet-panel/lib/client.js
0 = stale; >0 = current

3. 复制 + 重启:

cp -rf /path/to/dsh-pet-panel/lib/. $DSH_HOME/profiles//node_modules/dsh-pet-panel/lib/
cp /path/to/dsh-pet-panel/package.json $DSH_HOME/profiles//node_modules/dsh-pet-panel/package.json
taskkill /F /PID
cd $DSH_HOME/profiles/ && dsh --profile  --port

将 package.json 与 lib/ 一起复制。agent card 的 version 是从它读取的,因此只同步
lib/ 会让 card 宣传的是已安装的版本,而不是你刚刚构建的版本——
而且由于 pluginVersion() 按进程缓存,这个陈旧的值会一直保留到重启。其他一切
看起来都没问题,这正是它被忽视的原因。A2A 面板会在 card URL 旁边显示所宣传的版本,
所以现在这个不匹配从 UI 就能看到,而不再只是存在于原始 JSON 中。

(taskkill 在 MSYS/Git-Bash 上需要单斜杠 /F——//F 无法被识别。)

4. 验证所提供服务的字节,而不仅仅是磁盘上的文件。 Web 应用在 /plugins/??&rev= 提供客户端 bundle——该列表是从页面的  读取的;一个裸的 /plugins/??dsh-pet-panel/client.js 会返回 404。获取完整 URL 并 grep 一个标记,然后在浏览器中硬刷新(Ctrl+Shift+R)以清除缓存的 bundle。

6. 重启后出现 EADDRINUSE

在后台运行 dsh --profile  --no-open 会 fork 一个子进程;杀掉父进程会让子进程继续占用端口。找到残留的监听者并杀掉它:

netstat -ano | grep ':' | grep LISTEN
taskkill /F /PID

如果完全没有 LISTEN 行,那么服务器就是已经没了——在后台启动的开发服务器会连同其终端会话一起被回收,再怎么折腾端口也无法把它弄回来。重启它。TIME_WAIT 行是正常的 2MSL 回收(在 Windows 上约 2 分钟),不是症状;SYN_SENT 条目才是没有任何东西在监听时客户端的样子。

7. ERR_MODULE_NOT_FOUND: Cannot find package 'lightningcss'(仅限开发者)

当在没有 lightningcss 的情况下从源码构建时,这会在 pnpm install(prepare 构建)期间触发。它已经是此仓库的 devDependency,所以只有在你移植构建工具时才会遇到——将 lightningcss 添加到 devDependencies。

8. 程序化会话因 prompt variable "{{cwd}}" has no value 而终止

harness 内置的系统提示补丁会追加 Your working directory is {{cwd}}.,而 {{cwd}} 从 session.header.cwd 解析。任何在没有 meta.cwd 的情况下创建的会话都会在提示组装时抛出异常——模型根本不会被调用。 交互式 WebUI 会话总是带有 cwd,所以只有程序化入口点会暴露:计划任务、一次性插件 agent,以及任何其他调用 ctx.agents.create 的东西。记录的原因如下:

模型调用失败:prompt variable "{{cwd}}" has no value for this assembly (section "deployment:persona-suffix")

按此顺序诊断:

1. 任务的 lastResult.text / 返回的原因已经同时指出了变量和补丁 section。
2. 检查会话文件大小:$DSH_HOME/sessions//session.v3.jsonl.zstd 只包含一行(几百字节)意味着会话在组装期间就终止了。停止调查 provider、model 或余额。
3. $DSH_HOME/sessions/ 中出现 _no-cwd slug 即可确认。

修复方法:将 meta: { cwd } 传给 agents.create —— 对于一个只读任务,一个稳定的临时目录(join(tmpdir(), '…') + mkdir -p)就足够了。验证时有两个陷阱:修复必须位于已部署的 lib/index.js 中,而不仅仅在 src/ 中;并且一个正在被终止的服务器进程仍会用旧代码触发最后一次 tick,因此要先重启,再触发,然后才能判断结果。

开发

类型从 node_modules 解析:tsconfig.json 未声明任何 paths,也没有项目引用,因此每个 @deepseek-ai/ 导入都绑定到 package.json 中的版本(参见“依赖版本”)。不涉及任何同级 harness 检出。

pnpm install
pnpm run build      # tsc -b && tsdown && postbuild — 生成 lib/index.js + lib/client.js + 类型
pnpm run typecheck  # 类型门禁(针对已安装的 @deepseek-ai/ 类型运行 tsc -b)
pnpm run test       # 行为门禁 — 运行每个离线验证脚本(需要先构建 lib/)
pnpm run verify     # 一条命令完成构建 + 测试

从本地检出构建并安装一个 file: 包:

pnpm run build
dsh plugin --profile  add file:.
dsh --profile

打包 Windows 桌面安装程序

desktop/ 是一个独立的 Electron 外壳,拥有自己的 package.json。它将插件以及一个
内置的 dsh 运行时打包为一个 NSIS 安装程序。共四步,必须严格按此顺序执行:

pnpm run build                       # 1. 插件 -> lib/index.js + lib/client.js
node scripts/prepare-plugin.mjs      # 2. 插件 -> desktop/vendor/dsh-pet-panel-.tgz
node scripts/prepare-runtime.mjs     # 3. 内置 dsh + node + 该 tgz -> desktop/resources
cd desktop && npm run dist           # 4. -> desktop/release/DSH Pet Desktop--x64-setup.exe

这个顺序至关重要:第 3 步会使用第 2 步写出的 tarball,因此跳过或重排这些步骤会导致
在一个全新构建的安装程序中打包一个过期的插件 —— 构建会成功,且不会有任何警告。

| 步骤 | 脚本 | 作用 |
| --- | --- | --- |
| 2 | scripts/prepare-plugin.mjs | pnpm build + pnpm pack → desktop/vendor/.tgz |
| 3 | scripts/prepare-runtime.mjs | 复制 dsh 目录树 + node.exe,将该 tgz 解包到应用根目录,解析离线依赖闭包,重写内置的 package.json,以便 profile 能够 import('dsh-pet-panel') |

前提条件与开销:

- Windows x64,Node ≥ 22.19(engines 下限)。
- 在 desktop/ 内运行一次 npm install(electron + electron-builder)。
- 第 3 步会复制约 250 MB 的 dsh 以及一个 83 MB 的 node 运行时 —— 预计需要 1-2 分钟。
- 第 3 步需要一个真实的 tty —— 请在前台运行它。 在非 tty 环境中(后台
进程、CI,或任何 stdin 被管道化的场景),它会以 stdin is not a tty 退出并返回 1,而且
在复制任何内容之前就会如此。该失败来自它生成的 --dump-config 探测,而非
复制本身。如果你必须以分离方式运行它,请为它提供一个 pty。
- 在步骤 3 中添加 --verify,以便同时运行 vendored-tree / profile-materialization / boot 检查。

两个图标,两个生成器——它们写入不同的位置,且不可互换:

python scripts/make-icon.py         # 桌面应用图标
python scripts/make-agent-icon.py   # A2A agent card 图标

两者都需要 Pillow,并且仅在图标发生变化时才需要重新运行。

验证脚本

四个离线门禁,外加一个需要凭据的门禁和一个实时探测。每一个都会导入构建后的 lib/(先运行 pnpm run build)——除了 verify-remote-manifest.mjs,它读取源代码且无需构建——在动态导入之前将 DSH_HOME / DSH_PROFILE_DIR 指向一个临时目录,按断言打印 PASS/FAIL 以及总计,并在任何失败时以非零状态退出。

pnpm run test 运行四个离线门禁并对其进行汇总;失败时,它会重放子脚本的完整输出,因为失败的断言比计数更重要。CI 在每次推送到 main 以及向 main 发起 PR 时运行 typecheck → build → test(.github/workflows/ci.yml)。

pnpm run test                          # 下面的四个离线门禁,已汇总

node scripts/verify-remote-manifest.mjs  # 7 个断言 — @Remote 方法与手写清单
node scripts/verify-a2a-task.mjs         # 25 个断言 — A2A Task 状态与响应结构
node scripts/verify-artifacts.mjs        # 40 个断言 — artifact 负载 + JSONL 字段往返
node scripts/verify-scene-note.mjs       # 12 个断言 — 群组场景备注已发送,在 1:1 中不存在

不在 pnpm run test 中 — 这些需要来自仓库外部的某些东西:
node scripts/verify-asr.mjs              # 需要在环境中设置 MODELVERSE_API_KEY
node scripts/verify-real-a2a.mjs         # 探测,而非门禁:dsh -> 一个实时 Hermes 网关(无断言)

这两个不纳入 CI 是刻意为之,而非疏忽:verify-asr.mjs 需要 API 密钥,且其判定取决于远程模型如何处理该负载;而 verify-real-a2a.mjs 需要在本地运行一个 Hermes A2A 网关,并且会真正调用 LLM。

前三个直接驱动 TeamGateway——它们从原型上取出该类,并替换 selfAgent / refreshMembers,因此无需运行中的 harness 或真实对等端即可演练群组发送。verify-artifacts.mjs 还覆盖了 storage 层:它写入一行填充了每个 ChatMessage 字段的 JSONL,通过 readMessages 读回,并断言每个字段都得以保留。readMessages 逐字段重建对象,因此,一个添加到写入器但未添加到读取器的字段,对编译器、类型检查以及写入侧测试都是不可见的——其症状是数据从 UI 中悄然消失(这曾发生过一次,涉及 artifacts)。

make-icon.py 写入 desktop/build/icon.ico + icon.png。icon.ico 是
electron-builder.yml 所引用的文件(win.icon + 三个 nsis 图标槽位);icon.png 是 1024
master,electron-builder 会按约定为非 Windows 目标选用它。

make-agent-icon.py 会生成 src/agentIcon.ts —— 一张 128×128 的 PNG,以 base64 内联。插件
在 /.well-known/agent-icon.png 提供它,这正是 A2A agent card 的 iconUrl 所指向的地址。
它被内联而不是从磁盘读取,是因为该插件的三种部署形态(pnpm file:
依赖、打包后的 tgz、桌面资源)对 files 的过滤方式不同,所以 src/ 并不总是
存在。生成的文件会被提交。

桌面打包的坑

- 先让 IDE 释放它的索引句柄。 Cursor/VSCode 索引器会打开
desktop/release/win-unpacked/resources/app.asar,之后 electron-builder 会以
EBUSY: resource busy or locked, unlink ...app.asar 失败,而且第 3 步也无法删除旧的
desktop/resources 目录树。本仓库已经通过 .cursorignore
和 .vscode/settings.json 排除了构建输出 —— 但必须重启 IDE 才能生效;
该配置只会阻止未来的扫描,并不会关闭已经打开的句柄。
若不想重启就绕过:npx electron-builder --win nsis --x64 -c.directories.output=release-work。
- setup.exe /S 会卡住。 这个构建是 oneClick: false,并带有
allowToChangeInstallationDirectory: true,而 NSIS 静默模式会卡在这里(数分钟,不写入任何文件)。
要验证打包后的构建,直接运行 desktop/release/win-unpacked/DSH Pet Desktop.exe
—— 那与安装程序会复制的文件逐字节一致。
- prepare-runtime.mjs --verify 可能报告假阴性。 它的
boot manifest does not expose dsh-pet-panel 检查会抓取服务器的骨架 HTML
(约 68 个字符,不包含模块名);客户端的两半只有在页面执行 JS 之后才会组装完成。
判断插件是否加载,要看渲染后的 DOM,而不是那个响应。
- 临时测试会抛错:临时禁用应用内登录门禁的做法是把
/feishu.json 重命名为 feishu.json.disabled(之后要恢复)。

不打包直接运行桌面外壳(开发循环):

node scripts/prepare-runtime.mjs     # 仅在插件或运行时发生变化后执行
cd desktop && npm start              # = npm run build && electron .

工作原理

源码布局

src/index.ts 过去包含所有内容。它按自然边界拆分为每个关注点一个文件 ——
行为相同,逻辑未变。宿主那一半:

src/
├── index.ts            1628   入口:apply(ctx) 注册 12 个插件 + team/inbound/self-agent 运行时
├── shared.ts             82   profile 路径解析(profileNameFromArgv、a2aConfigFile、…)+ 字符串清理
├── sessionEvents.ts     130   读取 agent 会话事件:工具调用、被阻止的审批、forge 进度
├── skillForge.ts        264   SKILL.md 增删改查 + 模型生成的技能 + 每个 profile 的技能提供器
├── mcpTools.ts          140   MCP 服务器增删改查与热重挂载
├── a2aConfig.ts         389   本机的 agent card、外部 agent 名册、card 端点
├── a2aProtocol.ts       192   A2A 消息解析与出站发送
├── a2aTypes.ts           12   TeamImage,由出站和入站路径共享
├── feishuAuth.ts        542   飞书 OAuth 登录门禁 + HMAC 签名 cookie
├── scheduledTasks.ts    178   任务调度器及其 Typert 接口
├── scheduler.ts          97   任务持久化与时间计算(纯函数)
├── asr.ts               290   语音转文字客户端
├── voiceAsr.ts           56   语音网关 + 实时配置源
├── atomicWrite.ts        34   原子写入(临时文件 + 重命名)
├── skill-examples.ts     86   技能模板
├── agentIcon.ts          15   agent card 图标(base64 内联,脚本生成)
└── client/                    浏览器端部分(39 个文件)

拆分遵循两条规则,如果你要添加文件,这两条都值得保留:

- 文件保持平铺在 src/ 中。 scripts/postbuild.mjs 只从 lib/types/ 复制顶层的 .js,
因此子目录虽然能正常编译,但会在加载时以 ERR_MODULE_NOT_FOUND 失败——本地 lib/ 看起来
是健康的,故障只在宿主中才会显现。
- 任何从 index.ts export 的内容都保持在那里重新导出。 package.json 的 types 指向
lib/types/index.d.ts,因此把一个符号移出去而不重新导出,会悄无声息地收窄公共 API。
scripts/verify-.mjs 从 lib/index.js 导入,会在下一次运行时大声失败。

该包在 package.json 中声明了两个 manifest:

- dsh.bundle.patch → cordis.patch.yml,它将插件行插入到 profile 中。安装该包会自动应用补丁层。
- dsh.client(platform: web、inject: [...])→ web 模块表会将此包扫描进浏览器名册,并提供 lib/client.js。

客户端 bundle 由 build/tsdown.client.ts 构建——这是 DeepSeek Harness 自身客户端 bundle 预设的自包含移植版。它将插件包裹在 window.__ModuleLoader__.load({ id, factory }) 中,通过 lightningcss 编译 CSS Modules,并通过 shell 的冻结模块表解析 @deepseek-ai/* + react(无全局变量,无 import map)。

宿主端将其服务暴露为 Typert 远程:src/client/remote.ts 保存手写的 TYPERT_REMOTE manifest,描述每个方法的线上编解码器(skillForge、toolIntegrations、a2aConfig、team),客户端将这些命名空间挂载到 Typert 客户端远程上。

入站 A2A 端点(/a2a)通过 ctx.agents.create / resume + followup + whenIdle 驱动真实的 agent 循环,因此它共享 WebUI 的模型、工具、技能和 MCP。由于 A2A 上没有交互式审批通道,审批被设置为故障关闭的 never 策略:需要审批的工具会被确定性地拒绝,响应会携带一个 metadata.approvalsBlocked 数组(工具 + 原因),而不是静默失败。
配置存储:按 profile 划分 vs. 全局

$DSH_HOME 是 dsh 的数据根目录——在打包的 Windows 应用中它是
%APPDATA%\DSH Pet Desktop\dsh-home。它有两层,并且并非所有内容都跟随 profile:

$DSH_HOME/
├── .credentials.yaml      global       API 密钥,所有 profile 共享
├── settings.yaml          global       应用设置
├── sessions/ storages/    global       会话和缓存
└── profiles//
├── a2a-agents.json    per-profile  A2A 卡片 + 外部 agent 名册
├── teams.json         per-profile  团队定义
├── team-chats/        per-profile  团队会话线程(.meta.json + .jsonl)
├── team-assets/       per-profile  产物字节,每个线程一个目录
├── mcp-servers.json   per-profile  MCP 服务器注册
└── skills/            per-profile  为此 profile 加载的技能

有两个值得了解的后果:

- 凭据是全局的,身份不是。 切换 profile 会替换 A2A 名册、团队、
MCP 服务器和技能,但保留相同的 API 密钥和会话历史。这是有意为之,但很容易
让人感到意外。
- 桌面应用固定使用 --profile web(desktop/src/sidecar.ts),且没有 UI 可以更改它——
因此在打包的应用中,按 profile 划分的层实际上是一个固定目录。从 CLI 运行
dsh --profile  会使用一组独立、分离的这些文件。

插件从 process.argv 中的 --profile(profileNameFromArgv)解析这些路径,而从不
从工作目录解析——这样可以避免 dsh --profile web 和 cd profiles/web && dsh 写入
两个不同的位置。没有 --profile 时,它会回退到全局位置
($DSH_HOME/xxx),只有在直接运行 lib/ 进行调试时才会走到这里。

技能是有意限定作用域的:ProfileSkillProviderPlugin 注册了一个 provider,
并设置 includeDefaultRoots: false,这会关闭 dsh 的全局技能根目录($DSH_HOME/skills、
~/.agents/skills),因此只会扫描 profiles//skills。

文档

项目文档由 OpenWiki 自动生成到 openwiki/(外加 AGENTS.md / CLAUDE.md 入口片段)。一个定时的 GitHub Action(.github/workflows/openwiki-update.yml)会按 cron 保持它们同步,并在每次更新时打开一个 PR;它从仓库 secrets 中读取 OPENAI_COMPATIBLE_API_KEY / OPENWIKI_LANGSMITH_API_KEY,因此在启用该工作流之前请先设置这些。

许可证

MIT

上游仓库有新提交时邮件通知你(每天最多一封,无更新不打扰),随时一键退订。

💬 加入 DPharness 群聊

插件用法、部署报错、新插件第一时间同步——群里问,比一个人翻文档快。

点击加入 QQ 群
DPharness 群聊二维码,手机 QQ 扫码进群
扫码进群