dsh-no-long-sit
Verifieddsh-no-long-sit · v0.1.3 · MIT · Web UI
[DSH 0.1.7–0.2.x] A cat desktop pet for DeepSeek Harness whose first job is to keep you from sitting too long. The pet lives in the corner, can be dragged anywhere (position remembered), and plays a looping animation per state (idle / getting up / drinkin
Install
dsh plugin add dsh-no-long-sit Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Tags
Readme
dsh-no-long-sit · 猫猫桌宠(久坐提醒)
DeepSeek Harness 的桌宠:一只住在 Web GUI 角落的猫猫,可拖到任意位置,四个状态各有循环动画。 它现在最先给你干的一件事是久坐提醒——到点居中弹窗 + 三声滴滴喊你起来走动、喝口水; 你说"再等会"它就掏出带真实来源的案例劝你;本次工作结束时给一份好/中/差评价。 后续会继续长功能,目标是做一只有用的桌宠。

功能
| 能力 | 说明 |
|---|---|
| 猫猫桌宠 | 右下角常驻,四个状态循环动画(趴着/起身/喝水/睡着);可拖到任意位置并记住;不透明度可调;素材缺失自动回落内置 SVG |
| 定时提醒 | 专注周期到点 → 居中弹窗(遮罩,无法忽略)+ 三声"滴滴滴"提示音,带休息倒计时 |
| 三按钮闭环 | 【休息好了】直接开下一轮专注 /【再等会】看案例、N 分钟后重弹 /【结束】出总结 |
| 伤害案例窗 | 内置 17 条真实案例(久坐 9 / 缺水 8),每次只挑 1 条久坐 + 1 条缺水:走洗牌袋——一轮内不重复、相邻两次必不同,袋子存 localStorage,刷新/重启也接着上次继续(不会每次开机都从第一条开始);每条带来源媒体、发布日期、原文链接 |
| 休息结束 | 休息倒计时跑完 → 再响一次"滴滴滴" + 弹「回来干活」,点【开始干活】进下一轮;若一直没人点,超过等待上限自动开下一轮(默认 10 分钟,可设 0 关闭) |
| 总结评价 | 本次工作时长、完成专注轮数、成功中断次数、提醒触发次数、再等会次数、建议喝水次数 → 好/中/差 + 猫猫点评 |
| 设置面板 | 称呼、专注周期 30–90 分钟、休息周期 3–15 分钟、再等会间隔与上限、提示音开关与音量、桌宠不透明度、等待上限、评价阈值、悬浮宠物开关;带 「恢复默认值」 一键回退(两段确认) |
| 桌宠可拖动 | 按住猫猫拖到任意位置(自动夹在视口内),位置用 localStorage 记住;拖动不会误触菜单,单击才开菜单 |
| 计时权威在宿主 | 刷新页面/切标签不丢计时;多标签页不会各弹各的 |
默认:每 30 分钟专注 → 提醒 → 休息 5 分钟 → 回来确认 → 下一轮。
安装
# 桌面版(Electron):DSH_HOME 指向桌面数据目录
$env:DSH_HOME='D:\dsh-desktop-home\data'
& '<DSHzm>\resources\runtime\cli\bin\dsh.cmd' plugin --profile desktop add <本插件路径或包名>
# Web 版
$env:DSH_HOME='C:\Users\<你>\.dsh'
dsh plugin --profile web add dsh-no-long-sit
装完重启 DSH(bundle 层在启动期合成)。重启后右下角出现猫猫即表示加载成功。
设置项
设置 → 插件 → 猫猫 · 久坐提醒:
| 字段 | 默认 | 范围 | 含义 |
|---|---|---|---|
address |
大人 | ≤24 字 | 猫猫对你的称呼(宿主没有统一的"用户称呼"字段,所以由插件自己持有) |
focusMinutes |
30 | 30–90 | 专注周期(分钟) |
breakMinutes |
5 | 3–15 | 休息周期(分钟) |
snoozeMinutes |
5 | 1–15 | 点【再等会】后多久重新弹提醒(案例窗一关就立刻恢复提醒) |
maxSnooze |
2 | 0–5 | 每轮最多"再等会"几次,超出记一次"未中断" |
soundEnabled |
开 | — | 到点播放"滴滴滴"提示音(Web Audio 现场合成,不依赖素材;设置页可试听) |
soundVolume |
0.6 | 0–1 | 提示音音量 |
awaitReturnTimeoutMinutes |
10 | 0–60 | 「回来干活」最多等多久,超时自动开下一轮(0 = 一直等) |
earlyBreakGuard |
开 | — | 休息没到点就点【休息好了】时,先软提示一次 |
goodRatio / midRatio |
0.8 / 0.5 | 0–1 | 评价「好」/「中」所需的中断命中率 |
caseRefreshEnabled |
开 | — | 启动后后台刷新案例库 |
caseRefreshDelayMinutes |
10 | 1–120 | 启动后多久触发一次刷新 |
showFloatingPet |
开 | — | 右下角常驻猫猫(可拖动,位置记忆在浏览器 localStorage) |
petOpacity |
0.75 | 0.2–1 | 桌宠不透明度;0.75 = 25% 透明,设 1 完全不透明 |
timeScale |
1 | 0.001–1 | 仅调试:把分钟压成秒(0.01 = 30 分钟变 18 秒) |
改完 ≤2 秒生效(宿主每 2 秒重读一次 settings.describe()),当前阶段会按新时长重锚。
工作原理
┌─ host 半身 index.mjs ──────────────────────────────┐ ┌─ client 半身 client.js ────────┐
│ 权威计时(1s tick)→ 状态机 focus/break/awaitReturn │ │ shell.overlay 浮层:猫猫 + │
│ 账本持久化 <DSH_HOME>/data/dsh-no-long-sit/*.json │←→ │ 提醒窗 / 案例窗 / 总结窗 │
│ 案例库:assets/cases.json + 运行期缓存 + 后台刷新 │ │ 3s 轮询宿主状态,按钮 POST 动作 │
│ 路由 /dsh-no-long-sit/{state,choice,cases,refresh- │ │ 设置卡片 settings.section + │
│ cases,art}(自查跨源,白名单素材) │ │ ctx.configForms.get(id) │
└───────────────────────────────────────────────────┘ └────────────────────────────────┘
- 计时放宿主:浏览器计时刷新即丢、多标签页无去重机制,所以状态机与账本都在 host,前端只渲染与发动作。
- 不做 host→client 事件推送:DSH 没有给第三方插件的推送通道(转发事件白名单是官方封闭常量), 所以前端用 3 秒轮询 + 本地 1 秒时钟算倒计时(宿主下发绝对时间戳,不用额外请求)。
- 持久化:
<DSH_HOME>/data/dsh-no-long-sit/state.json(同目录.tmp+ rename 原子替换,失败不阻塞插件)。 - 案例刷新:
ctx.web.search()拿来源 →ctx.llm.stream()(reasoningEffort:"off"、60s 超时)改写成短案例 → 只保留 url 出现在检索结果里的条目(防模型编造来源)→ 原子写盘;失败保留旧库并在设置页显示原因与上次成功时间。
已知限制
- DSH 所有页面都关掉时不会再提醒(架构决定:没有页面可弹,宿主也没有面向浏览器的第三方推送通道)。 要系统级提醒得再加浏览器通知或独立桌面窗口,本期未做。
- 提示音需要页面里有过一次用户交互(浏览器自动播放策略):正常使用 DSH 必然点过页面,插件会在第一次点击/按键时解锁音频。 若 DSH 窗口被最小化或标签页在后台,浏览器会节流定时器,弹窗与提示音可能迟到几秒(切回前台会立刻补一次状态)。
- 案例后台刷新可能失败:桌面版搜索凭据顺序是「账号 token →
DEEPSEEK_API_KEY」,且账号 token 只在 会话活跃时可解析。失败时保留旧库并把原因写进设置页状态区,不会静默变成空列表。 - 不统计真实喝水次数:程序无法感知你有没有喝水,按你的口径用"专注轮数"给出建议喝水次数。
- 弹窗只在 DSH GUI 内(
shell.overlay),不覆盖宿主设置弹窗之上(宿主设置面板 z-index 更高)。
开发
node --test # 40 项:状态机 / 配置 / 案例校验 / 路由鉴权与素材净化 / mock 宿主契约 / settings 同步
node --check index.mjs; node --check client.js
lib/state.mjs是纯函数状态机(零依赖,可单测);lib/config.mjs是 schemastery schema。- 前端是经典脚本(
window.__ModuleLoader__.load+React.createElement),没有构建步骤,改完刷新页面即生效。 - 依赖:
@deepseek-ai/schemastery是唯一运行时依赖(peer)。本地link:开发时把它放进插件自己的node_modules。
换猫猫素材
素材全部由 assets/pet-manifest.json 描述,换素材不用改代码:支持动图文件(WebP/GIF,当前形态)、
帧序列(frames/<状态>/<状态>-NN.png)与静态图(SVG/PNG)三种写法;清单缺失或某状态未定义时自动回落到内置猫猫 SVG。
当前装机素材:assets/anim/{idle,rise,drink,sleep}.webp —— 由用户的 512×512 透明 PNG 帧序列
(10 / 8 / 10 / 7 帧,fps 6 / 8 / 8 / 4)经「统一基线 → 缩到 256×256 → 合成为动图 WebP(q95)」流水线产出,
36 个文件 11.4 MB 压到 4 个文件 626 KB。
制作规格(画布、帧数、fps、命名、背景抠图要求)与现行装机素材对照见 docs/ART-SPEC.md; 最省事的交付方式是给我 4 段纯色背景的 MP4(或直接给 512×512 透明 PNG 帧序列),我来抠图/抽帧/压缩装机。
验收记录(2026-09-30)
- 单测 40/40 通过(状态机 / 配置 / 案例校验 / 路由鉴权与素材净化 / mock 宿主契约 / settings 同步)。
- 隔离实例(桌面 runtime 0.2.0-rc.2,独立
DSH_HOME+ 端口 3099)+ 无头 Chrome CDP 全流程: 宠物渲染 → 专注到点居中弹窗(实测几何中心 = 视口中心)→【再等会】案例窗(1 条久坐 + 1 条缺水,轮换) → 关窗后提醒窗立刻恢复并继续倒计时 → 休息结束弹「回来干活」→【开始干活】进入下一轮(breaksTaken=1)。 - 提示音路径用音频探针实测(统计
createOscillator调用):专注到点 +3、休息结束再 +3,两处都真的执行了。 - 设置生效链路:面板值 → profile patch →
settings.describe()→ 插件 ≤2s 生效(cfgSource=user)。
修复记录
| 日期 | 问题 | 根因 | 修复 |
|---|---|---|---|
| 09-30 | 到点没声音、弹窗被忽略 | 提示音从未实现;提醒窗是右下角贴边卡片 | Web Audio 现场合成三声"滴滴滴"(设置页可试听/调音量);提醒窗改为居中遮罩弹窗 |
| 09-30 | 看完案例后休息倒计时不再出现、跑完也不进下一轮 | 前端用 dismissedAt === phaseStartedAt 做永久屏蔽,点过一次【再等会】后整个阶段都不再弹窗 |
改为只由 snoozeUntil 控制;案例窗关闭时发 resume 立刻恢复提醒;休息结束时清空 snoozeUntil;另加「等待上限」兜底自动开下一轮 |
| 09-30 | 保存设置后整张卡片变空白,改过的值看着"改不回去" | form.subscribe() 的回调不带参数(store 只通知"变了"),旧代码把回调参数当新快照 → 快照被写成 undefined,全部字段渲染成空 |
回调里重新 getSnapshot(),并只接受有效快照;保存改为一次原子 mutate;新增「恢复默认值」与调试加速「一键刹车」 |
| 09-30 | 点「恢复默认值」后宿主仍按旧值跑 | 所有字段被 unset 后设置服务把 profile patch 里整条条目剪掉,宿主重读时"没有这一行"就直接 return → 配置停在上次的值 | 三档兜底:用户层非空用它 / 用户层清空但条目在 → 用 effective / 条目整条消失 → 回落 schema 默认值 |
| 10-02 | 案例窗"每次都显示同两条",像是案例库不更新 | 选案例用 arr[rotation % len],而 rotation 是每次页面加载都归零的 state → 开机第一次打开永远取第 0 条;后台刷新出的新案例恰好排在数组最前,于是天天看到的就是那两条 |
改为洗牌袋 + localStorage:一轮内不重复、相邻必不同、刷新/重启接着上次继续;新刷到的案例插队首;案例窗底部显示"共 N 条,每次弹窗都会换" |
License
MIT