# dsh-blackjack 插件

[English](./README.md) | **简体中文**

> ⚠️ **社区第三方项目声明**
>
> 本插件由个人开发者发布与维护，**与任何模型厂商均无关联**：不是任何厂商的官方
> 项目、不代表其立场、未获其背书或授权，也不使用其名称与标识作为本项目的品牌
> 元素。包名与命令中的 `dsh-` / `bj-` 前缀仅遵循社区插件的命名惯例，用于标识
> 本插件的兼容目标与命名空间，不代表 dsh 官方发布或认可。

安装进你自己的 dsh 后，在对话里直接打开一张 21 点牌桌：赢牌拿游戏币 **CHIP**，CHIP 可以单向兑换为「已兑换额度」，这份额
度只能在本插件的续命路由里消费——**当且仅当你自己配置的模型额度耗尽时**，插件
才会用这份额度接住那一次失败的请求，让你不用中断手上的任务去充值或等待。

参与**永远免费**：每天有固定的免费手数，玩输了不扣任何东西，也不影响你自己的
密钥或余额。全部规则细节见下方「参与规则」。

---

## 这是什么、怎么玩

牌桌由一个牌局服务端（`packages/server`，由运营者自托管）维持权威状态，插件只
是它的客户端：

- **文字命令**：`/blackjack` 系列命令零成本——它们全部走 dsh 的命令层而不是发
  一条模型消息，所以打牌本身不消耗你的任何 token 额度。
- **图形牌桌**（可选）：如果你的 dsh profile 带了 Web GUI（例如
  `@deepseek-ai/dsh-web-app`），对话流里会出现一张可点击的牌桌卡片，动作按钮
  与文字命令等价，只是换了个界面。没有 Web GUI 的纯终端 profile 里，文字命令
  照常可用，插件不会因为缺一个可选服务而报错。

一局牌：`/blackjack deal` 开局（默认用当天的免费手，也可以 `deal <注额>` 用余
额加注）→ 按提示用 `bj-hit` / `bj-stand` 等动作命令打完 → 结算，赢家拿 CHIP。

### 命令表

| 命令 | 作用 |
|---|---|
| `/blackjack` | 查看当前状态：有牌局在打就显示牌局，没有就显示余额/免费手/兑换门槛 |
| `/blackjack agree` | 显示须知全文（第三方声明、不可提现、不可转让等）。**只显示，不代表同意** |
| `/blackjack agree confirm` | 阅读须知后确认同意，注册牌桌账户。**这一步之前，开局与兑换都会被拒绝** |
| `/blackjack deal [注额]` | 开一局新牌。不带参数用当天的免费手；带一个正整数注额则从余额里加注 |
| `/blackjack exchange [数量]` | 把达标余额兑换为可消费额度。**首次**（还没绑过 GitHub）会给出一个配对链接，需要去浏览器完成绑定；**绑定之后**带上数量就在命令里直接兑换，不再需要浏览器 |
| `/blackjack reset confirm` | 清除**本机保存**的牌桌凭证（服务端记录不受影响，重新 `agree confirm` 会注册一个新账户） |
| `/bj-hit` | 要一张牌 |
| `/bj-stand` | 停牌，进入结算 |
| `/bj-double` | 加倍注额并再要一张牌 |
| `/bj-split` | 把一对相同点数的牌分成两手 |
| `/bj-insure` | 庄家明牌为 A 时购买保险 |
| `/bj-decline` | 不购买保险 |

`bj-*` 六个动作命令只在有进行中的牌局时才有意义；没有牌局时执行会提示先
`/blackjack deal`。

### 兑换的两条路径

兑换要求绑定 GitHub（一个 GitHub 身份只对应一个钱包，这是奖池出口唯一的反小号
闸门）。绑定这件事只能在浏览器里完成——它走的是 GitHub 官方的 OAuth 授权码流。
但**绑定只需要做一次**：

1. **首次**：`/blackjack exchange` 给你一个短期配对码和链接。在浏览器里打开、
   点「用 GitHub 继续」完成绑定，然后在同一页面的表单里填数量兑换。
2. **之后**：`/blackjack exchange <数量>`。插件用它自己持有的令牌直接调服务端
   的兑换接口，**不再生成配对码、不再需要打开浏览器**。不带数量时会先回显你的
   余额与门槛，并提示补上数量——兑换不可逆，插件不会替你猜一个数字。

余额没到门槛、或者数量超过余额，都会得到一句明确的提示（说清楚门槛是多少 CHIP、
或者干脆说余额不够），账本一分不动。提示的语言跟 `locale` 配置项走——默认是
英文，见下面「配置」一节。

---

## 安装

```sh
dsh plugin --profile web add -w dsh-blackjack
dsh --profile web
```

把 `web` 换成你实际要装的 profile 名。包已经发布到 npm，上面这条命令可以直接用。

### 启动页曾经是空白的（0.2.2 已修）

