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
@@ -8,6 +8,7 @@ const koishi_1 = require("koishi");
8
8
  const account_1 = require("./account");
9
9
  const send_image_1 = require("../send-image");
10
10
  const subscription_send_1 = require("../subscription-send");
11
+ const pet_data_service_1 = require("../pet-data-service");
11
12
  const node_fs_1 = __importDefault(require("node:fs"));
12
13
  const node_path_1 = __importDefault(require("node:path"));
13
14
  const logger = new koishi_1.Logger('rocom-query');
@@ -827,7 +828,7 @@ function panelPetDetailData(uid, pet, updatedAtText) {
827
828
  };
828
829
  }
829
830
  async function refreshPanelPets(deps, uid, userId = '') {
830
- const res = await deps.client.ingameHomeInfo(deps.ctx, uid, 20000);
831
+ const res = await deps.client.ingameHomeInfo(deps.ctx, uid, { waitMs: 20000, timeoutMs: 30000 });
831
832
  if (res) {
832
833
  const homeInfo = homeInfoPayload(res);
833
834
  const pets = buildPanelPetList(homeInfo);
@@ -860,7 +861,7 @@ function formatPanelUpdatedAt(ts) {
860
861
  return new Date().toLocaleString('zh-CN');
861
862
  return new Date(value * 1000).toLocaleString('zh-CN');
862
863
  }
863
- function extractHomePet(raw, index, guard = false) {
864
+ function extractHomePet(deps, raw, index, guard = false) {
864
865
  if (!raw || typeof raw !== 'object')
865
866
  return null;
866
867
  const homePet = raw.home_pet_info && typeof raw.home_pet_info === 'object' ? raw.home_pet_info : raw;
@@ -874,6 +875,24 @@ function extractHomePet(raw, index, guard = false) {
874
875
  const eggReady = hasEgg || (predictedEggTime > 0 && nowTs >= predictedEggTime);
875
876
  const feedRound = Number(homePet.feed_round || raw.feed_round || 0) || 0;
876
877
  const gender = Number(display.gender || raw.gender || 0) || 0;
878
+ let mutationType = Number(display.mutation_type || raw.mutation_type || homePet.mutation_type || 0) || 0;
879
+ // 上游 v3.4.0:mutation_type 缺失时按 real_speciality_ids 识别变体(103=异色、502=炫彩、两者兼有=异色炫彩)。
880
+ if (!mutationType) {
881
+ const specialityValues = [];
882
+ for (const value of [homePet.real_speciality_ids, raw.real_speciality_ids, homePet.speciality_id, raw.speciality_id]) {
883
+ if (Array.isArray(value))
884
+ specialityValues.push(...value);
885
+ else if (value !== undefined && value !== null && value !== '')
886
+ specialityValues.push(value);
887
+ }
888
+ const specialityIds = new Set(specialityValues.map(value => String(value).trim()).filter(Boolean));
889
+ if (specialityIds.has('103') && specialityIds.has('502'))
890
+ mutationType = 9;
891
+ else if (specialityIds.has('103'))
892
+ mutationType = 1;
893
+ else if (specialityIds.has('502'))
894
+ mutationType = 8;
895
+ }
877
896
  const isMale = gender === 1;
878
897
  const status = homePet.status ?? raw.status;
879
898
  const isGuard = guard || Boolean(raw.is_guard || raw.guard) || ['2', 'guard', '守卫'].includes(String(status).toLowerCase());
@@ -913,11 +932,18 @@ function extractHomePet(raw, index, guard = false) {
913
932
  name: String(homePet.name || homePet.pet_name || raw.name || raw.pet_name || `精灵 ${petId || ''}`),
914
933
  level: display.level || raw.level || homePet.level || '--',
915
934
  iconUrl: homePetIcon(petId, raw.icon_url || raw.pet_img_url || raw.petIcon || ''),
935
+ starIconUrl: [1, 8, 9].includes(mutationType)
936
+ ? deps.renderer.resourceUrl(`render-templates/home/img/rocomuid/star_${mutationType}.png`)
937
+ : '',
916
938
  badge: isGuard ? '守' : (hasEgg ? '蛋' : ''),
939
+ mutationType,
917
940
  isGuard,
918
941
  statusText,
919
942
  statusClass,
920
943
  note,
944
+ hasEgg,
945
+ eggReady,
946
+ eggTime: predictedEggTime || 0,
921
947
  inspireReady: eggReady,
922
948
  readyAt: predictedEggTime || 0,
923
949
  gender,
@@ -1038,7 +1064,7 @@ function buildHomeRenderData(deps, res, uid) {
1038
1064
  const indoorPets = [];
1039
1065
  const guardPets = [];
1040
1066
  indoorSources.forEach((raw, index) => {
1041
- const item = extractHomePet(raw, index);
1067
+ const item = extractHomePet(deps, raw, index);
1042
1068
  if (!item)
1043
1069
  return;
1044
1070
  if (item.isGuard)
@@ -1047,7 +1073,7 @@ function buildHomeRenderData(deps, res, uid) {
1047
1073
  indoorPets.push(item);
1048
1074
  });
1049
1075
  guardSources.forEach((raw, index) => {
1050
- const item = extractHomePet(raw, index, true);
1076
+ const item = extractHomePet(deps, raw, index, true);
1051
1077
  if (item)
1052
1078
  guardPets.push(item);
1053
1079
  });
@@ -1264,13 +1290,33 @@ async function resolveHomeUid(deps, session, uid = '') {
1264
1290
  return targetUid;
1265
1291
  return String(deps.userMgr.getPrimaryBinding(session?.userId || '')?.role_id || '');
1266
1292
  }
1293
+ const HOME_SUB_TEXT = {
1294
+ garden: {
1295
+ kindText: '菜园作物',
1296
+ actionText: '成熟',
1297
+ subscribedHint: '的家园菜园提醒:首个成熟和全部成熟时各推送一次。',
1298
+ needUidHint: '请提供玩家 UID,或先完成绑定后再订阅家园菜园。',
1299
+ },
1300
+ inspiration: {
1301
+ kindText: '精灵灵感',
1302
+ actionText: '完成',
1303
+ subscribedHint: '的家园精灵灵感提醒:首个完成和全部完成时各推送一次。',
1304
+ needUidHint: '请提供玩家 UID,或先完成绑定后再订阅家园灵感。',
1305
+ },
1306
+ egg: {
1307
+ kindText: '精灵生蛋',
1308
+ actionText: '可领取',
1309
+ subscribedHint: '的家园精灵生蛋提醒:首个可领取和全部可领取时各推送一次。',
1310
+ needUidHint: '请提供玩家 UID,或先完成绑定后再订阅家园生蛋。',
1311
+ },
1312
+ };
1267
1313
  async function subscribeHome(deps, session, uid, kind) {
1268
1314
  const target = sessionTarget(session);
1269
1315
  if (!target.userId && !isBotAdmin(session, deps.config.adminUserIds))
1270
1316
  return '此指令仅限管理员使用。';
1271
1317
  const targetUid = await resolveHomeUid(deps, session, uid);
1272
1318
  if (!targetUid)
1273
- return kind === 'garden' ? '请提供玩家 UID,或先完成绑定后再订阅家园菜园。' : '请提供玩家 UID,或先完成绑定后再订阅家园灵感。';
1319
+ return HOME_SUB_TEXT[kind].needUidHint;
1274
1320
  const key = homeSubscriptionKey(session, targetUid, kind);
1275
1321
  deps.homeSubMgr.upsert(key, {
1276
1322
  key,
@@ -1284,9 +1330,7 @@ async function subscribeHome(deps, session, uid, kind) {
1284
1330
  notify_state: {},
1285
1331
  updated_at: Math.floor(Date.now() / 1000),
1286
1332
  });
1287
- return kind === 'garden'
1288
- ? `已订阅 UID ${targetUid} 的家园菜园提醒:首个成熟和全部成熟时各推送一次。`
1289
- : `已订阅 UID ${targetUid} 的家园精灵灵感提醒:首个完成和全部完成时各推送一次。`;
1333
+ return `已订阅 UID ${targetUid} ${HOME_SUB_TEXT[kind].subscribedHint}`;
1290
1334
  }
1291
1335
  function homeSubscriptionState(data, kind) {
1292
1336
  if (kind === 'garden') {
@@ -1295,14 +1339,19 @@ function homeSubscriptionState(data, kind) {
1295
1339
  const names = readyItems.map((item) => `田地${item.landIndex} ${item.plantName}`);
1296
1340
  return { items, readyItems, names };
1297
1341
  }
1342
+ if (kind === 'egg') {
1343
+ const items = [...(data.indoorPets || []), ...(data.guardPets || [])].filter((item) => item.eggTime || item.hasEgg);
1344
+ const readyItems = items.filter((item) => item.eggReady);
1345
+ const names = readyItems.map((item) => item.name || '未知精灵');
1346
+ return { items, readyItems, names };
1347
+ }
1298
1348
  const items = [...(data.indoorPets || []), ...(data.guardPets || [])].filter((item) => item.readyAt);
1299
1349
  const readyItems = items.filter((item) => item.inspireReady);
1300
1350
  const names = readyItems.map((item) => item.name || '未知精灵');
1301
1351
  return { items, readyItems, names };
1302
1352
  }
1303
1353
  function homeSubscriptionMessage(uid, kind, level, totalCount, readyItems, names) {
1304
- const kindText = kind === 'garden' ? '菜园作物' : '精灵灵感';
1305
- const actionText = kind === 'garden' ? '成熟' : '完成';
1354
+ const { kindText, actionText } = HOME_SUB_TEXT[kind];
1306
1355
  const levelText = level === 'first' ? '首个' : '全部';
1307
1356
  return [
1308
1357
  `家园${kindText}${levelText}${actionText}提醒:${uid}`,
@@ -1316,11 +1365,11 @@ async function checkHomeSubscriptions(deps) {
1316
1365
  let checkedCount = 0;
1317
1366
  let pushedCount = 0;
1318
1367
  for (const [key, sub] of Object.entries(subs)) {
1319
- if (!sub.uid || !['garden', 'inspiration'].includes(sub.kind))
1368
+ if (!sub.uid || !['garden', 'inspiration', 'egg'].includes(sub.kind))
1320
1369
  continue;
1321
1370
  checkedCount++;
1322
1371
  if (!cache.has(sub.uid)) {
1323
- cache.set(sub.uid, await deps.client.ingameHomeInfo(deps.ctx, sub.uid));
1372
+ cache.set(sub.uid, await deps.client.ingameHomeInfo(deps.ctx, sub.uid, { timeoutMs: 30000 }));
1324
1373
  }
1325
1374
  const res = cache.get(sub.uid);
1326
1375
  if (!res)
@@ -1809,11 +1858,108 @@ function register(deps) {
1809
1858
  }
1810
1859
  if (!targetUid)
1811
1860
  return '请提供玩家 UID,或先完成绑定后使用 洛克.家园。';
1812
- const res = await client.ingameHomeInfo(ctx, targetUid);
1861
+ let queuedNotified = false;
1862
+ const res = await client.ingameHomeInfo(ctx, targetUid, {
1863
+ waitMs: deps.config.homeQueryWaitMs,
1864
+ intervalMs: deps.config.homeQueryPollIntervalMs,
1865
+ timeoutMs: deps.config.homeQueryTimeoutMs,
1866
+ onQueued: async () => {
1867
+ if (queuedNotified)
1868
+ return;
1869
+ queuedNotified = true;
1870
+ await session?.send?.(`UID ${targetUid} 的家园查询已进入队列,正在等待游戏侧返回,请稍候…`);
1871
+ },
1872
+ });
1813
1873
  if (!res)
1814
1874
  return `家园查询失败:${client.getLastErrorBrief()}`;
1815
1875
  await sendImage(deps, session, 'home', buildHomeRenderData(deps, res, targetUid), `【洛克家园】UID ${targetUid}`);
1816
1876
  });
1877
+ ctx.command('洛克').subcommand('.家园详情 [uid:string] [petGid:string] [npcId:string]', '查询家园摆放精灵的完整 ingame 数据')
1878
+ .alias('洛克家园详情')
1879
+ .alias('家园详情')
1880
+ .alias('洛克精灵数据')
1881
+ .alias('精灵数据')
1882
+ .action(async ({ session }, uid = '', petGid = '', npcId = '') => {
1883
+ const petGidText = String(petGid || '').trim();
1884
+ const npcIdText = String(npcId || '').trim();
1885
+ if ((petGidText && !npcIdText) || (npcIdText && !petGidText)) {
1886
+ return [
1887
+ '请同时提供 pet_gid 和 npc_id。用法:洛克.家园详情 <UID> 或 洛克.家园详情 <UID> <pet_gid> <npc_id>',
1888
+ '提示:该接口依赖目标玩家在线,npc_id 也可以使用 furniture_guid。',
1889
+ ].join('\n');
1890
+ }
1891
+ let targetUid = String(uid || '').trim();
1892
+ if (!targetUid) {
1893
+ const binding = deps.userMgr.getPrimaryBinding(session.userId);
1894
+ targetUid = String(binding?.role_id || '');
1895
+ }
1896
+ if (!targetUid)
1897
+ return '请提供玩家 UID,或先完成绑定后使用 洛克.家园详情。目标玩家需要在线。';
1898
+ let queuedNotified = false;
1899
+ const res = await client.ingamePetData(ctx, targetUid, { petGid: petGidText, npcId: npcIdText }, {
1900
+ waitMs: 20000,
1901
+ intervalMs: deps.config.homeQueryPollIntervalMs,
1902
+ timeoutMs: deps.config.homeQueryTimeoutMs,
1903
+ onQueued: async () => {
1904
+ if (queuedNotified)
1905
+ return;
1906
+ queuedNotified = true;
1907
+ await session?.send?.(`UID ${targetUid} 的家园详情查询已进入队列,正在等待游戏侧返回,请稍候…`);
1908
+ },
1909
+ });
1910
+ if (!res) {
1911
+ return [
1912
+ `家园详情查询失败:${client.getLastErrorBrief()}`,
1913
+ '提示:该接口需要目标玩家在线,且目标家园可访问;离线时通常无法获取完整数据。',
1914
+ ].join('\n');
1915
+ }
1916
+ const lowBandwidth = Boolean(deps.config.lowBandwidthMode);
1917
+ // Wiki 补全(技能详情/精灵体重范围);低带宽模式跳过技能图标但仍取技能名。
1918
+ const [optionsPayload, skillLookup, sizeLookup] = await Promise.all([
1919
+ deps.wikiService.getOptionsPayload().catch(() => ({})),
1920
+ (async () => {
1921
+ const lookup = {};
1922
+ const ids = (0, pet_data_service_1.petDataSkillIdsFromPayload)(res);
1923
+ await Promise.all(ids.map(async (id) => {
1924
+ const detail = await deps.wikiService.getSkillDetailCached(id).catch(() => null);
1925
+ if (detail)
1926
+ lookup[id] = detail;
1927
+ }));
1928
+ return lookup;
1929
+ })(),
1930
+ (async () => {
1931
+ const lookup = {};
1932
+ const ids = (0, pet_data_service_1.petDataPetIdsFromPayload)(res);
1933
+ await Promise.all(ids.map(async (id) => {
1934
+ const detail = await deps.wikiService.getPetDetailCached(id).catch(() => null);
1935
+ if (detail)
1936
+ lookup[id] = detail;
1937
+ }));
1938
+ return lookup;
1939
+ })(),
1940
+ ]);
1941
+ const data = (0, pet_data_service_1.buildPetDataRenderData)(res, targetUid || '当前绑定', {
1942
+ optionsPayload,
1943
+ skillLookup,
1944
+ sizeLookup,
1945
+ singleQuery: Boolean(petGidText && npcIdText),
1946
+ lowBandwidthMode: lowBandwidth,
1947
+ baseUrl: deps.config.apiBaseUrl.replace(/\/$/, ''),
1948
+ });
1949
+ const fallbackLines = [
1950
+ `家园详情 - UID ${data.uid}(${data.onlineText})`,
1951
+ data.notice,
1952
+ ];
1953
+ for (const pet of (data.pets || []).slice(0, 8)) {
1954
+ fallbackLines.push(`${pet.index}. ${pet.name} Lv.${pet.level} #${pet.baseId} ${pet.variantText} 分贝 ${pet.voiceText || '--'}`);
1955
+ }
1956
+ if (!data.pets?.length)
1957
+ fallbackLines.push(data.emptyText);
1958
+ fallbackLines.push('');
1959
+ fallbackLines.push('若服务器带宽过小导致生成超时,请在配置项打开低带宽模式。');
1960
+ fallbackLines.push('低带宽模式开启后,家园详情将不再加载技能图标。');
1961
+ await sendImage(deps, session, 'pet-data', data, fallbackLines.join('\n'));
1962
+ });
1817
1963
  ctx.command('洛克').subcommand('.刷新面板 [uid:string]', '刷新指定 UID 的精灵面板缓存')
1818
1964
  .alias('洛克刷新面板')
1819
1965
  .alias('刷新面板')
@@ -1922,12 +2068,14 @@ function register(deps) {
1922
2068
  .action(async ({ session }, uid = '') => subscribeHome(deps, session, uid, 'garden'));
1923
2069
  ctx.command('订阅家园灵感 [uid:string]', '订阅指定 UID 的家园精灵灵感完成提醒')
1924
2070
  .action(async ({ session }, uid = '') => subscribeHome(deps, session, uid, 'inspiration'));
2071
+ ctx.command('订阅家园生蛋 [uid:string]', '订阅指定 UID 的家园精灵生蛋提醒')
2072
+ .action(async ({ session }, uid = '') => subscribeHome(deps, session, uid, 'egg'));
1925
2073
  ctx.command('取消订阅家园 [kind:string] [uid:string]', '取消当前会话的家园订阅')
1926
2074
  .action(async ({ session }, kind = '全部', uid = '') => {
1927
2075
  const target = sessionTarget(session);
1928
2076
  if (!target.userId && !isBotAdmin(session, deps.config.adminUserIds))
1929
2077
  return '此指令仅限管理员使用。';
1930
- const kindMap = { '菜园': 'garden', '灵感': 'inspiration', '全部': '', all: '', garden: 'garden', inspiration: 'inspiration' };
2078
+ const kindMap = { '菜园': 'garden', '灵感': 'inspiration', '生蛋': 'egg', '全部': '', all: '', garden: 'garden', inspiration: 'inspiration', egg: 'egg' };
1931
2079
  const deleted = deps.homeSubMgr.deleteMatching(target, kindMap[String(kind || '全部')] ?? '', String(uid || '').trim());
1932
2080
  return deleted ? `已取消 ${deleted} 条家园订阅。` : '当前会话没有匹配的家园订阅。';
1933
2081
  });
@@ -4,6 +4,7 @@ exports.register = register;
4
4
  const koishi_1 = require("koishi");
5
5
  const activities_service_1 = require("../activities-service");
6
6
  const send_image_1 = require("../send-image");
7
+ const subscription_send_1 = require("../subscription-send");
7
8
  const logger = new koishi_1.Logger('rocom-tools');
8
9
  const activitiesService = new activities_service_1.ActivitiesService();
9
10
  function trimText(value) {
@@ -54,6 +55,70 @@ function firstArray(payload, keys) {
54
55
  function announcementId(item) {
55
56
  return trimText(item?.thread_id || item?.id);
56
57
  }
58
+ function announcementTimestamp(item) {
59
+ for (const key of ['published_at_ts', 'publish_at_ts', 'created_at_ts']) {
60
+ const value = Number(item?.[key]);
61
+ if (Number.isFinite(value) && value > 0)
62
+ return value;
63
+ }
64
+ for (const key of ['publishAt', 'published_at', 'createdAt']) {
65
+ const text = trimText(item?.[key]);
66
+ if (!text)
67
+ continue;
68
+ const date = new Date(text);
69
+ if (!Number.isNaN(date.getTime()))
70
+ return Math.floor(date.getTime() / 1000);
71
+ }
72
+ return 0;
73
+ }
74
+ function announcementSubscriptionKey(session) {
75
+ const privateChat = !session?.guildId;
76
+ return [session?.platform || '', session?.channelId || 'private', privateChat ? (session?.userId || '') : '', 'announcement'].join(':');
77
+ }
78
+ // 上游 v3.2.0:轮询最新公告,向订阅会话推送新公告提醒。
79
+ async function checkAnnouncementSubscriptions(deps) {
80
+ const { ctx, client, announcementSubMgr } = deps;
81
+ const subs = announcementSubMgr.getAll();
82
+ const subEntries = Object.entries(subs);
83
+ if (!subEntries.length)
84
+ return { subscriptions: 0, pushed: 0 };
85
+ const latest = await client.getLatestAnnouncement(ctx, { category_id: 99 });
86
+ const item = latest?.detail || latest?.announcement || latest;
87
+ const latestId = announcementId(item);
88
+ if (!latestId)
89
+ return { subscriptions: subEntries.length, pushed: 0 };
90
+ const latestTs = announcementTimestamp(item);
91
+ let pushed = 0;
92
+ for (const [key, sub] of subEntries) {
93
+ if (latestId === String(sub.last_id || ''))
94
+ continue;
95
+ if (latestTs && sub.since_ts && latestTs <= sub.since_ts)
96
+ continue;
97
+ const message = [`【洛克王国新公告】`, buildAnnouncementDetailText(latest)].join('\n');
98
+ try {
99
+ const sent = await (0, subscription_send_1.sendScheduledMessage)(ctx, {
100
+ platform: sub.platform,
101
+ channelId: sub.channel_id || sub.guild_id || sub.user_id || '',
102
+ guildId: sub.guild_id || '',
103
+ userId: sub.user_id || '',
104
+ }, message);
105
+ if (!sent)
106
+ continue;
107
+ }
108
+ catch (e) {
109
+ logger.warn(`公告订阅推送失败: ${e}`);
110
+ continue;
111
+ }
112
+ pushed++;
113
+ announcementSubMgr.upsert(key, {
114
+ ...sub,
115
+ last_id: latestId,
116
+ since_ts: latestTs || Math.floor(Date.now() / 1000),
117
+ updated_at: Math.floor(Date.now() / 1000),
118
+ });
119
+ }
120
+ return { subscriptions: subEntries.length, pushed };
121
+ }
57
122
  function buildAnnouncementListText(data, page) {
58
123
  const list = firstArray(data, ['list', 'items']);
59
124
  if (!list.length)
@@ -147,6 +212,45 @@ function register(deps) {
147
212
  return `公告详情查询失败:${client.getLastErrorBrief()}`;
148
213
  return buildAnnouncementDetailText(data);
149
214
  });
215
+ ctx.command('订阅洛克公告', '订阅洛克王国新公告推送')
216
+ .action(async ({ session }) => {
217
+ const privateChat = !session?.guildId;
218
+ if (!privateChat && !config.adminUserIds.includes(session?.userId || ''))
219
+ return '此指令仅限管理员使用。';
220
+ const key = announcementSubscriptionKey(session);
221
+ // 以当前最新公告为基线,只推送之后的新公告。
222
+ const latest = await client.getLatestAnnouncement(ctx, { category_id: 99 }, session?.userId || '');
223
+ const item = latest?.detail || latest?.announcement || latest;
224
+ deps.announcementSubMgr.upsert(key, {
225
+ key,
226
+ platform: session?.platform || '',
227
+ channel_id: session?.channelId || '',
228
+ guild_id: session?.guildId || '',
229
+ user_id: privateChat ? (session?.userId || '') : '',
230
+ updated_by: session?.userId || '',
231
+ last_id: announcementId(item),
232
+ since_ts: announcementTimestamp(item) || Math.floor(Date.now() / 1000),
233
+ updated_at: Math.floor(Date.now() / 1000),
234
+ });
235
+ return '已订阅洛克王国新公告推送,检测到新公告时会在当前会话提醒。';
236
+ });
237
+ ctx.command('取消订阅洛克公告', '取消洛克王国新公告推送')
238
+ .action(async ({ session }) => {
239
+ const privateChat = !session?.guildId;
240
+ if (!privateChat && !config.adminUserIds.includes(session?.userId || ''))
241
+ return '此指令仅限管理员使用。';
242
+ const deleted = deps.announcementSubMgr.deleteMatching({
243
+ platform: session?.platform || '',
244
+ channelId: session?.channelId || '',
245
+ userId: privateChat ? (session?.userId || '') : '',
246
+ });
247
+ return deleted ? '已取消洛克公告订阅。' : '当前会话没有洛克公告订阅。';
248
+ });
249
+ if (config.announcementSubscriptionEnabled) {
250
+ ctx.setInterval(() => {
251
+ checkAnnouncementSubscriptions(deps).catch(err => logger.warn(`公告订阅检查失败: ${err}`));
252
+ }, Math.max(1, config.announcementPollIntervalMinutes || 10) * 60000);
253
+ }
150
254
  ctx.command('洛克').subcommand('.同步配置', '手动同步 RoCom 远端配置')
151
255
  .alias('洛克同步配置')
152
256
  .action(async ({ session }) => {
@@ -1,13 +1,174 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.register = register;
4
- const WIKI_CLOSED_MESSAGE = '该功能暂时关闭';
7
+ const koishi_1 = require("koishi");
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const send_image_1 = require("../send-image");
10
+ const wiki_service_1 = require("../wiki-service");
11
+ const logger = new koishi_1.Logger('rocom-wiki-cmd');
12
+ async function sendImage(deps, session, templateName, data, fallback) {
13
+ const png = await deps.renderer.renderHtml(deps.ctx, templateName, data);
14
+ await (0, send_image_1.sendImageWithFallback)(session, png, fallback, `wiki:${templateName}`, deps.config);
15
+ }
16
+ // 统一 Wiki 入口:目录菜单 / 分类查询 / 全局搜索 / 精灵与技能专属渲染。
17
+ async function handleWikiQuery(deps, session, rawText) {
18
+ const { ctx, wikiService } = deps;
19
+ const trimmed = String(rawText || '').trim();
20
+ const rawKey = trimmed.toLowerCase();
21
+ const catalogsMeta = await wikiService.getCatalogsPayload();
22
+ if (!trimmed || ['帮助', 'help', '类型', '目录', '专题'].includes(rawKey)) {
23
+ const optionsMeta = await wikiService.getOptionsPayload();
24
+ const data = wikiService.buildCatalogRenderData(catalogsMeta, optionsMeta);
25
+ await sendImage(deps, session, 'wiki/menu', data, wikiService.catalogUsageText());
26
+ return;
27
+ }
28
+ let { catalog, query, pageNo } = wikiService.parseWikiCommand(trimmed);
29
+ if (catalog) {
30
+ catalog = wikiService.catalogForKeyFromPayload(String(catalog.key || ''), catalogsMeta) || catalog;
31
+ }
32
+ if (!catalog) {
33
+ const { parts, pageNo: parsedPageNo } = (0, wiki_service_1.splitWikiCommandParts)(trimmed);
34
+ if (parts.length) {
35
+ const dynamicCatalog = wikiService.dynamicCatalogByToken(parts[0], catalogsMeta);
36
+ if (dynamicCatalog) {
37
+ catalog = dynamicCatalog;
38
+ query = parts.slice(1).join(' ').trim();
39
+ pageNo = parsedPageNo;
40
+ }
41
+ }
42
+ }
43
+ if (!catalog) {
44
+ const { payload, mode, error } = await wikiService.fetchGlobalSearch(query, pageNo);
45
+ if (error)
46
+ return error;
47
+ if (mode === 'global-detail') {
48
+ const sourceCatalog = payload?._catalog || {};
49
+ const detail = payload?.item && typeof payload.item === 'object' ? payload.item : {};
50
+ const sourceKey = String(sourceCatalog?.key || '');
51
+ if (sourceKey === 'pets') {
52
+ const sections = await wikiService.fetchWikiPetSections(detail.pet_id);
53
+ const data = (0, wiki_service_1.buildWikiPetRenderData)(detail, sections.profile, sections.skills, sections.family, sections.handbook, query);
54
+ await sendImage(deps, session, 'wiki/pet', data, [
55
+ `${data.name} #${data.number}`,
56
+ `属性:${data.type_names.join(' / ') || '暂无'}`,
57
+ `蛋组:${data.egg_groups.join(' / ') || '暂无'}`,
58
+ data.description,
59
+ ].join('\n'));
60
+ return;
61
+ }
62
+ if (sourceKey === 'skills') {
63
+ const pets = await deps.client.getWikiSkillPets(ctx, detail.skill_id);
64
+ const data = (0, wiki_service_1.buildWikiSkillRenderData)(detail, pets && typeof pets === 'object' ? pets : {}, query);
65
+ await sendImage(deps, session, 'wiki/skill', data, `${data.name} #${data.skill_id}\n${data.description}`);
66
+ return;
67
+ }
68
+ const data = wikiService.buildGenericRenderData(sourceCatalog?.key ? sourceCatalog : { title: '全局搜索', key: 'global' }, detail, query, 'detail', pageNo);
69
+ await sendImage(deps, session, 'wiki/detail', data, `${data.title}\n${data.summary || ''}`);
70
+ return;
71
+ }
72
+ const data = wikiService.buildGenericRenderData({ title: '全局搜索', key: 'global' }, payload, query, 'global-search', pageNo);
73
+ await sendImage(deps, session, 'wiki/list', data, `${data.title}\n${data.summary || ''}`);
74
+ return;
75
+ }
76
+ if (['pets', 'pet'].includes(catalog.key) && query) {
77
+ const { detail: overview, candidates, error } = await wikiService.resolveWikiPet(query);
78
+ if (error)
79
+ return error;
80
+ if (candidates.length)
81
+ return (0, wiki_service_1.wikiCandidateText)(query, candidates, '精灵');
82
+ if (!overview)
83
+ return '获取 Wiki 精灵详情失败:接口未返回有效数据。';
84
+ const sections = await wikiService.fetchWikiPetSections(overview.pet_id);
85
+ const data = (0, wiki_service_1.buildWikiPetRenderData)(overview, sections.profile, sections.skills, sections.family, sections.handbook, query);
86
+ await sendImage(deps, session, 'wiki/pet', data, `${data.name} #${data.number}\n${data.description}`);
87
+ return;
88
+ }
89
+ if (['skills', 'skill'].includes(catalog.key) && query) {
90
+ const { detail, candidates, error } = await wikiService.resolveWikiSkill(query);
91
+ if (error)
92
+ return error;
93
+ if (candidates.length)
94
+ return (0, wiki_service_1.wikiCandidateText)(query, candidates, '技能');
95
+ if (!detail)
96
+ return '获取技能详情失败:接口未返回有效数据。';
97
+ const pets = await deps.client.getWikiSkillPets(ctx, detail.skill_id);
98
+ const data = (0, wiki_service_1.buildWikiSkillRenderData)(detail, pets && typeof pets === 'object' ? pets : {}, query);
99
+ await sendImage(deps, session, 'wiki/skill', data, `${data.name} #${data.skill_id}\n${data.description}`);
100
+ return;
101
+ }
102
+ const { payload, mode, error } = await wikiService.fetchGenericCatalog(catalog, query, pageNo);
103
+ if (error)
104
+ return error;
105
+ const data = wikiService.buildGenericRenderData(catalog, payload, query, mode, pageNo);
106
+ let templateName = 'wiki/detail';
107
+ if (mode === 'list')
108
+ templateName = 'wiki/list';
109
+ else if (mode === 'suggestions')
110
+ templateName = 'wiki/suggestions';
111
+ await sendImage(deps, session, templateName, data, `${data.title}\n${data.summary || ''}`);
112
+ }
5
113
  function register(deps) {
6
- const { ctx } = deps;
7
- ctx.command('洛克').subcommand('.wiki <name:text>', '查询精灵 wiki')
114
+ const { ctx, atlasService } = deps;
115
+ ctx.command('洛克').subcommand('.wiki [name:text]', '查询洛克 Wiki(精灵/技能/物品等,支持全局搜索)')
8
116
  .alias('洛克wiki')
9
- .action(() => WIKI_CLOSED_MESSAGE);
10
- ctx.command('洛克').subcommand('.技能 <name:text>', '查询技能 wiki')
117
+ .alias('洛克百科')
118
+ .action(async ({ session }, name = '') => handleWikiQuery(deps, session, name));
119
+ ctx.command('洛克').subcommand('.技能 <name:text>', '查询技能 Wiki')
11
120
  .alias('洛克技能')
12
- .action(() => WIKI_CLOSED_MESSAGE);
121
+ .action(async ({ session }, name = '') => handleWikiQuery(deps, session, `技能 ${String(name || '').trim()}`));
122
+ ctx.command('图鉴下载', '下载 Rocom-Atlas 精灵图鉴到本地缓存')
123
+ .alias('洛克图鉴下载')
124
+ .action(async ({ session }) => {
125
+ if (!deps.config.adminUserIds.includes(session?.userId || ''))
126
+ return '此指令仅限管理员使用。';
127
+ await session?.send?.('开始下载 Rocom-Atlas 图鉴,请稍候…');
128
+ let lastPercent = -20;
129
+ try {
130
+ const { imageCount, totalBytes } = await atlasService.download(ctx, async (percent, stage) => {
131
+ // 20% 粒度进度提醒(对应上游行为)
132
+ if (percent - lastPercent >= 20 || percent >= 100) {
133
+ lastPercent = percent;
134
+ await session?.send?.(`图鉴下载进度 ${percent}%:${stage}`);
135
+ }
136
+ });
137
+ const sizeMb = (totalBytes / 1024 / 1024).toFixed(1);
138
+ return `图鉴下载完成:共 ${imageCount} 张精灵图片,占用约 ${sizeMb} MB。\n使用:精灵图鉴 <精灵名>`;
139
+ }
140
+ catch (e) {
141
+ logger.warn(`图鉴下载失败: ${e}`);
142
+ return `图鉴下载失败:${e instanceof Error ? e.message : e}`;
143
+ }
144
+ });
145
+ ctx.command('精灵图鉴 <name:text>', '使用本地 Rocom-Atlas 查询精灵图鉴图片')
146
+ .alias('洛克精灵图鉴')
147
+ .action(async ({ session }, name = '') => {
148
+ const query = String(name || '').trim();
149
+ if (!query)
150
+ return '请输入精灵名称。用法:精灵图鉴 <精灵名>';
151
+ if (!atlasService.isReady()) {
152
+ return '本地图鉴尚未下载。请管理员先执行:图鉴下载';
153
+ }
154
+ const { name: matchedName, imagePath, candidates } = atlasService.findMatch(query);
155
+ if (imagePath) {
156
+ try {
157
+ const image = node_fs_1.default.readFileSync(imagePath);
158
+ await session?.send?.((0, koishi_1.h)('message', {}, koishi_1.h.text(`【精灵图鉴】${matchedName}\n`), koishi_1.h.image(image, 'image/png')));
159
+ return;
160
+ }
161
+ catch (e) {
162
+ logger.warn(`读取图鉴图片失败: ${e}`);
163
+ return `读取图鉴图片失败:${e instanceof Error ? e.message : e}`;
164
+ }
165
+ }
166
+ if (candidates.length) {
167
+ const lines = [`未精确匹配「${query}」,可能是:`];
168
+ candidates.forEach((item, idx) => lines.push(`${idx + 1}. ${item}`));
169
+ lines.push('请使用更精确名称重新查询。');
170
+ return lines.join('\n');
171
+ }
172
+ return `未在本地图鉴中找到「${query}」。可尝试其他名称,或联系管理员更新图鉴(图鉴下载)。`;
173
+ });
13
174
  }
@@ -27,6 +27,8 @@ export declare class EggService {
27
27
  private rangeMatchScore;
28
28
  private formatPetCard;
29
29
  private formatSizeApiCard;
30
+ private splitNewSizeApiItems;
31
+ private sizeApiResultGroups;
30
32
  private formatEggSearchCard;
31
33
  private formatEggSearchTextLine;
32
34
  private mergeCardsByName;