桌宠卧室 · dsh-pet-bedroom
给 dsh-pet(DSH 的桌宠插件)盖一间它自己的卧室:
一个可以独立打开的网页。房间图就是整间屋子,桌宠在里面走动、躺床、坐电脑前、
给植物喷水;桌上放着它真实的日记与读书笔记;手机里是真实的 QQ 聊天记录;
还能切换时辰、天气、壁纸,自己添置家具。
它不改 dsh-pet 一行代码,只是复用 dsh-pet 已经对外开放的素材与动画池。
本仓库是脱敏的通用版本:不含任何作者的个人路径、QQ 号、聊天内容或私人服务地址。
所有和个人环境相关的接入点都改成了配置项(见 配置项)。
目录
这是什么
一句话:把桌宠放进一间可以用浏览器打开的卧室,房间里的东西会跟着你的真实数据变。
它能做的事:
| 能力 | 说明 |
|---|
| 独立网页 | 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 与抖音是可选的外部能力,需要你自己接(见下文)。
房间预览
卧室与桌宠
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 |
最新一版(链接永远有效):
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 令牌;只落本机,不回显 |
对应的环境变量(优先级:显式配置 > 环境变量 > 默认值):
| 环境变量 | 作用 |
|---|
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 就地暂停,得到真背面(不翻转、不裁腿) |
| 动画 点击回应 / 开心 / 害羞 | 点桌宠 | 随机回应 |
| 动画 螃蟹走路 / 漂浮踏步 / 奔跑 | 自由走动 | 同上 |
不引用的部分: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 |
给 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 上报,校验令牌、按
去重、只收主人私聊,消息进手机并追加 流水。
仓库从 v2.0.0 开始按 git 提交记录演进,后续改动请写进 CHANGELOG.md。
许可与贡献
- 许可:MIT。可自由使用、修改、分发、商用,保留版权与许可声明即可。
- 贡献:欢迎 issue / PR。提交前请保证
node audit.cjs 与全部测试通过;
不要提交任何个人路径、QQ 号、聊天内容、令牌或真实记忆库数据。
- 上游:dsh-pet 的素材与动画版权归 dsh-pet 及其作者所有,本项目只做引用。
致谢