**0.2.2 起，在 dsh 的启动页（新开 dsh 时那个 “Into the Unknown” 界面）直接敲
`/blackjack` 就能出牌桌**，不用先发一句废话，也不用从侧边栏翻一个旧对话出来。

**0.2.1 及更早版本不行**：那时命令会真的执行，但**画面上什么都不会出现**，看起来
就像插件坏了。装的是旧版本的话，先让对话真正开始（随便发一句，或从侧边栏打开一个
已有对话），再执行 `/blackjack agree`。点“新建会话”没用——那会把你送回启动页。

原因不在本插件，而在宿主的一行判定（`dsh-client-runtime@0.1.0-rc.6`,
`lib/client.js:7753`）：

```js
function hasVisibleConversationContent(chat) {
  return chat.order.some((key) => chat.nodes.get(key)?.kind !== "command");
}
```

通用 command 行被**刻意**排除在“可见对话内容”之外，于是会话视图判定这个会话
还是空的，直接不渲染。宿主自己的 `/goal` 不受影响，是因为 `ui-goal` 额外投影了一个
**非 command** 的节点把对话激活。本插件照这条路自救：把你敲的那行命令投影成自己的
一个 Chat 节点（只是把已有事件换个 kind 画出来，不写库、不发新事件、不进模型可见
的历史）。

**这只是插件侧的自救，宿主那个缺口还在**——任何只有服务端、没有浏览器侧的插件，
今天仍然没有办法让自己的命令在启动页上可见。上游讨论（含根因逐行分析与本插件的
完整做法）：https://github.com/deepseek-ai/deepseek-harness/discussions/4066。
当初复现用的最小插件（13 行、不碰 slot 也不碰服务）在本仓库
[`docs/repro/dsh-probe-repro/`](../../docs/repro/dsh-probe-repro/)。

> **想改代码、或者想装一个还没发到 npm 的开发版**，从源码装：
>
> ```sh
> git clone https://github.com/yul761/dsh-blackjack
> cd dsh-blackjack && pnpm install          # 会自动构建插件的 dist/
> dsh plugin --profile web add -w "$PWD/packages/plugin"
> ```
>
> 注意**不能**直接 `pnpm add github:yul761/dsh-blackjack`：这是个 monorepo，
> 从 git 源装会拿到仓库根（一个没有入口的私有 workspace 根），而不是
> `packages/plugin` 里的插件包。

**为什么一定要带 `-w`：** dsh 的每个 profile 目录本身就是一个 pnpm 工作区（自
带 `pnpm-workspace.yaml`），而 pnpm 默认拒绝在没有 `-w`/`--workspace-root` 的
情况下向工作区**根**添加依赖——省掉这个参数，安装命令会直接失败退出，而不是装
出一个残缺的状态。这不是本插件独有的坑，其它社区 dsh 插件的安装文档也踩过同一
处并确认了同样的结论。

### 版本要求：本插件锁定 dsh `0.1.0-rc.6`

插件把 `@deepseek-ai/dsh-*` 的 peer 全部**精确锁**在 `0.1.0-rc.6`，不是写成
`^0.1.0-rc.6`。这不是保守，是必须的：这套包锁步发布，rc 之间并不互相兼容，而
`^` 会让包管理器给宿主没有的那个 peer 装上更新的 rc——0.1.0 就是这么崩的
（`dsh-llm-deepseek@rc.8` 配宿主的 `dsh-llm@rc.6`，启动即报
`does not provide an export named 'offloadRequestImages'`）。

所以：**请用 `dsh@0.1.0-rc.6`**。跑在别的 rc 上会看到 peer 版本警告，届时等插件
跟进发新版即可。

安装成功后，dsh 会读取插件 `package.json` 里的 `dsh.bundle.patch` 声明并自动把
`blackjack.cordis.yml` 接进 profile 的 bundle 层——不需要你手工再写一份
`--patch` 覆盖层。

### 配置

装完开箱即用：`serverUrl` 默认指向本项目运营者自托管的那一份服务，不改配置也能
直接玩。要在 profile 的 `cordis.patch.yml` 里覆盖也可以：

| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| `serverUrl` | string | `https://server-production-493c.up.railway.app` | 牌局服务端地址。**想自建奖池就改成你自己的部署地址**——服务端与本插件同仓开源，见仓库根 [README.zh-CN.md](../../README.zh-CN.md) 的部署一节 |
| `enabled` | boolean | `true` | 设为 `false` 可保留安装但完全停用：不注册任何命令，也不挂任何路由或续命钩子 |
| `locale` | `'en'` \| `'zh'` | `'en'` | `/blackjack` 与 `/bj-*` 等**命令**回复的语言。**图形牌桌不看这一项**——它自动跟随 dsh 自己的 Settings → General → Language；用图形界面的玩家完全不用管这个配置。只有纯终端（没有 Web GUI）的 profile，才需要在这里手动设一次，因为 Node 侧读不到宿主的语言设置 |

```yaml
# ~/.dsh/profiles/<profile>/cordis.patch.yml
- id: blackjack
  config:
    serverUrl: https://your-blackjack-server.example.com
```

