娘口三三是《夏目友人帐》里那只圆得像团子的三花猫老师。这个插件把它请进 DSH 的 Web 界面:它蹲在屏幕角落自己发呆、打哈欠、睡一会儿、到处溜达,你点它一下它会不高兴或者很得意地叫一声,你把它拎起来甩出去它会撞墙弹回来,你干活的时候它跟着你的进度换表情。
纯前端桌宠,不改动 DSH 的任何 DOM 结构,不占用任何 UI 插槽,卸载后界面上不留痕迹。
功能预览
这个仓库里没有图片。 动图与截图都是角色画面,而角色画面派生于使用者自己提供的 美术素材,带有与素材相同的再分发限制,因此不随仓库分发。下面的路径在本地构建后 就会出现,构建步骤见开发。
在真实页面里的样子(截图由 tools/verify-browser.mjs 的端到端验证自动产出,不是手工摆拍,因此永远和当前代码一致):
docs/screenshot.png
13 个动作的循环预览由构建逐动作写出(tools/gen_motion.py 生成):
| 类别 | 动作 |
|---|---|
| 待机循环 | idle 呼吸、breathe_deep 深呼吸、look_around 张望、sleep 打盹 |
| 移动 | walk 走路 |
| 小动作 | ear_flick 抖耳 |
| 反应 | hop 跳、bounce_land 落地压弹、happy 高兴、angry 生气、surprised 吃惊、spin 转圈 |
| 拖拽 | drag 悬空挣扎 |
预览图路径为 docs/preview/<动作名>.gif。构建命令:
python tools/gen_motion.py # 需要 work/sprites/base.png
python tools/motion_sheet.py # 逐帧审阅图,判断动作好坏看这个
动作资源的构建是增量的:
assets/anims/里缺哪个动作,客户端就会跳过它并退回idle,桌宠本身照常可用。
两个方向的构建路径,按手上有没有出图凭据选:
| 需要出图 key | 还原度 | 动作生动度 | |
|---|---|---|---|
tools/gen_art.py 出图 → build_assets.py 合成 |
是 | 近似(照文字描述重画) | 高,可多姿势 |
tools/gen_motion.py 形变已有美术 |
否 | 精确(像素就是原画) | 中,单姿势 |
见 STATUS.md 了解两条路各自的取舍与实测结论。
安装
插件以 npm 包的形态发布在 GitHub 上,通过 DSH 的插件管理命令装进 web profile。
从 GitHub 安装(推荐)
dsh plugin --profile web add github:Qihang-He/dsh-nyanko-sensei
dsh web
第二条命令是必要的:插件行与浏览器端的 bundle 在进程启动时装载,装完必须重启 dsh web 才会生效(刷新浏览器页面不够)。
从源码安装(本地路径)
git clone https://github.com/Qihang-He/dsh-nyanko-sensei.git
cd dsh-nyanko-sensei
dsh plugin --profile web add .
dsh web
add . 装的是当前目录——请先 cd 进 package.json 所在的仓库根目录再执行,否则 pnpm 会去找别的包。
更新
dsh plugin --profile web add github:Qihang-He/dsh-nyanko-sensei
dsh web
GitHub 来源的依赖按 commit 解析,重新执行 add 即可拉到最新版本。源码安装则在该仓库里 git pull 后重启 dsh web。
卸载
dsh plugin --profile web remove dsh-nyanko-sensei
dsh web
设置不会被清除。如果想一并删掉,在浏览器控制台执行 localStorage.removeItem("dsh-nyanko-sensei:settings");自己录的语音文件在用户目录里(见语音),需要手动删除。
⚠️ 只保留一种安装来源
同一个插件不要同时从 GitHub 和本地路径安装两次。两条记录都写进 web profile 的依赖里时,插件行会出现两次,DSH 启动阶段解析到重复条目会直接失败,服务起不来。
补救办法是回到单来源状态:
dsh plugin --profile web remove dsh-nyanko-sensei
dsh plugin --profile web add github:Qihang-He/dsh-nyanko-sensei
dsh web
如何使用
装载完成后,娘口三三出现在视口右下角(默认 180px,可在设置里改)。
| 操作 | 效果 | 对应实现 |
|---|---|---|
| 左键单击 | 随机播放 happy / angry / surprised / eat / beast 之一并说一句台词,同时标记为「已选中」(出现虚线圈) |
lib/client.js clickReaction() |
| 点头部(上方 42% 区域) | 额外有 50% 概率冒出开心的吐槽 | 同上,region === "head" |
| 点尾巴(右侧 32% 区域) | 额外有 50% 概率冒出骂人的话 | 同上,region === "tail" |
| 按住拖动 | 播放 drag(四条腿下垂挣扎);松手若移动超过 4px 视为拖拽,不触发点击 |
bindGlobal() / pointermove |
| 快速甩出 | 带着惯性飞出去,撞到屏幕四边按 0.55 的恢复系数弹回,落地后变回 happy |
step() 中的惯性段 |
| 单击选中 → 点击别处 | 猫走到你点的位置,中途播放 walk,朝向自动翻转 |
startWalkTo() |
| 右键 | 打开快捷菜单(项见下表) | openMenu() |
| 听它自言自语 | 按活跃度档位随机冒气泡,台词从 LINES 表里挑 |
autonomyStep() |
| 自己溜达 | 空闲计时器到点后按概率随机换位置 | scheduleAutonomy() |
| 窗口缩放 | 位置自动夹回可视区域内,不会卡在屏幕外 | window 的 resize 监听 |
| 跟随 Agent 状态 | 侦测到「生成中 / 等待批准 / 出错 / 完成」时切换动作与台词 | watchAgent() / detectBusy() |
| 系统「减弱动态效果」 | 关闭交叉淡入淡出与走路上下颠簸 | reducedMotion() |
右键快捷菜单
| 菜单项 | 行为 |
|---|---|
| 呼唤「なつめ」 | 播放点击台词音频(默认 clip natsume)+ happy + 气泡「なつめ!」;音频不存在时该项置灰 |
| 摸摸头 | 播放 happy |
| 变身 | 播放 beast(巨大的白色妖狐真身)并说一句「退下!」 |
| 睡一会儿 | 强制切到 sleep |
| 散步 | 随机挑一个 x 坐标走过去 |
| 大小 NNNpx ( + / − ) | 每次点击增大 20px(80–420 区间内循环夹取) |
| 显示 / 隐藏 | 切换桌宠可见性,写入设置 |
| 回到初始位置 | 速度清零,回到设置里的角落 |
| 打开完整设置… | 打开右侧设置面板 |
「大小」这一项每次点击 +20px,超过上限后再点仍停在上限;想精确调节请用设置面板里的滑杆。
功能说明
动画状态机
lib/client.js 的 ANIMS 表定义了 12 个动作,格式统一为 VP9 带 Alpha 通道的 WebM,画布 360×360。下表是客户端声明的完整清单;其中 idle、walk、sit 三个目前已有资源文件,其余动作的提示词齐备但资源尚未构建,客户端会把缺失的动作跳过并退回 idle(见功能预览):
| 动作 | 类型 | 时长 | 说明 |
|---|---|---|---|
idle |
循环 | — | 站立、呼吸 |
blink |
单次 | 420ms | 眨眼,结束回 idle |
walk |
循环 | — | 迈步,移动时使用 |
sit |
循环 | — | 端坐 |
yawn |
单次 | 1400ms | 打哈欠,结束回 idle |
sleep |
循环 | — | 蜷成一团睡觉 |
happy |
单次 | 1200ms | 开心跳跃,结束回 idle |
angry |
单次 | 1200ms | 炸毛,结束回 idle |
surprised |
单次 | 1000ms | 受惊,撞墙时也会放 |
eat |
单次 | 1600ms | 吃团子 |
drag |
循环 | — | 被拎起来四肢乱蹬 |
beast |
单次 | 2000ms | 变身巨大白兽 |
单次动作结束后回到 idle。实现上有三道保险:ended 事件、busyUntil 超时兜底、以及 step() 里的一次轮询,所以即使某个视频没能正常结束事件也不会把猫卡在最后一帧。
渲染用两个 <video> 元素双缓冲:切动作是交叉淡入淡出,不会闪一帧空白。assets/anims/ 里缺哪个文件,该动作就会被跳过、退回 idle,猫仍然可用。
与 Agent 活动联动
娘口三三会观察对话区域的 DOM 变化(MutationObserver 挂在 document.querySelector("main"),退化到 document.body)来猜你现在的状态。这是一个启发式判断,不是 RPC 订阅:
- 它有 900ms 的防抖,流式输出的密集更新不会把它打爆;
- 状态要连续稳定 1200ms 才会被采纳(迟滞),避免抖动;
- 判断依据是最多 4000 字符的正文文本,用几组关键词匹配:
停止 / Stop→ 生成中,允许 / 拒绝 / Approve / Deny / 等待批准→ 等你确认,出错 / 失败 / Error / Failed→ 出错,完成 / Done / Finished→ 干完了。
映射出来的反应:
| 侦测状态 | 动作 | 台词(带概率) |
|---|---|---|
| 生成中 | walk |
work 18% |
| 等你确认 | sit |
waiting 30% |
| 出错 | surprised |
fail 70% |
| 完成 | happy |
done 60% |
它选择了监听 DOM 而不是订阅宿主事件,是因为这样不需要任何宿主 API、不依赖 GUI 内部结构、坏了也只是「收不到信号」而不会报错——收不到信号时它就退回全自主行为。可以在设置里用「跟随 Agent 状态」关掉。
自主行为与活跃度
空闲计时器在 ACTIVITY 表里定义了三档,设置面板里对应「活跃度」:
| 档位 | 空闲间隔 | 溜达概率 | 自言自语概率 |
|---|---|---|---|
quiet 安静 |
14–30 秒 | 12% | 0% |
balanced 均衡(默认) |
7–16 秒 | 30% | 5% |
lively 活泼 |
3.5–8 秒 | 50% | 14% |
到点后依次判断:先掷骰子决定要不要溜达,其次掷骰子决定要不要冒一句话配一个 happy / angry / surprised,都没有就换一个休息姿势。休息姿势从 idle(权重 3 倍)、sit、blink、sleep、yawn、walk 的池子里抽。拖动中、菜单打开中、设置面板打开中都会跳过自主行为。
气泡台词
打开「气泡台词」后,LINES 表里的中文台词会在猫头顶的气泡里显示 bubbleMs 毫秒(默认 4200ms)。台词按情绪分组:greet、happy、angry、sleepy、work、done、fail、waiting、beast。气泡用 DSH 的主题变量着色,自动跟随浅色/深色主题。
减弱动态效果
系统开启了 prefers-reduced-motion: reduce 时:
- 切动作的交叉淡入淡出被关掉,直接换帧;
- 走路时的上下颠簸被去掉,只有平移;
- 甩出去的惯性弹跳被禁用,松手直接落地。
该偏好是实时采样的,你在系统设置里改完不需要重载页面。
设置
设置存在 localStorage 的 dsh-nyanko-sensei:settings 键下,全部通过宠物自己的界面修改(右键 → 打开完整设置…),DSH 的设置面板里没有对应卡片。
| 字段 | 含义 | 默认值 |
|---|---|---|
visible |
是否显示桌宠 | true |
size |
桌宠边长(px),面板滑杆范围 80–420 | 180 |
corner |
初始停靠的角落:bottom-right / bottom-left / top-right / top-left |
"bottom-right" |
marginX |
初始停靠时距左右边缘的距离(px) | 28 |
marginY |
初始停靠时距上下边缘的距离(px) | 28 |
speed |
移动速度参数(当前版本尚未接线,见下方说明) | 60 |
activity |
活跃度:quiet / balanced / lively |
"balanced" |
voiceEnabled |
是否播放语音 | true |
voiceVolume |
音量,面板滑杆 0–100 映射到 0–1 | 0.9 |
voicePack |
使用哪个语音包目录名 | "default" |
clickVoice |
点击时优先播放的 clip id | "natsume" |
bubbles |
是否显示气泡台词 | true |
bubbleMs |
气泡停留时间(ms) | 4200 |
reactToAgent |
是否跟随 Agent 状态 | true |
wander |
是否允许自主漫游 | true |
showSettingsHint |
设置面板提示文案的开关(当前版本尚未接线,见下方说明) | true |
corner、marginX、marginY只在页面加载和「回到初始位置」时生效;平时猫停在你上次把它放在的地方。speed与showSettingsHint是当前版本尚未接线的预留字段:它们会被持久化,但设置面板里没有对应控件,改它们不会改变现在的行为。
设置面板底部有「恢复默认设置」,会把上面整张表恢复成默认值。
语音
这一节请仔细读——插件不附带任何原版动画语音。
点击时发生了什么
点击桌宠时,宠物会尝试播放 clip id 为 natsume 的音频(由 DEFAULTS.clickVoice 指定,可在设置面板里改成别的 clip id)。
如果这个音频不存在,什么都不会发生——不报错、不弹窗,只是安静地切动作。右键菜单里的「呼唤「なつめ」」在这一情况下会是灰色不可点。
仓库里只有合成占位音
assets/voice/ 下确实有 7 个 MP3 文件(natsume、happy、angry、surprised、eat、purr、sleep),它们全部由 tools/gen_voice.py 程序化合成:声门脉冲串 + 共振峰 + 包络 + 一点呼吸噪声,是一只卡通猫式的「喵」,刻意做得不像真人配音。
它们不是动画原声,也不可能是——原版配音是商业录音,本插件无权分发,所以只提供能跑通播放链路的占位文件。
换成你自己的音频
宿主端从两个根目录提供媒体文件,用户根优先于包内资源,所以放一个同名文件就能覆盖,不需要重新构建、不需要改包内任何文件:
<用户根>\voice\<语音包名>\<clip id>.<扩展名>
Windows 上默认 DSH_HOME 是 %USERPROFILE%\.dsh,因此最常见的样子是:
%USERPROFILE%\.dsh\dsh-nyanko-sensei\voice\custom\natsume.mp3
- **
voice\根目录下的文件**属于名为default的语音包; - **
voice\<子目录>\里的文件**构成一个以该子目录命名的语音包,在上面的例子里语音包叫custom,在设置面板的「语音包」下拉框里选中它即可; - 文件名(去掉扩展名)就是 clip id,
natsume.mp3的 clip id 是natsume; - 放了音频后刷新浏览器页面即可生效,不需要重启
dsh web(宿主端每次请求都重新读盘,manifest 也标了no-store)。
支持的扩展名:mp3、m4a、aac、ogg、oga、opus、wav、flac。同名文件存在多个格式时,按上面这个顺序取第一个。
已识别的 clip id:natsume、happy、angry、surprised、eat、purr、sleep。其中 happy 动作自带 ["natsume", "happy"] 两条语音,其余动作只会念自己的 id。
用麦克风录一条
tools/record-voice.ps1 会从麦克风录一段、掐掉前后静音、做响度归一化,然后直接写进用户语音包。
先列出 ffmpeg 能看到的录音设备:
pwsh -File tools/record-voice.ps1 -ListDevices
只找到一个设备时会自动选中,直接录:
pwsh -File tools/record-voice.ps1 -Seconds 6
多个设备时用 -Device 指定(名字照抄列表里的输出):
pwsh -File tools/record-voice.ps1 -Device "Microphone (Realtek(R) Audio)" -Seconds 6 -Clip natsume -Pack custom
有 3 秒准备时间,按 Enter 可以提前停止。产物写到 %USERPROFILE%\.dsh\dsh-nyanko-sensei\voice\custom\natsume.mp3。参数:-Clip(默认 natsume)、-Seconds(默认 8)、-Device、-Pack(默认 custom)。脚本需要 ffmpeg 在 PATH 上。
npm 脚本里也有一条等价入口:
npm run voice:record
⚠️ 音频责任
你自己放进语音包里的音频由你自己负责。 如果你从动画原声里截取、或从任何商业唱片、影视作品里提取了片段,那是受版权保护的录音:请只在自己机器上做个人用途使用,不要把该文件随插件一起发布、提交到仓库或通过任何渠道再分发。插件作者没有、也不会分发这类音频。
资源来源与许可
这个项目里有三种性质完全不同的东西,请分别看待:
- 代码——
lib/index.js、lib/client.js、tools/下的脚本,采用 MIT 许可,见 LICENSE。 - 美术素材——
assets/anims/里的动画帧,由tools/gen_art.py通过 OfoxAI 中继调用 Google Gemini 图像模型生成,提示词原文记录在脚本里(CHAR、BEAST、ACTIONS表),中间产物在work/目录。所有画面都是为这个项目生成的,没有使用任何原作截图或官方素材。 - 语音——
assets/voice/下的占位音由tools/gen_voice.py纯程序合成,不含任何第三方录音。
娘口三三 / 猫咪老师 / ニャンコ先生 是《夏目友人帐》的角色,本项目是粉丝性质的非官方作品,与原作的著作权人、发行方没有任何隶属关系,也未获得其授权或背书。 角色形象属于原作者与相关权利方。如果你是在原作的著作权人,并对本仓库中的 AI 生成形象有异议,请提 issue,我们会处理。
非代码文件的完整来源说明见 THIRD_PARTY_ASSETS.md。
开发
资源流水线
所有美术和语音资源都是脚本产物,顺序如下:
# 0. (可选)先做风格选型:同一段角色描述喂给 6 个图像模型,结果落在 work/candidates/
python tools/candidates.py
# 1. 参考视图与动作表:每个动作一张 2x2 雪碧图,落在 work/views、work/sheets
python tools/gen_art.py sheet
python tools/gen_art.py actions # 也可以只做几个:python tools/gen_art.py actions idle walk
# 2. 抠像、对齐、编码 WebM、生成预览 GIF
python tools/build_assets.py all # 或 frames / encode / preview 单步跑
# 3. 静态自检:客户端 bundle 能否解析、manifest 指向的文件是否存在、
# 客户端声明的每个动作是否都有对应资源、宿主端能否 import
node tools/check-package.mjs
tools/build-all.cmd 把上面除选型之外的全部步骤串了一遍,Windows 上双击即可。
npm 脚本是更短的别名:
| 命令 | 等价于 |
|---|---|
npm run art:views |
python tools/gen_art.py sheet |
npm run art:actions |
python tools/gen_art.py actions |
npm run art:build |
python tools/build_assets.py all |
npm run voice:record |
pwsh -File tools/record-voice.ps1 |
npm run check |
node tools/check-package.mjs |
图像阶段需要 OfoxAI 凭据
gen_art.py 和 candidates.py 会真的调用图像生成接口,因此需要一个 OfoxAI API key。
tools/ofox.py 不从环境变量、命令行或配置文件里读这个密钥,而是从 DSH 的凭据库读取:$DSH_HOME/.credentials.yaml(Windows 默认 %USERPROFILE%\.dsh\.credentials.yaml)里的 refs.OFOX_API_KEY 字段。密钥因此不会被写进任何文件、命令行或日志。
refs:
OFOX_API_KEY: "你的密钥"
缺少 key 时脚本会直接退出并打印凭据库路径。像素阶段的 build_assets.py 是本地的(Pillow + numpy + ffmpeg),不需要网络也不需要 key;ffmpeg 需要能在 PATH 上找到,或在 D:\ffmpeg-*\bin\ffmpeg.exe 下。
环境要求
- Node.js
^22.19.0 || >=24.0.0(见package.json的engines) - Python 3.10+,仅资源流水线需要:
numpy、Pillow、requests、PyYAML - ffmpeg / ffprobe,仅编码与录音需要
浏览器端没有构建步骤——lib/client.js 是手写的 __ModuleLoader__ bundle,纯 DOM 无框架,这样它可被单文件审阅,也不会因为 GUI 的 React 树改结构而失效。
目录结构
dsh-nyanko-sensei/
├── package.json # 包清单:exports、dsh.bundle.patch、dsh.client
├── cordis.patch.yml # bundle 补丁层:往 web profile 里插一行插件
├── lib/
│ ├── index.js # 宿主端:媒体路由 + manifest(需要 webServer 服务)
│ └── client.js # 浏览器端:桌宠本体(动画机、交互、语音、自主行为)
├── assets/
│ ├── anims/ # 动作资源:VP9-alpha WebM(360x360),由流水线生成
│ └── voice/ # 7 个合成占位 MP3,用户可用自己的音频覆盖
├── tools/
│ ├── ofox.py # OfoxAI 中继客户端,密钥取自 DSH 凭据库
│ ├── candidates.py # 多模型风格选型
│ ├── gen_art.py # 第 1 阶段:提示词 → 2x2 动作雪碧图
│ ├── build_assets.py # 第 2 阶段:抠像对齐 → 帧 → WebM → 预览 GIF
│ ├── gen_voice.py # 合成占位语音
│ ├── record-voice.ps1 # 麦克风录音 + 修剪 + 归一化
│ ├── check-package.mjs # 静态自检:bundle 能否解析、资源是否齐全
│ ├── verify-browser.mjs # 端到端验证:用 CDP 驱动 Edge 实测桌宠
│ └── build-all.cmd # 一把梭跑完整条流水线
├── docs/
│ ├── screenshot.png # 运行时截图(由 verify-browser.mjs 自动产出)
│ └── preview/ # 预览 GIF 输出目录(由 art:build 生成)
├── work/ # 中间产物(雪碧图、帧、选型结果),可随时删除
├── README.md # 中文说明(本文件)
├── README.en.md # English README
├── LICENSE # MIT + 媒体资源说明
└── THIRD_PARTY_ASSETS.md # 非代码文件的来源与再分发约束
用户侧的文件都在 DSH 家里,不在包内:
%USERPROFILE%\.dsh\dsh-nyanko-sensei\voice\<语音包>\<clip id>.<扩展名>
常见问题
装完看不到宠物
- 确认执行过
dsh web重启——插件行是进程启动时装载的,只刷新页面不够。 - 打开浏览器控制台,找
[dsh-nyanko-sensei]开头的日志。没有pet awake at ...说明浏览器端 bundle 没被加载。 - 检查是不是被自己隐藏了:右键点击宠物原本所在的位置,或清掉
localStorage里的dsh-nyanko-sensei:settings后刷新。 - 宠物容器是个
position: fixed的全屏覆盖层,z-index为 2147483000。如果页面里挂着更高层级的浮层,猫会被压在下面。
点击没声音
这是预期行为,不是 bug——插件只带合成占位音,不含原版配音。
- 先确认设置面板里「启用语音」是开的、音量不是 0。
- 打开设置面板,看底部提示:它会列出当前发现的语音包和 clip 数量。只要包里自带的
assets/voice/*.mp3被识别到,这里至少会显示default(7)。如果显示「未发现任何语音文件」,说明宿主端的媒体根没有读到包内资源,检查插件是否装全了。 - 按语音一节把
natsume.mp3放到%USERPROFILE%\.dsh\dsh-nyanko-sensei\voice\下,然后刷新页面。 - 浏览器的自动播放策略会拦截没有用户交互的音频播放——点击本身是用户交互,正常不受影响,但如果整页刚加载你就用脚本触发点击,可能被拦。控制台会有相应提示。
- 右键菜单里「呼唤「なつめ」」是灰的,同样表示
natsume这个 clip 没找到。
有些动作看不到
assets/anims/ 目前只构建了 idle、walk、sit 三个动作(见功能预览)。其余 9 个动作的提示词已经写在 tools/gen_art.py 里,但资源还没生成——客户端发现文件不存在时会跳过它并退回 idle,所以你不会看到报错,只是猫的动作变少了。补齐方式:
python tools/gen_art.py actions
python tools/build_assets.py all
宠物跑到屏幕外了
正常拖拽时横向允许超出约 30% 的身位,纵向不允许超出。松手后惯性会把它推回边界内并弹一下。
如果它彻底不见了:
- 缩放一下窗口——
resize监听会把位置夹回可视区域。 - 右键点击屏幕角落碰运气,或者直接在浏览器控制台执行:
然后刷新页面,它会回到默认的右下角。localStorage.removeItem("dsh-nyanko-sensei:settings")
动画不透明/有黑底(Safari)
动画资源是 VP9 编码、Alpha 通道存在独立平面里的 WebM。Safari 至今不能解码 VP9 的 alpha 平面,结果就是视频能播但透明通道丢失——猫成了一个带黑底或绿底的方块。
这不是播放逻辑的问题,是编解码器的能力边界。可行的做法:
- 用 Chrome / Edge / Firefox 打开 DSH 的 Web 界面;
- 或者把
assets/anims/*.webm换成带 alpha 的 HEVC/MP4(lib/client.js的animUrl()目前固定请求.webm,需要相应改一行)。
宠物消失了但设置还在
设置存在 localStorage,卸载插件不会清除它。重新装回来后,大小、位置、语音包等会沿用上次的值。
本项目与 DeepSeek 官方无隶属关系,是面向 DeepSeek Harness 的社区开源插件。
No comments yet. Be the first to write one.