@newbeebox/newbeebox-client-web-sdk 1.0.11 → 1.0.14

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.
Files changed (4) hide show
  1. package/README.md +37 -0
  2. package/index.js +185 -142
  3. package/package.json +29 -29
  4. package/types/index.d.ts +38 -0
package/README.md CHANGED
@@ -25,6 +25,10 @@ await client.ShowSubscriptionPage();
25
25
  // 获取当前用户订阅信息
26
26
  let info = await client.GetUserSubscription();
27
27
  console.log(info);
28
+
29
+ // 获取当前登录用户信息
30
+ let user = await client.GetUserInfo();
31
+ console.log(user);
28
32
  ```
29
33
 
30
34
  ### API
@@ -45,6 +49,10 @@ console.log(info);
45
49
 
46
50
  获取当前用户的订阅信息。
47
51
 
52
+ #### `GetUserInfo(): Promise<UserAccountInfo>`
53
+
54
+ 获取当前登录用户的基础信息。用户未登录时抛出错误。
55
+
48
56
  #### `InstallWoWAddon(game_version_id, addon_id, secret_key?): Promise<any>`
49
57
 
50
58
  安装魔兽世界插件。
@@ -64,6 +72,18 @@ console.log(info);
64
72
  | tool_name | string | 完整API路径,如 `/tool/some_api` |
65
73
  | data | object | 请求数据(可选) |
66
74
 
75
+ ### 错误处理
76
+
77
+ 所有接口失败时都会抛出 `Error`,`message` 格式统一为 `<操作>发生错误:<原因>`。
78
+
79
+ 如果用户安装的客户端版本过低、没有所调用的接口,`<原因>` 固定为:
80
+
81
+ ```
82
+ 当前客户端不支持该接口,请升级新手盒子客户端
83
+ ```
84
+
85
+ 所有接口(含 `Patch`)行为一致,第三方应用可据此提示用户升级客户端。
86
+
67
87
  ---
68
88
 
69
89
  **订阅信息返回结构**:
@@ -86,3 +106,20 @@ console.log(info);
86
106
  "sign": "485d82860578ff..." // 签名,用于服务端验证
87
107
  }
88
108
  ```
109
+
110
+
111
+ **用户信息返回结构**:
112
+
113
+ ```javascript
114
+ {
115
+ "id": 10001, // 用户ID
116
+ "username": "Kyuu", // 用户名
117
+ "avatar": "https://cdn8.newbeebox.com/....",
118
+ "isVip": true, // 是否是 VIP
119
+ "vip": { // 非 VIP 时为 null
120
+ "isYearly": false, // 是否年费会员
121
+ "adFree": true, // 是否免广告
122
+ "endTime": 1790000000000 // 会员到期时间戳
123
+ }
124
+ }
125
+ ```
package/index.js CHANGED
@@ -1,11 +1,114 @@
1
+ // 当前 SDK 版本号(发布新版本时请同步 package.json 的 version 字段)
2
+ const SDK_VERSION = "1.0.13";
1
3
  // 设置超时时间
2
4
  const CLIENT_CHECK_TIMEOUT = 200; // 0.2秒
3
5
  // 任务超时时间
4
6
  const TASK_REQUEST_TIMEOUT = 1000; // 1秒
5
7
  // 客户端可能使用的端口列表
6
- const LOCAL_PORT_LIST = [35560, 35561, 35562, 35563, 35564, 35565, 35566, 35567, 35568, 35569, 35570, 35571, 35572, 35573, 35574, 35575, 35576, 35577, 35578, 35579];
8
+ // 原始端口列表(兼容已部署的老 SDK,必须保留且优先)
9
+ const LEGACY_PORT_LIST = [
10
+ 35560, 35561, 35562, 35563, 35564, 35565, 35566, 35567, 35568, 35569,
11
+ 35570, 35571, 35572, 35573, 35574, 35575, 35576, 35577, 35578, 35579,
12
+ ];
13
+ // 新增端口列表(抗端口被整段占用,分散在多个不相邻区间)
14
+ const EXTRA_PORT_LIST = [
15
+ 1278, 6817, 11456, 14923, 19372, 23814, 28651, 33279, 41537, 46813,
16
+ ];
17
+ // 实际使用的候选端口:原始端口优先,其后接新增端口
18
+ const LOCAL_PORT_LIST = [...LEGACY_PORT_LIST, ...EXTRA_PORT_LIST];
7
19
  // 服务端地址
8
20
  const CLIENT_SERVICE_BASE_URL = "http://127.0.0.1";
