桌宠卧室 · dsh-pet-bedroom
给 dsh-pet(DSH 的桌宠插件)盖一间它自己的卧室: 一个可以独立打开的网页。房间图就是整间屋子,桌宠在里面走动、躺床、坐电脑前、 给植物喷水;桌上放着它真实的日记与读书笔记;手机里是真实的 QQ 聊天记录; 还能切换时辰、天气、壁纸,自己添置家具。
它不改 dsh-pet 一行代码,只是复用 dsh-pet 已经对外开放的素材与动画池。
本仓库是脱敏的通用版本:不含任何作者的个人路径、QQ 号、聊天内容或私人服务地址。 所有和个人环境相关的接入点都改成了配置项(见 配置项)。
目录
- 这是什么
- 先看效果
- 名词表
- 前置条件
- 下载与安装
- 快速开始
- 配置项
- ⚠️ 两块需要你自己接入的能力
- 本项目引用了 dsh-pet 的哪些功能
- 页面与接口清单
- 目录结构
- 自定义
- 本地验证
- 安全与边界
- 常见问题
- 收录与市场
- 版本与演进
- 许可与贡献
这是什么
一句话:把桌宠放进一间可以用浏览器打开的卧室,房间里的东西会跟着你的真实数据变。
它能做的事:
| 能力 | 说明 |
|---|---|
| 独立网页 | http://<你的 DSH 地址>/dsh-pet-bedroom/,不是 DSH 应用里的浮层,关掉不影响主界面 |
| 桌宠走动 | 点床、电脑桌、椅子、书架、落地窗、龟背竹、地毯,它会走过去做对应动作 |
| 真实动画 | 动作素材全部来自 dsh-pet 自己的动画池,不写死动画名,你在 dsh-pet 里换皮/加动画它自动跟着变 |
| 桌上日记 | 读聊天服务(默认 Aurabot)每天自己写的那篇,按天聚合,只读、不代写 |
| 桌上笔记 | 读你记忆库里的 Markdown 读书笔记;agent 也能通过工具写新笔记 |
| 手机 QQ | 手机界面里是真实的 QQ 会话与聊天记录,有真实头像;群聊标出发言人 |
| QQ 连接窗口 | 右上角「QQ 连接」:填 SnowLuma 的 HTTP API 地址/令牌/主人 QQ,真连一次再保存;消息上报真的进手机 |
| QQ 命令 | 主人在 QQ 私聊里说「睡觉 / 刷抖音 / 去电脑桌 / 看窗外」,桌宠在卧室里照做 |
| 刷抖音 | 卧室里的「现在刷一轮」打的是你抖音后端的接口,拒绝就如实说理由,绝不假装在刷 |
| 时辰天气 | 清晨/中午/下午/晚间 × 晴天/雨/雪,可跟着真实时间自动走 |
| 家具与壁纸 | 往房间槽位里加/删家具(会真的出现在房间里),手机壁纸可换 |
| agent 感知 | 注册 6 个工具,让 agent 能读房间状态、读文档、写笔记、调天气、摆家具、换壁纸 |
它不是:不是聊天机器人,不是抖音自动机,不是 QQ 客户端。 QQ 与抖音是可选的外部能力,需要你自己接(见下文)。



