koishi-plugin-rocom 1.0.13 → 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 (36) 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 +19 -0
  4. package/lib/client.js +101 -1
  5. package/lib/commands/account.js +1 -0
  6. package/lib/commands/merchant.js +11 -2
  7. package/lib/commands/query.js +141 -9
  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 +5 -0
  13. package/lib/index.js +3153 -188
  14. package/lib/pet-data-service.d.ts +62 -0
  15. package/lib/pet-data-service.js +468 -0
  16. package/lib/render-templates/pet-data/index.html +172 -0
  17. package/lib/render-templates/pet-data/style.css +553 -0
  18. package/lib/render-templates/wiki/detail/index.html +84 -0
  19. package/lib/render-templates/wiki/list/index.html +84 -0
  20. package/lib/render-templates/wiki/menu/index.html +65 -0
  21. package/lib/render-templates/wiki/pet/index.html +158 -0
  22. package/lib/render-templates/wiki/pet/style.css +393 -0
  23. package/lib/render-templates/wiki/shared/style.css +431 -0
  24. package/lib/render-templates/wiki/skill/index.html +79 -0
  25. package/lib/render-templates/wiki/skill/style.css +233 -0
  26. package/lib/render-templates/wiki/suggestions/index.html +84 -0
  27. package/lib/render.js +37 -8
  28. package/lib/types.d.ts +9 -1
  29. package/lib/user.d.ts +23 -1
  30. package/lib/user.js +32 -1
  31. package/lib/wiki-catalog.d.ts +19 -0
  32. package/lib/wiki-catalog.js +124 -0
  33. package/lib/wiki-service.d.ts +220 -0
  34. package/lib/wiki-service.js +1463 -0
  35. package/package.json +1 -1
  36. package/readme.md +4 -5
