pet
Verified@dsh-pet/pet · v0.5.0 · MIT · Web UI
Pet-raising companion for DeepSeek Harness in one package: the host half (XP economy from token use, evolution stages, spritesheet validation, /pet/* routes, /pet/events SSE), the browser dock (a dsh.client module), and the self-installing bundle patch (d
Install
dsh plugin add @dsh-pet/pet Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Creators
Readme
@deepseek-ai/dsh-pet
English | 中文
一个可持久化的养成宠物插件,用于交互式 Web UI。宠物以进程内单行形式保存在 storage-domain 的全局槽位中,并随每一步助手回复的 token 消耗量成长:按 assistant/message 事件把 token 求和,换算为经验(XP)与货币,进化阶段由等级驱动。用户可以给宠物命名、上传自定义精灵表单(按进化阶段分行的动画网格),并用挣得的货币购买/装备外观饰品与背景。形象完全由进化阶段决定——v1 刻意没有饥饿/心情/生病状态、没有衰减定时器、也没有能力道具。
本包提供宿主机服务(context.pet)、存储域、经济与精灵表单校验,以及四个 webServer JSON 路由,供浏览器半边消费。它本身没有面向模型的工具、提示词片段或 Session 事件——宠物是用户能在聊天中看到、而模型读不到的持久化侧车数据。
公共请求/值/失败类型从包根与 @deepseek-ai/dsh-pet/types 导出;src/types.ts 是它们的来源。
宠物模型
持久化行 PetRow 保存 name、level、xp、currency、已拥有的外观道具 id、当前装备的饰品/背景,以及提交的精灵表单。xpToNext 与 stage 是派生值而非存储值:xpToNext(level) 按几何分档表查找(1M → 10M → 100M → 1B → 上限),stage = floor(level / evolutionEveryNLevels)。跨过阶段阈值即切换渲染形象。
新装与「重置精灵」都落到内置占位 sprite(defaultPetSpritesheet,单帧 64×64 IDLE),保证桌宠首次启动就能渲染出形象;用户用 POST /pet/upload 提交自己的多形态清单即可替换。
- id: pet
name: '@deepseek-ai/dsh-pet'
config:
defaultName: 'Délì'
xpPerToken: 1
coinsPerToken: 1
maxXpToNextPerLevel: 2000000000
evolutionEveryNLevels: 5
maxImageBytes: 2097152
配置
| key | 含义 | 默认值 |
|---|---|---|
defaultName |
新宠物显示名。 | 'Délì' |
xpPerToken |
token → 经验倍率,向下取整。 | 1 |
coinsPerToken |
token → 货币倍率,向下取整。 | 1 |
maxXpToNextPerLevel |
顶级档位的每级 XP 上限。 | 2_000_000_000 |
evolutionEveryNLevels |
每多少级进一阶;阶段按等级阈值推进。 | 5 |
maxImageBytes |
允许上传的最大图片字节数。 | 2MiB |
经济与成长
服务订阅 session/event,对每条带 usage 的 assistant/message,将互斥的输入、缓存读、缓存写、输出四个 token 桶求和。该和向下取整后计入 xp += floor(tokens × xpPerToken)、currency += floor(tokens × coinsPerToken)。当累积经验越过下一阈值即升级并进入下一档;一次奖励可能连续升多级。
每级所需 token 按几何分档(xpPerToken = 1 时即等于每级所需 XP):
| 等级区间 | 每级 token |
|---|---|
| 1-10 | 1M |
| 11-20 | 10M |
| 21-30 | 100M |
| 31-40 | 1B |
| 41+ | maxXpToNextPerLevel(默认 2B) |
rewardPet/advanceLevel/tokenRewards/xpToNext 均为纯函数并配有单测,各边界被直接断言。
外观道具仅影响形象。buy 按目录价扣款并记入所有权;equip 将饰品放入饰品槽、背景放入背景槽,equip(null) 清除;已冻结的目录 ACCESSORIES 同时被宿主机(定价/所有权)与浏览器半边(渲染)引用,跨路由边界保持一致。
精灵表单接收
自定义图片以 data URL 加可选网格 manifest 提交。缺省约定为每个进化阶段一行、每行一帧;manifest 覆盖为 { cellW, cellH, stages: [{ row, frames }] }。服务端校验拒绝非标准媒体类型、非整数或非正 cell、非帧尺寸整倍的图片、越界阶段行,以及超过 maxImageBytes 的载荷。通过校验的 png/webp/jpeg/gif 图片以 base64 持久化到存储行,并作为渲染源返回。
数据与持久化
持久化域为 pet,只有一个全局行(无表)。写入经服务私有的 promise 链串行化,保证并发奖励/操作写入按顺序落盘;轮询 /pet/state 从内存同步读行。随附的 invariant companion 校验每条 domain/changed 写入与内存行一致,绕过写入链的路径会触发断言。
Host Remote / webServer 面
四个路由为无类型的纯 JSON 边界:
| Method | Path | Request | 成功 value |
拒绝 error.code |
|---|---|---|---|---|
| GET | /pet/state |
— | { state, accessories } |
— |
| POST | /pet/buy |
{ itemId } |
{ currency } |
invalid-item, already-owned, insufficient-currency |
| POST | /pet/equip |
itemId 或 null |
{ equipped } |
invalid-item, not-owned |
| POST | /pet/upload |
{ dataUrl, manifest? } |
{ id } |
invalid-data-url, unsupported-format, image-too-large, manifest-invalid |
| (any) | — | 畸形或超大请求体 | — | bad-json, body-error |
由于是 webServer 路由(而非 Typert Remote),它们不携带鉴权或会话身份;部署时应仅通过 SPA 的受信同源暴露。宿主半边刻意保持单宠物:一个全局槽位,无按会话或按用户的多宠物。
Model Experience
本地宠物状态
模型看到什么
无。context.pet 不注册任何工具、提示词片段、面向模型的上下文或 Session 事件;宠物只读取 token 用量的一列求和来积累经济,从不向模型请求注入 token、文本或状态。精灵表单是用户上传、仅服务于浏览器的图片数据。
Token 影响
零直接。服务只消费 assistant/message 的 usage 载荷来积累宠物经济;不修改、不重发、也不追加任何模型调用。
KV 缓存影响
独立。读取 token 用量、写入宠物行不触碰模型请求前缀,因此不会使本可复用的 provider 缓存条目失效。
五子棋 —— 内网 P2P
桌宠的环绕 dock 后藏着一副 15×15 五子棋棋盘,基于 WebRTC 点对点直连,不使用信令服务器。同一内网的两台浏览器靠手动交换两段短码完成连接。
环绕 dock
悬浮桌宠时,桌宠边缘旋出两个 icon:⚙ 设置 与 五子棋棋盘。dock 由悬浮触发,带约 140ms 的离开防抖——在 桌宠↔dock↔面板 之间移动时不会让 icon 闪烁。打开面板后 dock 收起;关掉面板再悬浮又旋出。点击任一 icon,其面板在桌宠正上方、右对齐展开(与设置面板同一位置),绝不居中于屏幕。
手动握手(无服务器)
WebRTC 的 offer/answer 是双向的,没有服务器中转,双方只能手动交换 SDP:
- 房主 → 生成邀请码 → 把邀请码(offer + 房主 ICE 候选的 base64)发给访客。
- 访客 → 加入 → 粘贴邀请码 → 把回执码(answer + 访客 ICE 候选的 base64)发回给房主。
- 房主 → 粘贴回执码 → 连接 → 数据通道打开。
两段码在面板里都有指纹(长度 + 首尾字符),房主可据此区分 offer/answer,也能发现「把邀请码当成回执码粘回去」这类常见误操作。
纯内网 ICE
iceServers: [] 只收集 host 候选(无 STUN/TURN、无外部依赖)。关闭 trickle:每方等 iceGatheringState === 'complete(以及 icecandidate === null 的 gathering 结束标志),把所有候选烘进那一段交换的描述里,并拒绝产出无候选的码(否则对端 ICE 会卡在 new)。面板实时显示 ICE/连接状态与已收集候选,握手卡住时可诊断,而非黑盒「等待中」。
注意:现代浏览器把内网 IP 混淆成 mDNS *.local 主机名。两台互相解析不了对方 .local 名的机器(跨子网、组播被墙、某些虚拟机/容器网络)连不上。同机双标签页是最稳的测试方式;否则在 chrome://flags/#enable-webrtc-hide-local-ips-with-mdns 关掉混淆以获得原始 IP 候选。
面板生命周期
- ✕ / 点面板外 / Esc 只是隐藏面板——peer 连接与棋局状态都保留,再经环绕 icon 展开可接着下,无需重新握手。会话挂载(
gomokuActive)独立于其可见性(panelMode)。 - 退出房间(双方都有)才真正拆掉 peer(
closePeer),对端收到「已断开」。 - 重开仅房主可用。访客不能重开;只有房主的重开会经通道下发同步。
- 五子连珠时,恰好那几颗获胜子脉动(缩放 + 亮度),直到房主重开。
文件
| 文件 | 职责 |
|---|---|
src/client/gomoku-net.ts |
WebRTC 手动信令:createHost/createGuest/acceptAnswer、非 trickle 的 iceComplete、wireDataChannel、wirePeerState、base64 编解码。无 React。 |
src/client/Gomoku.tsx |
棋盘 + 连接面板:useReducer 棋局状态机、winLine、握手 UI、ICE 遥测、隐藏 vs 退出生命周期。 |
src/client/OrbitDock.tsx |
两个悬浮 icon(齿轮 + 五子棋 glyph)及其旋入动画。 |
线上协议:{t:'hello'}(打开时存活确认)、{t:'move';x;y;c}(落子)、{t:'reset'}(房主发起的重开)。双方各自维护棋盘,并用 winLine 本地判定五连。
Known Limitations and Deferred Work
- 单进程单宠物 —— 行是一个全局槽位,仅在一个服务实例内串行化;不支持多宿主机进程共享同一存储根,也没有按会话或按用户的多宠物。
- 无鉴权路由边界 —— webServer 路由不带会话身份与归属;在加入鉴权前只能在 SPA 受信同源网关内提供服务。
- base64 图片传输 —— 上传图片以 base64 data URL 放入 JSON 请求体并原样持久化;超大图片受
maxImageBytes与路由请求体上限约束,而非流式通道。 - 无饥饿/心情/生病与衰减 —— v1 形象纯粹由阶段驱动,无状态压力或衰减定时器;多人宠物、抽卡与独立
/pet页面均暂缓。 - 五子棋依赖可达的 host 候选 ——
iceServers: []下连接取决于双方能否互相连通 host 候选;mDNS*.local混淆在跨子网/虚拟机场景可能阻断。按设计无中继回退(无服务器)。