DeepSeek Harness Plugin Hub

Publish and manage complete Harness Profiles. Discover Plugins for your next setup.

Explore

PluginsPresetsDocsNews

Community

Publish a pluginContactReport an issue

Resources

Plugin Hub on GitHubDeepSeek HarnessSystem statusPrivacy notice
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

Independent and unofficial. Not affiliated with, authorized by, or endorsed by DeepSeek.

Whale Emote — DSH Plugin for DeepSeek Harness
DeepSeek Harness Plugin Hub
ProfilesPluginsCategoriesNewsDocsSign inManage Profiles
ProfilesPluginsCategoriesNewsDocsSign in
← Plugins
W

dsh-whale-emote

Whale Emote

Let the 小鲸鱼挂件 widget express emotions: an emoji pop-up overlay triggered by the agent via HTTP (without modifying third-party widget packages)

The plugin will be installed here. Keep web if you are unsure.

npx -y @deepseek-ai/dsh plugin --profile web add github:harmless0819-dev/dsh-whale-emote#1907f93f7664218c220225c569dd2ee4607cb989
READMECompatibilityVersions

Compatibility and provenance

Whale Emote is published as dsh-whale-emote and currently resolves to version 0.1.0. The Hub verifies its manifest and preserves the exact installation source for reproducible installs.

DSH compatibility
*
Runtime surfaces
any
Release source
github
Registry updated
9/20/2026

Versions

0.1.0stable
9/20/2026

Related plugins

Loading related plugins…

Latest
0.1.0
DSH
*
HMR
Process restart
Tree shaking
Safe tree shaking not declared
Unpacked size
Unavailable
Files
Unavailable
Surface
any
License
MIT
Source
github
GitHub
★ 0
Weekly downloads
0
Last push
9/20/2026
View source ↗
README badge

Click the badge to copy Markdown for your README.

Do you maintain this Plugin?Claim benefit · Priority security scan

Verify the GitHub repository declared in package.json to manage this listing. After you claim it, Hub will prioritize a security scan of the current version and publish the result when it passes.

Claim this Plugin →
Report an issue

Related plugins

More verified plugins in ui-customization.

Web App@deepseek-ai/dsh-web-appThe dsh browser-surface bundle: the web patch layer over dsh-base plus the runtime glue plugin (frontend dist serving, web-surface prompt, bash runtime variables, URL line)Experimental Agent Team Web Profile@deepseek-ai/dsh-experimental-agent-team-web-profileExperimental Web profile layer for Agent Teams Remote and UI pluginsClient Ui Task Board@linxin666/dsh-client-ui-task-boardHost-authoritative task board for the DSH Web GUI with real session execution, Host cron scheduling, and optional cross-platform idle-sleep protection; mounted without DSH source changes.Pet@linxin666/dsh-petMulti-pet companion plugin for the dsh web GUI: a registry-driven floating pet that reacts to model activity, with per-pet naming, petting/feeding interactions and an affinity score

README

dsh-whale-emote

让小鲸鱼挂件表演情绪:agent 通过一个本地 HTTP 调用,就能让表情包从挂件旁边弹出来。

为什么需要单独一层

dsh-whale-widget 本身没有任何接受外部文字/指令的接口。它的全部路由里,只有 /dsh-whale/last-turn.json 会被前端每秒轮询(内容由宿主按真实用量生成,只读);size.json 只在页面初始化时读一次;图片和音效都是初始化或手势时加载。所以想让挂件「表达情绪」,只能另加一层。

这一层不修改第三方挂件的任何文件:它自己注册路由,并用挂件同款的 tapIndex 机制注入一小段脚本,操作挂件根节点 .dshwv-root 的坐标把表情弹在它旁边。

路由

