dsh-blackjack 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +189 -0
- package/blackjack.cordis.yml +13 -0
- package/dist/adapter.d.ts +33 -0
- package/dist/adapter.js +58 -0
- package/dist/api.d.ts +85 -0
- package/dist/api.js +55 -0
- package/dist/client/Table.d.ts +30 -0
- package/dist/client/Table.js +74 -0
- package/dist/client/index.d.ts +12 -0
- package/dist/client/index.js +68 -0
- package/dist/client.js +26 -0
- package/dist/commands.d.ts +17 -0
- package/dist/commands.js +262 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.js +16 -0
- package/dist/identity.d.ts +12 -0
- package/dist/identity.js +28 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +39 -0
- package/dist/render.d.ts +16 -0
- package/dist/render.js +70 -0
- package/dist/router.d.ts +100 -0
- package/dist/router.js +179 -0
- package/dist/routes.d.ts +54 -0
- package/dist/routes.js +195 -0
- package/package.json +95 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yuchen Lin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# dsh-blackjack 插件
|
|
2
|
+
|
|
3
|
+
> ⚠️ **社区第三方项目声明**
|
|
4
|
+
>
|
|
5
|
+
> 本插件由个人开发者发布与维护,**与任何模型厂商均无关联**:不是任何厂商的官方
|
|
6
|
+
> 项目、不代表其立场、未获其背书或授权,也不使用其名称与标识作为本项目的品牌
|
|
7
|
+
> 元素。包名与命令中的 `dsh-` / `bj-` 前缀仅遵循社区插件的命名惯例,用于标识
|
|
8
|
+
> 本插件的兼容目标与命名空间,不代表 dsh 官方发布或认可。
|
|
9
|
+
|
|
10
|
+
安装进你自己的 dsh 后,在对话里直接打开一张 21 点牌桌:赢牌拿游戏币 **CHIP**,CHIP 可以单向兑换为「已兑换额度」,这份额
|
|
11
|
+
度只能在本插件的续命路由里消费——**当且仅当你自己配置的模型额度耗尽时**,插件
|
|
12
|
+
才会用这份额度接住那一次失败的请求,让你不用中断手上的任务去充值或等待。
|
|
13
|
+
|
|
14
|
+
参与**永远免费**:每天有固定的免费手数,玩输了不扣任何东西,也不影响你自己的
|
|
15
|
+
密钥或余额。全部规则细节见下方「参与规则」。
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 这是什么、怎么玩
|
|
20
|
+
|
|
21
|
+
牌桌由一个牌局服务端(`packages/server`,由运营者自托管)维持权威状态,插件只
|
|
22
|
+
是它的客户端:
|
|
23
|
+
|
|
24
|
+
- **文字命令**:`/blackjack` 系列命令零成本——它们全部走 dsh 的命令层而不是发
|
|
25
|
+
一条模型消息,所以打牌本身不消耗你的任何 token 额度。
|
|
26
|
+
- **图形牌桌**(可选):如果你的 dsh profile 带了 Web GUI(例如
|
|
27
|
+
`@deepseek-ai/dsh-web-app`),对话流里会出现一张可点击的牌桌卡片,动作按钮
|
|
28
|
+
与文字命令等价,只是换了个界面。没有 Web GUI 的纯终端 profile 里,文字命令
|
|
29
|
+
照常可用,插件不会因为缺一个可选服务而报错。
|
|
30
|
+
|
|
31
|
+
一局牌:`/blackjack deal` 开局(默认用当天的免费手,也可以 `deal <注额>` 用余
|
|
32
|
+
额加注)→ 按提示用 `bj-hit` / `bj-stand` 等动作命令打完 → 结算,赢家拿 CHIP。
|
|
33
|
+
|
|
34
|
+
### 命令表
|
|
35
|
+
|
|
36
|
+
| 命令 | 作用 |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `/blackjack` | 查看当前状态:有牌局在打就显示牌局,没有就显示余额/免费手/兑换门槛 |
|
|
39
|
+
| `/blackjack agree` | 首次使用必须先执行:阅读须知并同意后才能上桌 |
|
|
40
|
+
| `/blackjack deal [注额]` | 开一局新牌。不带参数用当天的免费手;带一个正整数注额则从余额里加注 |
|
|
41
|
+
| `/blackjack exchange [数量]` | 把达标余额兑换为可消费额度。**首次**(还没绑过 GitHub)会给出一个配对链接,需要去浏览器完成绑定;**绑定之后**带上数量就在命令里直接兑换,不再需要浏览器 |
|
|
42
|
+
| `/blackjack reset confirm` | 清除**本机保存**的牌桌凭证(服务端记录不受影响,重新 `agree` 会注册一个新账户) |
|
|
43
|
+
| `/bj-hit` | 要一张牌 |
|
|
44
|
+
| `/bj-stand` | 停牌,进入结算 |
|
|
45
|
+
| `/bj-double` | 加倍注额并再要一张牌 |
|
|
46
|
+
| `/bj-split` | 把一对相同点数的牌分成两手 |
|
|
47
|
+
| `/bj-insure` | 庄家明牌为 A 时购买保险 |
|
|
48
|
+
| `/bj-decline` | 不购买保险 |
|
|
49
|
+
|
|
50
|
+
`bj-*` 六个动作命令只在有进行中的牌局时才有意义;没有牌局时执行会提示先
|
|
51
|
+
`/blackjack deal`。
|
|
52
|
+
|
|
53
|
+
### 兑换的两条路径
|
|
54
|
+
|
|
55
|
+
兑换要求绑定 GitHub(一个 GitHub 身份只对应一个钱包,这是奖池出口唯一的反小号
|
|
56
|
+
闸门)。绑定这件事只能在浏览器里完成——它走的是 GitHub 官方的 OAuth 授权码流。
|
|
57
|
+
但**绑定只需要做一次**:
|
|
58
|
+
|
|
59
|
+
1. **首次**:`/blackjack exchange` 给你一个短期配对码和链接。在浏览器里打开、
|
|
60
|
+
点「用 GitHub 继续」完成绑定,然后在同一页面的表单里填数量兑换。
|
|
61
|
+
2. **之后**:`/blackjack exchange <数量>`。插件用它自己持有的令牌直接调服务端
|
|
62
|
+
的兑换接口,**不再生成配对码、不再需要打开浏览器**。不带数量时会先回显你的
|
|
63
|
+
余额与门槛,并提示补上数量——兑换不可逆,插件不会替你猜一个数字。
|
|
64
|
+
|
|
65
|
+
余额没到门槛、或者数量超过余额,都会得到一句明确的中文提示("余额还没到兑换门
|
|
66
|
+
槛。" / "余额不足以完成这个动作。"),账本一分不动。
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 安装
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
dsh plugin --profile web add -w dsh-blackjack
|
|
74
|
+
dsh --profile web
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
把 `web` 换成你实际要装的 profile 名。
|
|
78
|
+
|
|
79
|
+
> **尚未发布到 npm 时**,上面这条命令还装不到东西。从源码装:
|
|
80
|
+
>
|
|
81
|
+
> ```sh
|
|
82
|
+
> git clone https://github.com/yul761/dsh-blackjack
|
|
83
|
+
> cd dsh-blackjack && pnpm install # 会自动构建插件的 dist/
|
|
84
|
+
> dsh plugin --profile web add -w "$PWD/packages/plugin"
|
|
85
|
+
> ```
|
|
86
|
+
>
|
|
87
|
+
> 注意**不能**直接 `pnpm add github:yul761/dsh-blackjack`:这是个 monorepo,
|
|
88
|
+
> 从 git 源装会拿到仓库根(一个没有入口的私有 workspace 根),而不是
|
|
89
|
+
> `packages/plugin` 里的插件包。
|
|
90
|
+
|
|
91
|
+
**为什么一定要带 `-w`:** dsh 的每个 profile 目录本身就是一个 pnpm 工作区(自
|
|
92
|
+
带 `pnpm-workspace.yaml`),而 pnpm 默认拒绝在没有 `-w`/`--workspace-root` 的
|
|
93
|
+
情况下向工作区**根**添加依赖——省掉这个参数,安装命令会直接失败退出,而不是装
|
|
94
|
+
出一个残缺的状态。这不是本插件独有的坑,其它社区 dsh 插件的安装文档也踩过同一
|
|
95
|
+
处并确认了同样的结论。
|
|
96
|
+
|
|
97
|
+
安装成功后,dsh 会读取插件 `package.json` 里的 `dsh.bundle.patch` 声明并自动把
|
|
98
|
+
`blackjack.cordis.yml` 接进 profile 的 bundle 层——不需要你手工再写一份
|
|
99
|
+
`--patch` 覆盖层。
|
|
100
|
+
|
|
101
|
+
### 配置
|
|
102
|
+
|
|
103
|
+
装完开箱即用:`serverUrl` 默认指向本项目运营者自托管的那一份服务,不改配置也能
|
|
104
|
+
直接玩。要在 profile 的 `cordis.patch.yml` 里覆盖也可以:
|
|
105
|
+
|
|
106
|
+
| 配置项 | 类型 | 默认值 | 说明 |
|
|
107
|
+
|---|---|---|---|
|
|
108
|
+
| `serverUrl` | string | `https://server-production-493c.up.railway.app` | 牌局服务端地址。**想自建奖池就改成你自己的部署地址**——服务端与本插件同仓开源,见仓库根 README 的部署一节 |
|
|
109
|
+
| `enabled` | boolean | `true` | 设为 `false` 可保留安装但完全停用:不注册任何命令,也不挂任何路由或续命钩子 |
|
|
110
|
+
|
|
111
|
+
```yaml
|
|
112
|
+
# ~/.dsh/profiles/<profile>/cordis.patch.yml
|
|
113
|
+
- id: blackjack
|
|
114
|
+
config:
|
|
115
|
+
serverUrl: https://your-blackjack-server.example.com
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## 续命路由到底怎么触发(务必读完)
|
|
121
|
+
|
|
122
|
+
插件会在 `llm/stream` 上挂一个观察者,但它**绝不会拦截正常调用**。触发续命只
|
|
123
|
+
有在同时满足以下条件时才会发生:
|
|
124
|
+
|
|
125
|
+
1. 这次调用用的是**你自己配置的模型 provider**(不是插件自己的奖池路由,避免
|
|
126
|
+
死循环);
|
|
127
|
+
2. 这次调用**没有产出任何内容**——哪怕已经吐出一个字,也不算数,插件绝不会在
|
|
128
|
+
内容已经开始返回后中途换源,那样只会造成重复或截断;
|
|
129
|
+
3. 调用是以**配额耗尽**这一类错误收尾的(不是网络错误、不是参数错误、不是别
|
|
130
|
+
的任何原因);
|
|
131
|
+
4. 你在本插件里**已经完成过 `/blackjack agree`**(本机存有牌桌凭证;没有这份
|
|
132
|
+
凭证,插件根本不会尝试)。
|
|
133
|
+
|
|
134
|
+
这里只看凭证,**不预先检查你的已兑换额度够不够**。这是刻意的:查余额要在每
|
|
135
|
+
次失败调用上多打一次网络请求,拿到的还是一个可能已经过期的快照;而"有凭证、
|
|
136
|
+
余额为零"这条路本身就是安全的——奖池调用会被服务端直接拒绝,插件于是把**你
|
|
137
|
+
原本的那个错误**原样交还给你,跟没装插件完全一样。
|
|
138
|
+
|
|
139
|
+
四条同时成立时,插件才会把这一次失败的调用重发到奖池路由。
|
|
140
|
+
|
|
141
|
+
**切换提示去哪了(请注意):** 成功切换时插件会输出一行「已切换至奖池余额继续
|
|
142
|
+
本次请求。」,但在当前 dsh 版本里它**只写进 dsh 自己的日志,不会出现在对话界面
|
|
143
|
+
里**——这个版本没有给插件提供任何"往对话里插一条非模型消息"的通道,唯一现成的
|
|
144
|
+
通道会把内容写进模型可见的历史,那是本项目明确不做的(不污染上下文、不让模型看
|
|
145
|
+
到)。换句话说:一次调用可能是靠奖池额度完成的,而你在界面上看不到提示;要确认
|
|
146
|
+
消耗,请用 `/blackjack` 查看已兑换额度的变化。等 dsh 提供插件可用的会话内通知位,
|
|
147
|
+
这条会改成界面内提示。
|
|
148
|
+
|
|
149
|
+
**如果奖池也用完了、或者奖池服务本身连不上,插件不会伪造一个成功结果**——你会
|
|
150
|
+
原样收到你自己那次调用本来的失败原因,就像没装这个插件一样。换句话说:装上这
|
|
151
|
+
个插件,一次调用的结果**只可能不变或变好,不会变坏**。
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## 参与规则
|
|
156
|
+
|
|
157
|
+
- **参与永远免费。** 每日固定几手免费牌,零成本参与;不存在、也不会出现"付费
|
|
158
|
+
换手数 / 换概率 / 换赔率"的任何设计。
|
|
159
|
+
- **CHIP 与已兑换额度单向、封闭。**
|
|
160
|
+
- **不可提现**,不折算任何法定货币,界面不显示金额;
|
|
161
|
+
- **不可转让**(含玩家之间转让),不可退换;
|
|
162
|
+
- **不会进入任何模型厂商的账户**;
|
|
163
|
+
- **唯一用途**是通过牌局服务端的续命路由消费——除此之外没有任何出口。
|
|
164
|
+
- **有赛季有效期。** 已兑换额度有效期至本赛季末(含宽限期);连续一段时间无
|
|
165
|
+
游戏且无消费的账户余额会被回收进奖池。这些提醒会在插件里提前给出,兑换前也
|
|
166
|
+
会再提示一次。
|
|
167
|
+
- **你自己的 API key 永远不会进入这套系统**:插件不读取、不托管、不代管你自
|
|
168
|
+
己 provider 的任何密钥,续命路由用的是牌局服务端自己签发的凭证。
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## 卸载与清除本机凭证
|
|
173
|
+
|
|
174
|
+
只想清掉本地保存的牌桌登录凭证(例如换机器、或者想用一个全新账户重新开始),
|
|
175
|
+
不需要卸载插件:
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
/blackjack reset confirm
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
(不带 `confirm` 时会先回显一遍这条操作的说明,避免误触。)这只清除**本机**保
|
|
182
|
+
存的凭证,服务端那边的账户记录不受影响;用 `/blackjack agree` 即可重新注册一
|
|
183
|
+
个新账户。
|
|
184
|
+
|
|
185
|
+
要完全移除插件本身:
|
|
186
|
+
|
|
187
|
+
```sh
|
|
188
|
+
dsh plugin --profile web remove dsh-blackjack
|
|
189
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# dsh bundle patch for dsh-blackjack. Declared by package.json's
|
|
2
|
+
# `dsh.bundle.patch`, so `dsh plugin --profile <name> add -w dsh-blackjack`
|
|
3
|
+
# activates the plugin without a manual --patch overlay.
|
|
4
|
+
- insert:
|
|
5
|
+
- id: blackjack
|
|
6
|
+
name: dsh-blackjack
|
|
7
|
+
config:
|
|
8
|
+
# 牌局服务端地址。默认已指向本项目运营者自托管的那一份,不填也能玩。
|
|
9
|
+
# 想自建奖池就改成你自己的部署地址(服务端同仓开源)。
|
|
10
|
+
# serverUrl: https://your-own-deployment.example.com
|
|
11
|
+
|
|
12
|
+
# 设为 false 可保留安装但完全停用(不注册命令、不挂路由)。
|
|
13
|
+
# enabled: true
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import { type LlmAdapter } from '@deepseek-ai/dsh-llm';
|
|
3
|
+
import { type DeepSeekConnectionOptions } from '@deepseek-ai/dsh-llm-deepseek';
|
|
4
|
+
/**
|
|
5
|
+
* The pool route's connection facts. A standalone, side-effect-free function
|
|
6
|
+
* (rather than only a closure inside `createPoolAdapter`) so `baseURL` and
|
|
7
|
+
* `apiKeyEnv` — the two facts this whole feature depends on — are directly
|
|
8
|
+
* assertable in a test without driving a live `DeepSeekAdapter.stream()`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function poolConnectionOptions(serverUrl: string): DeepSeekConnectionOptions;
|
|
11
|
+
/**
|
|
12
|
+
* Build the per-request credential resolver, also exported standalone so its
|
|
13
|
+
* `MISSING_CREDENTIAL` throw and `assertUsableApiKey` validation are directly
|
|
14
|
+
* testable against a fake `ctx.credentials`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function createPoolResolveApiKey(ctx: Context): (connection: DeepSeekConnectionOptions) => Promise<string>;
|
|
17
|
+
/**
|
|
18
|
+
* Build the pool-route adapter by pointing `DeepSeekAdapter` — the same
|
|
19
|
+
* tested wire-format translator `@deepseek-ai/dsh-llm-deepseek` uses for its
|
|
20
|
+
* own `deepseek-official` route — at our metering proxy's chat-completions-
|
|
21
|
+
* compatible endpoint. No hand-written SSE/tool-call translation here.
|
|
22
|
+
*
|
|
23
|
+
* Mirrors `dsh-llm-deepseek`'s own `apply()` wiring line-by-line (verified
|
|
24
|
+
* against the installed `@deepseek-ai/dsh-llm-deepseek@0.1.0-rc.6` source):
|
|
25
|
+
* `options` is re-read per operation, `resolveApiKey` resolves through
|
|
26
|
+
* `ctx.credentials`, validates the value with the same `assertUsableApiKey`
|
|
27
|
+
* the vendor's own `apply()` applies (trims whitespace, refuses a blank or
|
|
28
|
+
* header-unsafe value with `LlmError` `INVALID_CREDENTIAL` instead of
|
|
29
|
+
* letting `fetch` fail on it opaquely), and throws `LlmError`
|
|
30
|
+
* `MISSING_CREDENTIAL` when no token is stored at all; `resolveUserId`
|
|
31
|
+
* reuses the harness-home anonymous id.
|
|
32
|
+
*/
|
|
33
|
+
export declare function createPoolAdapter(ctx: Context, serverUrl: string): LlmAdapter;
|
package/dist/adapter.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { LlmError, assertUsableApiKey, resolveRetryPolicy } from '@deepseek-ai/dsh-llm';
|
|
2
|
+
import { DeepSeekAdapter, DEFAULT_CONTEXT_WINDOW, DEFAULT_MAX_TOKENS, DEFAULT_STREAM_IDLE_TIMEOUT_MS, } from '@deepseek-ai/dsh-llm-deepseek';
|
|
3
|
+
import { credentialRef } from '@deepseek-ai/dsh-credentials';
|
|
4
|
+
import { getOrCreateAnonymousUserId } from '@deepseek-ai/dsh-anonymous-user-id';
|
|
5
|
+
import { TOKEN_REF } from './config.js';
|
|
6
|
+
/**
|
|
7
|
+
* The pool route's connection facts. A standalone, side-effect-free function
|
|
8
|
+
* (rather than only a closure inside `createPoolAdapter`) so `baseURL` and
|
|
9
|
+
* `apiKeyEnv` — the two facts this whole feature depends on — are directly
|
|
10
|
+
* assertable in a test without driving a live `DeepSeekAdapter.stream()`.
|
|
11
|
+
*/
|
|
12
|
+
export function poolConnectionOptions(serverUrl) {
|
|
13
|
+
return {
|
|
14
|
+
baseURL: `${serverUrl}/v1`,
|
|
15
|
+
apiKeyEnv: credentialRef(TOKEN_REF),
|
|
16
|
+
defaults: {},
|
|
17
|
+
maxTokens: DEFAULT_MAX_TOKENS,
|
|
18
|
+
defaultContextWindow: DEFAULT_CONTEXT_WINDOW,
|
|
19
|
+
models: [],
|
|
20
|
+
streamIdleTimeoutMs: DEFAULT_STREAM_IDLE_TIMEOUT_MS,
|
|
21
|
+
retryPolicy: resolveRetryPolicy(undefined, 'dsh-blackjack: pool retryPolicy'),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Build the per-request credential resolver, also exported standalone so its
|
|
26
|
+
* `MISSING_CREDENTIAL` throw and `assertUsableApiKey` validation are directly
|
|
27
|
+
* testable against a fake `ctx.credentials`.
|
|
28
|
+
*/
|
|
29
|
+
export function createPoolResolveApiKey(ctx) {
|
|
30
|
+
return async (connection) => {
|
|
31
|
+
const hit = await ctx.credentials.resolve(connection.apiKeyEnv);
|
|
32
|
+
if (hit !== undefined)
|
|
33
|
+
return assertUsableApiKey(hit.value, 'dsh-blackjack', connection.apiKeyEnv);
|
|
34
|
+
throw new LlmError(`dsh-blackjack: no player token for the pool provider route; accept the plugin's one-time notice first`, 'MISSING_CREDENTIAL');
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Build the pool-route adapter by pointing `DeepSeekAdapter` — the same
|
|
39
|
+
* tested wire-format translator `@deepseek-ai/dsh-llm-deepseek` uses for its
|
|
40
|
+
* own `deepseek-official` route — at our metering proxy's chat-completions-
|
|
41
|
+
* compatible endpoint. No hand-written SSE/tool-call translation here.
|
|
42
|
+
*
|
|
43
|
+
* Mirrors `dsh-llm-deepseek`'s own `apply()` wiring line-by-line (verified
|
|
44
|
+
* against the installed `@deepseek-ai/dsh-llm-deepseek@0.1.0-rc.6` source):
|
|
45
|
+
* `options` is re-read per operation, `resolveApiKey` resolves through
|
|
46
|
+
* `ctx.credentials`, validates the value with the same `assertUsableApiKey`
|
|
47
|
+
* the vendor's own `apply()` applies (trims whitespace, refuses a blank or
|
|
48
|
+
* header-unsafe value with `LlmError` `INVALID_CREDENTIAL` instead of
|
|
49
|
+
* letting `fetch` fail on it opaquely), and throws `LlmError`
|
|
50
|
+
* `MISSING_CREDENTIAL` when no token is stored at all; `resolveUserId`
|
|
51
|
+
* reuses the harness-home anonymous id.
|
|
52
|
+
*/
|
|
53
|
+
export function createPoolAdapter(ctx, serverUrl) {
|
|
54
|
+
const resolveApiKey = createPoolResolveApiKey(ctx);
|
|
55
|
+
let userId;
|
|
56
|
+
const resolveUserId = () => (userId ??= getOrCreateAnonymousUserId());
|
|
57
|
+
return new DeepSeekAdapter({ options: () => poolConnectionOptions(serverUrl), resolveApiKey, resolveUserId });
|
|
58
|
+
}
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export interface Card {
|
|
2
|
+
rank: string;
|
|
3
|
+
suit: 'S' | 'H' | 'D' | 'C';
|
|
4
|
+
}
|
|
5
|
+
export type HiddenCard = {
|
|
6
|
+
hidden: true;
|
|
7
|
+
};
|
|
8
|
+
export interface HandView {
|
|
9
|
+
cards: Card[];
|
|
10
|
+
total: number;
|
|
11
|
+
soft: boolean;
|
|
12
|
+
doubled: boolean;
|
|
13
|
+
done: boolean;
|
|
14
|
+
outcome: string | null;
|
|
15
|
+
}
|
|
16
|
+
export interface RoundView {
|
|
17
|
+
id: string;
|
|
18
|
+
mode: 'free' | 'raise';
|
|
19
|
+
phase: 'insurance' | 'player' | 'settled';
|
|
20
|
+
betChips: number;
|
|
21
|
+
hands: HandView[];
|
|
22
|
+
dealer: {
|
|
23
|
+
cards: (Card | HiddenCard)[];
|
|
24
|
+
total?: number;
|
|
25
|
+
};
|
|
26
|
+
actions: string[];
|
|
27
|
+
payoutChips?: number;
|
|
28
|
+
pointsGranted?: number;
|
|
29
|
+
poolShortfall?: boolean;
|
|
30
|
+
}
|
|
31
|
+
export interface MeView {
|
|
32
|
+
playerId: string;
|
|
33
|
+
chips: number;
|
|
34
|
+
exchangedChips: number;
|
|
35
|
+
points: number;
|
|
36
|
+
freeHandsRemaining: number;
|
|
37
|
+
seasonId: string;
|
|
38
|
+
sponsorText: string;
|
|
39
|
+
poolEmpty: boolean;
|
|
40
|
+
exchangeThresholdChips: number;
|
|
41
|
+
baseBetChips: number;
|
|
42
|
+
githubLogin: string | null;
|
|
43
|
+
}
|
|
44
|
+
/** Every failure this client surfaces: HTTP status plus the server's error code. */
|
|
45
|
+
export declare class ApiError extends Error {
|
|
46
|
+
readonly status: number;
|
|
47
|
+
readonly code?: string | undefined;
|
|
48
|
+
constructor(status: number, code?: string | undefined, message?: string);
|
|
49
|
+
}
|
|
50
|
+
export interface Api {
|
|
51
|
+
register(): Promise<{
|
|
52
|
+
playerId: string;
|
|
53
|
+
token: string;
|
|
54
|
+
}>;
|
|
55
|
+
me(token: string): Promise<MeView>;
|
|
56
|
+
activeRound(token: string): Promise<RoundView | undefined>;
|
|
57
|
+
startRound(token: string, body: {
|
|
58
|
+
mode: 'free' | 'raise';
|
|
59
|
+
betChips?: number;
|
|
60
|
+
}): Promise<RoundView>;
|
|
61
|
+
act(token: string, roundId: string, action: string): Promise<RoundView>;
|
|
62
|
+
/**
|
|
63
|
+
* Exchange `chips` of game balance for spendable quota directly, with no
|
|
64
|
+
* browser round-trip. The server requires an already-bound GitHub identity
|
|
65
|
+
* (403 `github binding required`), so this is the SECOND exchange path — the
|
|
66
|
+
* one an already-bound player takes on every repeat exchange. First-time
|
|
67
|
+
* binding still has to go through `startPairing` and the browser, because
|
|
68
|
+
* only a browser can complete GitHub's authorization-code flow.
|
|
69
|
+
*/
|
|
70
|
+
exchange(token: string, chips: number): Promise<{
|
|
71
|
+
chips: number;
|
|
72
|
+
exchangedChips: number;
|
|
73
|
+
note: string;
|
|
74
|
+
}>;
|
|
75
|
+
startPairing(token: string): Promise<{
|
|
76
|
+
code: string;
|
|
77
|
+
url: string;
|
|
78
|
+
expiresInSeconds: number;
|
|
79
|
+
}>;
|
|
80
|
+
chatCompletionsUrl(): string;
|
|
81
|
+
}
|
|
82
|
+
export declare function createApi(opts: {
|
|
83
|
+
serverUrl: string;
|
|
84
|
+
fetchImpl?: typeof fetch;
|
|
85
|
+
}): Api;
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** Every failure this client surfaces: HTTP status plus the server's error code. */
|
|
2
|
+
export class ApiError extends Error {
|
|
3
|
+
status;
|
|
4
|
+
code;
|
|
5
|
+
constructor(status, code, message) {
|
|
6
|
+
super(message ?? `api error ${status}${code ? `: ${code}` : ''}`);
|
|
7
|
+
this.status = status;
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.name = 'ApiError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function createApi(opts) {
|
|
13
|
+
const base = opts.serverUrl.replace(/\/+$/, '');
|
|
14
|
+
const f = opts.fetchImpl ?? fetch;
|
|
15
|
+
async function call(path, init = {}) {
|
|
16
|
+
const { token, ...rest } = init;
|
|
17
|
+
const headers = new Headers(rest.headers);
|
|
18
|
+
if (token)
|
|
19
|
+
headers.set('authorization', `Bearer ${token}`);
|
|
20
|
+
if (rest.body)
|
|
21
|
+
headers.set('content-type', 'application/json');
|
|
22
|
+
let res;
|
|
23
|
+
try {
|
|
24
|
+
res = await f(`${base}${path}`, { ...rest, headers });
|
|
25
|
+
}
|
|
26
|
+
catch (cause) {
|
|
27
|
+
throw new ApiError(0, undefined, `cannot reach ${base}: ${cause.message}`);
|
|
28
|
+
}
|
|
29
|
+
if (res.status === 204)
|
|
30
|
+
return undefined;
|
|
31
|
+
const text = await res.text();
|
|
32
|
+
const body = text ? safeParse(text) : {};
|
|
33
|
+
if (!res.ok)
|
|
34
|
+
throw new ApiError(res.status, typeof body?.error === 'string' ? body.error : undefined);
|
|
35
|
+
return body;
|
|
36
|
+
}
|
|
37
|
+
function safeParse(text) {
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(text);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
register: () => call('/api/register', { method: 'POST' }),
|
|
47
|
+
me: (token) => call('/api/me', { token }),
|
|
48
|
+
activeRound: (token) => call('/api/round/active', { token }),
|
|
49
|
+
startRound: (token, body) => call('/api/round/start', { method: 'POST', token, body: JSON.stringify(body) }),
|
|
50
|
+
act: (token, roundId, action) => call(`/api/round/${encodeURIComponent(roundId)}/action`, { method: 'POST', token, body: JSON.stringify({ action }) }),
|
|
51
|
+
exchange: (token, chips) => call('/api/exchange', { method: 'POST', token, body: JSON.stringify({ chips }) }),
|
|
52
|
+
startPairing: (token) => call('/api/pairing/start', { method: 'POST', token }),
|
|
53
|
+
chatCompletionsUrl: () => `${base}/v1/chat/completions`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { MeView, RoundView } from '../api.js';
|
|
2
|
+
/**
|
|
3
|
+
* Pure presentational graphical blackjack table, rendered inside the
|
|
4
|
+
* `conversation.chat.commandview` slot for the `/blackjack` command row.
|
|
5
|
+
*
|
|
6
|
+
* Consent lives entirely on the command plane: before `state.consented` is
|
|
7
|
+
* true this component renders ONLY the pointer to `/blackjack agree` — no
|
|
8
|
+
* play controls, and (deliberately) no button that could grant consent from
|
|
9
|
+
* the browser, matching the host route's 403-before-consent behaviour
|
|
10
|
+
* (`routes.ts`).
|
|
11
|
+
*
|
|
12
|
+
* All player-facing copy here must stay within the same red lines as the
|
|
13
|
+
* command-plane text renderer (`render.ts`): no gambling vocabulary, no
|
|
14
|
+
* model-vendor branding tied to the game, CHIP (never RMB) as the only unit.
|
|
15
|
+
* @module dsh-blackjack/client/Table
|
|
16
|
+
*/
|
|
17
|
+
/** Shape of the `GET /blackjack/api/state` response (see `routes.ts`). */
|
|
18
|
+
export interface BlackjackApiState {
|
|
19
|
+
consented: boolean;
|
|
20
|
+
me?: MeView;
|
|
21
|
+
round?: RoundView;
|
|
22
|
+
}
|
|
23
|
+
export interface BlackjackTableProps {
|
|
24
|
+
state: BlackjackApiState;
|
|
25
|
+
onAction: (action: string) => void;
|
|
26
|
+
onDeal: (betChips?: number) => void;
|
|
27
|
+
/** Brief inline notice for a failed action/deal call (container-owned; cleared on the next successful one). */
|
|
28
|
+
error?: string;
|
|
29
|
+
}
|
|
30
|
+
export declare function BlackjackTable({ state, onAction, onDeal, error }: BlackjackTableProps): JSX.Element;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
const SUITS = { S: '♠', H: '♥', D: '♦', C: '♣' };
|
|
3
|
+
const RED_SUITS = new Set(['H', 'D']);
|
|
4
|
+
const n = (v) => v.toLocaleString('en-US');
|
|
5
|
+
const ACTION_LABELS = {
|
|
6
|
+
hit: '要牌', stand: '停牌', double: '双倍', split: '分牌', insure: '买保险', decline: '不买保险',
|
|
7
|
+
};
|
|
8
|
+
const CSS = `
|
|
9
|
+
.bj-table { --bj-felt: #0f3d2e; --bj-line: rgba(255,255,255,.18); font-size: 14px; }
|
|
10
|
+
.bj-hand { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; margin: 4px 0; }
|
|
11
|
+
.bj-card {
|
|
12
|
+
width: 38px; height: 54px; border-radius: 6px; display: flex; align-items: center;
|
|
13
|
+
justify-content: center; font-weight: 700; font-size: 15px; box-shadow: 0 1px 2px rgba(0,0,0,.35);
|
|
14
|
+
background: #fdfdf8; border: 1px solid rgba(0,0,0,.15);
|
|
15
|
+
}
|
|
16
|
+
.bj-card-red { color: #c0392b; }
|
|
17
|
+
.bj-card-black { color: #1a1a1a; }
|
|
18
|
+
.bj-card-back {
|
|
19
|
+
background-image: repeating-linear-gradient(45deg, #2a5aa0, #2a5aa0 4px, #1d4278 4px, #1d4278 8px);
|
|
20
|
+
border: 1px solid rgba(255,255,255,.4);
|
|
21
|
+
}
|
|
22
|
+
.bj-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }
|
|
23
|
+
.bj-actions button {
|
|
24
|
+
font: inherit; padding: 6px 14px; border-radius: 6px; border: 1px solid var(--bj-line);
|
|
25
|
+
background: rgba(255,255,255,.08); color: inherit; cursor: pointer;
|
|
26
|
+
}
|
|
27
|
+
.bj-actions button:hover { background: rgba(255,255,255,.16); }
|
|
28
|
+
.bj-meta { opacity: .85; margin-bottom: 8px; }
|
|
29
|
+
.bj-error {
|
|
30
|
+
color: #c0392b; border: 1px solid rgba(192,57,43,.4); border-radius: 6px;
|
|
31
|
+
padding: 4px 8px; margin-bottom: 8px;
|
|
32
|
+
}
|
|
33
|
+
`;
|
|
34
|
+
function CardFace({ card }) {
|
|
35
|
+
if ('hidden' in card) {
|
|
36
|
+
return _jsx("div", { className: "bj-card bj-card-back", "aria-label": "\u6697\u724C" });
|
|
37
|
+
}
|
|
38
|
+
const suit = SUITS[card.suit] ?? card.suit;
|
|
39
|
+
const cls = RED_SUITS.has(card.suit) ? 'bj-card-red' : 'bj-card-black';
|
|
40
|
+
return _jsx("div", { className: `bj-card ${cls}`, children: `${card.rank}${suit}` });
|
|
41
|
+
}
|
|
42
|
+
function HandRow({ hand, label }) {
|
|
43
|
+
const soft = hand.soft ? '软' : '';
|
|
44
|
+
return (_jsxs("div", { children: [_jsxs("div", { className: "bj-meta", children: [label, "\uFF08", soft, hand.total, "\uFF09", hand.doubled ? ' [双倍]' : ''] }), _jsx("div", { className: "bj-hand", children: hand.cards.map((card, i) => (_jsx(CardFace, { card: card }, i))) })] }));
|
|
45
|
+
}
|
|
46
|
+
function DealerRow({ dealer }) {
|
|
47
|
+
return (_jsxs("div", { children: [_jsxs("div", { className: "bj-meta", children: ["\u5E84\u5BB6", dealer.total !== undefined ? `(${dealer.total})` : ''] }), _jsx("div", { className: "bj-hand", children: dealer.cards.map((card, i) => (_jsx(CardFace, { card: card }, i))) })] }));
|
|
48
|
+
}
|
|
49
|
+
function ConsentPrompt() {
|
|
50
|
+
return (_jsx("div", { className: "bj-table", children: _jsxs("p", { children: ["\u8BF7\u5148\u5728\u547D\u4EE4\u884C\u8F93\u5165 ", _jsx("code", { children: "/blackjack agree" }), "\uFF0C\u9605\u8BFB\u5E76\u540C\u610F\u89C4\u5219\u540E\u624D\u80FD\u5F00\u59CB\u3002"] }) }));
|
|
51
|
+
}
|
|
52
|
+
function MeHeader({ me }) {
|
|
53
|
+
return (_jsxs("div", { className: "bj-meta", children: ["\u4F59\u989D\uFF1A", n(me.chips), " CHIP\u3000\u4ECA\u65E5\u5269\u4F59\u514D\u8D39\u624B\uFF1A", me.freeHandsRemaining, me.points > 0 ? ` 积分:${n(me.points)}` : '', me.poolEmpty ? '(本期奖池已发完,现在赢牌将发放积分)' : '', me.sponsorText ? _jsx("div", { children: me.sponsorText }) : null] }));
|
|
54
|
+
}
|
|
55
|
+
function RoundFooter({ round }) {
|
|
56
|
+
if (round.phase !== 'settled')
|
|
57
|
+
return null;
|
|
58
|
+
const payout = round.payoutChips ?? 0;
|
|
59
|
+
return (_jsxs("div", { className: "bj-meta", children: [payout > 0 ? `本手赢得 ${n(payout)} CHIP。` : '本手没有收获,明天再来。', round.pointsGranted ? _jsxs("div", { children: ["\u672C\u671F\u5956\u6C60\u5DF2\u53D1\u5B8C\uFF0C\u6539\u4E3A\u53D1\u653E ", n(round.pointsGranted), " \u79EF\u5206\u3002"] }) : null] }));
|
|
60
|
+
}
|
|
61
|
+
function ActionBar({ round, onAction }) {
|
|
62
|
+
if (round.actions.length === 0)
|
|
63
|
+
return null;
|
|
64
|
+
return (_jsx("div", { className: "bj-actions", children: round.actions.map((action) => (_jsx("button", { type: "button", onClick: () => onAction(action), children: ACTION_LABELS[action] ?? action }, action))) }));
|
|
65
|
+
}
|
|
66
|
+
function NoRound({ me, onDeal }) {
|
|
67
|
+
return (_jsxs("div", { children: [_jsx("div", { className: "bj-meta", children: "\u5F53\u524D\u6CA1\u6709\u8FDB\u884C\u4E2D\u7684\u724C\u5C40\u3002" }), _jsxs("div", { className: "bj-actions", children: [_jsx("button", { type: "button", onClick: () => onDeal(), children: "\u5F00\u59CB\u4E00\u5C40\uFF08\u9650\u514D\uFF09" }), me ? (_jsxs("button", { type: "button", onClick: () => onDeal(me.baseBetChips), children: ["\u5F00\u59CB\u4E00\u5C40\uFF08", n(me.baseBetChips), " CHIP\uFF09"] })) : null] })] }));
|
|
68
|
+
}
|
|
69
|
+
export function BlackjackTable({ state, onAction, onDeal, error }) {
|
|
70
|
+
if (!state.consented)
|
|
71
|
+
return _jsx(ConsentPrompt, {});
|
|
72
|
+
const { me, round } = state;
|
|
73
|
+
return (_jsxs("div", { className: "bj-table", children: [_jsx("style", { children: CSS }), error ? _jsx("div", { className: "bj-error", children: error }) : null, me ? _jsx(MeHeader, { me: me }) : null, round ? (_jsxs("div", { children: [round.hands.map((hand, i) => (_jsx(HandRow, { hand: hand, label: round.hands.length > 1 ? `你的第 ${i + 1} 手` : '你的牌' }, i))), _jsx(DealerRow, { dealer: round.dealer }), _jsx(RoundFooter, { round: round }), _jsx(ActionBar, { round: round, onAction: onAction })] })) : (_jsx(NoRound, { me: me, onDeal: onDeal }))] }));
|
|
74
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
/**
|
|
3
|
+
* Client module entry: registers the graphical table into the KEYED
|
|
4
|
+
* `conversation.chat.commandview` slot under `key: 'blackjack'`, so every
|
|
5
|
+
* `/blackjack` command row in the conversation renders `BlackjackTable`
|
|
6
|
+
* instead of (or alongside) the plain-text reply. Registration shape
|
|
7
|
+
* mirrors `dsh-statecore`'s client module (`dist/client.js`), the only
|
|
8
|
+
* difference being the keyed slot needs a `key` instead of an `id`/`order`.
|
|
9
|
+
* @module dsh-blackjack/client
|
|
10
|
+
*/
|
|
11
|
+
export declare const inject: string[];
|
|
12
|
+
export declare function apply(ctx: Context): void;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
3
|
+
import { BlackjackTable } from './Table.js';
|
|
4
|
+
/**
|
|
5
|
+
* Client module entry: registers the graphical table into the KEYED
|
|
6
|
+
* `conversation.chat.commandview` slot under `key: 'blackjack'`, so every
|
|
7
|
+
* `/blackjack` command row in the conversation renders `BlackjackTable`
|
|
8
|
+
* instead of (or alongside) the plain-text reply. Registration shape
|
|
9
|
+
* mirrors `dsh-statecore`'s client module (`dist/client.js`), the only
|
|
10
|
+
* difference being the keyed slot needs a `key` instead of an `id`/`order`.
|
|
11
|
+
* @module dsh-blackjack/client
|
|
12
|
+
*/
|
|
13
|
+
export const inject = ['slots'];
|
|
14
|
+
async function getJson(path, init) {
|
|
15
|
+
const res = await fetch(path, init);
|
|
16
|
+
const text = await res.text();
|
|
17
|
+
const body = text.length > 0 ? JSON.parse(text) : {};
|
|
18
|
+
if (!res.ok) {
|
|
19
|
+
const error = typeof body?.error === 'string' ? body.error : `${res.status}`;
|
|
20
|
+
throw new Error(error);
|
|
21
|
+
}
|
|
22
|
+
return body;
|
|
23
|
+
}
|
|
24
|
+
/** Brief, vocabulary-safe notice shown when an action/deal call fails; never echoes the raw server error text. */
|
|
25
|
+
const ACTION_FAILED_NOTICE = '上一次操作没有成功,已为你刷新到最新状态。';
|
|
26
|
+
/** Container: owns data fetching. Refetches state after every action — no polling timer. */
|
|
27
|
+
function BlackjackTableContainer() {
|
|
28
|
+
const [state, setState] = useState(null);
|
|
29
|
+
const [actionError, setActionError] = useState(null);
|
|
30
|
+
const refresh = useCallback(() => {
|
|
31
|
+
getJson('/blackjack/api/state')
|
|
32
|
+
.then(setState)
|
|
33
|
+
.catch(() => setState({ consented: false }));
|
|
34
|
+
}, []);
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
refresh();
|
|
37
|
+
}, [refresh]);
|
|
38
|
+
const runAction = useCallback((path, body) => {
|
|
39
|
+
getJson(path, {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: { 'content-type': 'application/json' },
|
|
42
|
+
body: JSON.stringify(body),
|
|
43
|
+
})
|
|
44
|
+
.then(() => {
|
|
45
|
+
setActionError(null);
|
|
46
|
+
refresh();
|
|
47
|
+
})
|
|
48
|
+
.catch(() => {
|
|
49
|
+
// A failed action (including the 404 "no active round" case) still
|
|
50
|
+
// resyncs state so the UI never shows a stale action bar, but the
|
|
51
|
+
// notice stays up until the next successful action clears it.
|
|
52
|
+
setActionError(ACTION_FAILED_NOTICE);
|
|
53
|
+
refresh();
|
|
54
|
+
});
|
|
55
|
+
}, [refresh]);
|
|
56
|
+
const onDeal = useCallback((betChips) => runAction('/blackjack/api/deal', betChips === undefined ? {} : { betChips }), [runAction]);
|
|
57
|
+
const onAction = useCallback((action) => runAction('/blackjack/api/action', { action }), [runAction]);
|
|
58
|
+
if (state === null)
|
|
59
|
+
return null;
|
|
60
|
+
return _jsx(BlackjackTable, { state: state, onAction: onAction, onDeal: onDeal, error: actionError ?? undefined });
|
|
61
|
+
}
|
|
62
|
+
export function apply(ctx) {
|
|
63
|
+
ctx.slots.inject('conversation.chat.commandview', () => ctx.slots.register({
|
|
64
|
+
name: 'conversation.chat.commandview',
|
|
65
|
+
key: 'blackjack',
|
|
66
|
+
inject: () => ({}),
|
|
67
|
+
}, BlackjackTableContainer));
|
|
68
|
+
}
|