koishi-plugin-rocom 1.0.12 → 1.0.17

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 (69) hide show
  1. package/lib/atlas-service.d.ts +33 -0
  2. package/lib/atlas-service.js +439 -0
  3. package/lib/client.d.ts +36 -1
  4. package/lib/client.js +221 -21
  5. package/lib/commands/account.js +1 -0
  6. package/lib/commands/merchant.js +351 -91
  7. package/lib/commands/query.js +162 -14
  8. package/lib/commands/tools.js +104 -0
  9. package/lib/commands/wiki.js +167 -6
  10. package/lib/egg-service.d.ts +2 -0
  11. package/lib/egg-service.js +50 -15
  12. package/lib/index.d.ts +9 -0
  13. package/lib/index.js +3626 -303
  14. package/lib/pet-data-service.d.ts +62 -0
  15. package/lib/pet-data-service.js +468 -0
  16. package/lib/render-templates/home/img/rocomuid/a_title.png +0 -0
  17. package/lib/render-templates/home/img/rocomuid/banner_bg.png +0 -0
  18. package/lib/render-templates/home/img/rocomuid/bg.jpg +0 -0
  19. package/lib/render-templates/home/img/rocomuid/img_head.png +0 -0
  20. package/lib/render-templates/home/img/rocomuid/jindu_bar.png +0 -0
  21. package/lib/render-templates/home/img/rocomuid/jindu_tc.png +0 -0
  22. package/lib/render-templates/home/img/rocomuid/level_icon.png +0 -0
  23. package/lib/render-templates/home/img/rocomuid/pet_bg.png +0 -0
  24. package/lib/render-templates/home/img/rocomuid/plant_bg.png +0 -0
  25. package/lib/render-templates/home/img/rocomuid/star_1.png +0 -0
  26. package/lib/render-templates/home/img/rocomuid/star_8.png +0 -0
  27. package/lib/render-templates/home/img/rocomuid/star_9.png +0 -0
  28. package/lib/render-templates/home/img/rocomuid/title_fg.png +0 -0
  29. package/lib/render-templates/home/img/rocomuid/top_bg.png +0 -0
  30. package/lib/render-templates/home/img/rocomuid/touxiang_mask.png +0 -0
  31. package/lib/render-templates/home/index.html +81 -97
  32. package/lib/render-templates/home/style.css +316 -375
  33. package/lib/render-templates/pet-data/index.html +172 -0
  34. package/lib/render-templates/pet-data/style.css +553 -0
  35. package/lib/render-templates/wiki/detail/index.html +84 -0
  36. package/lib/render-templates/wiki/list/index.html +84 -0
  37. package/lib/render-templates/wiki/menu/index.html +65 -0
  38. package/lib/render-templates/wiki/pet/index.html +158 -0
  39. package/lib/render-templates/wiki/pet/style.css +393 -0
  40. package/lib/render-templates/wiki/shared/style.css +431 -0
  41. package/lib/render-templates/wiki/skill/index.html +79 -0
  42. package/lib/render-templates/wiki/skill/style.css +233 -0
  43. package/lib/render-templates/wiki/suggestions/index.html +84 -0
  44. package/lib/render-templates/yuanxing-shangren/CHANGELOG.md +138 -0
  45. package/lib/render-templates/yuanxing-shangren/README.md +86 -0
  46. package/lib/render-templates/yuanxing-shangren/img/coin.png +0 -0
  47. package/lib/render-templates/yuanxing-shangren/img//345/272/225/351/203/250/344/277/241/346/201/257/346/241/206.png +0 -0
  48. package/lib/render-templates/yuanxing-shangren/img//346/240/207/351/242/230.png +0 -0
  49. package/lib/render-templates/yuanxing-shangren/img//347/203/255/351/224/200.png +0 -0
  50. package/lib/render-templates/yuanxing-shangren/img//347/274/226/345/217/267/350/203/214/346/231/257/346/241/206.png +0 -0
  51. package/lib/render-templates/yuanxing-shangren/index.html +198 -170
  52. package/lib/render-templates/yuanxing-shangren/index.html.backup +356 -0
  53. package/lib/render-templates/yuanxing-shangren/merchant.html +461 -0
  54. package/lib/render-templates/yuanxing-shangren/style.css +201 -256
  55. package/lib/render-templates/yuanxing-shangren/today.html +461 -0
  56. package/lib/render-templates/yuanxing-shangren/today.style.css +366 -0
  57. package/lib/render.js +48 -12
  58. package/lib/subscription-send.js +4 -5
  59. package/lib/ttf/rocom_skill_origin.ttf +0 -0
  60. package/lib/ttf//351/200/240/345/255/227/345/267/245/346/210/277/345/220/257/351/273/221/344/275/223.ttf +0 -0
  61. package/lib/types.d.ts +13 -1
  62. package/lib/user.d.ts +23 -1
  63. package/lib/user.js +32 -1
  64. package/lib/wiki-catalog.d.ts +19 -0
  65. package/lib/wiki-catalog.js +124 -0
  66. package/lib/wiki-service.d.ts +220 -0
  67. package/lib/wiki-service.js +1463 -0
  68. package/package.json +1 -1
  69. package/readme.md +4 -5