方法路径作用
GET/dsh-emote/manifest.json可用情绪清单(key / 字幕 / 描述 / readable)+ 音效预设(sounds)
GET/dsh-emote/state.json当前情绪,客户端每秒轮询(seq 递增表示新事件)
GET/dsh-emote/sticker?key=<key>表情图片本体
GET/dsh-emote/sound?key=<key>音效本体(按扩展名给 audio/* 类型,支持 Range)
GET/POST/dsh-emote/set?emotion=<key>&text=<气泡>触发情绪(POST 用 JSON body)
GET/POST/dsh-emote/clear立刻收起
GET/dsh-emote/client.js注入用的客户端脚本
GET/dsh-emote/health.json探针:客户端是否在轮询、有没有真的显示出来
POST/dsh-emote/ack客户端显示成功后的回执(agent 靠它确认"真的弹了")
POST/dsh-emote/activity-ack状态条几何回执:客户端上报 left/top/w/h/display/inView/whaleRect
GET/dsh-emote/activity.json当前活动状态(写文件=奋笔疾书 等),含诊断信息 + client 回传
GET/POST/dsh-emote/activity手动覆盖活动状态(POST JSON {label,emoji,detail,ttlMs};?clear=1 立刻清空)
GET/dsh-emote/state.json当前情绪 + activity 字段(客户端从这里读状态条,不用额外请求)

emotion 支持模糊匹配:给 嚣张、最强 或者 key 的一部分都能命中;命中不了会返回 404 并附上全部可用 key。

诊断:用户说「没弹」时先查探针,别先猜代码

agent 看不到浏览器,所以「发了但没显示」和「根本没发」在它眼里长得一样。加了探针之后,一条查询就能定性:

curl.exe http://127.0.0.1:3080/dsh-emote/health.json
读出来结论
poll.connected: false客户端不在(页面关了 / 浏览器重启了)—— 最常见
connected: true 但 ack.seq 落后 seq客户端在跑,是显示环节的问题,才值得查代码
ack.seq 追上了 seq确实弹了,属于用户没看到(时机 / 位置),不是没发
  • poll.count 每个 state.json 请求 +1,客户端每秒一次 → 4 秒涨约 4 才是正常。
  • poll.ua 能看出是哪个浏览器在轮询。
  • ack.lastAgoMs 是「上一次真正显示」距今多久。

踩过的坑:先怀疑过「SPA 把节点冲成游离节点」,还加了自愈补丁 —— 结果真凶只是页面关着 / 服务器没重启。教训:先看探针,再动代码。

用法

# 图形化一点
.\emote.ps1 -List                      # 列出全部情绪
.\emote.ps1 嚣张                        # 弹图
.\emote.ps1 委屈 -Text "才、才不是呢"     # 弹图 + 自定义气泡
.\emote.ps1 撒娇 -TtlMs 0               # 不自动收起
.\emote.ps1 -Decision 困惑 -Text "选哪个?"  # 决策提醒:必弹 + 小鲸鱼唱歌音效
.\emote.ps1 -Clear                     # 收起

# 或者直接打接口
curl.exe "http://127.0.0.1:3080/dsh-emote/set?emotion=%E5%9A%A3%E5%BC%A0&text=hello"

参数:ttlMs(自动收起毫秒,0 = 不自动收)、text(挂在图下方的气泡文字)、sizePx(覆写尺寸)、sound(是否放音效,默认关)、soundKey(用哪段音效,默认 press)。

状态条:反映「当前在干嘛」(2026-09-21 新增)

用户要求:右下角挂件要有一条反映当前对话状态的表达 —— 例如「写入 → 奋笔疾书」。 现在这条状态条是全自动的:宿主监听会话事件(session/event),把正在跑的工具翻成动作, 常驻在鲸鱼头顶(与挂件同层,z-index:9999)。agent 不用每次手写。

  • 常驻:闲置时显示 🐟 摸鱼中,干活时显示具体动作,不会自动消失。
  • 可拖动:按住拖到任意位置,松手吸附到最近的视口边缘;位置记在 localStorage。
  • 单击还原:没拖动就松手 = 回到「自动跟随鲸鱼头顶」。
  • 跟着鲸鱼走:拖鲸鱼 / 挂件吸附 / 改挂件大小时,药丸一起动。 手动摆过之后仍然保持相对关系一起位移(挂件缩放则退回头顶重新锚定)。
触发显示detail
write / edit✍️ 奋笔疾书文件名
read / read_image📖 翻书中文件名
grep / glob🔍 翻箱倒柜pattern
pwsh / bash / job_*⌨️ 敲命令命令首词
web_search / web_fetch🌐 上网查query
browser_*🖱️ 逛网页—
subagent / workflow / ralph🐣 摇人帮忙—
ask_user_question🙋 等你拍板—
todo_write / goal 系📋 列计划—
hindsight_*🧠 翻记忆—
present / skill📦 交东西—
未知工具🔧 干活中工具名(兜底,绝不静默变空)
turn 开着但没在跑工具💭 思考中—
turn/end✅ 收工(定格 3s 后淡出)—

几个关键设计(都有 harness 锁着):

  • 粘滞期(默认 4s):工具常常只跑几十毫秒,不粘一会儿的话状态条永远只会闪「思考中」。 所以 tool/result 在粘滞期内不覆盖当前动作。
  • 多会话判收工:subagent / 另一个标签页也会开 turn,只有最后一个 turn/end 才喊收工。
  • 丢事件的兜底:turn 一直挂着但 90s 没有任何会话事件(崩溃/强杀)→ 自动按 idle 处理。
  • 与表情是两条独立通道:情绪是 agent 主动表演的一次性事件(有 ttl、会收起), 活动是宿主推出来的持续状态。状态条复用同一次 state.json 轮询(不额外发请求), 服务端没给 activity 字段时客户端不炸(新旧混装安全)。
  • Live2D 联动:dsh-whale-live2d 读同一个 activity,把活动映射到模型表情 (writing→画笔、thinking→放空脸……共 14 张)。活动脸是底噪层:情绪脸盖在上面, 情绪到点回到活动脸,活动没了才回默认脸;闲置随机小动作不抢活动脸。

配置项(profile patch 的 config 段):

键默认作用
autoActivitytrue是否启用自动状态条
activityHoldMs4000动作粘滞期(防闪)
activityDoneMs3000「收工」定格时长
activityIdleMs90000丢 turn/end 的兜底超时

探针与手动通道:

curl.exe http://127.0.0.1:3080/dsh-emote/activity.json   # 现在挂的是什么 + 诊断
.\emote.ps1 -ActivityStatus                              # 同上(CLI 版)
.\emote.ps1 -Activity "正在读图纸" -ActivityEmoji "📐"     # 强制覆盖(演示/排查用)
.\emote.ps1 -ActivityClear                               # 立刻收起

视觉旋钮在 lib/client.js(改完只要 F5):ACTIVITY_FADE_MS(淡出延迟)、ACTIVITY_GAP(与鲸鱼间距)、ACT_SNAP_MARGIN(吸附留边)。

拖动与吸附

胶囊可以直接拖:按住拖到想放的地方,松手自动吸附到最近的视口边缘(左右二选一, 竖直自由但不出屏)。位置记在 localStorage,F5 后还在。 单击(没拖动)= 还原成自动跟随鲸鱼,不用去翻设置。

⚠️ 实现上的一个坑:挂件自己在 document 捕获阶段 监听 pointerdown 并用 isWhaleHit()(PNG alpha 采样)判断「有没有戳到鲸鱼」。捕获阶段先于 target, 所以我们在胶囊上的 stopPropagation 拦不住它 —— 拖胶囊会把鲸鱼一起拖走。 解法是拖动开始时给 document 补发一个 pointercancel:挂件监听了 pointercancel 并会因此放弃它那次拖拽。改这块代码前请先看 onActDown() 的注释。

决策提醒音效(-Decision)

用户要求:每次我弹表情提醒他做决策(问问题、等确认、等选方案),就放「小鲸鱼唱歌」。 所以有 -Decision 这个组合开关,它等于 -Force(绕过概率必弹)+ -Sound sing:

.\emote.ps1 -Decision 困惑 -Text "这两个方案选哪个?"

音效走每次事件单独指定这条路:set 时带上 soundKey,服务端把它翻成 soundUrl 塞进 state.json,客户端 show() 时优先用 s.soundUrl,没有才回落到挂件自带的 duck。 所以普通情绪照旧是「咔哒」一声,只有决策提醒才唱歌,互不干扰。

自动联动(2026-09-17 起)

决策提醒的唱歌音效已改成自动触发:agent 每次调 ask_user_question(要向用户拍板), 插件在 tools/pre-execute 钩子里自动弹一个 decisionEmotion 情绪 + sing 音效, 不用再靠 agent 每次手动记着补 .\emote.ps1 -Decision。问一次拍板 → 唱一次歌。

配置项(profile patch 的 config 段,默认值如下):

键默认作用
autoDecisiontrue是否启用自动联动
decisionEmotion困惑拍板时自动弹哪个情绪
decisionText''气泡文案;留空则自动取问题标题(header),再退到问题正文(截 40 字)
decisionTtlMs25000自动弹出的停留时间(毫秒)

/dsh-emote/health.json 的 autoDecision 字段实时报告 enabled / emotion / count / lastAt, 可以用来验证联动有没有真的触发。改这些配置 / 或改 lib/index.js 里的联动逻辑 → 要重启 dsh web。

-Decision 组合开关仍然可用(手动强制 + 唱歌),只是从「唯一通道」降级成「手动通道」。

预设表在 lib/index.js 的 DEFAULT_SOUNDS(也可用 profile 配置的 sounds 覆盖):

key是什么怎么来的
press(默认)挂件自带 duck press直接指向 /dsh-whale/sound/press.mp3?set=duck
release挂件自带 duck release同上,换 release
sing小鲸鱼唱歌file: 小鲸鱼唱歌.wav,走插件自己的 /dsh-emote/sound 路由

带 file 的预设从 mediaDir(默认就是表情目录 D:\deepseek\表情)取文件。为什么不用挂件那条 /dsh-whale/sound/…:它只认四个写死的候选文件,而且不管什么文件都回 audio/mpeg; 自定义 wav 从那里出去类型就是错的(能响只是浏览器嗅探的运气)。所以自己开一条路由, 按扩展名给 audio/wav,并老实实现 206 / Range —— 浏览器拿媒体时会带 Range: bytes=0-。

加一段新音效:把文件丢进 D:\deepseek\表情,在 DEFAULT_SOUNDS 里加一条 { key: 'xxx', label: '…', file: 'xxx.wav' },改的是 lib/index.js → 要重启 dsh web。 (只想换掉 sing 对应的文件则不用动代码:清单按 mtime 重读,路由每次从磁盘读文件。)

情绪清单(以 .\emote.ps1 -List 为准)

适合得意/炫耀:嚣张(最强) 挑衅(闭源模型在哪) 暴富 傲娇 嘴硬

适合卖萌/亲近:撒娇 卖萌 开心 糊弄 自嘲 无辜

适合认错/崩坏:委屈(我不是大肥鱼) 耍赖 崩溃 慌乱 大哭 阵亡 害怕

适合吐槽/阴阳:腹黑(四个橙子五个小孩) 嘲讽(真坐得住啊你) 不耐烦 装懂 生气

其他:发疯 讲课 教学 困惑 整活 涨价 庆祝 小心翼翼

完整字幕见 emote-manifest.json 或 .\emote.ps1 -List。

改表情 / 加表情

  • 图片目录:D:\deepseek\表情(插件里是 DEFAULT_IMAGES_DIR)
  • 清单:emote-manifest.json,改动后无需重启(插件按 mtime 自动重读)

加新图:把 png 丢进图片目录,然后在 emote-manifest.json 里加一条:

{ "file": "新文件名.png", "key": "得意", "caption": "字幕原文", "desc": "画面描述", "readable": true }

readable: false 表示字幕小、缩小后看不清 —— 这类会自动放大 1.4 倍显示。

安装

# 从 GitHub(推荐)
dsh plugin --profile web add github:harmless0819-dev/dsh-whale-emote

# 或从本地克隆
dsh plugin --profile web add link:<本仓库绝对路径>

装完重启 dsh web,然后浏览器 F5。

卸载

dsh plugin --profile web remove dsh-whale-emote

然后重启 dsh web。第三方挂件不受影响。

可调旋钮(都在 lib/client.js,改完只要 F5,不用重启)

常量当前值作用
SIZE_SCALE0.5最终像素 = 服务端 sizePx × 这个值(默认 340 × 0.5 = 170px)
EDGE'random''random' 左右随机 / 'left' / 'right' / 'whale' 跟随鲸鱼所在侧
V_MIN V_MAX0.35 0.65竖直位置的随机区间(占视口高度比例),即「中间 30%」
SOUND_ALWAYStrue每次弹出都放音效
SOUND_URLpress.mp3?set=duck兜底音效:服务端没给 soundUrl 时才用它
SOUND_VOLUME0.6音量;唱歌那段 20 秒,嫌吵就调小
POLL_MS1000轮询间隔,决定「触发 → 出现」的延迟上限

两个容易踩的实现细节

  • 随机位置必须在 show() 里掷骰子,不能写在 place() 里。place() 一次弹出会被调用 3 次(立即 / 图片 onload / requestAnimationFrame),写在那里表情弹出时会左右横跳。现在用 curSide / curAnchor 缓存一次结果,连窗口 resize 都不会跳。
  • 浏览器自动播放策略:页面还没有过用户手势时 audio.play() 会被拒(NotAllowedError,且是静默失败)。现在被拒时记 soundPending,并在首次 pointerdown / keydown 时补放一声(仅当表情还挂着,避免空响)。

架构要点(维护必读)

为什么客户端脚本是单独一个文件

lib/client.js 是独立文件,/dsh-emote/client.js 路由每次请求都从磁盘读。原因:改插件模块(lib/index.js)必须重启 dsh web 才生效 —— 实测 HMR 不会重载它(改完代码后服务器仍在吐旧字节)。把客户端逻辑拆出去之后:

你要改的东西改哪个文件怎么生效
视觉:位置、动效、尺寸、CSSlib/client.js浏览器 F5 即可,不用重启
宿主:路由、状态机、清单逻辑lib/index.js必须重启 dsh web

重启 dsh web(restart-web.ps1)

改完 lib/index.js 之后,用配套脚本重启 —— 请在用户自己的终端里跑:

& 'D:\deepseek\dsh-whale-emote\restart-web.ps1'          # 停掉 + 拉起 + 自检
& 'D:\deepseek\dsh-whale-emote\restart-web.ps1' -DryRun  # 只看会杀谁、会怎么拉

它会:找到监听 3080 的进程 → 停掉 → 等在 -WorkDir(默认 D:\deepseek)用 dsh web 拉起 → 轮询首页 → 查 /dsh-emote/health.json 和 /dsh-emote/sound?key=sing(206 表示 Range 那条路通) → 提醒你回浏览器 F5。日志写在 D:\deepseek\dsh-web.log。

为什么必须由用户在终端跑,不能让 agent 代跑:agent 的 shell 跑在文件沙箱里, 写不了 %DSH_HOME%\...,dsh web 连 profile 都启不开 —— 实测直接死在 EPERM: open '%DSH_HOME%\profiles\web\cordis.yml'。 就算硬拉起来,也可能是个残的服务。(顺带一提:重启会掐断正在进行的那一轮会话, 所以别在 agent 干到一半时重启。)

鲸鱼的真实几何(定位的坑)

挂件根节点是个正方形大方块,而鲸鱼本体只占它右下角的 59.45%:

.dshwv-root { position:fixed; width:var(--dshw-base); height:var(--dshw-base); }
.dshwv-img  { position:absolute; right:0; bottom:0; width:59.45%; height:59.45%; }

所以定位必须量 .dshwv-img。量 .dshwv-root 会得到一个含大片空白的矩形,表情会被推到离鲸鱼很远的地方(V1 的「位置很怪」就是这个 bug)。挂件左吸附时会加 transform:scaleX(-1),但 getBoundingClientRect() 已经把变换算进去了,直接量 img 就是对的。

⚠️ Windows PowerShell 5.1 的两个坑

这套环境跑的是 Windows PowerShell 5.1(Desktop),不是 PowerShell 7。写脚本会连踩两次:

  1. .ps1 里有中文 → 必须存成 UTF-8 with BOM。5.1 读无 BOM 的 .ps1 会按 ANSI(GBK) 解码,中文变乱码;乱码字节里可能含引号,直接把脚本解析搞崩(报 Unexpected token / Missing closing ')')。 用 write 类工具改完 emote.ps1 后 BOM 会丢,要补回来:

    $p='D:\deepseek\dsh-whale-emote\emote.ps1'
    $c=[System.IO.File]::ReadAllText($p,[System.Text.UTF8Encoding]::new($false))
    [System.IO.File]::WriteAllText($p,$c,[System.Text.UTF8Encoding]::new($true))
    
  2. Invoke-RestMethod -Body <string> 按 Latin-1 编码,而且 5.1 的 ConvertTo-Json 不会把中文转义成 \uXXXX —— 两者相叠,中文 JSON 到服务端就是乱码(表现为 unknown emotion 的 404)。必须自己转字节:

    $bytes = [System.Text.Encoding]::UTF8.GetBytes(($payload | ConvertTo-Json -Compress))
    Invoke-RestMethod -Method Post -Uri $uri -ContentType 'application/json; charset=utf-8' -Body $bytes
    

    emote.ps1 里的 Send-Json 已经这么做了。

  3. ConvertTo-Json 会把 .NET 数组序列化成 { "value": [...], "Count": n } —— 不是 JSON 数组!而且 @($json | ConvertFrom-Json) 的行为也不符合直觉,可能把整个数组包成单元素。

    • 我踩的坑:用 PowerShell 合并清单(32 + 17)时,输出变成了 [{value:[32],Count:32},{value:[17],Count:17}],插件解析不出任何 key,emote.ps1 求饶 直接 404。
    • 结论:碰 JSON 就用 node。JSON.parse / JSON.stringify 没有这些歧义。修复脚本见 repair-manifest.mjs(它会递归钻进 {value} 里把条目捞出来,校验 49 条 + key 唯一后才写回)。

(命令行参数里的中文是没问题的,会烂的是「文件编码」「请求体编码」和「用 PowerShell 拼 JSON」这三处。)

已知限制

  • 客户端每秒轮询一次,所以从触发到出现最多约 1 秒延迟。
  • 表情包是整幅梗图(1254×1254,字烧在图里),不能替换挂件本体的抠图 DSniang1.png,所以是「弹在旁边」而不是「换脸」。
  • 挂件拖动/吸附时表情不会跟着走(只在弹出瞬间按当时位置定位);窗口 resize 会重新定位。
  • 首次安装、或改 lib/index.js 时需要重启 dsh web;改 lib/client.js 不需要。