packages/dsh-task-board
zhu1090093659/dsh-web侧边栏多列任务看板:卡片交给真实 DSH 智能体会话执行,支持 cron 定时(Host 侧到点执行,关浏览器也生效)。
会说话的问候卡(156 句预生成语音,卡角一键开声/静音)、每轮任务的完成报告、长跑时的陪伴提示,并在需要授权时接管输入框请示。
进入 DSH 工作界面时,自动向主人问一声好;一轮任务跑完时,再向主人交一张报告; 跑得太久时说一句「我在」;工具要权限时,由小鲸来请示 —— 全都长在对话区里,不是浮在窗口角落。
场景 · 插在哪 · 长什么样 已有对话 · conversation.input.dock(输入框下沿那条 strip) · 头像 + 气泡:下午好,主人~ 小鲸泡好了茶,就等主人坐下啦~,12 秒后淡出 空白 / 新对话 · conversation.hero.options(首屏「探索未至之境」那一行) · 一条同样文案的小气泡,点了才收 一轮跑完 · 同一个 conversation.input.dock · 换成绿色的交付卡:任务完成啦,主人! 总共用了 2 分 10 秒,小鲸把现场收拾干净啦~,署名改成「任务完成 · 小鲸交付」,14 秒后淡出 长跑陪伴 · 同一个 conversation.input.dock · 青绿色的陪伴卡:已经跑了 6 分 20 秒啦,小鲸一步都没挪开,就在旁边守着。,署名「长跑陪伴 · 小鲸在」 需要授权 · conversation.composer(接管整个输入框) · 橙黄色的请示卡:工具 bash 想动一点超出平时的权限:…… + 工具详情 + 两个答复按钮,署名「等待授权 · 小鲸请示」
同一个问候只出现一次:两个 slot 都是 session 作用域的 list,宿主按当前对话只渲染其中一个。
在 dsh-market 里搜 dsh-whale-greeting 点安装;或者命令行:
dsh plugin --profile <你的 profile> add dsh-whale-greeting官方 DSH Desktop 请在应用自己的 设置 → 插件 里装(桌面端的 profile 归应用管理,跑着的 时候命令行改不动它)。这个包声明了 dsh.bundle,装进 profile 时由 cordis.patch.yml 插入 一行 id: whale-greeting,不往 profile 的补丁层写任何东西。
装完刷新页面即可,没有需要重启的 Host 侧改动。
再随机触发一次,阈值在 10~15 分钟之间每次重抽;任务跑满 4~8 分钟的随机时刻说一句陪伴; 一旦出现待审批就换成请示面板
hero 会强制亮出,哪怕上一个会话的问候还没收
天气/夜话/碎碎念分组),sessionStorage 记住最近用过的 48 句,一轮之内不重复;用完自动洗牌
.dshba)还在时不动,等它消失、且页面可见,才开始assets/avatars/ 里的 24 张表情包头像,每次问候换一张(连续不重复),由 Host 的 /plugins/dsh-whale-greeting/avatar/.png 提供;路由失效时回落到内联的 assets/avatar.png
--dsw-alias-* token,深浅色自动适配一轮任务的终点在宿主里是一个明确的事实:Session 快照的 running 由 true 变回 false。 卡片从 slot 拿到的 useSession 座椅上盯着这一个布尔值:
发生什么 · 卡片说什么 running true → false,且这一轮 ≥ 1.2 秒 · 交付卡(成功色):任务完成啦,主人! + 用时 + 今天第几件 同时 promptError 有值 · 中断卡(警告色):这一件卡住了,主人……,文案里说明用了多久、停在哪 这一轮短于 1.2 秒 · 不报(回车即答的闲聊不值得一张卡) 停在子 Agent 会话里 · 不报(只报主人自己的那一轮)
running 变 true 的那一刻起表,变 false 时结算,写成 42 秒 / 3 分 12 秒 / 1 小时 5 分run.count,文案里可以写「第 3 件收工」run,dock 与 hero 两个 slot 的同款观察者共用它,先到的报,后到的看到 run.reported 就安静 —— 不会因为同时挂着两个 slot 刷两张卡
force 亮出,即使上一条问候还没到 12 秒也会被替换,因为「做完了」比「你来了」更该被看见任务在后台跑完,主人切回来仍然看得到这张报告,而不是「打开就没了」
.dshg,只把 --dshg-accent 换成 state-success-primary(完成)或 state-warn-primary(中断),署名与对勾跟着 accent 走
已知限制:宿主没把「这一轮是被主人按 Stop 掐掉的」放进 Session 快照,所以手动中止的一轮也会走 完成卡(文案只承诺用时,不承诺结果细节)。要区分中止,得等宿主暴露对应的事实。
一轮任务跑得久,主人最容易在「还不知道要多久」的时候烦躁。所以小鲸不报进度(它看不见进度), 只负责说一句「我在」:
发生什么 · 卡片说什么 这一轮从开始算起,到了本次抽出的随机时刻(4~8 分钟之间) · 跑得有点久了,主人还顺利吗? + 已经跑了 6 分 20 秒啦,小鲸一步都没挪开,就在旁边守着。 说完还接着跑 · 立刻重抽一个 4~8 分钟的窗口,到点再说一次(跑得越久,陪得越久) 卡片正在显示别的内容(问候 / 交付 / 请示) · 让位,不抢戏;下一个窗口再补 这一轮结束 · 计时归零,下一次陪伴重新从新一轮的 4 分钟起算
COMFORT_MIN_MS~COMFORT_MAX_MS 之间每次重抽,所以不是「第 5 分钟固定响一次」,而是每一轮都不一样;40 秒一轮的巡检节拍只负责「到点没有」,不负责精确到毫秒
comfortAgent,dock / hero 谁挂上来就谁启动,最后一个离开才停 ——两个 slot 同时在场也只有一份节拍、不会说两句
仍然会在页面不可见时停走(回来还看得见)
需要授权时,宿主会把整个输入框交给审批面板(conversation.input.dock 连同问候卡一起被藏起来), 所以小鲸必须接管输入框才在画面上 —— 这也是这个功能只能做成 composer 链条目的原因。
发生什么 · 面板说什么 会话出现 pendingInteraction.kind === 'approval' · 请示卡:主人,小鲸要请示一下~ + 工具 {tool} 想动一点超出平时的权限:{reason}。 这条审批带 callId · 原样把 conversation.approval.detail 渲染在卡片里 —— 要批准的那条命令还是原来那份 主人点「允许一次」/ 按 Enter · pending.answer('allowed-once') 主人点「拒绝」/ 按 Esc · pending.answer('rejected') 已经答过、或宿主撤回 · 两个按钮一起禁用,等它自己消失
answer() 用的还是宿主给的那个 pending 对象,失败时按钮会还回来(不会卡在「小鲸在处理…」)
question / plan-review 一律返回 null,那些面板照旧由宿主自己渲染select 里检查 typeof pending.answer === 'function',一旦宿主换了形状,这个条目直接返回 null,输入框还给宿主,不会白屏
priority: 1 靠前,所以审批时才轮到小鲸;其它局面它返回 null,一枚字节都不占
不会在等待授权时误杀任务
问候卡会用肥鱼的声音把话念出来,卡角上一个喇叭按钮随时开关。
音色直接取 AstrBot 插件 astrbot_plugin_feiyu_voice 配套的肥鱼官方声库 FeiyuVoice-CosyVoice3: cute F6 的 LLM 权重 + references/cute_N01.wav 做 zero-shot prompt,用 CosyVoice3 0.5B (官方声库本身就是这套模型)逐句合成,所以耳朵里听到的和那个插件 /肥鱼说话 是同一个声线。
项目 · 说明 覆盖范围 · 只覆盖「问候」:hello / return / idle 三类卡。完成报告、长跑陪伴、授权请示里带用时、第几件、工具名,这些变量没法预先录音,所以它们不出声 —— 语音只负责问候 语音包 · assets/voice/ 下 156 个 mp3(8 句时段开场白 + 120 句问候正文 + 16 句「回来啦」+ 12 句「等到你啦」)+ manifest.json 声音从哪来 · manifest.json 里的 clips 在构建时注入浏览器半:{ 'line-7': { f: 'line-7.mp3', t: '…原文…' } }。字和声音同源,改文案不会出现「写着一句、念着另一句」 合成方式 · 声库 FeiyuVoice-CosyVoice3(cute F6)→ CosyVoice3 inference_zero_shot,参考音 cute_N01.wav/参考文本「我已经准备好了,我们开始吧。」;逐句 24 kHz 合成后削首尾静音、峰值归一到 0.89 音色微调 · 官方参考音本身就不低(基频中位数 292 Hz),但短句生成时会往下塌到 250 Hz 左右,听感偏成熟。所以成片再用 Praat 的 PSOLA 做一次保时长的微调:音高中位数拉到 300 Hz、共振峰 ×1.10 —— 不改权重、不改 prompt,只把音色扶回参考音的音域,越听越像 B 站那条《人是猫》里的大肥鱼 开关 · 卡角喇叭(data-voice=on/off),状态存 localStorage 的 dsh-whale-greeting:voice,默认开;关掉立刻停声,再打开会把当前这张卡补念一遍 自动播放被拦 · 首屏问候没有用户手势,浏览器会拒播。这时把队列停在原地,等第一次点击/按键再补上(voice-blocked 会记进诊断) 收起 / 切走窗口 · 立刻停声,不留一句在后台念 没有语音包时 · assets/voice/manifest.json 不存在 → 构建注入空清单 → 喇叭按钮自动隐藏,卡片回到纯文字形态
这套语音包不是「近似克隆」出来的:权重和参考音都来自官方声库,跟插件里 /肥鱼说话 走的是 同一套模型与 prompt,所以音色、口音、语气是同一个;音高再按上表扶到 300 Hz 那一档,比默认输出 更细更甜。想重做(改文案、换 cute/gentle 风格、调音色高低),跑一遍 tools/voice/feiyu_gen.py 即可:它读 texts.json(从 src/client.js 抠出来的文案),按 key 覆盖 assets/voice/*.mp3 与 manifest.json,再 node build.mjs 重建浏览器半。音色高低两个旋钮是 --formant(共振峰 倍数,越大越细)和 --pitch-median(目标基频,Hz),不想要微调就传 --pitch-median 0。
半 · 文件 · 作用 Host · index.js · /plugins/dsh-whale-greeting/status.json、/diag、/avatar/.png、/voice/.mp3 四条路由;entry 被装载,dsh.client 声明的浏览器模块才会进页面 Browser · src/client.js → lib/client.js · 两个 list slot 各注册一个 Greeting(variant, props)(渲染问候 / 交付 / 陪伴卡,内部再挂一个不渲染的 ReportWatch 盯 running 并驱动陪伴心跳),外加 conversation.composer 链上的一个 ApprovalComposer(只在审批挂起时获选)
浏览器半是纯 JS + React.createElement,不 import 任何包(React 由页面的模块表给)。 构建做四件事:把 src/style.css 内联成 CSS 常量、把 assets/avatar.png 内联成兜底 data URI、 把 assets/avatars/*.png 的文件名注入成随机头像名单、把 assets/voice/manifest.json 的 clips 注入成语音包清单。
node build.mjs # src/* + assets/* -> lib/client.js + preview.html
node test/smoke.mjs # 假 loader / 假 React / 假 ctx,断言注册、形态、三类卡片与文案库
node test/report.mjs # 假 Session 快照,断言完成报告只报一次、用时、出错与短轮次
node test/comfort.mjs # 假时钟 + 假心跳,断言陪伴只在 4~8 分钟之间出声、请示只认 approval
node test/voice.mjs # 假语音包,断言喇叭的出现/隐藏、开关持久化、补念当前卡浏览器半会往 localStorage 写一条诊断,阶段是 module → apply → mounted:dock|hero → shown:dock|hero → hidden:dock(报告是 show:dock:done):
localStorage.getItem('dsh-whale-greeting:diag')读不到控制台时,直接从磁盘上看:
C:\Users\<你>\AppData\Roaming\@deepseek-ai\dsh-desktop\Local Storage\leveldb\*.logmodule / apply → 客户端插件没被激活(dsh.client 声明或 entry 有问题)mounted 没 shown → 覆盖层没撤、或页面一直在后台shown 但看不见 → 是 slot 内布局问题,不是加载问题show:*:done → 报告没装上:宿主没给 useSession 座椅,或这一轮短于 1.2 秒show:dock:comfort → 这一轮被判定成子会话,或卡片当时正显示别的内容select 让位了(answer() 不在、或 kind 不是 approval),输入框会退回宿主自己的审批面板,功能不受影响
想要 · 改哪 换时段问候语 · src/client.js 里的 SALUTATIONS(每个时段一组 variants) 换正文轮换池 · LINES(一句一行,注释按主题分组) 换完成 / 中断文案 · DONE_HEADS + DONE_LINES、FAIL_HEADS + FAIL_LINES({time} 用时、{count} 第几件、{at} 完成时刻) 换陪伴文案 · COMFORT_HEADS + COMFORT_LINES({time} = 已经跑了多久) 换陪伴的触发窗口 · COMFORT_MIN_MS / COMFORT_MAX_MS(默认 4 / 8 分钟,每次重抽);巡检节拍是 COMFORT_TICK_MS 换请示文案 · PERMISSION_HEADS + PERMISSION_LINES({tool} 工具名、{reason} 申请理由) 让请示不再接管输入框 · apply() 里删掉 conversation.composer 那一段(陪伴与其余卡片不受影响) 换时段分界 · salutationFor() 换停留时长 · SHOW_MS(问候 / 陪伴)、REPORT_SHOW_MS(报告),毫秒 多短的轮次不报 · MIN_TURN_MS 关掉完成报告 · Greeting 里把 watcher 那段去掉 只保留一种形态 · apply() 里删掉不需要的那个 ctx.slots.inject 段 换头像 · 把正方形透明 PNG 放进 assets/avatars/(随机池,文件名只留小写字母/数字/-);assets/avatar.png 只是兜底图。改完重跑 node build.mjs
改完 node build.mjs,重启 DSH(客户端 bundle 的 rev 是宿主装配时算出来的,只刷新会拿到旧 rev)。
作者本机是怎么挂的(开发自用,与使用者无关)
用 link: 挂在 profile 里,改完 node build.mjs + 重启即可,不需要 pnpm install, 也不用往安装目录复制 lib/client.js:
项 · 值 依赖 · profile package.json → "dsh-whale-greeting": "link:" 挂载 · dsh.profile.bundles 里有 dsh-whale-greeting,由本插件的 cordis.patch.yml 插入 id: whale-greeting 实体 · node_modules/dsh-whale-greeting 指向插件目录(junction / symlink)
profile 的 cordis.patch.yml 里不要再手写一段 - insert: id: whale-greeting, 那会和 bundle 层叠成两份(两条 Host 半边、两张卡)。
要装新插件前,先确认 profile 里 pnpm-workspace.yaml 的 storeDir 还在:pnpm 11 只认这里, 项目级 .npmrc 的 store-dir 会被忽略;那一行丢了或写错,pnpm add 就会抛 ERR_PNPM_UNEXPECTED_STORE(提示让你重装,其实只要把 storeDir 写回去)。
preview.html 是三栏离线预览:问候大卡、首屏紧凑卡、任务完成交付卡(浅色 / 深色各一栏, 直接双击就能看,不需要 DSH)。
从市场装的:在插件列表里点卸载即可。
用 link: 手动挂的,两处一起拆:
# 1) 从 <profile>\package.json 的 dsh.profile.bundles 里删掉 "dsh-whale-greeting"
# 以及 dependencies 里的 "dsh-whale-greeting": "link:<插件目录>"
# 2) 删链接(junction 只删链接,不会跟到插件目录里)
cmd /c rmdir "<profile>\node_modules\dsh-whale-greeting"不要再改 profile 的 cordis.patch.yml —— 这个插件走的是 bundle 通道,那里本来就没有它的行。
MIT。头像素材仅用于本插件的问候卡。