21
+
22
+ // 端口缓存的 key
23
+ const PORT_CACHE_KEY = "nbb_client_port";
24
+ // 内存兜底缓存:用于没有 localStorage 的环境(Electron 主进程 / Node / 受限 webview)
25
+ let _memPortCache = null;
26
+
27
+ // 读取缓存端口:优先 localStorage,不可用时回退内存缓存(兼容浏览器/Electron/Tauri/Node)
28
+ function readPortCache() {
29
+ try {
30
+ if (typeof localStorage !== "undefined" && localStorage) {
31
+ const v = localStorage.getItem(PORT_CACHE_KEY);
32
+ if (v) return Number(v) || null;
33
+ }
34
+ } catch (e) { /* localStorage 不可用,忽略,走内存缓存 */ }
35
+ return _memPortCache;
36
+ }
37
+
38
+ // 写入缓存端口:内存 + localStorage(若可用)
39
+ function writePortCache(port) {
40
+ _memPortCache = port;
41
+ try {
42
+ if (typeof localStorage !== "undefined" && localStorage) {
43
+ localStorage.setItem(PORT_CACHE_KEY, String(port));
44
+ }
45
+ } catch (e) { /* 忽略 */ }
46
+ }
47
+
48
+ // 清除缓存端口
49
+ function clearPortCache() {
50
+ _memPortCache = null;
51
+ try {
52
+ if (typeof localStorage !== "undefined" && localStorage) {
53
+ localStorage.removeItem(PORT_CACHE_KEY);
54
+ }
55
+ } catch (e) { /* 忽略 */ }
56
+ }
57
+
58
+ // 探测单个端口的 /ping:成功 resolve(port),失败 reject
59
+ function pingPort(port) {
60
+ return new Promise((resolve, reject) => {
61
+ const controller = new AbortController();
62
+ const timeoutId = setTimeout(() => controller.abort(), CLIENT_CHECK_TIMEOUT);
63
+ fetch(CLIENT_SERVICE_BASE_URL + ":" + port + "/ping", {
64
+ method: "GET",
65
+ signal: controller.signal,
66
+ })
67
+ .then((response) => response.json())
68
+ .then((data) => {
69
+ clearTimeout(timeoutId);
70
+ if (data && data.code === 1) resolve(port);
71
+ else reject(new Error("not newbee client"));
72
+ })
73
+ .catch((e) => {
74
+ clearTimeout(timeoutId);
75
+ reject(e);
76
+ });
77
+ });
78
+ }
79
+
80
+ // 并行探测多个端口,返回第一个成功的端口
81
+ // 不依赖 Promise.any(ES2021),自己实现以兼容老版本 webview
82
+ function probeFirstPort(ports) {
83
+ return new Promise((resolve, reject) => {
84
+ let pending = ports.length;
85
+ if (pending === 0) {
86
+ reject(new Error("no candidate ports"));
87
+ return;
88
+ }
89
+ let settled = false;
90
+ ports.forEach((port) => {
91
+ pingPort(port)
92
+ .then((p) => {
93
+ if (!settled) {
94
+ settled = true;
95
+ resolve(p);
96
+ }
97
+ })
98
+ .catch(() => {
99
+ pending--;
100
+ if (pending === 0 && !settled) {
101
+ reject(new Error("all ports failed"));
102
+ }
103
+ });
104
+ });
105
+ });
106
+ }
107
+
108
+ // 客户端接口不存在(老版本客户端没有对应路由)时,SDK 统一合成的错误响应
109
+ const CODE_NOT_SUPPORTED = -404;
110
+ const MSG_NOT_SUPPORTED = "当前客户端不支持该接口,请升级新手盒子客户端";
111
+
9
112
  export class NewBeeClient {
10
113
  app_id = "";
11
114
 
@@ -20,48 +123,72 @@ export class NewBeeClient {
20
123
  }
21
124
  }
22
125
 