package/lib/client.js CHANGED
@@ -3,6 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RocomClient = void 0;
4
4
  const koishi_1 = require("koishi");
5
5
  const logger = new koishi_1.Logger('rocom-client');
6
+ // ingame 异步任务状态分类(来自服务端 /ingame/tasks/{task_id} 返回的 status 字段)
7
+ const INGAME_PENDING_STATUSES = ['queued', 'pending', 'running', 'processing', 'accepted'];
8
+ const INGAME_FAILED_STATUSES = ['failed', 'error', 'timeout', 'cancelled', 'canceled'];
9
+ const INGAME_COMPLETED_STATUSES = ['done', 'success', 'succeeded', 'completed', 'finished'];
6
10
  class RocomClient {
7
11
  baseUrl;
8
12
  apiKey;
@@ -349,6 +353,117 @@ class RocomClient {
349
353
  async getIngameTask(ctx, taskId, includeApiKey = true) {
350
354
  return this.requestWithStatus(ctx, 'GET', `/api/v1/games/rocom/ingame/tasks/${taskId}`, this.wegameHeaders('', '', '', '', includeApiKey), { acceptedStatuses: [200, 202] });
351
355
  }
356
+ static normalizeTaskStatus(value) {
357
+ return String(value ?? '').trim().toLowerCase();
358
+ }
359
+ static extractTaskId(payload) {
360
+ return String(payload?.task_id || payload?.taskId || payload?.taskID || '').trim();
361
+ }
362
+ // 判断一份 payload 是否为“已完成的业务结果”(而非排队占位)。
363
+ // 家园接口完成时返回 rows/home_info;玩家、商店完成时返回 source/title/rows;
364
+ // 家园详情(pet/data)完成时返回 npc_pets/npc_pet。
365
+ static isCompletedGatewayPayload(payload) {
366
+ if (!payload || typeof payload !== 'object')
367
+ return false;
368
+ if (Array.isArray(payload.rows))
369
+ return true;
370
+ if (payload.home_info !== undefined)
371
+ return true;
372
+ if (payload.source !== undefined)
373
+ return true;
374
+ if (Array.isArray(payload.npc_pets))
375
+ return true;
376
+ if (payload.npc_pet && typeof payload.npc_pet === 'object')
377
+ return true;
378
+ if (String(payload.title || '').trim())
379
+ return true;
380
+ return false;
381
+ }
382
+ static extractCompletedIngamePayload(payload) {
383
+ if (!payload || typeof payload !== 'object')
384
+ return null;
385
+ if (RocomClient.isCompletedGatewayPayload(payload))
386
+ return payload;
387
+ if (RocomClient.isCompletedGatewayPayload(payload.result))
388
+ return payload.result;
389
+ if (RocomClient.isCompletedGatewayPayload(payload.data))
390
+ return payload.data;
391
+ return null;
392
+ }
393
+ static taskErrorMessage(payload, fallbackStatus = '') {
394
+ const candidates = [
395
+ payload?.message, payload?.error, payload?.error_message, payload?.reason,
396
+ payload?.result?.message, payload?.result?.error, payload?.result?.error_message,
397
+ payload?.data?.message, payload?.data?.error,
398
+ ];
399
+ for (const candidate of candidates) {
400
+ const text = String(candidate ?? '').trim();
401
+ if (text && text.toLowerCase() !== 'failed')
402
+ return text;
403
+ }
404
+ return String(payload?.status || fallbackStatus || '').trim() || '任务执行失败';
405
+ }
406
+ // 排队等候轮询:把 ingame 接口提交后返回的 202(task_id/status=queued)轮询到出结果或超时。
407
+ // 入参 first 是首请求拿到的 { status, data, usedApiKey }。
408
+ async pollIngameTask(ctx, first, options, labels) {
409
+ const { status, data, usedApiKey } = first;
410
+ // 服务端在 wait_ms 内已直接返回业务结果,或任务壳里已经带了完成结果。
411
+ const initialCompletedPayload = RocomClient.extractCompletedIngamePayload(data);
412
+ if (status !== null && initialCompletedPayload)
413
+ return initialCompletedPayload;
414
+ const taskId = RocomClient.extractTaskId(data);
415
+ if (!taskId) {
416
+ if (status === 202)
417
+ this.setLastError(labels.queuedNoTaskId);
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
+ }
429
+ }
430
+ const intervalMs = Math.max(300, Number(options.intervalMs) || 3000);
431
+ const timeoutMs = Math.max(intervalMs, Number(options.timeoutMs) || 180000);
432
+ const startedAt = Date.now();
433
+ let lastStatus = RocomClient.normalizeTaskStatus(data?.status);
434
+ while (Date.now() - startedAt < timeoutMs) {
435
+ if (lastStatus && INGAME_FAILED_STATUSES.includes(lastStatus)) {
436
+ this.setLastError(RocomClient.taskErrorMessage(data, lastStatus));
437
+ return null;
438
+ }
439
+ await new Promise(resolve => setTimeout(resolve, intervalMs));
440
+ const task = await this.getIngameTask(ctx, taskId, usedApiKey);
441
+ if (task.status === null)
442
+ return null;
443
+ const completedPayload = RocomClient.extractCompletedIngamePayload(task.data);
444
+ if (completedPayload)
445
+ return completedPayload;
446
+ lastStatus = RocomClient.normalizeTaskStatus(task.data?.status);
447
+ if (INGAME_FAILED_STATUSES.includes(lastStatus)) {
448
+ this.setLastError(RocomClient.taskErrorMessage(task.data, lastStatus));
449
+ return null;
450
+ }
451
+ // 状态标记完成,但结果可能嵌套在 result/data 中。
452
+ if (INGAME_COMPLETED_STATUSES.includes(lastStatus)) {
453
+ this.setLastError(`Ingame 任务已完成但未返回可解析结果:${taskId}`);
454
+ return null;
455
+ }
456
+ if (lastStatus && !INGAME_PENDING_STATUSES.includes(lastStatus)) {
457
+ this.setLastError(`Ingame 任务状态异常:${lastStatus}`);
458
+ return null;
459
+ }
460
+ // 既无 task_id 也无状态,说明拿到的就是裸结果。
461
+ if (!RocomClient.extractTaskId(task.data) && !lastStatus)
462
+ return task.data;
463
+ }
464
+ this.setLastError(labels.stillQueued(taskId));
465
+ return null;
466
+ }
352
467
  async qqQrLogin(ctx, userIdentifier) {
353
468
  const params = { client_type: 'bot', client_id: 'koishi', provider: 'rocom' };
354
469
  if (userIdentifier)
@@ -496,9 +611,24 @@ class RocomClient {
496
611
  return this.get(ctx, '/api/v1/games/rocom/exchange/posters', this.wegameHeaders(fwToken, userIdentifier), params);
497
612
  }
498
613
  async getMerchantInfo(ctx, refresh = false) {
499
- return this.get(ctx, '/api/v1/games/rocom/merchant/info', this.wegameHeaders(), { refresh: refresh ? 'true' : 'false' });
614
+ return this.get(ctx, '/api/v1/games/rocom/merchant/info', this.wegameHeaders(), {
615
+ refresh: refresh ? 'true' : 'false',
616
+ random_goods: 'all',
617
+ });
500
618
  }
501
619
  async queryPetSize(ctx, diameter, weight, sameRideEgg = false, userIdentifier = '') {
620
+ // 新版蛋尺寸反查:/wiki/pet-size/query(items[].pet/egg_size/match 结构),失败时回退旧接口。
621
+ const newParams = this.scopedParams({
622
+ diameter,
623
+ weight,
624
+ pool: sameRideEgg ? 'ride' : 'magic',
625
+ include_display_only: 'false',
626
+ page_no: 1,
627
+ page_size: 30,
628
+ }, userIdentifier);
629
+ const newRes = await this.get(ctx, '/api/v1/games/rocom/wiki/pet-size/query', this.wegameHeaders('', userIdentifier, 'bot', 'koishi'), newParams, { silentFailureDetails: true });
630
+ if (newRes && Array.isArray(newRes.items))
631
+ return newRes;
502
632
  const params = this.scopedParams({ diameter, weight }, userIdentifier);
503
633
  if (sameRideEgg)
504
634
  params.sameRideEgg = 1;
@@ -562,39 +692,47 @@ class RocomClient {
562
692
  params.after_event_id = afterEventId;
563
693
  return this.get(ctx, '/api/v1/games/rocom/community/egg-exchange-events', this.wegameHeaders('', userIdentifier, 'bot', 'koishi'), this.scopedParams(params, userIdentifier));
564
694
  }
565
- async ingameHomeInfo(ctx, uid, waitMs = 5000) {
695
+ async ingameHomeInfo(ctx, uid, options = {}) {
566
696
  const sanitizedUid = this.sanitizeUid(uid);
567
697
  if (!sanitizedUid) {
568
698
  this.setLastError('UID 不能为空');
569
699
  return null;
570
700
  }
701
+ const waitMs = Number(options.waitMs) || 5000;
571
702
  const path = '/api/v1/games/rocom/ingame/home/info';
572
703
  const payload = { uid: sanitizedUid, wait_ms: waitMs };
573
- const { status, data, usedApiKey } = await this.requestIngameWithFallback(ctx, path, payload);
574
- if (status === 200 && data && !(data.task_id || data.taskId || data.taskID))
575
- return data;
576
- const taskId = data?.task_id || data?.taskId || data?.taskID;
577
- if (!taskId) {
578
- if (status === 202)
579
- this.setLastError('家园查询任务已入队,但未返回 task_id');
580
- return null;
581
- }
582
- for (let i = 0; i < 10; i++) {
583
- await new Promise(resolve => setTimeout(resolve, 1000));
584
- const task = await this.getIngameTask(ctx, taskId, usedApiKey);
585
- if (task.status === 200)
586
- return task.data;
587
- if (task.status === null)
588
- return null;
589
- }
590
- this.setLastError(`Home query task is still queued, please retry later (task_id: ${taskId})`);
591
- return null;
704
+ const first = await this.requestIngameWithFallback(ctx, path, payload);
705
+ return this.pollIngameTask(ctx, first, options, {
706
+ queuedNoTaskId: '家园查询任务已入队,但未返回 task_id',
707
+ stillQueued: (taskId) => `家园查询任务仍在排队,请稍后重试(task_id: ${taskId})`,
708
+ });
592
709
  }
593
710
  async ingameMerchantInfo(ctx, shopId) {
594
711
  const params = { shop_id: shopId };
595
712
  const { status, data } = await this.requestIngameWithFallback(ctx, '/api/v1/games/rocom/ingame/merchant/info', params);
596
713
  return status === null ? null : data;
597
714
  }
715
+ // 家园详情:批量或单只查询家园摆放精灵的完整 ingame 数据(上游 v3.6.1)。
716
+ // UID 参数名为 target_uin;单只查询需同时提供 pet_gid 和 npc_id。
717
+ async ingamePetData(ctx, uid, extras = {}, options = {}) {
718
+ const sanitizedUid = this.sanitizeUid(uid);
719
+ if (!sanitizedUid) {
720
+ this.setLastError('UID 不能为空');
721
+ return null;
722
+ }
723
+ const waitMs = Number(options.waitMs) || 20000;
724
+ const payload = { target_uin: sanitizedUid, wait_ms: waitMs };
725
+ if (extras.petGid !== undefined && extras.petGid !== null && String(extras.petGid) !== '')
726
+ payload.pet_gid = extras.petGid;
727
+ if (extras.npcId !== undefined && extras.npcId !== null && String(extras.npcId) !== '')
728
+ payload.npc_id = extras.npcId;
729
+ const path = '/api/v1/games/rocom/ingame/pet/data';
730
+ const first = await this.requestIngameWithFallback(ctx, path, payload);
731
+ return this.pollIngameTask(ctx, first, options, {
732
+ queuedNoTaskId: '家园详情任务已入队,但未返回 task_id',
733
+ stillQueued: (taskId) => `家园详情任务仍在排队,请稍后重试(task_id: ${taskId})`,
734
+ });
735
+ }
598
736
  async getFriendship(ctx, fwToken, userIds, userIdentifier = '') {
599
737
  return this.get(ctx, '/api/v1/games/rocom/social/friendship', this.rocomHeaders(fwToken, userIdentifier), { user_ids: userIds });
600
738
  }
@@ -626,5 +764,67 @@ class RocomClient {
626
764
  params.name = options.name;
627
765
  return this.get(ctx, '/api/v1/games/rocom/pet/detail', this.wegameHeaders(), params);
628
766
  }
767
+ // ===== 新版 RoCom Wiki API(上游 v3.6.0)=====
768
+ wikiPagedParams(q = '', pageNo = 1, pageSize = 10, filters = {}) {
769
+ const params = {
770
+ page_no: Math.max(Number(pageNo) || 1, 1),
771
+ page_size: Math.min(Math.max(Number(pageSize) || 10, 1), 100),
772
+ };
773
+ if (q)
774
+ params.q = q;
775
+ for (const [key, value] of Object.entries(filters)) {
776
+ if (value !== undefined && value !== null && value !== '')
777
+ params[key] = value;
778
+ }
779
+ return params;
780
+ }
781
+ async listWikiPets(ctx, q = '', pageNo = 1, pageSize = 10, filters = {}) {
782
+ return this.get(ctx, '/api/v1/games/rocom/wiki/pets', this.wegameHeaders(), this.wikiPagedParams(q, pageNo, pageSize, filters));
783
+ }
784
+ async getWikiPet(ctx, petId) {
785
+ return this.get(ctx, `/api/v1/games/rocom/wiki/pets/${petId}`, this.wegameHeaders());
786
+ }
787
+ async getWikiPetProfile(ctx, petId) {
788
+ return this.get(ctx, `/api/v1/games/rocom/wiki/pets/${petId}/profile`, this.wegameHeaders(), undefined, { silentFailureDetails: true });
789
+ }
790
+ async getWikiPetSkills(ctx, petId) {
791
+ return this.get(ctx, `/api/v1/games/rocom/wiki/pets/${petId}/skills`, this.wegameHeaders(), undefined, { silentFailureDetails: true });
792
+ }
793
+ async getWikiPetFamily(ctx, petId) {
794
+ return this.get(ctx, `/api/v1/games/rocom/wiki/pets/${petId}/family`, this.wegameHeaders(), undefined, { silentFailureDetails: true });
795
+ }
796
+ async getWikiPetHandbook(ctx, petId) {
797
+ return this.get(ctx, `/api/v1/games/rocom/wiki/pets/${petId}/handbook`, this.wegameHeaders(), undefined, { silentFailureDetails: true });
798
+ }
799
+ async listWikiSkills(ctx, q = '', pageNo = 1, pageSize = 10, filters = {}) {
800
+ return this.get(ctx, '/api/v1/games/rocom/wiki/skills', this.wegameHeaders(), this.wikiPagedParams(q, pageNo, pageSize, filters));
801
+ }
802
+ async getWikiSkill(ctx, skillId) {
803
+ return this.get(ctx, `/api/v1/games/rocom/wiki/skills/${skillId}`, this.wegameHeaders(), undefined, { silentFailureDetails: true });
804
+ }
805
+ async getWikiSkillPets(ctx, skillId) {
806
+ return this.get(ctx, `/api/v1/games/rocom/wiki/skills/${skillId}/pets`, this.wegameHeaders(), undefined, { silentFailureDetails: true });
807
+ }
808
+ async getWikiCatalogs(ctx) {
809
+ return this.get(ctx, '/api/v1/games/rocom/wiki/catalogs', this.wegameHeaders());
810
+ }
811
+ async getWikiOptions(ctx) {
812
+ return this.get(ctx, '/api/v1/games/rocom/wiki/options', this.wegameHeaders(), undefined, { silentFailureDetails: true });
813
+ }
814
+ async getWikiPath(ctx, path, params = {}) {
815
+ const trimmed = String(path || '').trim();
816
+ if (!trimmed.startsWith('/api/v1/games/rocom/wiki/')) {
817
+ this.setLastError('非法 Wiki 路径');
818
+ return null;
819
+ }
820
+ return this.get(ctx, trimmed, this.wegameHeaders(), params);
821
+ }
822
+ async listWikiCatalogItems(ctx, path, q = '', pageNo = 1, pageSize = 10, search = true) {
823
+ const params = this.wikiPagedParams(search ? q : '', pageNo, pageSize);
824
+ return this.getWikiPath(ctx, path, params);
825
+ }
826
+ get wikiAssetBaseUrl() {
827
+ return this.baseUrl;
828
+ }
629
829
  }
630
830
  exports.RocomClient = RocomClient;
@@ -97,6 +97,7 @@ function register(deps) {
97
97
  const { ctx, client, userMgr } = deps;
98
98
  ctx.command('洛克').subcommand('.QQ登录', 'QQ 扫码登录')
99
99
  .alias('洛克QQ登录')
100
+ .alias('洛克qq登录')
100
101
  .action(async ({ session }) => {
101
102
  const userId = session.userId;
102
103
  const qrData = await client.qqQrLogin(ctx, userId);