koishi-plugin-rocom 1.0.18 → 1.0.19
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/lib/announcement-service.d.ts +21 -0
- package/lib/announcement-service.js +63 -0
- package/lib/async-control.d.ts +3 -0
- package/lib/async-control.js +48 -0
- package/lib/client.d.ts +48 -7
- package/lib/client.js +244 -125
- package/lib/commands/egg.js +66 -4
- package/lib/commands/merchant.js +144 -146
- package/lib/commands/query.js +148 -117
- package/lib/commands/ranking.d.ts +2 -0
- package/lib/commands/ranking.js +31 -0
- package/lib/commands/share-code.d.ts +2 -0
- package/lib/commands/share-code.js +66 -0
- package/lib/commands/tools.js +47 -59
- package/lib/egg-service.d.ts +82 -8
- package/lib/egg-service.js +235 -19
- package/lib/img//345/274/202/350/211/262.png +0 -0
- package/lib/img//347/202/253/345/275/251.png +0 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.js +16 -3
- package/lib/merchant-clock.d.ts +22 -0
- package/lib/merchant-clock.js +95 -0
- package/lib/merchant-service.d.ts +24 -0
- package/lib/merchant-service.js +148 -0
- package/lib/permissions.d.ts +16 -0
- package/lib/permissions.js +66 -0
- package/lib/pet-data-service.js +7 -9
- package/lib/pet-size.d.ts +13 -0
- package/lib/pet-size.js +54 -0
- package/lib/player-service.d.ts +37 -0
- package/lib/player-service.js +201 -0
- package/lib/ranking-service.d.ts +46 -0
- package/lib/ranking-service.js +137 -0
- package/lib/render-templates/announcement/detail.html +8 -0
- package/lib/render-templates/announcement/list.html +5 -0
- package/lib/render-templates/announcement/style.css +1 -0
- package/lib/render-templates/pet-ranking/index.html +99 -0
- package/lib/render-templates/pet-ranking/style.css +338 -0
- package/lib/render-templates/player-search/index.html +7 -0
- package/lib/render-templates/searcheggs/index.html +1 -1
- package/lib/render-templates/searcheggs/size.html +70 -68
- package/lib/render-templates/share-code-team/index.html +105 -0
- package/lib/render-templates/share-code-team/style.css +345 -0
- package/lib/render.d.ts +15 -2
- package/lib/render.js +120 -37
- package/lib/send-image.d.ts +1 -1
- package/lib/send-image.js +12 -1
- package/lib/share-code-service.d.ts +32 -0
- package/lib/share-code-service.js +184 -0
- package/lib/subscription-runner.d.ts +6 -0
- package/lib/subscription-runner.js +21 -0
- package/lib/subscription-send.d.ts +4 -2
- package/lib/subscription-send.js +45 -20
- package/lib/types.d.ts +5 -0
- package/lib/user.d.ts +5 -0
- package/package.json +4 -1
- package/readme.md +145 -124
package/lib/client.js
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.RocomClient = void 0;
|
|
3
|
+
exports.RocomClient = exports.isObject = void 0;
|
|
4
|
+
exports.inheritGoodsMapping = inheritGoodsMapping;
|
|
4
5
|
const koishi_1 = require("koishi");
|
|
6
|
+
const async_control_1 = require("./async-control");
|
|
5
7
|
const logger = new koishi_1.Logger('rocom-client');
|
|
6
8
|
// ingame 异步任务状态分类(来自服务端 /ingame/tasks/{task_id} 返回的 status 字段)
|
|
7
9
|
const INGAME_PENDING_STATUSES = ['queued', 'pending', 'running', 'processing', 'accepted'];
|
|
8
10
|
const INGAME_FAILED_STATUSES = ['failed', 'error', 'timeout', 'cancelled', 'canceled'];
|
|
9
11
|
const INGAME_COMPLETED_STATUSES = ['done', 'success', 'succeeded', 'completed', 'finished'];
|
|
12
|
+
const isObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
13
|
+
exports.isObject = isObject;
|
|
14
|
+
// 每剥离一层 envelope 时继承 goods_mapping,避免只改商品渲染后拿不到名称/图标(上游 v4.1.0)。
|
|
15
|
+
function inheritGoodsMapping(parent, child) {
|
|
16
|
+
const mapping = child._goods_mapping ?? child.goods_mapping
|
|
17
|
+
?? parent._goods_mapping ?? parent.goods_mapping;
|
|
18
|
+
return mapping === undefined ? { ...child } : { ...child, _goods_mapping: mapping };
|
|
19
|
+
}
|
|
10
20
|
class RocomClient {
|
|
11
21
|
baseUrl;
|
|
12
22
|
apiKey;
|
|
@@ -23,6 +33,12 @@ class RocomClient {
|
|
|
23
33
|
return '';
|
|
24
34
|
return uid.trim().replace(/[^a-zA-Z0-9_\- \u4e00-\u9fa5]/g, '').trim();
|
|
25
35
|
}
|
|
36
|
+
// HTTP/任务 envelope 的 goods_mapping 复制到业务对象,供商品名称与图标解析。
|
|
37
|
+
attachGoodsMapping(resp, data) {
|
|
38
|
+
if ((0, exports.isObject)(resp) && (0, exports.isObject)(data))
|
|
39
|
+
return inheritGoodsMapping(resp, data);
|
|
40
|
+
return data;
|
|
41
|
+
}
|
|
26
42
|
wegameHeaders(fwToken = '', userIdentifier = '', clientType = '', clientId = '', includeApiKey = true) {
|
|
27
43
|
const headers = {};
|
|
28
44
|
if (includeApiKey && this.apiKey)
|
|
@@ -192,6 +208,7 @@ class RocomClient {
|
|
|
192
208
|
headers,
|
|
193
209
|
params,
|
|
194
210
|
timeout: this.timeout,
|
|
211
|
+
signal: options?.signal,
|
|
195
212
|
});
|
|
196
213
|
if (resp?.code !== 0) {
|
|
197
214
|
this.setLastError(resp?.message || '接口返回异常');
|
|
@@ -201,7 +218,7 @@ class RocomClient {
|
|
|
201
218
|
}
|
|
202
219
|
return null;
|
|
203
220
|
}
|
|
204
|
-
return resp?.data ?? {};
|
|
221
|
+
return this.attachGoodsMapping(resp, resp?.data ?? {});
|
|
205
222
|
}
|
|
206
223
|
catch (e) {
|
|
207
224
|
const message = this.formatHttpError(e);
|
|
@@ -219,12 +236,13 @@ class RocomClient {
|
|
|
219
236
|
return null;
|
|
220
237
|
}
|
|
221
238
|
}
|
|
222
|
-
async post(ctx, path, headers, json, params) {
|
|
239
|
+
async post(ctx, path, headers, json, params, signal) {
|
|
223
240
|
try {
|
|
224
241
|
const resp = await ctx.http.post(`${this.baseUrl}${path}`, json, {
|
|
225
242
|
headers,
|
|
226
243
|
params,
|
|
227
244
|
timeout: this.timeout,
|
|
245
|
+
signal,
|
|
228
246
|
});
|
|
229
247
|
if (resp?.code !== 0) {
|
|
230
248
|
this.setLastError(resp?.message || '接口返回异常');
|
|
@@ -232,7 +250,7 @@ class RocomClient {
|
|
|
232
250
|
this.logRequestFailureDetails('POST', path, headers, params, json, resp);
|
|
233
251
|
return null;
|
|
234
252
|
}
|
|
235
|
-
return resp?.data ?? {};
|
|
253
|
+
return this.attachGoodsMapping(resp, resp?.data ?? {});
|
|
236
254
|
}
|
|
237
255
|
catch (e) {
|
|
238
256
|
const message = this.formatHttpError(e);
|
|
@@ -263,7 +281,7 @@ class RocomClient {
|
|
|
263
281
|
this.logRequestFailureDetails('DELETE', path, headers, params, undefined, resp);
|
|
264
282
|
return null;
|
|
265
283
|
}
|
|
266
|
-
return resp?.data ?? {};
|
|
284
|
+
return this.attachGoodsMapping(resp, resp?.data ?? {});
|
|
267
285
|
}
|
|
268
286
|
catch (e) {
|
|
269
287
|
const message = this.formatHttpError(e);
|
|
@@ -287,14 +305,16 @@ class RocomClient {
|
|
|
287
305
|
headers,
|
|
288
306
|
params: options.params,
|
|
289
307
|
data: options.json,
|
|
290
|
-
timeout: this.timeout,
|
|
308
|
+
timeout: Math.max(this.timeout, Number(options.json?.wait_ms ?? options.params?.wait_ms ?? 0) + 2000),
|
|
309
|
+
signal: options.signal,
|
|
291
310
|
validateStatus: () => true,
|
|
292
311
|
});
|
|
293
|
-
const
|
|
312
|
+
const rawStatus = Number(response?.status);
|
|
313
|
+
const status = Number.isFinite(rawStatus) ? rawStatus : null;
|
|
294
314
|
const body = response?.data !== undefined ? response.data : response;
|
|
295
315
|
if (body?.code !== undefined && body.code !== 0) {
|
|
296
316
|
const rawMessage = body.message || body.msg || '接口返回异常';
|
|
297
|
-
const message = !acceptedStatuses.includes(status)
|
|
317
|
+
const message = status !== null && !acceptedStatuses.includes(status)
|
|
298
318
|
? `HTTP ${status}: ${rawMessage}`
|
|
299
319
|
: rawMessage;
|
|
300
320
|
this.setLastError(message);
|
|
@@ -303,9 +323,9 @@ class RocomClient {
|
|
|
303
323
|
}
|
|
304
324
|
return { status: null, data: null };
|
|
305
325
|
}
|
|
306
|
-
const data = body?.code !== undefined ? (body.data ?? {}) : (body ?? {});
|
|
307
|
-
if (!acceptedStatuses.includes(status)) {
|
|
308
|
-
this.setLastError(`HTTP ${status}`);
|
|
326
|
+
const data = body?.code !== undefined ? this.attachGoodsMapping(body, body.data ?? {}) : (body ?? {});
|
|
327
|
+
if (status === null || !acceptedStatuses.includes(status)) {
|
|
328
|
+
this.setLastError(status === null ? 'HTTP 响应缺少状态码' : `HTTP ${status}`);
|
|
309
329
|
if (!options.silentFailureDetails) {
|
|
310
330
|
this.logRequestFailureDetails(method, path, headers, options.params, options.json, response);
|
|
311
331
|
}
|
|
@@ -324,18 +344,21 @@ class RocomClient {
|
|
|
324
344
|
}
|
|
325
345
|
async requestIngameWithFallback(ctx, path, payload, options = {}) {
|
|
326
346
|
const acceptedStatuses = options.acceptedStatuses || [200, 202];
|
|
347
|
+
const auth = options.auth || {};
|
|
327
348
|
const requestOnce = async (includeApiKey, silentFailureDetails) => {
|
|
328
|
-
const headers = this.wegameHeaders('', '', '', '', includeApiKey);
|
|
349
|
+
const headers = this.wegameHeaders(auth.fwToken || '', auth.userIdentifier || '', '', '', includeApiKey);
|
|
329
350
|
let result = await this.requestWithStatus(ctx, 'POST', path, headers, {
|
|
330
351
|
json: payload,
|
|
331
352
|
acceptedStatuses,
|
|
332
353
|
silentFailureDetails,
|
|
354
|
+
signal: options.signal,
|
|
333
355
|
});
|
|
334
|
-
if (result.status === null) {
|
|
356
|
+
if (result.status === null && !options.signal?.aborted) {
|
|
335
357
|
result = await this.requestWithStatus(ctx, 'GET', path, headers, {
|
|
336
358
|
params: payload,
|
|
337
359
|
acceptedStatuses,
|
|
338
360
|
silentFailureDetails,
|
|
361
|
+
signal: options.signal,
|
|
339
362
|
});
|
|
340
363
|
}
|
|
341
364
|
return result;
|
|
@@ -343,15 +366,15 @@ class RocomClient {
|
|
|
343
366
|
let result = await requestOnce(false, Boolean(this.apiKey));
|
|
344
367
|
if (result.status !== null)
|
|
345
368
|
return { ...result, usedApiKey: false };
|
|
346
|
-
if (this.shouldRetryIngameWithApiKey(this.getLastError())) {
|
|
369
|
+
if (!options.signal?.aborted && this.shouldRetryIngameWithApiKey(this.getLastError())) {
|
|
347
370
|
result = await requestOnce(true, false);
|
|
348
371
|
if (result.status !== null)
|
|
349
372
|
return { ...result, usedApiKey: true };
|
|
350
373
|
}
|
|
351
374
|
return { status: null, data: null, usedApiKey: false };
|
|
352
375
|
}
|
|
353
|
-
async getIngameTask(ctx, taskId, includeApiKey = true) {
|
|
354
|
-
return this.requestWithStatus(ctx, 'GET', `/api/v1/games/rocom/ingame/tasks/${taskId}`, this.wegameHeaders('', '', '', '', includeApiKey), { acceptedStatuses: [200, 202] });
|
|
376
|
+
async getIngameTask(ctx, taskId, includeApiKey = true, auth = {}, signal) {
|
|
377
|
+
return this.requestWithStatus(ctx, 'GET', `/api/v1/games/rocom/ingame/tasks/${taskId}`, this.wegameHeaders(auth.fwToken || '', auth.userIdentifier || '', '', '', includeApiKey), { acceptedStatuses: [200, 202], signal });
|
|
355
378
|
}
|
|
356
379
|
static normalizeTaskStatus(value) {
|
|
357
380
|
return String(value ?? '').trim().toLowerCase();
|
|
@@ -361,7 +384,8 @@ class RocomClient {
|
|
|
361
384
|
}
|
|
362
385
|
// 判断一份 payload 是否为“已完成的业务结果”(而非排队占位)。
|
|
363
386
|
// 家园接口完成时返回 rows/home_info;玩家、商店完成时返回 source/title/rows;
|
|
364
|
-
// 家园详情(pet/data)完成时返回 npc_pets/npc_pet
|
|
387
|
+
// 家园详情(pet/data)完成时返回 npc_pets/npc_pet;
|
|
388
|
+
// v4.1 玩家嵌套结构返回 player_info/player_card_brief_info,实时商人返回 goods/shop/meta。
|
|
365
389
|
static isCompletedGatewayPayload(payload) {
|
|
366
390
|
if (!payload || typeof payload !== 'object')
|
|
367
391
|
return false;
|
|
@@ -369,26 +393,46 @@ class RocomClient {
|
|
|
369
393
|
return true;
|
|
370
394
|
if (payload.home_info !== undefined)
|
|
371
395
|
return true;
|
|
372
|
-
if (payload.source !== undefined)
|
|
373
|
-
return true;
|
|
374
396
|
if (Array.isArray(payload.npc_pets))
|
|
375
397
|
return true;
|
|
376
398
|
if (payload.npc_pet && typeof payload.npc_pet === 'object')
|
|
377
399
|
return true;
|
|
378
|
-
if (
|
|
400
|
+
if (payload.player_info && typeof payload.player_info === 'object')
|
|
401
|
+
return true;
|
|
402
|
+
if (payload.player_card_brief_info && typeof payload.player_card_brief_info === 'object')
|
|
403
|
+
return true;
|
|
404
|
+
if (Array.isArray(payload.goods))
|
|
405
|
+
return true;
|
|
406
|
+
if (payload.shop && typeof payload.shop === 'object')
|
|
407
|
+
return true;
|
|
408
|
+
if (payload.meta && typeof payload.meta === 'object' && Array.isArray(payload.goods))
|
|
379
409
|
return true;
|
|
380
410
|
return false;
|
|
381
411
|
}
|
|
382
|
-
static
|
|
383
|
-
if (!payload ||
|
|
384
|
-
return
|
|
412
|
+
static inspectIngamePayload(payload, depth = 0, completed = false) {
|
|
413
|
+
if (!(0, exports.isObject)(payload) || depth > 6)
|
|
414
|
+
return { error: '任务返回结构无法解析' };
|
|
415
|
+
const state = RocomClient.normalizeTaskStatus(payload.status);
|
|
416
|
+
if (INGAME_FAILED_STATUSES.includes(state))
|
|
417
|
+
return { error: RocomClient.taskErrorMessage(payload, state) };
|
|
418
|
+
if (INGAME_PENDING_STATUSES.includes(state))
|
|
419
|
+
return { pending: true };
|
|
420
|
+
if (state && !INGAME_COMPLETED_STATUSES.includes(state))
|
|
421
|
+
return { error: 'Ingame 任务状态异常:' + state };
|
|
422
|
+
completed ||= INGAME_COMPLETED_STATUSES.includes(state);
|
|
385
423
|
if (RocomClient.isCompletedGatewayPayload(payload))
|
|
386
|
-
return payload;
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
424
|
+
return { value: payload };
|
|
425
|
+
for (const key of ['result', 'data']) {
|
|
426
|
+
if ((0, exports.isObject)(payload[key]))
|
|
427
|
+
return RocomClient.inspectIngamePayload(inheritGoodsMapping(payload, payload[key]), depth + 1, completed);
|
|
428
|
+
}
|
|
429
|
+
if (completed)
|
|
430
|
+
return { error: '任务已完成但未返回可解析结果' };
|
|
431
|
+
if (state)
|
|
432
|
+
return { error: 'Ingame 任务状态异常:' + state };
|
|
433
|
+
if (RocomClient.extractTaskId(payload))
|
|
434
|
+
return { pending: true };
|
|
435
|
+
return { value: payload };
|
|
392
436
|
}
|
|
393
437
|
static taskErrorMessage(payload, fallbackStatus = '') {
|
|
394
438
|
const candidates = [
|
|
@@ -405,64 +449,65 @@ class RocomClient {
|
|
|
405
449
|
}
|
|
406
450
|
// 排队等候轮询:把 ingame 接口提交后返回的 202(task_id/status=queued)轮询到出结果或超时。
|
|
407
451
|
// 入参 first 是首请求拿到的 { status, data, usedApiKey }。
|
|
408
|
-
async pollIngameTask(ctx, first, options, labels) {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const
|
|
412
|
-
if (
|
|
413
|
-
|
|
414
|
-
|
|
452
|
+
async pollIngameTask(ctx, first, options, labels, auth = {}) {
|
|
453
|
+
if (first.status === null || options.signal?.aborted)
|
|
454
|
+
return null;
|
|
455
|
+
const initial = RocomClient.inspectIngamePayload(first.data);
|
|
456
|
+
if (initial.error) {
|
|
457
|
+
this.setLastError(initial.error);
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
const taskId = RocomClient.extractTaskId(first.data);
|
|
461
|
+
if (!initial.pending && initial.value !== undefined && first.status !== 202)
|
|
462
|
+
return initial.value;
|
|
415
463
|
if (!taskId) {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
// 既不是任务也不是已知完成结构:把原始 data 透传给上层兼容旧逻辑。
|
|
419
|
-
return status === null ? null : data;
|
|
420
|
-
}
|
|
421
|
-
// 已进入队列,回调通知“排队中”。
|
|
422
|
-
if (typeof options.onQueued === 'function') {
|
|
423
|
-
try {
|
|
424
|
-
await options.onQueued(taskId);
|
|
425
|
-
}
|
|
426
|
-
catch (e) {
|
|
427
|
-
logger.warn(`ingame 排队提示回调失败: ${e}`);
|
|
428
|
-
}
|
|
464
|
+
this.setLastError(labels.queuedNoTaskId);
|
|
465
|
+
return null;
|
|
429
466
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
return null;
|
|
455
|
-
}
|
|
456
|
-
if (lastStatus && !INGAME_PENDING_STATUSES.includes(lastStatus)) {
|
|
457
|
-
this.setLastError(`Ingame 任务状态异常:${lastStatus}`);
|
|
467
|
+
try {
|
|
468
|
+
return await (0, async_control_1.withDeadline)(Math.max(1, Number(options.timeoutMs) || 180000), options.signal, async (signal) => {
|
|
469
|
+
if (options.onQueued) {
|
|
470
|
+
try {
|
|
471
|
+
await options.onQueued(taskId);
|
|
472
|
+
}
|
|
473
|
+
catch (error) {
|
|
474
|
+
logger.warn('排队提示失败: ' + error);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
while (!signal.aborted) {
|
|
478
|
+
await (0, async_control_1.delay)(Math.max(300, Number(options.intervalMs) || 3000), signal);
|
|
479
|
+
const task = await this.getIngameTask(ctx, taskId, first.usedApiKey, auth, signal);
|
|
480
|
+
signal.throwIfAborted();
|
|
481
|
+
if (task.status === null)
|
|
482
|
+
return null;
|
|
483
|
+
const result = RocomClient.inspectIngamePayload(task.data);
|
|
484
|
+
if (result.error) {
|
|
485
|
+
this.setLastError(result.error);
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
if (!result.pending && result.value !== undefined && task.status !== 202)
|
|
489
|
+
return result.value;
|
|
490
|
+
}
|
|
458
491
|
return null;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
catch {
|
|
495
|
+
this.setLastError(options.signal?.aborted ? '查询已取消' : labels.stillQueued(taskId));
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
async queuedQuery(ctx, path, payload, options, labels) {
|
|
500
|
+
try {
|
|
501
|
+
return await (0, async_control_1.withDeadline)(Number(options.timeoutMs) || 180000, options.signal, async (signal) => {
|
|
502
|
+
const first = await this.requestIngameWithFallback(ctx, path, payload, { auth: options.auth, signal });
|
|
503
|
+
signal.throwIfAborted();
|
|
504
|
+
return this.pollIngameTask(ctx, first, { ...options, signal }, labels, options.auth);
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
catch {
|
|
508
|
+
this.setLastError(options.signal?.aborted ? '查询已取消' : '查询等待超时,请稍后重试');
|
|
509
|
+
return null;
|
|
463
510
|
}
|
|
464
|
-
this.setLastError(labels.stillQueued(taskId));
|
|
465
|
-
return null;
|
|
466
511
|
}
|
|
467
512
|
async qqQrLogin(ctx, userIdentifier) {
|
|
468
513
|
const params = { client_type: 'bot', client_id: 'koishi', provider: 'rocom' };
|
|
@@ -562,39 +607,40 @@ class RocomClient {
|
|
|
562
607
|
|| Array.isArray(data.notes)
|
|
563
608
|
|| data.title
|
|
564
609
|
|| data.nickname
|
|
565
|
-
|| data.uid
|
|
610
|
+
|| data.uid
|
|
611
|
+
|| (data.player_info && typeof data.player_info === 'object')
|
|
612
|
+
|| (data.player_card_brief_info && typeof data.player_card_brief_info === 'object'));
|
|
566
613
|
}
|
|
567
|
-
async ingamePlayerSearch(ctx, uid) {
|
|
614
|
+
async ingamePlayerSearch(ctx, uid, options = {}) {
|
|
568
615
|
const sanitizedUid = this.sanitizeUid(uid);
|
|
569
|
-
if (!sanitizedUid) {
|
|
616
|
+
if (!sanitizedUid && !options.auth?.fwToken) {
|
|
570
617
|
this.setLastError('UID 不能为空');
|
|
571
618
|
return null;
|
|
572
619
|
}
|
|
573
620
|
const path = '/api/v1/games/rocom/ingame/player/search';
|
|
574
|
-
const payload = { uid: sanitizedUid, wait_ms: 5000 };
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
621
|
+
const payload = { ...(sanitizedUid ? { uid: sanitizedUid } : {}), wait_ms: Number(options.waitMs) || 5000 };
|
|
622
|
+
return this.queuedQuery(ctx, path, payload, options, {
|
|
623
|
+
queuedNoTaskId: '玩家搜索任务已入队,但未返回 task_id',
|
|
624
|
+
stillQueued: (taskId) => `玩家搜索任务仍在排队,请稍后重试(task_id: ${taskId})`,
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
// 玩家名片(上游 v4.1.0):需要补充收集数/名片图时按需查询。
|
|
628
|
+
async ingamePlayerCard(ctx, uid, options = {}) {
|
|
629
|
+
const sanitizedUid = this.sanitizeUid(uid);
|
|
630
|
+
if (!sanitizedUid && !options.auth?.fwToken) {
|
|
631
|
+
this.setLastError('UID 不能为空');
|
|
579
632
|
return null;
|
|
580
|
-
if (this.isIngamePlayerPayload(data))
|
|
581
|
-
return data;
|
|
582
|
-
const taskId = data.task_id || data.taskId || data.taskID;
|
|
583
|
-
if (!taskId) {
|
|
584
|
-
if (status === 202)
|
|
585
|
-
this.setLastError('玩家搜索任务已入队,但未返回 task_id');
|
|
586
|
-
return data;
|
|
587
|
-
}
|
|
588
|
-
for (let i = 0; i < 8; i++) {
|
|
589
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
590
|
-
const task = await this.getIngameTask(ctx, taskId, usedApiKey);
|
|
591
|
-
if (task.status === 200)
|
|
592
|
-
return task.data;
|
|
593
|
-
if (task.status === null)
|
|
594
|
-
return null;
|
|
595
633
|
}
|
|
596
|
-
|
|
597
|
-
|
|
634
|
+
const path = '/api/v1/games/rocom/ingame/player/card';
|
|
635
|
+
const payload = {
|
|
636
|
+
...(sanitizedUid ? { uid: sanitizedUid } : {}),
|
|
637
|
+
wait_ms: Number(options.waitMs) || 5000,
|
|
638
|
+
source: options.source || 'friend',
|
|
639
|
+
};
|
|
640
|
+
return this.queuedQuery(ctx, path, payload, options, {
|
|
641
|
+
queuedNoTaskId: '玩家名片任务已入队,但未返回 task_id',
|
|
642
|
+
stillQueued: (taskId) => `玩家名片任务仍在排队,请稍后重试(task_id: ${taskId})`,
|
|
643
|
+
});
|
|
598
644
|
}
|
|
599
645
|
async getPets(ctx, fwToken, petSubset = 0, pageNo = 1, pageSize = 10, userIdentifier = '') {
|
|
600
646
|
const params = { pet_subset: petSubset, page_no: pageNo, page_size: pageSize };
|
|
@@ -610,11 +656,26 @@ class RocomClient {
|
|
|
610
656
|
const params = { page_no: Math.max(pageNo, 1), refresh: 'false' };
|
|
611
657
|
return this.get(ctx, '/api/v1/games/rocom/exchange/posters', this.wegameHeaders(fwToken, userIdentifier), params);
|
|
612
658
|
}
|
|
613
|
-
|
|
614
|
-
|
|
659
|
+
// 远行商人:优先实时 ingame 接口,失败/协议无法识别时回退旧 merchant/info。
|
|
660
|
+
// 成功的空商品列表(goods: [])视为有效结果,不触发回退。
|
|
661
|
+
async getMerchantInfo(ctx, refresh = false, options = {}) {
|
|
662
|
+
const live = await this.ingameMerchantInfo(ctx, null, options);
|
|
663
|
+
if (live && typeof live === 'object' && (Array.isArray(live.goods)
|
|
664
|
+
|| Array.isArray(live.merchantActivities)
|
|
665
|
+
|| Array.isArray(live.merchant_activities))) {
|
|
666
|
+
return { ...live, _source: 'live' };
|
|
667
|
+
}
|
|
668
|
+
if (options.signal?.aborted)
|
|
669
|
+
return null;
|
|
670
|
+
const legacy = await this.get(ctx, '/api/v1/games/rocom/merchant/info', this.wegameHeaders(options.auth?.fwToken, options.auth?.userIdentifier), {
|
|
615
671
|
refresh: refresh ? 'true' : 'false',
|
|
616
672
|
random_goods: 'all',
|
|
617
|
-
});
|
|
673
|
+
}, { signal: options.signal });
|
|
674
|
+
if (legacy && typeof legacy === 'object') {
|
|
675
|
+
logger.warn('[Rocom] ingame/merchant/info 不可用,已回退 merchant/info');
|
|
676
|
+
return { ...legacy, _source: 'legacy' };
|
|
677
|
+
}
|
|
678
|
+
return null;
|
|
618
679
|
}
|
|
619
680
|
async queryPetSize(ctx, diameter, weight, pool = 'magic', pageNo = 1, pageSize = 30, userIdentifier = '') {
|
|
620
681
|
const params = this.scopedParams({
|
|
@@ -627,6 +688,55 @@ class RocomClient {
|
|
|
627
688
|
}, userIdentifier);
|
|
628
689
|
return this.get(ctx, '/api/v1/games/rocom/wiki/pet-size/query', this.wegameHeaders('', userIdentifier, 'bot', 'koishi'), params);
|
|
629
690
|
}
|
|
691
|
+
// 后端查蛋(上游 v3.8.0):按展示身高/体重查询,参数名是 height/weight。
|
|
692
|
+
async searchEggBySize(ctx, heightMeters, weightKg, pageNo = 1, pageSize = 30, userIdentifier = '') {
|
|
693
|
+
const params = this.scopedParams({
|
|
694
|
+
height: heightMeters,
|
|
695
|
+
weight: weightKg,
|
|
696
|
+
page_no: Math.max(Number(pageNo) || 1, 1),
|
|
697
|
+
page_size: Math.min(Math.max(Number(pageSize) || 30, 1), 100),
|
|
698
|
+
}, userIdentifier);
|
|
699
|
+
return this.get(ctx, '/api/v1/games/rocom/egg/search', this.wegameHeaders('', userIdentifier, 'bot', 'koishi'), params);
|
|
700
|
+
}
|
|
701
|
+
// 异色/炫彩收集排行榜(上游 v3.9.0)。
|
|
702
|
+
async getPetCollectionRanking(ctx, rankType, limit = 10, uid = '', options = {}) {
|
|
703
|
+
if (!['shining', 'glass'].includes(rankType)) {
|
|
704
|
+
this.setLastError('无效榜单类型');
|
|
705
|
+
return null;
|
|
706
|
+
}
|
|
707
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 50) {
|
|
708
|
+
this.setLastError('无效数量');
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
const params = { limit, resolve_names: '1' };
|
|
712
|
+
if (uid)
|
|
713
|
+
params.uid = this.sanitizeUid(uid);
|
|
714
|
+
return this.get(ctx, `/api/v1/games/rocom/ingame/player/card/pet-stats/rankings/${rankType}`, this.wegameHeaders('', options.userIdentifier || '', 'bot', 'koishi'), this.scopedParams(params, options.userIdentifier || ''));
|
|
715
|
+
}
|
|
716
|
+
// 阵容分享码解析与历史查询(上游 v3.9.0)。
|
|
717
|
+
async parseShareCode(ctx, shareCode, userIdentifier = '') {
|
|
718
|
+
const code = String(shareCode || '').trim();
|
|
719
|
+
if (!code) {
|
|
720
|
+
this.setLastError('分享码不能为空');
|
|
721
|
+
return null;
|
|
722
|
+
}
|
|
723
|
+
return this.post(ctx, '/api/v1/games/rocom/tools/share-code/parse', this.wegameHeaders('', userIdentifier, 'bot', 'koishi'), { share_code: code }, this.scopedParams({}, userIdentifier));
|
|
724
|
+
}
|
|
725
|
+
async getShareCodeRecords(ctx, options = {}, userIdentifier = '') {
|
|
726
|
+
const params = {
|
|
727
|
+
page_no: Math.max(Number(options.pageNo) || 1, 1),
|
|
728
|
+
page_size: Math.min(Math.max(Number(options.pageSize) || 20, 1), 100),
|
|
729
|
+
};
|
|
730
|
+
if (options.shareCode)
|
|
731
|
+
params.share_code = String(options.shareCode).trim();
|
|
732
|
+
if (options.hash)
|
|
733
|
+
params.hash = String(options.hash).trim();
|
|
734
|
+
if (options.modeId !== undefined)
|
|
735
|
+
params.mode_id = options.modeId;
|
|
736
|
+
if (options.magicId !== undefined)
|
|
737
|
+
params.magic_id = options.magicId;
|
|
738
|
+
return this.get(ctx, '/api/v1/games/rocom/tools/share-code/records', this.wegameHeaders('', userIdentifier, 'bot', 'koishi'), this.scopedParams(params, userIdentifier));
|
|
739
|
+
}
|
|
630
740
|
async getActivitiesInfo(ctx, refresh = false, userIdentifier = '') {
|
|
631
741
|
return this.get(ctx, '/api/v1/games/rocom/activities/info', this.wegameHeaders('', userIdentifier, 'bot', 'koishi'), this.scopedParams({ refresh: refresh ? 'true' : 'false' }, userIdentifier));
|
|
632
742
|
}
|
|
@@ -636,8 +746,8 @@ class RocomClient {
|
|
|
636
746
|
async getAnnouncementList(ctx, params = {}, userIdentifier = '') {
|
|
637
747
|
return this.get(ctx, '/api/v1/games/rocom/announcement/list', this.wegameHeaders('', userIdentifier, 'bot', 'koishi'), this.scopedParams(params, userIdentifier));
|
|
638
748
|
}
|
|
639
|
-
async getLatestAnnouncement(ctx, params = {}, userIdentifier = '') {
|
|
640
|
-
return this.get(ctx, '/api/v1/games/rocom/announcement/latest', this.wegameHeaders('', userIdentifier, 'bot', 'koishi'), this.scopedParams(params, userIdentifier));
|
|
749
|
+
async getLatestAnnouncement(ctx, params = {}, userIdentifier = '', signal) {
|
|
750
|
+
return this.get(ctx, '/api/v1/games/rocom/announcement/latest', this.wegameHeaders('', userIdentifier, 'bot', 'koishi'), this.scopedParams(params, userIdentifier), { signal });
|
|
641
751
|
}
|
|
642
752
|
async getAnnouncementDetail(ctx, threadId, userIdentifier = '') {
|
|
643
753
|
return this.get(ctx, '/api/v1/games/rocom/announcement/detail', this.wegameHeaders('', userIdentifier, 'bot', 'koishi'), this.scopedParams({ thread_id: threadId }, userIdentifier));
|
|
@@ -684,41 +794,50 @@ class RocomClient {
|
|
|
684
794
|
}
|
|
685
795
|
async ingameHomeInfo(ctx, uid, options = {}) {
|
|
686
796
|
const sanitizedUid = this.sanitizeUid(uid);
|
|
687
|
-
if (!sanitizedUid) {
|
|
797
|
+
if (!sanitizedUid && !options.auth?.fwToken) {
|
|
688
798
|
this.setLastError('UID 不能为空');
|
|
689
799
|
return null;
|
|
690
800
|
}
|
|
691
801
|
const waitMs = Number(options.waitMs) || 5000;
|
|
692
802
|
const path = '/api/v1/games/rocom/ingame/home/info';
|
|
693
|
-
const payload = {
|
|
694
|
-
|
|
695
|
-
|
|
803
|
+
const payload = { wait_ms: waitMs };
|
|
804
|
+
if (sanitizedUid)
|
|
805
|
+
payload.uid = sanitizedUid;
|
|
806
|
+
return this.queuedQuery(ctx, path, payload, options, {
|
|
696
807
|
queuedNoTaskId: '家园查询任务已入队,但未返回 task_id',
|
|
697
808
|
stillQueued: (taskId) => `家园查询任务仍在排队,请稍后重试(task_id: ${taskId})`,
|
|
698
809
|
});
|
|
699
810
|
}
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
811
|
+
// 远行商人实时接口(上游 v4.1.0):优先 ingame live,返回 goods/shop/meta 或任务轮询结果。
|
|
812
|
+
// 未指定商店时不传 shop_id;调用方负责在新接口不可用时回退旧 merchant/info。
|
|
813
|
+
async ingameMerchantInfo(ctx, shopId, options = {}) {
|
|
814
|
+
const waitMs = Number(options.waitMs) || 5000;
|
|
815
|
+
const payload = { wait_ms: waitMs };
|
|
816
|
+
if (shopId !== undefined && shopId !== null && String(shopId) !== '')
|
|
817
|
+
payload.shop_id = shopId;
|
|
818
|
+
return this.queuedQuery(ctx, '/api/v1/games/rocom/ingame/merchant/info', payload, options, {
|
|
819
|
+
queuedNoTaskId: '远行商人任务已入队,但未返回 task_id',
|
|
820
|
+
stillQueued: (taskId) => `远行商人任务仍在排队,请稍后重试(task_id: ${taskId})`,
|
|
821
|
+
});
|
|
704
822
|
}
|
|
705
823
|
// 家园详情:批量或单只查询家园摆放精灵的完整 ingame 数据(上游 v3.6.1)。
|
|
706
824
|
// UID 参数名为 target_uin;单只查询需同时提供 pet_gid 和 npc_id。
|
|
707
825
|
async ingamePetData(ctx, uid, extras = {}, options = {}) {
|
|
708
826
|
const sanitizedUid = this.sanitizeUid(uid);
|
|
709
|
-
if (!sanitizedUid) {
|
|
827
|
+
if (!sanitizedUid && !options.auth?.fwToken) {
|
|
710
828
|
this.setLastError('UID 不能为空');
|
|
711
829
|
return null;
|
|
712
830
|
}
|
|
713
831
|
const waitMs = Number(options.waitMs) || 20000;
|
|
714
|
-
const payload = {
|
|
832
|
+
const payload = { wait_ms: waitMs };
|
|
833
|
+
if (sanitizedUid)
|
|
834
|
+
payload.target_uin = sanitizedUid;
|
|
715
835
|
if (extras.petGid !== undefined && extras.petGid !== null && String(extras.petGid) !== '')
|
|
716
836
|
payload.pet_gid = extras.petGid;
|
|
717
837
|
if (extras.npcId !== undefined && extras.npcId !== null && String(extras.npcId) !== '')
|
|
718
838
|
payload.npc_id = extras.npcId;
|
|
719
839
|
const path = '/api/v1/games/rocom/ingame/pet/data';
|
|
720
|
-
|
|
721
|
-
return this.pollIngameTask(ctx, first, options, {
|
|
840
|
+
return this.queuedQuery(ctx, path, payload, options, {
|
|
722
841
|
queuedNoTaskId: '家园详情任务已入队,但未返回 task_id',
|
|
723
842
|
stillQueued: (taskId) => `家园详情任务仍在排队,请稍后重试(task_id: ${taskId})`,
|
|
724
843
|
});
|