Skip to content

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:

  1. 房主 → 生成邀请码 → 把邀请码(offer + 房主 ICE 候选的 base64)发给访客。
  2. 访客 → 加入 → 粘贴邀请码 → 把回执码(answer + 访客 ICE 候选的 base64)发回给房主。
  3. 房主 → 粘贴回执码 → 连接 → 数据通道打开。

两段码在面板里都有指纹(长度 + 首尾字符),房主可据此区分 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 混淆在跨子网/虚拟机场景可能阻断。按设计无中继回退(无服务器)。