---

## 续命路由到底怎么触发（务必读完）

插件会在 `llm/stream` 上挂一个观察者，但它**绝不会拦截正常调用**。触发续命只
有在同时满足以下条件时才会发生：

1. 这次调用用的是**你自己配置的模型 provider**（不是插件自己的奖池路由，避免
   死循环）；
2. 这次调用**没有产出任何内容**——哪怕已经吐出一个字，也不算数，插件绝不会在
   内容已经开始返回后中途换源，那样只会造成重复或截断；
3. 调用是以**配额耗尽**这一类错误收尾的（不是网络错误、不是参数错误、不是别
   的任何原因）；
4. 你在本插件里**已经完成过 `/blackjack agree confirm`**（本机存有牌桌凭证；没有这份
   凭证，插件根本不会尝试）。

   这里只看凭证，**不预先检查你的已兑换额度够不够**。这是刻意的：查余额要在每
   次失败调用上多打一次网络请求，拿到的还是一个可能已经过期的快照；而"有凭证、
   余额为零"这条路本身就是安全的——奖池调用会被服务端直接拒绝，插件于是把**你
   原本的那个错误**原样交还给你，跟没装插件完全一样。

四条同时成立时，插件才会把这一次失败的调用重发到奖池路由。

**奖池用哪个模型（重要）：** 重发到奖池的请求，**由运营者在服务端指定模型**，
不一定是你在界面上选中的那个。这是刻意的——奖池花的是运营者自己的上游余额，
型号由出钱的人定；这样也免去了"你选了个服务端不认识的型号，续命白接管一场"
这类失败。所以一次由奖池完成的调用，**界面上显示的型号可能与实际服务的型号
不同**。当前运营者实例用的是 `deepseek-v4-flash`。

**同一时刻最多 4 次并发奖池调用。** 服务端把单玩家并发上限设为 4：dsh 创建会
话的那一轮会同时发 3 个 LLM 调用，闸设成 1 时另外两个会被直接拒掉，续命恰好在
最该生效的第一句话上失效（2026-08-22 真机实测后从 1 改到 4，3 加一格余量）。超
出上限会得到明确的「请求太频繁」（429）。另外，奖池请求会先冻结一笔估算额度，
刚过兑换门槛的余额只装得下一笔，所以真并发时后面几笔可能因额度不足被拒——那是
一条写清了「需要多少 / 还有多少」的明确提示，不是含糊的失败。

**切换提示去哪了（请注意）：** 成功切换时插件会输出一行「已切换至奖池余额继续
本次请求。」，但在当前 dsh 版本里它**只写进 dsh 自己的日志，不会出现在对话界面
里**——这个版本没有给插件提供任何"往对话里插一条非模型消息"的通道，唯一现成的
通道会把内容写进模型可见的历史，那是本项目明确不做的（不污染上下文、不让模型看
到）。换句话说：一次调用可能是靠奖池额度完成的，而你在界面上看不到提示；要确认
消耗，请用 `/blackjack` 查看已兑换额度的变化。等 dsh 提供插件可用的会话内通知位，
这条会改成界面内提示。

**如果奖池也用完了、或者奖池服务本身连不上，插件不会伪造一个成功结果**——你会
原样收到你自己那次调用本来的失败原因，就像没装这个插件一样。换句话说：装上这
个插件，一次调用的结果**只可能不变或变好，不会变坏**。

---

## 参与规则

- **参与永远免费。** 每日固定几手免费牌，零成本参与；不存在、也不会出现"付费
  换手数 / 换概率 / 换赔率"的任何设计。
- **CHIP 与已兑换额度单向、封闭。**
  - **不可提现**，不折算任何法定货币，界面不显示金额；
  - **不可转让**（含玩家之间转让），不可退换；
  - **不会进入任何模型厂商的账户**；
  - **唯一用途**是通过牌局服务端的续命路由消费——除此之外没有任何出口。
- **不活跃会回收。** 连续 7 天无游戏且无消费的账户，余额会被回收进奖池。
- **已兑换额度目前不会过期。** 早期文案写作"有效期至本赛季末"，但服务端的赛季
  结算并没有触碰已兑换额度，那句话与实现不符，因此改成如实描述。是否在赛季末
  清零尚未决定；真要做，会先公告。
- **你自己的 API key 永远不会进入这套系统**：插件不读取、不托管、不代管你自
  己 provider 的任何密钥，续命路由用的是牌局服务端自己签发的凭证。

---

## 卸载与清除本机凭证

只想清掉本地保存的牌桌登录凭证（例如换机器、或者想用一个全新账户重新开始），
不需要卸载插件：

```
/blackjack reset confirm
```

（不带 `confirm` 时会先回显一遍这条操作的说明，避免误触。）这只清除**本机**保
存的凭证，服务端那边的账户记录不受影响；用 `/blackjack agree confirm` 即可重新注册一
个新账户。

要完全移除插件本身：

```sh
dsh plugin --profile web remove dsh-blackjack
```