@@ -549,6 +549,7 @@ async function checkMerchantSubscriptions(deps) {
549
549
  function register(deps) {
550
550
  const { ctx, config, client, merchantSubMgr } = deps;
551
551
  ctx.command(TEXT.merchant, '\u67e5\u770b\u8fdc\u884c\u5546\u4eba\u5546\u54c1')
552
+ .alias('yxsr')
552
553
  .action(async ({ session }) => {
553
554
  const res = await client.getMerchantInfo(ctx, true);
554
555
  if (!res)
@@ -619,6 +620,14 @@ function register(deps) {
619
620
  return `远行商人订阅检查完成:订阅 ${result.subscriptions} 条,命中 ${result.matched} 条,推送 ${result.pushed} 条。`;
620
621
  });
621
622
  if (config.merchantSubscriptionEnabled) {
623
+ // 上游 v3.1.0:检查前加入 ±30s 随机延迟,降低刷新窗口和并发请求冲突。
624
+ const MERCHANT_JITTER_MS = 30000;
625
+ const runCheckWithJitter = () => {
626
+ const jitter = Math.floor(Math.random() * MERCHANT_JITTER_MS);
627
+ setTimeout(() => {
628
+ checkMerchantSubscriptions(deps).catch(err => logger.warn(`远行商人订阅检查失败: ${err}`));
629
+ }, jitter);
630
+ };
622
631
  if (config.merchantCheckMode === 'times' && config.merchantCheckTimes.length > 0) {
623
632
  let lastMerchantCheckKey = '';
624
633
  ctx.setInterval(async () => {
@@ -630,12 +639,12 @@ function register(deps) {
630
639
  if (checkKey === lastMerchantCheckKey)
631
640
  return;
632
641
  lastMerchantCheckKey = checkKey;
633
- await checkMerchantSubscriptions(deps);
642
+ runCheckWithJitter();
634
643
  }, 60000);
635
644
  }
636
645
  else {
637
646
  ctx.setInterval(async () => {
638
- await checkMerchantSubscriptions(deps);
647
+ runCheckWithJitter();
639
648
  }, config.merchantCheckInterval);
640
649
  }
641
650
  }
@@ -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');
@@ -874,7 +875,24 @@ function extractHomePet(deps, 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;
877
- const mutationType = Number(display.mutation_type || raw.mutation_type || homePet.mutation_type || 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
+ }
878
896
  const isMale = gender === 1;
879
897
  const status = homePet.status ?? raw.status;
880
898
  const isGuard = guard || Boolean(raw.is_guard || raw.guard) || ['2', 'guard', '守卫'].includes(String(status).toLowerCase());
@@ -923,6 +941,9 @@ function extractHomePet(deps, raw, index, guard = false) {
923
941
  statusText,
924
942
  statusClass,
925
943
  note,
944
+ hasEgg,
945
+ eggReady,
946
+ eggTime: predictedEggTime || 0,
926
947
  inspireReady: eggReady,
927
948
  readyAt: predictedEggTime || 0,
928
949
  gender,
@@ -1269,13 +1290,33 @@ async function resolveHomeUid(deps, session, uid = '') {
1269
1290
  return targetUid;
1270
1291
  return String(deps.userMgr.getPrimaryBinding(session?.userId || '')?.role_id || '');
1271
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
+ };
1272
1313
  async function subscribeHome(deps, session, uid, kind) {
1273
1314
  const target = sessionTarget(session);
1274
1315
  if (!target.userId && !isBotAdmin(session, deps.config.adminUserIds))
1275
1316
  return '此指令仅限管理员使用。';
1276
1317
  const targetUid = await resolveHomeUid(deps, session, uid);
1277
1318
  if (!targetUid)
1278
- return kind === 'garden' ? '请提供玩家 UID,或先完成绑定后再订阅家园菜园。' : '请提供玩家 UID,或先完成绑定后再订阅家园灵感。';
1319
+ return HOME_SUB_TEXT[kind].needUidHint;
1279
1320
  const key = homeSubscriptionKey(session, targetUid, kind);
1280
1321
  deps.homeSubMgr.upsert(key, {
1281
1322
  key,
@@ -1289,9 +1330,7 @@ async function subscribeHome(deps, session, uid, kind) {
1289
1330
  notify_state: {},
1290
1331
  updated_at: Math.floor(Date.now() / 1000),
1291
1332
  });
1292
- return kind === 'garden'
1293
- ? `已订阅 UID ${targetUid} 的家园菜园提醒:首个成熟和全部成熟时各推送一次。`
1294
- : `已订阅 UID ${targetUid} 的家园精灵灵感提醒:首个完成和全部完成时各推送一次。`;
1333
+ return `已订阅 UID ${targetUid} ${HOME_SUB_TEXT[kind].subscribedHint}`;
1295
1334
  }
1296
1335
  function homeSubscriptionState(data, kind) {
1297
1336
  if (kind === 'garden') {
@@ -1300,14 +1339,19 @@ function homeSubscriptionState(data, kind) {
1300
1339
  const names = readyItems.map((item) => `田地${item.landIndex} ${item.plantName}`);
1301
1340
  return { items, readyItems, names };
1302
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
+ }
1303
1348
  const items = [...(data.indoorPets || []), ...(data.guardPets || [])].filter((item) => item.readyAt);
1304
1349
  const readyItems = items.filter((item) => item.inspireReady);
1305
1350
  const names = readyItems.map((item) => item.name || '未知精灵');
1306
1351
  return { items, readyItems, names };
1307
1352
  }
1308
1353
  function homeSubscriptionMessage(uid, kind, level, totalCount, readyItems, names) {
1309
- const kindText = kind === 'garden' ? '菜园作物' : '精灵灵感';
1310
- const actionText = kind === 'garden' ? '成熟' : '完成';
1354
+ const { kindText, actionText } = HOME_SUB_TEXT[kind];
1311
1355
  const levelText = level === 'first' ? '首个' : '全部';
1312
1356
  return [
1313
1357
  `家园${kindText}${levelText}${actionText}提醒:${uid}`,
@@ -1321,7 +1365,7 @@ async function checkHomeSubscriptions(deps) {
1321
1365
  let checkedCount = 0;
1322
1366
  let pushedCount = 0;
1323
1367
  for (const [key, sub] of Object.entries(subs)) {
1324
- if (!sub.uid || !['garden', 'inspiration'].includes(sub.kind))
1368
+ if (!sub.uid || !['garden', 'inspiration', 'egg'].includes(sub.kind))
1325
1369
  continue;
1326
1370
  checkedCount++;
1327
1371
  if (!cache.has(sub.uid)) {
@@ -1830,6 +1874,92 @@ function register(deps) {
1830
1874
  return `家园查询失败:${client.getLastErrorBrief()}`;
1831
1875
  await sendImage(deps, session, 'home', buildHomeRenderData(deps, res, targetUid), `【洛克家园】UID ${targetUid}`);
1832
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
+ });
1833
1963
  ctx.command('洛克').subcommand('.刷新面板 [uid:string]', '刷新指定 UID 的精灵面板缓存')
1834
1964
  .alias('洛克刷新面板')
1835
1965
  .alias('刷新面板')
@@ -1938,12 +2068,14 @@ function register(deps) {
1938
2068
  .action(async ({ session }, uid = '') => subscribeHome(deps, session, uid, 'garden'));
1939
2069
  ctx.command('订阅家园灵感 [uid:string]', '订阅指定 UID 的家园精灵灵感完成提醒')
1940
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'));
1941
2073
  ctx.command('取消订阅家园 [kind:string] [uid:string]', '取消当前会话的家园订阅')
1942
2074
  .action(async ({ session }, kind = '全部', uid = '') => {
1943
2075
  const target = sessionTarget(session);
1944
2076
  if (!target.userId && !isBotAdmin(session, deps.config.adminUserIds))
1945
2077
  return '此指令仅限管理员使用。';
1946
- const kindMap = { '菜园': 'garden', '灵感': 'inspiration', '全部': '', all: '', garden: 'garden', inspiration: 'inspiration' };
2078
+ const kindMap = { '菜园': 'garden', '灵感': 'inspiration', '生蛋': 'egg', '全部': '', all: '', garden: 'garden', inspiration: 'inspiration', egg: 'egg' };
1947
2079
  const deleted = deps.homeSubMgr.deleteMatching(target, kindMap[String(kind || '全部')] ?? '', String(uid || '').trim());
1948
2080
  return deleted ? `已取消 ${deleted} 条家园订阅。` : '当前会话没有匹配的家园订阅。';
1949
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;