先看效果
打开后你会看到:
- 一张 1536×1024 的房间图(
assets/room.png),所有交互都叠在图上; - 右下角是桌宠,素材来自 dsh-pet,尺寸按房间比例恒定;
- 中间偏下是一台手机:锁屏、桌面、图标、dock、QQ 聊天界面、拨号盘;
- 桌上摊着日记和读书笔记,点开是全屏阅读器;
- 顶部控制条:时辰、天气、自动、现在刷一轮、别刷了、离开卧室。
名词表
给第一次接触这套东西的人:
| 名词 | 是什么 |
|---|---|
| DSH(DeepSeek Harness) | 一个在本地运行的 AI agent 桌面应用,插件都跑在它里面 |
| 插件 / plugin | 给 DSH 增加功能的包,有「宿主半侧」(Node 进程里跑的代码)和「浏览器半侧」(页面里跑的代码) |
| profile | DSH 的一套配置目录(通常在 ~/.dsh/profiles/<名字>/),里面的 package.json 决定装哪些插件 |
| Cordis | DSH 的插件运行时;ctx.get('xxx') 用来取别的插件暴露的服务 |
| dsh-pet | DSH 的桌宠插件,负责宠物素材、动画池、桌面显示 |
| Aurabot | 一个 QQ 机器人插件(dsh-aurabot-qq),经 SnowLuma / OneBot 接 QQ。本项目的 QQ 与抖音默认适配它,但不是必需 |
| QQ 后端 / 抖音后端 | 你自己实现的、提供聊天流水与刷抖音动作的服务。本项目只按约定读它、调它 |
前置条件
- Node.js ≥ 20(本项目用了
fetch、AbortSignal.timeout、import.meta.dirname等)。 - DSH 已经能正常运行,并且知道你的 profile 目录在哪。
- dsh-pet 已安装并在设置里打开显示(卧室要读它的宠物列表与动画素材)。
- 没有 dsh-pet 时,卧室页面本身还能打开,但看不到桌宠。
- (可选)QQ 接入:Aurabot 插件,或你自己实现的服务(见 QQ 部分)。
- (可选)抖音接入:你自己的刷抖音 HTTP 后端(见 抖音部分)。
下载与安装
每个版本都在 GitHub Releases 提供预构建包, 不用装依赖、不用构建、不用改任何源码:
| 版本 | 内容 |
|---|---|
| v2.3.5 | 带一键试用脚本 start-trial.ps1;自检条数改为自统计,不再手写 |
| v2.3.4 | 修「失败也报成功」:/qq/test、/qq/config 的 ok 被状态对象覆盖 |
| v2.3.3 | 验证脚本随包发布:从 Release 装下来也能直接跑 README 里的每条命令 |
| v2.3.2 | 自检条数与测试套数改为与实际一致 |
| v2.3.1 | Release 说明补上版本正文;release-notes.mjs 接进发版流程 |
| v2.3.0 | 按收录指南优化条目:分类改 fun、描述可核对、新增 test-entry.cjs |
| v2.2.2 | 修复发版工作流:先建 dist 再打包,推 tag 即可自动发版 |
| v2.2.1 | 修正 latest 指向:发版时显式标记最新,旧版不再抢 latest |
| v2.2.0 | 发布形态整理:Releases 资产、截图、release.mjs、发版工作流 |
| v2.1.0 | 新增「QQ 连接」窗口(默认 SnowLuma / OneBot 11)与 /qq/event 上报 |
| v2.0.0 | 首个脱敏通用版(MIT) |
最新一版(链接永远有效):
https://github.com/yiy367389-del/dsh-pet-bedroom/releases/latest/download/dsh-pet-bedroom.tgz
一行安装:
dsh plugin add https://github.com/yiy367389-del/dsh-pet-bedroom/releases/download/v2.3.5/dsh-pet-bedroom.tgz
想固定某个版本就把 URL 里的 v2.3.5 换成对应 tag;想始终跟最新就把整段换成
releases/latest/download/dsh-pet-bedroom.tgz。
从源码安装(改代码时用):
git clone https://github.com/yiy367389-del/dsh-pet-bedroom.git
dsh plugin add ./dsh-pet-bedroom
维护者发版:
node release.mjs --tag vX.Y.Z会从该 tag 打包并上传资产; 推v*tag 时.github/workflows/release.yml也会自动发一版。资产文件名固定dsh-pet-bedroom.tgz,所以latest/download/不会因为版本号变更而 404。 Release 创建时显式make_latest,批量补发旧版也不会把latest抢走。
快速开始
第 1 步:把项目放进 profile
把整个仓库目录放到你喜欢的路径(例如 D:/plugins/dsh-pet-bedroom),
然后在你的 profile 的 package.json 里加一条 link 依赖 和一条 bundle:
{
"dependencies": {
"dsh-pet-bedroom": "link:D:/plugins/dsh-pet-bedroom"
},
"dsh": {
"profile": {
"bundles": [
"@deepseek-ai/dsh-base",
"@deepseek-ai/dsh-web-app",
"dsh-pet",
"dsh-pet-bedroom"
]
}
}
}
dsh-pet-bedroom必须排在dsh-pet之后:卧室启动时会去读 dsh-pet 的宠物列表。 顺序不对也能起来(会稍后自动重试),但第一次打开可能暂时没有宠物。
第 2 步:确认插件被加载
cordis.patch.yml 已经写好插入项,正常情况下不需要改:
- insert:
- id: dsh-pet-bedroom
name: 'dsh-pet-bedroom'
如果它没有自动出现,检查 profile 的 package.json 里 dsh.profile.bundles 是否包含
dsh-pet-bedroom,以及依赖是否真的安装成功(node_modules/dsh-pet-bedroom 是否指向你的目录)。
第 3 步:重启 DSH,打开卧室
重启后:
- 侧边栏底部会出现一个 「卧室」 按钮,点它同窗口跳到卧室;
- 也可以直接访问
http://<你的 DSH 地址>/dsh-pet-bedroom/。
宿主半侧的改动(index.js、cordis.patch.yml)需要重启 DSH 生效;
web/index.html 的改动刷新页面即可。
配置项
全部配置都是可选的,不填就用默认值。默认值刻意保持通用,不指向任何人的私人目录。
在 cordis.patch.yml 里这样写:
- insert:
- id: dsh-pet-bedroom
name: 'dsh-pet-bedroom'
config:
petRoute: '/dsh-pet-7340'
petId: ''
vaultDir: '/绝对路径/memory-vault'
notesDir: 'notes'
chatService: 'aurabot'
douyinApiBase: 'http://127.0.0.1:8080/api'
douyinEnabled: true
allowAgentWrite: true
| 配置项 | 默认值 | 含义 |
|---|---|---|
petRoute |
/dsh-pet-7340 |
dsh-pet 的 HTTP 路由前缀。你的 dsh-pet 路由不一样时改这里 |
petId |
空(取第一只) | 指定哪只宠物进卧室,值来自 dsh-pet 的宠物 id |
vaultDir |
$DSH_HOME/memory-vault |
记忆库根目录,读书笔记在里面找 |
notesDir |
notes |
笔记目录,相对 vaultDir 或绝对路径 |
chatService |
aurabot |
聊天服务名,用 ctx.get(名字) 取。接自己的 QQ 实现时改成你的服务名 |
chatDataDir |
$DSH_HOME/plugins-data/<chatService> |
聊天数据目录(diary.json、chatlog/) |
chatApiBase |
/<chatService>/api |
聊天服务的 HTTP 前缀(抖音默认跟随它) |
douyinApiBase |
空(跟随 chatApiBase) |
抖音后端基地址,可填完整 URL(支持别的端口/域名) |
douyinEnabled |
true |
是否允许卧室触发真实刷抖音 |
allowAgentWrite |
true |
是否允许 agent 通过工具写笔记、改天气 |
roomFile |
$DSH_HOME/dsh-pet-bedroom/room.json |
房间状态(时辰/天气/壁纸/家具)的持久化文件 |
qqApiBase |
http://127.0.0.1:3001 |
QQ 连接窗口的默认 OneBot HTTP 地址(页面保存的值优先) |
qqToken |
空 | OneBot HTTP 令牌;只落本机,不回显 |
qqSelfId |
空 | 主人 QQ 号,只有它的私聊进卧室 |
qqFile |
$DSH_HOME/dsh-pet-bedroom/qq.json |
QQ 连接配置的持久化文件(权限 600) |
对应的环境变量(优先级:显式配置 > 环境变量 > 默认值):
| 环境变量 | 作用 |
|---|---|
BEDROOM_VAULT_DIR |
记忆库根目录 |
BEDROOM_CHAT_DIR |
聊天数据目录(优先于 AURABOT_DATA_DIR) |
AURABOT_DATA_DIR |
兼容旧名:Aurabot 的数据目录 |
BEDROOM_PET_ROUTE |
dsh-pet 路由前缀 |
BEDROOM_QQ_API |
QQ 直连的 OneBot HTTP 地址(窗口未保存时的默认值) |
BEDROOM_QQ_TOKEN |
QQ 直连的访问令牌 |
BEDROOM_QQ_SELF_ID |
主人 QQ 号 |
DSH_HOME |
DSH 配置根目录(默认 ~/.dsh) |
路径里的
~不会被自动展开,请写绝对路径(Windows 如D:\memory\vault, macOS/Linux 如/Users/me/vault)。
⚠️ 两块需要你自己接入的能力
这两块不是插件自带的,每个人都不一样。 仓库里只保留「约定」和「默认适配 Aurabot」, 你需要按自己的环境接一个实现,或者干脆不接。
QQ 有两条路:不想写代码就直接用页面右上角的「QQ 连接」窗口(默认 SnowLuma,见方案一); 已经在用 Aurabot 就什么都不用改(见方案二);也可以自己实现一个聊天服务(方案三)。 抖音只有一个接口约定,需要你后端实现(见下文)。
不接也能用:房间、桌宠、日记/笔记、时辰天气、家具、壁纸都正常, 只是手机 QQ 显示「还没有聊天记录」、抖音按钮如实说「没接上」。
QQ 部分:你需要接入自己的
卧室不直接连 QQ,它只认一个 DSH 服务(Cordis service)。默认服务名 aurabot。
约定 A:服务与事件
你的插件在宿主半侧这样做:
ctx.provide('你的服务名', service); // 然后用配置 chatService: '你的服务名' 指过来
service 需要提供:
{
on(event: 'activity', handler): () => void, // 返回取消订阅函数
hub?: {
groupNames?: Map<string, string>, // 群号 → 群名(可选,用于内存缓存)
groupName?: (gid: string) => Promise<string>,// 查群名(可选,查不到就退回「群 <号>」)
},
}
activity 事件里,卧室关心的字段:
{
kind: 'in', // 'in' = 收到对方消息;她发出去的不算
owner: true, // 是不是「主人本人」。只认 true
direct: true, // 是不是私聊。群聊是 false
text: '睡觉', // 消息正文,会先过一遍命令词表
chatId: 'private:10001', // 形状 private:<号> / group:<群号>
}
只要满足 kind === 'in' && owner === true && direct === true,这条消息就会:
- 进卧室的「手机」未读消息;
- 命中命令词(睡觉/起床/刷抖音/去电脑桌/看手机/看窗外/自由活动/别刷了)时, 让桌宠在卧室里真的去做。
为什么用
owner/direct两个布尔标记,而不是解析消息前缀? 靠正则扒文本很容易因为一句文案改动整体失效;结构化字段稳得多。
约定 B:聊天流水(手机里的聊天界面)
手机里显示的是真实聊天记录,来自这个目录:
<chatDataDir>/chatlog/YYYY-MM-DD.jsonl
每行一个 JSON:
{"t": 1730000000000, "chatId": "private:10001", "who": "小明", "uid": "10001", "text": "在吗"}
{"t": 1730000060000, "chatId": "private:10001", "who": "bot", "text": "在的"}
{"t": 1730000120000, "chatId": "group:20002", "who": "小红", "uid": "10002", "text": "刷抖音"}
who: 'bot'表示是她自己发的,其余是对方昵称;uid是发言人 QQ 号,用来取头像(可空);chatId必须是private:<数字>或group:<数字>,否则卧室拒绝显示。
约定 C:日记(桌上那篇)
<chatDataDir>/diary.json
{
"plan": { "...": "随便" },
"history": [
{ "at": 1730000000000, "text": "今天把房间换成了这张图。", "ok": true }
]
}
卧室按天聚合:同一天多篇取最后一篇当主日记,其余记进 count;
点开能看到当天所有篇目(带时间小标题)。只读,不代写。
方案一:直接用页面上的「QQ 连接」窗口(默认 SnowLuma)
不想写代码就用这个。 卧室页面右上角有一个「QQ 连接」按钮,点开就是配置窗口: 不用装 Aurabot,也不用改插件配置,填三个值、点两下就好。
窗口里要填的:
| 字段 | 填什么 | 默认 |
|---|---|---|
| 后端 | 固定 SnowLuma(OneBot 11 协议;NapCat 等其他 OneBot 11 后端同样适用) | SnowLuma |
| HTTP API 地址 | SnowLuma 面板「HTTP API」里显示的地址 | http://127.0.0.1:3001 |
| 访问令牌 | 你在 SnowLuma 里设的令牌;没设就留空 | 空 |
| 主人 QQ | 你自己的 QQ 号。只有这个号发来的私聊会进卧室 | 空 |
| 消息上报地址 | 窗口自动按当前页面地址算好,点「复制」 | <你的卧室地址>/qq/event |
在 SnowLuma 里的三步:
- 打开面板 →「HTTP API」:记下端口和令牌,与窗口里的地址/令牌保持一致;
- 「HTTP 上报」:地址填窗口里那条(结尾是
/qq/event),令牌同上, 消息类型至少勾上「私聊消息」;群聊消息可以一并上报,卧室会自己过滤掉; - 回到卧室点「测试连接」——能看到机器人昵称和好友数就说明 HTTP API 通了; 再点「保存」,配置立即生效(不用重启)。
保存后会发生什么(都是真的,不是演示):
- 主人私聊里说「睡觉 / 起床 / 刷抖音 / 去电脑桌 / 看手机 / 看窗外 / 自由活动 / 别刷了」, 桌宠在卧室里真的照做;
- 每条主人私聊都会进「手机」的未读消息,同时追加一份流水到
$DSH_HOME/dsh-pet-bedroom/qq-log/YYYY-MM-DD.jsonl,手机聊天列表就能看到; - 别人私聊、群聊、图片/表情会被过滤(窗口状态行会显示「过滤 N」)。
安全与边界:
- 令牌只存在本机
$DSH_HOME/dsh-pet-bedroom/qq.json(权限 600),页面只显示 「已设置 / 未设置」,永远不回显令牌; - 上报接口会校验令牌(
Authorization: Bearer <令牌>,也接受?access_token=), 不匹配直接 401; - 卧室不登录 QQ、不保存 QQ 密码;QQ 登录态完全由 SnowLuma 自己管;
- 同一条消息被 Aurabot 与直连同时上报时会按
message_id去重,不会进两次。
这些值也可以写进插件配置(
qqApiBase/qqToken/qqSelfId)当默认值, 或用环境变量BEDROOM_QQ_API/BEDROOM_QQ_TOKEN/BEDROOM_QQ_SELF_ID; 页面里保存的值优先。
方案二:用现成的 Aurabot
如果你已经装了 dsh-aurabot-qq(Aurabot,经 SnowLuma / OneBot 接 QQ),
它正好满足上面全部约定,服务名就是 aurabot,什么都不用改。
方案三:自己实现(最小示意)
// 你自己的 QQ 插件(宿主半侧)
export const name = 'my-qq';
export function apply(ctx, config = {}) {
const listeners = new Set();
const service = {
on(event, fn) {
if (event !== 'activity') return () => {};
listeners.add(fn);
return () => listeners.delete(fn);
},
};
ctx.provide('my-qq', service);
// 你的 QQ 网关回调里:把收到的消息转成 activity
function onIncoming(msg) {
const activity = {
kind: 'in',
owner: msg.from === config.ownerQq, // 只有主人本人
direct: msg.type === 'private', // 只有私聊
text: msg.text,
chatId: (msg.type === 'private' ? 'private:' : 'group:') + msg.chatId,
};
// 同时追加聊天流水(约定 B)
appendChatlog(activity, msg.nickname, msg.uid);
for (const fn of listeners) fn(activity);
}
}
然后在卧室配置里写 chatService: 'my-qq'、chatDataDir: '你写 chatlog 的目录'。
不要把凭据、QQ 密码、令牌写进这个仓库或配置示例里。 本项目不读取、不保存任何凭据。
抖音部分:你需要接入自己的
卧室不自己开浏览器、不登录抖音。它只调你后端的两个 HTTP 接口。
接口契约
GET <douyinApiBase>/douyin/state
→ { "ok": true, "busy": false, "watched": 12,
"pauseReason": "", "lastSkipReason": "", "history": [] }
POST <douyinApiBase>/douyin/run body: {}
→ { "ok": true, "message": "刚开始刷,这一轮看 3 个左右" }
→ { "ok": false, "message": "今天额度用完了" } ← 拒绝理由会原样显示
douyinApiBase默认跟随聊天服务(默认即 Aurabot 的/aurabot/api), 也可以指向完全不同的地址,例如http://127.0.0.1:8080/api。- 约定
run是后台开始、马上返回(一轮要几分钟); 卧室只负责说「开刷了」,进度从state读。 - 约定没有停止接口:卧室的「别刷了」会如实返回
ok: false并说明停不了, 不会假装按停。如果你自己实现了停止接口,可以在此基础上扩展。
最小后端示意(Node,无依赖)
import http from 'node:http';
let busy = false, watched = 0, pauseReason = '';
http.createServer(async (req, res) => {
const send = (obj) => {
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify(obj));
};
if (req.url === '/api/douyin/state') {
return send({ ok: true, busy, watched, pauseReason, lastSkipReason: '', history: [] });
}
if (req.url === '/api/douyin/run' && req.method === 'POST') {
if (busy) return send({ ok: false, message: '正在刷,等这一轮结束' });
busy = true;
// 这里换成你自己的刷抖音逻辑(浏览器自动化 / 别的服务)
setTimeout(() => { busy = false; watched += 3; }, 60_000);
return send({ ok: true, message: '开刷了,这一轮约 1 分钟' });
}
res.writeHead(404).end();
}).listen(8080);
然后配置:
config:
douyinApiBase: 'http://127.0.0.1:8080/api'
默认适配:Aurabot 的「现在刷一轮」
Aurabot 的刷抖音模块正好提供了这两个接口(/aurabot/api/douyin/run|state),
所以默认配置下不用额外写后端;没有 Aurabot 时按钮会如实说没接上。
本项目引用了 dsh-pet 的哪些功能
明确声明:本项目不改 dsh-pet 一行代码,也不读写它的配置文件或数据库。 它只使用 dsh-pet 已经对外开着的 HTTP 能力与素材:
| 引用的 dsh-pet 能力 | 用在哪 | 具体怎么用 |
|---|---|---|
GET <petRoute>/config |
选宠物、取动画池 | 读宠物条目:pets[](id/name/素材根)、animations(idle、turn、drag、clicks、moves.actions、categories[].actions、events),拼成完整动画池 |
GET <petRoute>/thumb/<素材根>/<动画名>.webm |
卧室里所有桌宠画面 | 经本插件 /dsh-pet-bedroom/pet/thumb/... 代理转发(独立页面不能直接请求宿主路由);只放行 <根>/<文件>.webm|mov 两段,拒绝目录穿越 |
宠物 id 与素材根 |
petId 配置 |
默认取列表第一只;填了 petId 就挑那只 |
动画 待机呼吸休闲(idle 优先) |
默认站立 | 从 animations.idle 取,取不到再从池里按「待机 / 呼吸」找 |
| 动画 小憩 / 沉眠 / 睡觉 / 哈欠 | 躺床睡觉 | 按关键词在池里挑,没有就退回待机 |
| 动画 写代码 / 敲击桌面 / 轻快记录 | 坐电脑前 | 同上 |
| 动画 对屏 / 碎碎念 / 东张西望 | 拿手机看 | 同上 |
| 动画 东张西望 / 伸懒腰 / 摇扇 | 趴窗台看外面 | 同上 |
| 动画 玩水枪(优先)/ 浇水 / 浇花 | 给龟背竹喷水 | 池里没有「玩水枪」才退到别的 |
| 动画 360 / 旋转展示 | 坐电脑前的「背面」 | 播到约 2.3s 就地暂停,得到真背面(不翻转、不裁腿) |
| 动画 点击回应 / 开心 / 害羞 | 点桌宠 | 随机回应 |
| 动画 螃蟹走路 / 漂浮踏步 / 奔跑 | 自由走动 | 同上 |
| 素材尺寸约定 | 角色大小恒定 | 素材是 640×360 透明画布,角色实体占宽 33.8%、高 16.7%~91.7%;所以容器宽度固定 41.4%,换算后角色约占舞台 14% |
| 整个动画池 | 右键「动作演示」 | 把 config 里的全部动画名拿来随机演示(例如 106 条) |
不引用的部分:dsh-pet 的桌面窗口、拖拽、设置页、存储、皮肤包、内部状态, 本项目都不碰。dsh-pet 升级换皮后,卧室会自动跟着变(因为不写死动画名)。
如果你换了一只宠物,只要它的素材仍遵循上面的画布约定,卧室不用改代码; 例外情况见 自定义 里的尺寸换算。
页面与接口清单
HTTP 路由(都在 /dsh-pet-bedroom 前缀下,只接受本机请求)
| 方法 | 路径 | 作用 |
|---|---|---|
| GET | / |
卧室页面 |
| GET | /room.png、/chair.png |
房间图、椅子前景层 |
| GET | /wallpaper/<id>.jpg |
手机壁纸(白名单 4 张) |
| GET | /avatar/<QQ号> |
个人头像代理(q1.qlogo.cn,只放行数字号) |
| GET | /groupavatar/<群号> |
群头像代理(p.qlogo.cn) |
| GET | /pet/thumb/<根>/<文件> |
dsh-pet 素材代理 |
| GET | /state |
房间完整快照(网页每 2 秒轮询) |
| GET | /doc?kind=diary|notes&name=... |
读日记全文 / 笔记全文 |
| GET | /phone/chat?id=... |
读某个会话的消息 |
| POST | /room |
改时辰 / 天气 / 壁纸 / 自动时间 |
| POST | /slot |
加/删家具 |
| POST | /command |
面板按钮/输入框 → 执行命令 |
| POST | /phone/read、/phone/off、/phone/say |
手机已读/灭屏/说一句 |
| POST | /douyin/start、/douyin/stop |
刷抖音(转给你后端) |
| GET | /qq |
QQ 连接状态(地址/主人号/是否已设令牌/最近测试结果;不含令牌) |
| POST | /qq/config |
保存 QQ 连接配置(写 qq.json,立即生效) |
| POST | /qq/test |
真调一次 OneBot get_login_info + get_friend_list |
| POST | /qq/event |
OneBot 11 HTTP 上报入口(校验令牌、按 message_id 去重、只收主人私聊) |
给 agent 的工具
| 工具 | 作用 |
|---|---|
bedroom_status |
看房间现在什么样:时辰、天气、壁纸、桌上文档、未读消息、抖音状态 |
bedroom_read |
读日记(按日期)或读书笔记(按标题),或只列清单 |
bedroom_write |
写读书笔记(追加/替换)。日记不能代写,那是聊天服务自己写的 |
bedroom_atmosphere |
换天气(晴天/雨/雪)、换时辰(清晨/中午/下午/晚间)、开关自动时间 |
bedroom_furniture |
往房间槽位加/删家具(带介绍,鼠标移上去能看到) |
bedroom_wallpaper |
列手机壁纸 / 换壁纸 |
目录结构
dsh-pet-bedroom/
├─ index.js 宿主半侧:HTTP 路由、读日记/笔记/聊天流水、工具、QQ 事件
├─ client.js 浏览器半侧:只在侧边栏加一个「卧室」按钮(不挂浮层)
├─ cordis.patch.yml 插件插入项(id / name)
├─ package.json 包信息、dsh.bundle 清单、exports
├─ screenshots.json 市场截图清单(2 张,路径相对本文件)
├─ web/index.html 卧室页面(单文件:HTML + CSS + JS)
├─ assets/
│ ├─ room.png 房间图(1536×1024)
│ ├─ chair.png 椅子前景层(坐电脑前挡住后背)
│ ├─ icon-180.png 市场卡片图标(180×180 位图)
│ ├─ screenshot-room.jpg 卧室截图(房间 + 桌宠)
│ ├─ screenshot-qq.jpg 「QQ 连接」窗口截图
│ └─ wall/*.jpg 手机壁纸(4 张)
├─ .github/
│ ├─ workflows/release.yml 推 v* tag 自动发 Release
│ └─ awesome-plugin-entry.yml 收录条目草稿(含 icon/tarball)
├─ release.mjs 发版:从 tag 打包 + 建 Release + 上传资产
├─ test-entry.cjs 收录条目自检(对着 contributing.md 的 CI 检查项)
├─ locale/zh.json|en.json 文案
├─ audit.cjs 静态自检(138 条:尺寸、姿态、路由、配置项、QQ 窗口……)
├─ smoke.mjs 冒烟:页面/房间图/壁纸/日记/笔记/路径逃逸
├─ test-aurabot.mjs 端到端:QQ 过滤 / 日记聚合(假服务)
├─ test-phone.mjs 端到端:手机会话 / 抖音接口
├─ test-group.mjs 端到端:群聊标题与会话人
├─ test-config.mjs 端到端:通用接口(petRoute/petId/chatService/douyinApiBase)
├─ test-qq.mjs 端到端:QQ 连接窗口(保存/测试连接/上报校验/去重/进手机)
├─ test-qq-fail.mjs 端到端:写盘失败必须如实报 ok:false(不许假成功)
├─ preview.mjs 独立预览服务器(不重启 DSH 验证页面改动)
├─ start-trial.ps1 一键试用:另起 profile 装 Release 包,不碰日常 profile
├─ v-*.mjs Playwright 可视验证(头像/拨号/群聊/壁纸/圆角)
├─ make-chair.py 从 room.png 抠椅子生成前景层
└─ register-chair.py 配准新椅子图到房间坐标系
自定义
换房间图
- 把新图命名为
assets/room.png(建议同比例 3:2,如 1536×1024); - 重新标定
web/index.html里的SPOTS:x/y/w/h是图上的百分比,anchor是宠物该站的位置(anchor.y越大,人在画面上越靠下); - 全局上下挪动只改
const ANCHOR_LIFT,不要逐个改anchor—— 房间里还有写死的goTo({x,y})(躺床、起床、回中间、自由走动),只改 anchor 会漏; - 改完跑
node audit.cjs。
调整桌宠大小
只改一个数:web/index.html 里的 const widthPct = 41.4;
角色占舞台宽 ≈ widthPct × 0.338(41.4 → 14%,60 → 20.3%)。
换目标值后同步改 audit.cjs 里那条 14% 的断言。
换椅子 / 重新配准
python make-chair.py # 从 room.png 抠出椅子
python register-chair.py # 把新椅子图配准到房间坐标系(需要 scipy/Pillow)
脚本会打印 left/top/width/height 百分比,写进 web/index.html 的 .chair-front。
加家具 / 壁纸 / 命令词
- 家具:用
bedroom_furniture工具或页面右键菜单,会持久化到room.json; - 壁纸:把新图放
assets/wall/,在index.js的WALLPAPERS里加一条(id + 文件); - 命令词:改
index.js的COMMAND_RULES。注意顺序:「别刷了」必须排在「刷抖音」前面, 否则「别刷抖音」会被读成「刷抖音」。
去掉 QQ 或抖音
- 抖音:
douyinEnabled: false; - QQ:不配置
chatService即可(或指向一个不存在的服务名), 手机 QQ 会显示「还没有聊天记录」,其它功能不受影响。
本地验证
下列脚本都在 Release 的预构建包里(package.json 的 files 已包含),
所以从 Release 装下来的目录里也能直接跑,不用先 clone 仓库:
cd <插件目录> # profile 里的 node_modules/dsh-pet-bedroom,或 git clone 下来的目录
node audit.cjs # 138 条静态自检,必须 ALL PASS
node smoke.mjs # 冒烟(临时目录,不碰真实数据)
node test-aurabot.mjs # 假聊天服务:QQ 过滤 + 日记聚合
node test-phone.mjs # 手机会话 + 抖音接口
node test-group.mjs # 群聊标题 + 发言人
node test-config.mjs # 通用接口:自定义 petRoute/chatService/douyinApiBase 全生效
node test-qq.mjs # QQ 连接窗口:保存/测试连接/上报鉴权/去重/进手机
node test-qq-fail.mjs # 失败不许报成功:写盘失败时 /qq/config 必须回 ok:false
node test-entry.cjs # 收录条目:manifest / 分类 / 描述与代码一致性(16 项)
这些脚本只读本目录的文件 + 用临时目录跑假服务,不会改动已安装的插件本身。
audit.cjs/test-entry.cjs要在插件目录内运行(它们按相对路径读文件)。
所有测试都是隔离的:临时目录 + 假服务 + 假 fetch,不会读写你的真实记忆库、
不会真的刷抖音、不会真的发 QQ 消息。
一键起一个独立试用实例(不装进你日常用的 profile):
.\start-trial.ps1 # 默认 49790
.\start-trial.ps1 -Port 49820 # 换端口
它会用 bedroom-trial 这个 profile 起一个只装了 dsh-pet + 本插件的 DSH Web 实例;
启动后终端打印的带 token 地址就是入口,卧室在 /dsh-pet-bedroom/。
改插件源码不影响它(它装的是 Release 包)。
页面预览(不重启 DSH):
node preview.mjs # → http://127.0.0.1:64123/dsh-pet-bedroom/
preview.mjs 会照搬宿主的静态路由与素材代理,并把 /state 转发给真实宿主
(UPSTREAM 可覆盖)。
Playwright 可视验证(可选,需要 playwright-core 和 Chromium):
# 如果 playwright-core 不在当前 node_modules 里,用环境变量指过去
PLAYWRIGHT_CORE=file:///path/to/node_modules/playwright-core/index.mjs \
BEDROOM_URL=http://127.0.0.1:64123/dsh-pet-bedroom/ \
node v-dial.mjs
安全与边界
- 只接受本机请求:所有路由的
Host必须是127.0.0.1/localhost/::1,否则 403。 - 白名单:壁纸只放行 4 个 id;头像只放行纯数字号码;素材只放行
<根>/<文件>.webm|mov。 - 不做目录遍历:
/doc、/wallpaper、/pet/thumb都有校验,../一律拒绝。 - 不碰凭据:本项目不读取、不保存 QQ 密码、Cookie、Token。
- 不假装:抖音拒绝就显示拒绝理由;停不了就说停不了;日记没有就是没有。
- 手机消息是内存态:重启 DSH 即清空。它是「刚才谁找我」,不是聊天档案。
- 卧室不替抖音过登录/验证码:那是你后端的事。
- 桌面模式的透明小窗里没有卧室:卧室是浏览器页面。
常见问题
Q:打开 /dsh-pet-bedroom/ 是 404?
A:插件没加载。检查 profile 的 bundles 是否包含 dsh-pet-bedroom、
node_modules/dsh-pet-bedroom 是否指向正确目录,然后重启 DSH。
Q:页面能打开,但没有桌宠?
A:dsh-pet 没装或没打开显示,或者 petRoute 不对。先确认
http://<DSH>/dsh-pet-7340/config 能返回 JSON;不能就改 petRoute。
Q:桌宠尺寸不对/位置偏了?
A:看 调整桌宠大小。位置偏多半是容器结构变了,
用浏览器量一次脚底 y 的差值,改 ANCHOR_LIFT。
Q:手机里没有聊天记录?
A:QQ 部分没接(见 QQ 部分),
或 chatDataDir 里没有 chatlog/*.jsonl。
Q:点「现在刷一轮」说没接上?
A:douyinApiBase 没配,或后端没启动。先用
curl http://127.0.0.1:8080/api/douyin/state 确认它活着。
Q:日记是空的?
A:日记来自聊天服务(默认 Aurabot)的 diary.json;没接就没有。
本项目不会凭空编一篇日记。
Q:能同时装多只宠物吗?
A:一个卧室实例选一只(petId)。多实例需要不同路由前缀,目前不推荐。
Q:支持 macOS / Linux 吗?
A:代码是跨平台的(路径都用 path 拼)。Windows 是主要验证环境。
收录与市场(awesome-dsh-plugin)
按 awesome-dsh-plugin 贡献指南 逐项对照,本仓库的现状:
| 要求 | 状态 |
|---|---|
package.json 声明 dsh.bundle(含 patch) |
✅ "bundle": { "patch": "./cordis.patch.yml" },且 cordis.patch.yml 在仓库根 |
| 真实可用的代码(非占位/纯 README) | ✅ 138 条静态自检 + 7 套端到端/冒烟测试全部通过 |
| 仓库创建满 1 天 | ⏳ 建于 2026-11-03T11:34Z,2026-11-04T11:34Z 之后达标(CI 自动判定,代码无需改动) |
添加 dsh-plugin topic |
✅ 已添加(另有 desktop-pet、cordis-plugin、deepseek-harness) |
| 描述只讲功能、不夸大 | ✅ 描述里的「6 个工具」用 test-entry.cjs 对着 index.js 数过,QQ/抖音也各有一条断言 |
| 分类贴合实际做的事 | ✅ fun(主体是桌宠在房间活动的观赏体验,QQ/抖音为可选外接) |
| 一个 PR 只加 1 条(上限 3) | ✅ 草稿里只有 1 条 url: |
可选:预构建 tarball |
✅ 每个 Release 都挂 dsh-pet-bedroom.tgz;条目用 latest/download,资产名不带版本号(换版不会 404) |
可选:市场截图 screenshots.json |
✅ 2 张(assets/screenshot-room.jpg、assets/screenshot-qq.jpg) |
| 可选:市场图标 | ✅ assets/icon-180.png(180×180 PNG,GitHub 托管,非 SVG) |
收录条目草稿在 .github/awesome-plugin-entry.yml:
提交到对方仓库时,新增 data/plugins/yiy367389-del__dsh-pet-bedroom.yml,内容即该文件(去掉注释)。
本地先用 node test-entry.cjs 把 CI 会查的 16 项过一遍(含描述与代码对不对得上)。
版本与演进
- v2.3.5:加
start-trial.ps1(一键起一个只装 dsh-pet + 本插件的试用实例, 不碰日常 profile)。脚本必须带 UTF-8 BOM——Windows PowerShell 5.1 会把无 BOM 的 UTF-8 当 ANSI 读,中文注释直接把块结构读崩,报的还是无关行号。audit 新增两条守住这点。 另外把「自检条数」改成运行时自统计(数实际印出的行数),不再靠手写常量, 改条目时不会再出现「代码加了、README 没跟上」。 - v2.3.4:修「失败也报成功」。本地装上后实测发现
/qq/test与/qq/config的ok被尾部铺开的状态对象(自带ok:true)覆盖,端口填错时窗口仍会显示「通了」。 现在判定写在状态之后;顺带把 426/101 翻译成人话(那是 WebSocket 端口,不是 HTTP API)。 新增test-qq-fail.mjs锁住「写盘失败必须 ok:false」。 - v2.3.3:验证脚本随包发布。之前
files只带运行时代码,README 却让人在项目目录跑node audit.cjs——从 Release 装下来的用户照着敲会 file not found。现在audit.cjs、test-*.mjs、test-entry.cjs、smoke.mjs、preview.mjs、v-*.mjs、*.py、release*.mjs与.github/里的条目草稿都随包发出(43 个文件,包体 3.3 MB);audit 新增一条守卫, README 点名要跑的脚本只要没在files里就 FAIL。 - v2.3.2:文档数字与实际对齐:端到端/冒烟套数当时写错(多算了
一个不存在的测试文件)。这类数字现在由
audit.cjs与test-entry.cjs一起守住。 - v2.3.1:每个 Release 的说明补上该版本正文。
release-notes.mjs从CHANGELOG.md取对应小节写进 Release 正文并附安装命令,可重复执行;发版工作流发完自动跑一次。 - v2.3.0:按 awesome-dsh-plugin 指南优化收录条目。
category改fun;描述改成可核对的 写法(「6 个工具」对着index.js数);新增test-entry.cjs把 CI 会查的 16 项在本地跑一遍; audit 增至 132 条。 - v2.2.2:修复发版工作流。
npm pack --pack-destination dist在dist不存在时直接失败, v2.2.0 / v2.2.1 的 Action 都红在这一步;现在先mkdir -p dist,推 tag 由 Action 自动发版。 - v2.2.1:修正
latest。GitHub 的latest按 Release 创建时间算,批量补发旧版后 会指向旧版本;release.mjs现在创建时显式make_latest,并把旧版标为非 latest。 - v2.2.0:按收录指南整理发布形态。每个版本都有 Release 预构建包
(
dsh-pet-bedroom.tgz,文件名不带版本号);新增release.mjs、发版工作流、 市场截图与 180×180 图标;package.json补repository/homepage/bugs/keywords; 已验证dsh plugin add <tgz>会把插件写进 profile 并自动登记到dsh.profile.bundles。 - v2.1.0:新增「QQ 连接」窗口(默认 SnowLuma / OneBot 11):页面里填 HTTP API
地址/令牌/主人 QQ,真连一次再保存;
/qq/event接收 HTTP 上报,校验令牌、按message_id去重、只收主人私聊,消息进手机并追加qq-log流水。 - v2.0.0(本仓库起点):脱敏通用版。引入
petRoute/petId/vaultDir/notesDir/chatService/chatDataDir/chatApiBase/douyinApiBase等配置项, QQ 与抖音改为「按契约接入」;保留 v3 的独立网页架构与全部交互。 - 历史(原作者环境):v1 概念稿 → v2 全屏浮层(已废弃,遮罩会吃掉点击)→ v3 独立网页 + 真实日记/聊天流水 + 手机界面。
仓库从 v2.0.0 开始按 git 提交记录演进,后续改动请写进 CHANGELOG.md。
许可与贡献
- 许可:MIT。可自由使用、修改、分发、商用,保留版权与许可声明即可。
- 贡献:欢迎 issue / PR。提交前请保证
node audit.cjs与全部测试通过; 不要提交任何个人路径、QQ 号、聊天内容、令牌或真实记忆库数据。 - 上游:dsh-pet 的素材与动画版权归 dsh-pet 及其作者所有,本项目只做引用。
致谢
- dsh-pet:桌宠本体、动画池与素材接口。
- DeepSeek Harness:插件运行时与 Web 宿主。
- Aurabot(
dsh-aurabot-qq):本项目的 QQ 与抖音默认适配实现。
No comments yet. Be the first to write one.