23
- async Init(app_id) {
24
- if (!app_id || typeof app_id !== "string") {
25
- throw new Error("APPID格式不正确 请前往后台获取");
26
- }
126
+ // 统一请求客户端:POST JSON 到 path,自动带上 app_id,返回客户端标准响应 {code, data, message}
127
+ // - 接口不存在(老版本客户端返回 404):返回统一的错误响应 {code: -404, message: MSG_NOT_SUPPORTED}
128
+ // - 网络错误 / 超时 / 响应非 JSON:返回 null
129
+ // 调用方只需判断 code === 1
130
+ async _post(path, data, timeout) {
131
+ this._ensureInitialized();
27
132
 
28
- this.app_id = app_id;
133
+ const controller = new AbortController();
134
+ const timeoutId = timeout ? setTimeout(() => controller.abort("请求超时"), timeout) : null;
29
135
 
30
- for (const port of LOCAL_PORT_LIST) {
31
- // 创建 AbortController 实例
32
- const controller = new AbortController();
136
+ try {
137
+ const response = await fetch(CLIENT_SERVICE_BASE_URL + ":" + this.newbee_client_port + path, {
138
+ method: "POST",
139
+ signal: controller.signal,
140
+ headers: {
141
+ "Content-Type": 'application/json',
142
+ },
143
+ body: JSON.stringify({
144
+ app_id: this.app_id,
145
+ ...data
146
+ })
147
+ });
33
148
 
34
- const signal = controller.signal;
149
+ if (response.status === 404) {
150
+ console.log("客户端不支持该接口:", path);
151
+ return { code: CODE_NOT_SUPPORTED, data: null, message: MSG_NOT_SUPPORTED };
152
+ }
35
153
 
36
- let check_response;
154
+ const result = await response.json();
155
+ console.log(path, "响应:", result);
156
+ return result;
157
+ } catch (e) {
158
+ console.log(path, "请求错误:", e);
159
+ return null;
160
+ } finally {
161
+ if (timeoutId) clearTimeout(timeoutId);
162
+ }
163
+ }
37
164
 
38
- const timeoutId = setTimeout(() => {
39
- controller.abort(); // 超时后取消请求
40
- console.log("端口:", port, '请求超时');
41
- }, CLIENT_CHECK_TIMEOUT);
165
+ async Init(app_id) {
166
+ console.log(`[NewBeeBox SDK] version ${SDK_VERSION}`);
42
167
 
43
- try {
44
- let response = await fetch( CLIENT_SERVICE_BASE_URL + (port ? ":" + port : "") + "/ping", {
45
- method: "GET",
46
- signal: signal
47
- });
168
+ if (!app_id || typeof app_id !== "string") {
169
+ throw new Error("APPID格式不正确 请前往后台获取");
170
+ }
48
171
 
49
- check_response = await response.json();
172
+ this.app_id = app_id;
50
173
 
51
- clearTimeout(timeoutId);
174
+ // 1) 先试缓存端口:命中直接用,重连最快且不打扰其他端口
175
+ const cachedPort = readPortCache();
176
+ if (cachedPort) {
177
+ try {
178
+ this.newbee_client_port = await pingPort(cachedPort);
179
+ console.log("客户端开启的端口号(缓存命中):", this.newbee_client_port);
180
+ return;
52
181
  } catch (e) {
53
- console.error(port, "端口检查失败:", e);
54
- }
55
-
56
- console.log("端口检查:", check_response);
57
-
58
- if (check_response && check_response.code === 1) {
59
- this.newbee_client_port = port;
60
- break;
182
+ clearPortCache(); // 缓存失效,继续全量探测
61
183
  }
62
184
  }
63
185
 
64
- if (!this.newbee_client_port) {
186
+ // 2) 并行探测全部候选端口,取第一个成功的(老端口段 + 分散端口)
187
+ try {
188
+ this.newbee_client_port = await probeFirstPort(LOCAL_PORT_LIST);
189
+ writePortCache(this.newbee_client_port);
190
+ } catch (e) {
191
+ clearPortCache();
65
192
  throw new Error("未检测到新手盒子客户端 初始化失败");
66
193
  }
67
194
 
@@ -70,44 +197,10 @@ export class NewBeeClient {
70
197
 
71
198
  // 打开应用订阅界面
72
199
  async ShowSubscriptionPage() {
73
- this._ensureInitialized();
74
-
75
- // 创建 AbortController 实例
76
- const controller = new AbortController();
77
-
78
- const signal = controller.signal;
79
-
80
- const timeoutId = setTimeout(() => {
81
- controller.abort("请求超时"); // 超时后取消请求
82
- console.log('打开订单页面超时');
83
- }, TASK_REQUEST_TIMEOUT);
84
-
85
- let show_page_response;
200
+ const res = await this._post("/tool/open_subscription_dialog", null, TASK_REQUEST_TIMEOUT);
86
201
 
87
- try {
88
- // 测试成功就请求客户端授权
89
- let response = await fetch(CLIENT_SERVICE_BASE_URL + ":" + this.newbee_client_port + "/tool/open_subscription_dialog", {
90
- method: "POST",
91
- signal: signal,
92
- headers: {
93
- "Content-Type": 'application/json',
94
- },
95
- body: JSON.stringify({
96
- app_id: this.app_id
97
- })
98
- });
99
-
100
- clearTimeout(timeoutId);
101
-
102
- show_page_response = await response.json();
103
-
104
- console.log("开启页面结果:", show_page_response);
105
- } catch (e) {
106
- console.log("打开订单页面超时", e);
107
- }
108
-
109
- if (!show_page_response || show_page_response?.code !== 1) {
110
- throw new Error("打开订单页面发生错误:" + (show_page_response?.message || "未知错误"));
202
+ if (!res || res.code !== 1) {
203
+ throw new Error("打开订单页面发生错误:" + (res?.message || "未知错误"));
111
204
  }
112
205
 
113
206
  return true;
@@ -115,99 +208,49 @@ export class NewBeeClient {
115
208
 
116
209
  // 获取当前使用的用户的订阅信息
117
210
  async GetUserSubscription() {
118
- this._ensureInitialized();
211
+ const res = await this._post("/tool/subscription_status");
119
212
 
120
- let user_subscribe_response = null;
121
- try {
122
- // 测试成功就请求客户端授权
123
- let response = await fetch(CLIENT_SERVICE_BASE_URL + ":" + this.newbee_client_port + "/tool/subscription_status", {
124
- method: "POST",
125
- headers: {
126
- "Content-Type": 'application/json',
127
- },
128
- body: JSON.stringify({
129
- app_id: this.app_id
130
- })
131
- });
213
+ if (!res || res.code !== 1 || !res.data) {
214
+ throw new Error("获取用户订阅信息发生错误:" + (res?.message || "未知错误"));
215
+ }
132
216
 
133
- user_subscribe_response = await response.json();
217
+ return res.data;
218
+ }
134
219
 
135
- console.log("用户订阅信息响应:", user_subscribe_response);
136
- } catch (e) {
137
- console.log("获取用户订阅错误:", e);
138
- }
220
+ // 获取当前登录用户的基础信息(未登录时抛错)
221
+ async GetUserInfo() {
222
+ const res = await this._post("/user/info");
139
223
 
140
- if (!user_subscribe_response || user_subscribe_response?.code !== 1 || !user_subscribe_response?.data) {
141
- throw new Error("获取用户订阅信息发生错误:" + (user_subscribe_response?.message || "未知错误"));
224
+ if (!res || res.code !== 1 || !res.data) {
225
+ throw new Error("获取用户信息发生错误:" + (res?.message || "未知错误"));
142
226
  }
143
227
 
144
- return user_subscribe_response.data
228
+ return res.data;
145
229
  }
146
230
 
147
231
  // 安装魔兽世界插件
148
232
  async InstallWoWAddon(game_version_id, addon_id, secret_key) {
149
- this._ensureInitialized();
150
-
151
- let install_response = null;
152
- try {
153
- const body = {
154
- app_id: this.app_id,
155
- game_version_id: game_version_id,
156
- addon_id: addon_id
157
- };
158
- if (secret_key) {
159
- body.secret_key = secret_key;
160
- }
161
- let response = await fetch(CLIENT_SERVICE_BASE_URL + ":" + this.newbee_client_port + "/tool/wow_install_addon", {
162
- method: "POST",
163
- headers: {
164
- "Content-Type": 'application/json',
165
- },
166
- body: JSON.stringify(body)
167
- });
168
-
169
- install_response = await response.json();
170
-
171
- console.log("安装插件响应:", install_response);
172
- } catch (e) {
173
- console.log("安装插件错误:", e);
233
+ const body = { game_version_id, addon_id };
234
+ if (secret_key) {
235
+ body.secret_key = secret_key;
174
236
  }
237
+ const res = await this._post("/tool/wow_install_addon", body);
175
238
 
176
- if (!install_response || install_response?.code !== 1) {
177
- throw new Error("安装魔兽世界插件发生错误:" + (install_response?.message || "未知错误"));
239
+ if (!res || res.code !== 1) {
240
+ throw new Error("安装魔兽世界插件发生错误:" + (res?.message || "未知错误"));
178
241
  }
179
242
 
180
- return install_response.data;
243
+ return res.data;
181
244
  }
182
245
 
183
246
  // 通用请求 当没有导出指定接口时可以使用这个函数补充请求
184
247
  async Patch(tool_name, data) {
185
- this._ensureInitialized();
186
-
187
- let patch_response = null;
188
- try {
189
- let response = await fetch(CLIENT_SERVICE_BASE_URL + ":" + this.newbee_client_port + tool_name, {
190
- method: "POST",
191
- headers: {
192
- "Content-Type": 'application/json',
193
- },
194
- body: JSON.stringify({
195
- app_id: this.app_id,
196
- ...data
197
- })
198
- });
199
-
200
- patch_response = await response.json();
201
-
202
- console.log("Patch响应:", patch_response);
203
- } catch (e) {
204
- console.log("Patch请求错误:", e);
205
- }
248
+ const res = await this._post(tool_name, data);
206
249
 
207
- if (!patch_response || patch_response?.code !== 1) {
208
- throw new Error("Patch请求发生错误:" + (patch_response?.message || "未知错误"));
250
+ if (!res || res.code !== 1) {
251
+ throw new Error("Patch请求发生错误:" + (res?.message || "未知错误"));
209
252
  }
210
253
 
211
- return patch_response.data;
254
+ return res.data;
212
255
  }
213
- }
256
+ }
package/package.json CHANGED
@@ -1,29 +1,29 @@
1
- {
2
- "name": "@newbeebox/newbeebox-client-web-sdk",
3
- "version": "1.0.11",
4
- "description": "NewBeeBox Client SDK for Web",
5
- "main": "index.js",
6
- "type": "module",
7
- "types": "./types/index.d.ts",
8
- "files": [
9
- "*.js",
10
- "types/*.d.ts"
11
- ],
12
- "scripts": {
13
- "release:patch": "npm version patch && npm publish",
14
- "release:minor": "npm version minor && npm publish",
15
- "release:major": "npm version major && npm publish"
16
- },
17
- "keywords": [
18
- "newbeebox",
19
- "wow",
20
- "sdk",
21
- "client"
22
- ],
23
- "author": "NewBeeBoxTeam",
24
- "license": "ISC",
25
- "publishConfig": {
26
- "registry": "https://registry.npmjs.org/",
27
- "access": "public"
28
- }
29
- }
1
+ {
2
+ "name": "@newbeebox/newbeebox-client-web-sdk",
3
+ "version": "1.0.14",
4
+ "description": "NewBeeBox Client SDK for Web",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "types": "./types/index.d.ts",
8
+ "files": [
9
+ "*.js",
10
+ "types/*.d.ts"
11
+ ],
12
+ "scripts": {
13
+ "release:patch": "npm version patch && npm publish",
14
+ "release:minor": "npm version minor && npm publish",
15
+ "release:major": "npm version major && npm publish"
16
+ },
17
+ "keywords": [
18
+ "newbeebox",
19
+ "wow",
20
+ "sdk",
21
+ "client"
22
+ ],
23
+ "author": "NewBeeBoxTeam",
24
+ "license": "ISC",
25
+ "publishConfig": {
26
+ "registry": "https://registry.npmjs.org/",
27
+ "access": "public"
28
+ }
29
+ }
package/types/index.d.ts CHANGED
@@ -34,6 +34,14 @@ export class NewBeeClient {
34
34
  */
35
35
  GetUserSubscription(): Promise<UserSubscribeInfo>;
36
36
 
37
+ /**
38
+ * 获取当前登录用户信息
39
+ * @constructor GetUserInfo
40
+ * @return {Promise<UserAccountInfo>}
41
+ * @throws 未登录、客户端版本过低时抛出错误
42
+ */
43
+ GetUserInfo(): Promise<UserAccountInfo>;
44
+
37
45
  /**
38
46
  * 安装魔兽世界插件
39
47
  * @param game_version_id 游戏版本ID
@@ -82,6 +90,36 @@ export interface UserInfo {
82
90
  avatar:string;
83
91
  }
84
92
 
93
+ /**
94
+ * @interface UserVipInfo
95
+ * @description 用户 VIP 信息
96
+ * @property {boolean} isYearly - 是否年费会员
97
+ * @property {boolean} adFree - 是否免广告
98
+ * @property {number | null} endTime - 会员到期时间戳
99
+ */
100
+ export interface UserVipInfo {
101
+ isYearly: boolean;
102
+ adFree: boolean;
103
+ endTime: number | null;
104
+ }
105
+
106
+ /**
107
+ * @interface UserAccountInfo
108
+ * @description 当前登录用户信息(GetUserInfo 返回)
109
+ * @property {number | null} id - 用户ID
110
+ * @property {string} username - 用户名
111
+ * @property {string} avatar - 用户头像
112
+ * @property {boolean} isVip - 是否是 VIP
113
+ * @property {UserVipInfo | null} vip - VIP 信息,非 VIP 时为 null
114
+ */
115
+ export interface UserAccountInfo {
116
+ id: number | null;
117
+ username: string;
118
+ avatar: string;
119
+ isVip: boolean;
120
+ vip: UserVipInfo | null;
121
+ }
122
+
85
123
  /**
86
124
  * @interface UserSubscribeInfo
87
125
  * @description 用户订阅信息