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.
- package/lib/atlas-service.d.ts +33 -0
- package/lib/atlas-service.js +439 -0
- package/lib/client.d.ts +36 -1
- package/lib/client.js +221 -21
- package/lib/commands/account.js +1 -0
- package/lib/commands/merchant.js +351 -91
- package/lib/commands/query.js +162 -14
- package/lib/commands/tools.js +104 -0
- package/lib/commands/wiki.js +167 -6
- package/lib/egg-service.d.ts +2 -0
- package/lib/egg-service.js +50 -15
- package/lib/index.d.ts +9 -0
- package/lib/index.js +3626 -303
- package/lib/pet-data-service.d.ts +62 -0
- package/lib/pet-data-service.js +468 -0
- package/lib/render-templates/home/img/rocomuid/a_title.png +0 -0
- package/lib/render-templates/home/img/rocomuid/banner_bg.png +0 -0
- package/lib/render-templates/home/img/rocomuid/bg.jpg +0 -0
- package/lib/render-templates/home/img/rocomuid/img_head.png +0 -0
- package/lib/render-templates/home/img/rocomuid/jindu_bar.png +0 -0
- package/lib/render-templates/home/img/rocomuid/jindu_tc.png +0 -0
- package/lib/render-templates/home/img/rocomuid/level_icon.png +0 -0
- package/lib/render-templates/home/img/rocomuid/pet_bg.png +0 -0
- package/lib/render-templates/home/img/rocomuid/plant_bg.png +0 -0
- package/lib/render-templates/home/img/rocomuid/star_1.png +0 -0
- package/lib/render-templates/home/img/rocomuid/star_8.png +0 -0
- package/lib/render-templates/home/img/rocomuid/star_9.png +0 -0
- package/lib/render-templates/home/img/rocomuid/title_fg.png +0 -0
- package/lib/render-templates/home/img/rocomuid/top_bg.png +0 -0
- package/lib/render-templates/home/img/rocomuid/touxiang_mask.png +0 -0
- package/lib/render-templates/home/index.html +81 -97
- package/lib/render-templates/home/style.css +316 -375
- package/lib/render-templates/pet-data/index.html +172 -0
- package/lib/render-templates/pet-data/style.css +553 -0
- package/lib/render-templates/wiki/detail/index.html +84 -0
- package/lib/render-templates/wiki/list/index.html +84 -0
- package/lib/render-templates/wiki/menu/index.html +65 -0
- package/lib/render-templates/wiki/pet/index.html +158 -0
- package/lib/render-templates/wiki/pet/style.css +393 -0
- package/lib/render-templates/wiki/shared/style.css +431 -0
- package/lib/render-templates/wiki/skill/index.html +79 -0
- package/lib/render-templates/wiki/skill/style.css +233 -0
- package/lib/render-templates/wiki/suggestions/index.html +84 -0
- package/lib/render-templates/yuanxing-shangren/CHANGELOG.md +138 -0
- package/lib/render-templates/yuanxing-shangren/README.md +86 -0
- package/lib/render-templates/yuanxing-shangren/img/coin.png +0 -0
- 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
- package/lib/render-templates/yuanxing-shangren/img//346/240/207/351/242/230.png +0 -0
- package/lib/render-templates/yuanxing-shangren/img//347/203/255/351/224/200.png +0 -0
- 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
- package/lib/render-templates/yuanxing-shangren/index.html +198 -170
- package/lib/render-templates/yuanxing-shangren/index.html.backup +356 -0
- package/lib/render-templates/yuanxing-shangren/merchant.html +461 -0
- package/lib/render-templates/yuanxing-shangren/style.css +201 -256
- package/lib/render-templates/yuanxing-shangren/today.html +461 -0
- package/lib/render-templates/yuanxing-shangren/today.style.css +366 -0
- package/lib/render.js +48 -12
- package/lib/subscription-send.js +4 -5
- package/lib/ttf/rocom_skill_origin.ttf +0 -0
- 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
- package/lib/types.d.ts +13 -1
- package/lib/user.d.ts +23 -1
- package/lib/user.js +32 -1
- package/lib/wiki-catalog.d.ts +19 -0
- package/lib/wiki-catalog.js +124 -0
- package/lib/wiki-service.d.ts +220 -0
- package/lib/wiki-service.js +1463 -0
- package/package.json +1 -1
- package/readme.md +4 -5
package/lib/commands/query.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
});
|
package/lib/commands/tools.js
CHANGED
|
@@ -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 }) => {
|
package/lib/commands/wiki.js
CHANGED
|
@@ -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
|
|
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
|
|
114
|
+
const { ctx, atlasService } = deps;
|
|
115
|
+
ctx.command('洛克').subcommand('.wiki [name:text]', '查询洛克 Wiki(精灵/技能/物品等,支持全局搜索)')
|
|
8
116
|
.alias('洛克wiki')
|
|
9
|
-
.
|
|
10
|
-
|
|
117
|
+
.alias('洛克百科')
|
|
118
|
+
.action(async ({ session }, name = '') => handleWikiQuery(deps, session, name));
|
|
119
|
+
ctx.command('洛克').subcommand('.技能 <name:text>', '查询技能 Wiki')
|
|
11
120
|
.alias('洛克技能')
|
|
12
|
-
.action(() =>
|
|
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
|
}
|
package/lib/egg-service.d.ts
CHANGED
|
@@ -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;
|