@cx00123/lunetale-game-sdk 0.1.0 → 0.2.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/README.md +74 -36
- package/package.json +5 -3
- package/src/index.d.ts +32 -0
- package/src/index.js +36 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Lunetale
|
|
1
|
+
# Lunetale 小游戏角色 / AI / VIP 插件
|
|
2
2
|
|
|
3
|
-
为 Lunetale
|
|
3
|
+
为 Lunetale 网页小游戏提供所选角色资料、角色长期记忆对话、通用 AI 和真实 VIP/SVIP 查询。0.2.0 新增角色接口,保留原有 `chat()`。原生接入支持新版 Android / iOS App 的在线游戏;游戏不需要 App 用户 ID、App 登录 token 或模型 API Key。
|
|
4
4
|
|
|
5
5
|
## 1. 安装与最小接入
|
|
6
6
|
|
|
@@ -11,7 +11,7 @@ npm install @cx00123/lunetale-game-sdk
|
|
|
11
11
|
```js
|
|
12
12
|
import { createGameClient, GameSDKError } from '@cx00123/lunetale-game-sdk'
|
|
13
13
|
|
|
14
|
-
// Android 内无需填写域名、用户、游戏 ID 或密钥。
|
|
14
|
+
// Android / iOS App 内无需填写域名、用户、游戏 ID 或密钥。
|
|
15
15
|
const game = createGameClient()
|
|
16
16
|
const membership = await game.getVIP()
|
|
17
17
|
console.log(membership.isVip, membership.tier, membership.cost)
|
|
@@ -43,7 +43,7 @@ try {
|
|
|
43
43
|
|
|
44
44
|
```html
|
|
45
45
|
<script type="module">
|
|
46
|
-
import { createGameClient } from 'https://cdn.jsdelivr.net/npm/@cx00123/lunetale-game-sdk@0.
|
|
46
|
+
import { createGameClient } from 'https://cdn.jsdelivr.net/npm/@cx00123/lunetale-game-sdk@0.2.0/src/index.js'
|
|
47
47
|
const game = createGameClient()
|
|
48
48
|
const vip = await game.getVIP()
|
|
49
49
|
document.querySelector('#vip').textContent = vip.isVip ? '会员' : '普通用户'
|
|
@@ -52,38 +52,63 @@ try {
|
|
|
52
52
|
|
|
53
53
|
SDK 不包含 UI,也不会自动执行模型回复。请用文本绑定或 `textContent` 展示;不要把回复直接写入 `innerHTML`、`eval`。
|
|
54
54
|
|
|
55
|
+
### 与 App 所选角色对话(0.2.0+)
|
|
56
|
+
|
|
57
|
+
管理员先审核游戏并启用 `characterEnabled`;玩家在 App 进入游戏前选择**角色和聊天身份**,明确同意使用跨场景记忆。角色游戏首期需要联网进入。
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
const character = await game.getSelectedCharacter()
|
|
61
|
+
if (character) {
|
|
62
|
+
document.querySelector('#name').textContent = character.nickname
|
|
63
|
+
document.querySelector('#portrait').src = character.standingImageUrl || character.avatarUrl
|
|
64
|
+
const input = {
|
|
65
|
+
requestId: crypto.randomUUID(),
|
|
66
|
+
message: '还记得上次我们约定去哪里吗?',
|
|
67
|
+
scene: { location: '海边', description: '眼前有一艘停靠的小船' },
|
|
68
|
+
}
|
|
69
|
+
// 保存 input;NETWORK / 42001 时原样重试,不要重新生成 requestId。
|
|
70
|
+
const reply = await game.chatWithCharacter(input)
|
|
71
|
+
document.querySelector('#reply').textContent = reply.content
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- `getSelectedCharacter()` 返回 `{id,name,nickname,intro,avatarUrl,standingImageUrl,backgroundImageUrl,avatars,standingImages,backgroundImages,revision}`,未选角色时返回 `null`。`nickname` 当前等于角色名,不是玩家身份昵称。资料与本上下文的角色人设快照一致;不返回 Persona、身份 ID、凭证或记忆列表。
|
|
76
|
+
- `chatWithCharacter({requestId?,message,scene?})` 返回 `{requestId,content,characterId,messageId,charged,balance}`。只接受本轮用户消息,不接受游戏指定角色/身份、system 或伪造历史。`message` 为 1–4000 个 Unicode 码点;`scene.location` 最多 200、`scene.description` 最多 2000。场景是第三方游戏的描述,不是系统指令或已核实事实。
|
|
77
|
+
- 会话按游戏和程序版本分别保存;长期关系按**账号 + 聊天身份 + 角色**共享。相同三元组可以承接日常私聊、其他游戏及该角色有权获知的剧本经历;其他账号/身份/角色、其他 NPC 私聊不会开放。未通过本接口提交的游戏事件不会自动进入记忆。
|
|
78
|
+
- 当前采用近期记录 + 全范围词法检索及来源摘录,**不是无限记忆或全历史语义搜索**。新单角色对话保留完整受限原文,旧摘录不会自动重建。相关旧经历只作为关系背景,不自动迁移物品、资产、任务、规则或当前状态。
|
|
79
|
+
- **隐私提醒:游戏能读到回复,回复可能提及私聊。** 仅允许审核通过的可信游戏接入,不提供原始记忆查询 API;玩家必须在可信 App 内授权。不要把角色对话输入/回复上传到第三方分析平台。
|
|
80
|
+
- 角色游戏存档按账号/身份/角色/游戏/存档代际独立保存,兼容程序更新继续使用同一存档;不自动导入旧版账号级存档,不向网页暴露这些隔离键。
|
|
81
|
+
- `chat()` 仍为通用模型调用:由游戏提供 messages,不读写角色长期记忆。选择角色不会偷偷改变 `chat()` 的语义。
|
|
82
|
+
|
|
55
83
|
## 2. 平台管理员准备
|
|
56
84
|
|
|
57
|
-
1. 发布支持本 SDK
|
|
58
|
-
2. 「App 管理 →
|
|
59
|
-
3.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
- `runtimeOrigin`:最终运行页面的**精确 HTTPS origin**,例如 `https://games.example.com`,不带路径、末尾 `/`、通配符、用户名或密码。空值使用游戏 HTML 地址的 origin;跨域中转到 CDN 时需登记最终 CDN origin。
|
|
67
|
-
4. 游戏要上架。SDK 必须运行在该 origin 的**顶层页面**;iframe、官网连接页和未登记来源没有原生 AI 桥。
|
|
68
|
-
5. 后台「AI / VIP 插件接入」可查看简版说明。普通后台预览没有玩家授权,也不会偷偷消耗管理员或玩家积分。
|
|
69
|
-
|
|
70
|
-
授权粒度是账号 + 游戏 + origin,不是 CDN 路径。同一 CDN origin 下的页面与脚本处于同一个浏览器信任域;不可信制作者应使用独立 origin。平台只应登记受信任内容,不应在游戏页面加载不可信第三方脚本。
|
|
85
|
+
1. 发布支持本 SDK 的后端并完成游戏定向迁移(先核对目标库和备份;角色/剧本模块须已部署);安装新版 Android / iOS App。
|
|
86
|
+
2. 「App 管理 → 大模型管理」建立并启用模型分组,配置服务端上游密钥,明确填写**小游戏 AI 能量/次**(`pricing.gameAI`):最多一位小数,所有会员同价,0 免费;旧分组未配置时不能调用,不会自动变免费。
|
|
87
|
+
3. 「游戏管理 → 基本资料」的 AI 区域只需打开 `aiEnabled` 并选择 `aiGroup`;模型和能量价格由该分组决定。修改后下一次新调用生效,不随游戏版本发布/回滚改变。
|
|
88
|
+
4. 在版本管理上传/配置最终 HTTPS 游戏入口并发布,再在基本资料上架。SDK 必须运行在入口 origin 的**顶层页面**;精确来源自动推导,不能手工覆盖,iframe、官网连接页和其他来源没有原生 AI 桥。跨域中转应直接配置最终 HTTPS 入口。
|
|
89
|
+
5. 不再设置游戏自定价格、VIP限制、最大输出token或服务端系统提示词。后台「AI 插件接入」可查看简版说明;普通后台预览没有玩家授权,不会消耗管理员或玩家能量。
|
|
90
|
+
|
|
91
|
+
角色接入还需在「基本资料」打开 `characterEnabled`(默认关闭;只能对审核过的可信游戏开启)。用户同意后,App 调用 `character/prepare` 创建持久参与上下文,再随授权绑定,网页不能自行换角。
|
|
92
|
+
|
|
93
|
+
授权绑定账号 + 游戏 + 程序版本 + origin + 可选角色参与上下文;浏览器隔离边界仍是 origin,不是 CDN 路径。同一 CDN origin 下的页面与脚本处于同一个浏览器信任域;不可信制作者应使用独立 origin。平台只应登记受信任内容,不应在游戏页面加载不可信第三方脚本。
|
|
71
94
|
|
|
72
95
|
## 3. 授权与续期
|
|
73
96
|
|
|
74
|
-
### Android
|
|
97
|
+
### Android / iOS(在线)
|
|
75
98
|
|
|
76
|
-
`SDK →
|
|
99
|
+
`SDK → 原生主框架来源校验 → Flutter 当前账号 → 后端授权码 → SDK 兑换专用凭证`
|
|
77
100
|
|
|
78
101
|
- Flutter 用自己的 App JWT 调用 `POST /lunetale/game/authorize`;原生桥不接受网页指定 userId、gameId、模型、价格或任意 URL。
|
|
79
102
|
- 授权码随机生成,**60 秒、只能兑换一次**,数据库只存其哈希。
|
|
80
|
-
- Android 通过消息桥交付授权码,不把 App JWT 或授权码追加到游戏 URL。相比 fragment,原生消息桥避免额外跳转并保持已有 hash 路由。
|
|
103
|
+
- Android / iOS 通过消息桥交付授权码,不把 App JWT 或授权码追加到游戏 URL。相比 fragment,原生消息桥避免额外跳转并保持已有 hash 路由。
|
|
81
104
|
- SDK 兑换**2 小时**游戏专用签名凭证,只放内存;不会写 localStorage、sessionStorage、Cookie 或云存档。
|
|
82
|
-
- 后端检查专用签名/受众/有效期、绑定账号与游戏、精确 origin
|
|
105
|
+
- 后端检查专用签名/受众/有效期、绑定账号与游戏、精确 origin、账号状态、游戏上架、AI开关和模型分组可用性。修改 JSON 中的用户、游戏、价格或模型不能越权(未知字段直接拒绝)。
|
|
83
106
|
- 成功请求完成时,如果凭证**尚未过期且剩余 ≤10 分钟**,响应中携带新的 2 小时凭证,SDK 自动替换。续期失败不把已经完成的扣费请求伪装成失败;下次可重新授权。
|
|
84
107
|
- 已过期:SDK 通过 App 当前登录态重新签发授权码,最多补授权重试一次;App 登录失效则要求用户重新登录。请求取消/账号切换/页面导航后的迟到桥回调不会发送到新页面。
|
|
85
|
-
- App
|
|
86
|
-
- Android 的 Runtime HTTP 请求由 Flutter 固定路径代理;网页即使知道当前开发 API 地址,也不能要求代理访问任意 URL。HTTPS 游戏连接本地 HTTP 开发 API 不需要启用 WebView 混合内容。生产仍必须使用 HTTPS API。
|
|
108
|
+
- App 的正常后端退出登录接口撤销该用户全部小游戏凭证;禁用/注销账号、游戏下架后请求也会被拒绝。程序来源固定本次会话的已发布版本,发布新版本不会切换旧会话;AI分组和价格则读取实时配置。仅断网清除本机登录态无法承诺即时服务端撤销,未撤销凭证仍以服务端有效期为准。
|
|
109
|
+
- Android / iOS 的 Runtime HTTP 请求由 Flutter 固定路径代理;网页即使知道当前开发 API 地址,也不能要求代理访问任意 URL。HTTPS 游戏连接本地 HTTP 开发 API 不需要启用 WebView 混合内容。生产仍必须使用 HTTPS API。
|
|
110
|
+
|
|
111
|
+
iOS 离线缓存使用本地来源,不能冒用已登记的 HTTPS 来源申请角色/AI授权;重新联网并进入游戏后再调用。角色游戏首期不支持离线启动。
|
|
87
112
|
|
|
88
113
|
### 独立浏览器 / 外部可信登录入口
|
|
89
114
|
|
|
@@ -98,9 +123,9 @@ const game = createGameClient({
|
|
|
98
123
|
const vip = await game.getVIP()
|
|
99
124
|
```
|
|
100
125
|
|
|
101
|
-
可信入口在自己的已登录环境调用 `/lunetale/game/authorize`(`x-token: App JWT`,body 为 `{gameId}
|
|
126
|
+
可信入口在自己的已登录环境调用 `/lunetale/game/authorize`(`x-token: App JWT`,body 为 `{gameId,versionId}`;先从 `detail?gameId=...&protocol=2` 获取版本),校验跳转目标等于返回的 origin,再将授权码放进已登记游戏 URL 的 fragment。该入口必须隔离于第三方游戏,不能让游戏拿到 JWT。
|
|
102
127
|
|
|
103
|
-
**当前交付未新增独立官网玩家登录系统。** 顶层官网提供文档入口;离开
|
|
128
|
+
**当前交付未新增独立官网玩家登录系统。** 顶层官网提供文档入口;离开 App 后如果没有可信授权入口,SDK 返回 `AUTH_REQUIRED`,不会伪造游客身份、VIP 或免费 AI。普通网页刷新后丢失内存凭证,需要新授权码;这是预期行为。可用 `authorize` 回调接入已有可信入口,跨窗口通信须严格校验 origin/source,禁止 `postMessage('*')` 传授权码。
|
|
104
129
|
|
|
105
130
|
CDN 的 HTML、图片等静态文件仍可公开访问;这些凭证保护的是账号、VIP 与计费接口,**不是静态文件防下载机制**。
|
|
106
131
|
|
|
@@ -112,22 +137,28 @@ CDN 的 HTML、图片等静态文件仍可公开访问;这些凭证保护的
|
|
|
112
137
|
| `ready()` | 完成授权;其它方法也会按需自动授权 |
|
|
113
138
|
| `getVIP()` | `{isVip,tier,expiresAt,balance,aiEnabled,cost,vipOnly}` |
|
|
114
139
|
| `isVIP()` | `boolean`;有效 VIP、SVIP 都是 true,查询失败会抛异常,不伪装为 false |
|
|
115
|
-
| `chat({requestId?,messages})` |
|
|
140
|
+
| `chat({requestId?,messages})` | 通用 AI,`{requestId,content,charged,balance}`;不写角色记忆 |
|
|
141
|
+
| `getSelectedCharacter()` | 所选角色展示快照或 `null` |
|
|
142
|
+
| `chatWithCharacter({requestId?,message,scene?})` | `{requestId,content,characterId,messageId,charged,balance}`;服务端管理历史与长期记忆 |
|
|
116
143
|
| `dispose()` | 清除本页凭证并取消客户端等待,实例不能再次使用;不是退款或服务端撤销 |
|
|
117
144
|
|
|
118
|
-
`messages`:1–64 条,role 为 `system/user/assistant`,内容非空,UTF-8 内容合计最多 64 KiB,最后一条必须是 user。每轮需要游戏自己提供所需的历史;SDK
|
|
145
|
+
`messages`:1–64 条,role 为 `system/user/assistant`,内容非空,UTF-8 内容合计最多 64 KiB,最后一条必须是 user。每轮需要游戏自己提供所需的历史;SDK 不自动管理历史。服务端不再前置管理员系统提示词,也不覆盖模型的输出token默认值。
|
|
146
|
+
|
|
147
|
+
`getVIP().cost` 是当前分组小游戏AI单次能量;分组不可用时 `aiEnabled=false,cost=0`,不能据此认为免费。`vipOnly` 为兼容字段,固定 false;会员身份查询功能不变。
|
|
119
148
|
|
|
120
149
|
`requestId`:16–64 位字母、数字、`_`、`-`,推荐 `crypto.randomUUID()`。省略时 SDK 自动生成;错误对象携带该编号。推荐显式创建并保存 `input`,保证网络重试不改变编号或历史消息。
|
|
121
150
|
|
|
122
151
|
### 计费 / 并发 / 重试
|
|
123
152
|
|
|
124
|
-
-
|
|
153
|
+
- 使用**请求开始时**所选模型分组的小游戏AI价格,所有会员同价;本次模型与价格不会中途切换。上游成功且结果与能量事务一起提交后才收费;余额不足、分组未定价/停用、无权限、模型失败、超时不收费。
|
|
125
154
|
- 同一账号最多一个进行中的小游戏 AI 请求,且新请求间隔至少 2 秒(含免费调用);这是全后端实例共享的数据库控制。
|
|
126
155
|
- 相同账号 + 游戏 + requestId + 原始 messages 重试返回原成功结果,**不重复调用模型或扣费**。原成功响应中的 balance 是当次历史余额,当前余额请调用 getVIP。
|
|
127
156
|
- 相同 requestId 更换内容会被拒绝。不同的实际问题必须生成新编号。
|
|
128
157
|
- 响应丢失不代表未扣费:`NETWORK` 或 `42001` 请保留原 input,稍后重试。SDK 不会无限自动重试付费请求。
|
|
129
158
|
- 模型最长等待约 2 分钟;记录超过 3 分钟仍 pending 视为未完成终态,不再自动执行该编号。`42002` 确认本编号未提交扣费,可用新编号重试。
|
|
130
|
-
- 成功结果与扣费原子提交;故障遗留的 pending
|
|
159
|
+
- 成功结果与扣费原子提交;故障遗留的 pending 记录没有扣费。通用 chat 保存成功回复和内容哈希,不保存原始 messages。
|
|
160
|
+
- 角色对话还将轮次、来源记忆和关系互动更新纳入同一个扣费事务,使用同一个小游戏 AI 价格,不叠加私聊收费;与私聊/剧本共用关系生成锁。
|
|
161
|
+
- 角色对话幂等范围是服务端绑定的参与上下文 + requestId;message 和 scene 必须原样保留。跨版本或换角色后属于新上下文,不能拿旧队列当作原请求重试。
|
|
131
162
|
- 页面刷新后 SDK 不保存请求队列。若游戏需要跨刷新恢复,把 requestId 和原始 messages 存在**该游戏自己的待同步记录**中(不包含任何凭证),重新授权后重试。已有 App localStorage 云存档能力独立工作,SDK 不收集账号存储。
|
|
132
163
|
- 页面离开、网络断开和 `dispose()` 只停止客户端等待,不能撤回已在服务端成功提交的生成和扣费。
|
|
133
164
|
|
|
@@ -137,11 +168,13 @@ CDN 的 HTML、图片等静态文件仍可公开访问;这些凭证保护的
|
|
|
137
168
|
| --- | --- |
|
|
138
169
|
| `AUTH_REQUIRED` | 专用凭证失效,App 内自动补授权一次后仍失败则要求重新登录/重新打开 |
|
|
139
170
|
| `40201` | 积分不足,引导用户在 App 充值 |
|
|
140
|
-
| `40301` |
|
|
171
|
+
| `40301` | 旧版 VIP 限制错误;新版小游戏 AI 不再产生 |
|
|
141
172
|
| `42001` | 原编号仍在处理,稍后用相同 input 重试 |
|
|
142
173
|
| `42002` | 原编号失败且未扣费,可创建新编号 |
|
|
143
174
|
| `42003` | 原编号已绑定不同消息,修复编号复用问题 |
|
|
144
|
-
| `42901` |
|
|
175
|
+
| `42901` | 有进行中的请求(含同关系的私聊/剧本)或请求过于频繁,等待后重试 |
|
|
176
|
+
| `43001` | 本次游戏未绑定角色,请回 App 选角并授权 |
|
|
177
|
+
| `43002` | 游戏未通过角色与记忆接入授权,联系平台审核 |
|
|
145
178
|
| `7` | 游戏配置/参数/服务不可用,展示 message 并联系平台 |
|
|
146
179
|
| `NETWORK` | 结果未知,保留原 requestId 重试 |
|
|
147
180
|
| `NATIVE / UNSUPPORTED / ORIGIN / CONFIG / VALIDATION / PROTOCOL / DISPOSED / UNKNOWN` | 原生能力、运行环境、来源、参数、协议或生命周期问题 |
|
|
@@ -154,18 +187,23 @@ CDN 的 HTML、图片等静态文件仍可公开访问;这些凭证保护的
|
|
|
154
187
|
|
|
155
188
|
| 方法 | 路径 | 鉴权 | 请求 |
|
|
156
189
|
| --- | --- | --- | --- |
|
|
157
|
-
| POST | `/lunetale/game/
|
|
190
|
+
| POST | `/lunetale/game/character/prepare` | App `x-token` | `{gameId,versionId,characterId,identityId,memoryConsent:true}` → `{playContextId,storageKey,hasProgress,progress}`;仅可信启动器 |
|
|
191
|
+
| POST | `/lunetale/game/authorize` | App `x-token` | `{gameId,versionId,playContextId?}` → `{code,expiresAt,origin}`;省略版本只兼容初始版本 |
|
|
158
192
|
| POST | `/lunetale/game/revoke` | App `x-token` | `{gameId}`,撤销该账号该游戏全部专用授权 |
|
|
159
193
|
| POST | `/lunetale/game/runtime/exchange` | `Origin` + 一次性 code | `{code}` → `{accessToken,expiresAt,gameId}` |
|
|
160
194
|
| GET | `/lunetale/game/runtime/vip` | `Origin` + `Authorization: Bearer <游戏凭证>` | VIPStatus,可选 credential |
|
|
161
195
|
| POST | `/lunetale/game/runtime/chat` | 同上 | `{requestId,messages}` → ChatResult,可选 credential |
|
|
196
|
+
| GET | `/lunetale/game/runtime/character` | 同上 | `{character: SelectedCharacter|null}`,可选 credential |
|
|
197
|
+
| POST | `/lunetale/game/runtime/character-chat` | 同上 | `{requestId,message,scene?}` → CharacterChatResult,可选 credential |
|
|
162
198
|
|
|
163
199
|
`credential` 是自动续期数据,形状同 exchange;SDK 自动消费并从公开返回对象中去掉。不允许客户端传 gameId/userId/model/cost/maxTokens 给 runtime/chat。Runtime 凭证签名使用用途隔离的密钥,不能用于 App 或后台接口。
|
|
164
200
|
|
|
165
|
-
浏览器自动发送 Origin
|
|
201
|
+
浏览器自动发送 Origin,游戏不可自行设置。上述专用 runtime 端点提供无 Cookie 的 HTTPS 跨域预检,实际请求仍严格校验凭证绑定来源;不会扩大普通 App/后台接口 CORS 权限。反向代理不能移除 Origin 或记录 Authorization、授权 body 和对话正文。
|
|
166
202
|
|
|
167
203
|
## 6. 人工验收建议
|
|
168
204
|
|
|
169
|
-
由平台和制作者在目标 Android/后端环境验证:普通账号/VIP/SVIP、会员过期、余额不足、0 积分、重复请求不重复扣费、相同 ID 不同消息、上游超时、返回/刷新/快速重进、换账号、游戏下架、错误 origin、iframe 隔离、授权码重复兑换/过期、10 分钟阈值续期及新授权后恢复原请求。
|
|
205
|
+
由平台和制作者在目标 Android / iOS /后端环境验证:普通账号/VIP/SVIP、会员过期、余额不足、0 积分、重复请求不重复扣费、相同 ID 不同消息、上游超时、返回/刷新/快速重进、换账号、游戏下架、错误 origin、iframe 隔离、授权码重复兑换/过期、10 分钟阈值续期及新授权后恢复原请求。
|
|
206
|
+
|
|
207
|
+
角色接入另需验证:App 选角/身份/取消授权;头像与立绘;私聊 → 游戏1 → 游戏2 → 私聊/剧本的同关系记忆承接;换账号/身份/角色不串记忆或存档;游戏禁用角色授权、来源伪造、余额不足、并发、遗忘记忆、失败不扣费及重复请求只扣一次。
|
|
170
208
|
|
|
171
|
-
|
|
209
|
+
此包的静态语法/类型检查及隔离自动测试不代表 App、真实模型调用、真实扣费或会员功能已验收。部署前请完成真实环境人工联调。
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cx00123/lunetale-game-sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Lunetale HTML game
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Lunetale HTML game selected-character, memory chat, AI and VIP SDK for Android/iOS and authorized web games",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"types": "./src/index.d.ts",
|
|
@@ -35,7 +35,9 @@
|
|
|
35
35
|
"registry": "https://registry.npmjs.org/"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
|
-
"check": "node --check src/index.js && tsc --noEmit --strict --lib es2022,dom src/index.d.ts"
|
|
38
|
+
"check": "node --check src/index.js && tsc --noEmit --strict --lib es2022,dom src/index.d.ts",
|
|
39
|
+
"test": "node --test",
|
|
40
|
+
"prepublishOnly": "npm run check && npm test"
|
|
39
41
|
},
|
|
40
42
|
"devDependencies": {
|
|
41
43
|
"typescript": "^7.0.2"
|
package/src/index.d.ts
CHANGED
|
@@ -17,7 +17,9 @@ export interface VIPStatus {
|
|
|
17
17
|
expiresAt: string | null
|
|
18
18
|
balance: number
|
|
19
19
|
aiEnabled: boolean
|
|
20
|
+
/** Current group's game-AI energy per successful call; check aiEnabled first. */
|
|
20
21
|
cost: number
|
|
22
|
+
/** @deprecated Compatibility field; always false with live group-based game pricing. */
|
|
21
23
|
vipOnly: boolean
|
|
22
24
|
}
|
|
23
25
|
export interface ChatInput {
|
|
@@ -32,6 +34,32 @@ export interface ChatResult {
|
|
|
32
34
|
/** Balance at the original successful transaction; replay does not refresh it. */
|
|
33
35
|
balance: number
|
|
34
36
|
}
|
|
37
|
+
export interface SelectedCharacter {
|
|
38
|
+
id: number
|
|
39
|
+
name: string
|
|
40
|
+
/** Currently equals name; not the player's identity nickname. */
|
|
41
|
+
nickname: string
|
|
42
|
+
intro: string
|
|
43
|
+
avatarUrl: string
|
|
44
|
+
standingImageUrl: string
|
|
45
|
+
backgroundImageUrl: string
|
|
46
|
+
avatars: string[]
|
|
47
|
+
standingImages: string[]
|
|
48
|
+
backgroundImages: string[]
|
|
49
|
+
revision: number
|
|
50
|
+
}
|
|
51
|
+
export interface CharacterChatInput {
|
|
52
|
+
/** Preserve this ID AND the exact input on network/pending retries. */
|
|
53
|
+
requestId?: string
|
|
54
|
+
/** Current player message only; server owns persona and history. Max 4000 code points. */
|
|
55
|
+
message: string
|
|
56
|
+
/** Untrusted game scene, not a system instruction or verified permanent fact. */
|
|
57
|
+
scene?: { location?: string; description?: string }
|
|
58
|
+
}
|
|
59
|
+
export interface CharacterChatResult extends ChatResult {
|
|
60
|
+
characterId: number
|
|
61
|
+
messageId: number
|
|
62
|
+
}
|
|
35
63
|
export class GameSDKError extends Error {
|
|
36
64
|
code: string | number
|
|
37
65
|
requestId?: string
|
|
@@ -43,6 +71,10 @@ export interface GameClient {
|
|
|
43
71
|
/** Both active VIP and SVIP count as true. */
|
|
44
72
|
isVIP(): Promise<boolean>
|
|
45
73
|
chat(input: ChatInput): Promise<ChatResult>
|
|
74
|
+
/** Frozen display profile bound by the App; null for an unbound legacy game. */
|
|
75
|
+
getSelectedCharacter(): Promise<SelectedCharacter | null>
|
|
76
|
+
/** Uses and commits this character's cross-scene memory; one game-AI charge. */
|
|
77
|
+
chatWithCharacter(input: CharacterChatInput): Promise<CharacterChatResult>
|
|
46
78
|
/** Clears memory and cancels local waiting, not a server-side revoke/refund. */
|
|
47
79
|
dispose(): void
|
|
48
80
|
}
|
package/src/index.js
CHANGED
|
@@ -117,12 +117,12 @@ export function createGameClient(options = {}) {
|
|
|
117
117
|
const timer = setTimeout(() => controller.abort(), timeout)
|
|
118
118
|
try {
|
|
119
119
|
const response = await fetch(`${base.href.replace(/\/$/, '')}/lunetale/game/runtime/${action}`, {
|
|
120
|
-
method:
|
|
120
|
+
method: ['vip', 'character'].includes(action) ? 'GET' : 'POST',
|
|
121
121
|
headers: {
|
|
122
122
|
'Content-Type': 'application/json',
|
|
123
123
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
124
124
|
},
|
|
125
|
-
body:
|
|
125
|
+
body: ['vip', 'character'].includes(action) ? undefined : JSON.stringify(payload),
|
|
126
126
|
signal: controller.signal,
|
|
127
127
|
credentials: 'omit',
|
|
128
128
|
cache: 'no-store',
|
|
@@ -199,6 +199,40 @@ export function createGameClient(options = {}) {
|
|
|
199
199
|
},
|
|
200
200
|
getVIP() { return request('vip') },
|
|
201
201
|
async isVIP() { return (await request('vip')).isVip === true },
|
|
202
|
+
async getSelectedCharacter() {
|
|
203
|
+
const result = await request('character')
|
|
204
|
+
if (!Object.hasOwn(result, 'character') || (result.character !== null &&
|
|
205
|
+
(!Number.isSafeInteger(result.character.id) || result.character.id <= 0 || typeof result.character.name !== 'string'))) {
|
|
206
|
+
throw new GameSDKError('PROTOCOL', '角色资料响应格式错误')
|
|
207
|
+
}
|
|
208
|
+
return result.character
|
|
209
|
+
},
|
|
210
|
+
async chatWithCharacter(input) {
|
|
211
|
+
const requestId = input?.requestId || crypto.randomUUID()
|
|
212
|
+
try {
|
|
213
|
+
if (!input || typeof input !== 'object' || Array.isArray(input) ||
|
|
214
|
+
Object.keys(input).some(key => !['requestId', 'message', 'scene'].includes(key)) ||
|
|
215
|
+
typeof requestId !== 'string' || !/^[A-Za-z0-9_-]{16,64}$/.test(requestId) ||
|
|
216
|
+
typeof input.message !== 'string' || !input.message.trim() || [...input.message].length > 4000) {
|
|
217
|
+
throw new GameSDKError('VALIDATION', '需要1至4000字消息和16至64位 requestId;角色和历史由后端管理')
|
|
218
|
+
}
|
|
219
|
+
const payload = { requestId, message: input.message }
|
|
220
|
+
if (input.scene != null) {
|
|
221
|
+
const scene = input.scene
|
|
222
|
+
if (typeof scene !== 'object' || Array.isArray(scene) ||
|
|
223
|
+
Object.keys(scene).some(key => !['location', 'description'].includes(key)) ||
|
|
224
|
+
(scene.location !== undefined && (typeof scene.location !== 'string' || [...scene.location].length > 200)) ||
|
|
225
|
+
(scene.description !== undefined && (typeof scene.description !== 'string' || [...scene.description].length > 2000))) {
|
|
226
|
+
throw new GameSDKError('VALIDATION', '场景只能包含location(最多200字)和description(最多2000字)')
|
|
227
|
+
}
|
|
228
|
+
payload.scene = { location: scene.location || '', description: scene.description || '' }
|
|
229
|
+
}
|
|
230
|
+
return await request('character-chat', payload)
|
|
231
|
+
} catch (error) {
|
|
232
|
+
if (error instanceof GameSDKError) { error.requestId = requestId; throw error }
|
|
233
|
+
throw new GameSDKError('UNKNOWN', '角色对话请求失败', { requestId })
|
|
234
|
+
}
|
|
235
|
+
},
|
|
202
236
|
async chat(input) {
|
|
203
237
|
const requestId = input?.requestId || crypto.randomUUID()
|
|
204
238
|
try {
|