koishi-plugin-rocom 1.0.13 → 1.0.18
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 +20 -2
- package/lib/client.js +99 -9
- package/lib/commands/account.js +1 -0
- package/lib/commands/egg.js +41 -11
- package/lib/commands/merchant.js +11 -2
- package/lib/commands/query.js +141 -9
- package/lib/commands/tools.js +104 -0
- package/lib/commands/wiki.js +167 -6
- package/lib/egg-service.d.ts +45 -12
- package/lib/egg-service.js +177 -90
- package/lib/index.d.ts +5 -0
- package/lib/index.js +180 -5775
- package/lib/pet-data-service.d.ts +62 -0
- package/lib/pet-data-service.js +468 -0
- 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.js +37 -8
- package/lib/types.d.ts +9 -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/account.js
CHANGED
|
@@ -97,6 +97,7 @@ function register(deps) {
|
|
|
97
97
|
const { ctx, client, userMgr } = deps;
|
|
98
98
|
ctx.command('洛克').subcommand('.QQ登录', 'QQ 扫码登录')
|
|
99
99
|
.alias('洛克QQ登录')
|
|
100
|
+
.alias('洛克qq登录')
|
|
100
101
|
.action(async ({ session }) => {
|
|
101
102
|
const userId = session.userId;
|
|
102
103
|
const qrData = await client.qqQrLogin(ctx, userId);
|
package/lib/commands/egg.js
CHANGED
|
@@ -111,17 +111,10 @@ function register(deps) {
|
|
|
111
111
|
let fallback = '';
|
|
112
112
|
if (height != null && weight != null) {
|
|
113
113
|
const heightInMeters = heightMeters ?? height / 100;
|
|
114
|
-
const
|
|
115
|
-
if (
|
|
116
|
-
data = eggService.
|
|
117
|
-
fallback = eggService.
|
|
118
|
-
}
|
|
119
|
-
if (!data) {
|
|
120
|
-
const backendResults = await client.queryPetSize(ctx, heightInMeters, weight, false, session?.userId || '');
|
|
121
|
-
if (backendResults) {
|
|
122
|
-
data = eggService.buildSizeSearchDataFromApi(height, weight, backendResults, heightDisplay);
|
|
123
|
-
fallback = eggService.buildSizeSearchTextFromApi(height, weight, backendResults, heightDisplay);
|
|
124
|
-
}
|
|
114
|
+
const backendResults = await client.queryPetSize(ctx, heightInMeters, weight, 'magic', 1, 30, session?.userId || '');
|
|
115
|
+
if (backendResults) {
|
|
116
|
+
data = eggService.buildSizeSearchDataFromApi(height, weight, backendResults, heightDisplay);
|
|
117
|
+
fallback = eggService.buildSizeSearchTextFromApi(height, weight, backendResults, heightDisplay);
|
|
125
118
|
}
|
|
126
119
|
}
|
|
127
120
|
if (!data) {
|
|
@@ -135,6 +128,43 @@ function register(deps) {
|
|
|
135
128
|
const name = nameParts.join(' ');
|
|
136
129
|
if (!name)
|
|
137
130
|
return '请输入精灵名称。用法:洛克.查蛋 <精灵名>';
|
|
131
|
+
let backendDetail = null;
|
|
132
|
+
let backendProfile = null;
|
|
133
|
+
const backendList = await client.listWikiPets(ctx, name, 1, 10);
|
|
134
|
+
const backendItems = Array.isArray(backendList?.items) ? backendList.items : [];
|
|
135
|
+
if (backendItems.length) {
|
|
136
|
+
let selected = backendItems.find((item) => {
|
|
137
|
+
const itemName = String(item?.name || '').trim();
|
|
138
|
+
const itemForm = String(item?.form || '').trim();
|
|
139
|
+
return itemName === name || (itemForm && `${itemName}${itemForm}` === name);
|
|
140
|
+
});
|
|
141
|
+
if (!selected && backendItems.length === 1)
|
|
142
|
+
selected = backendItems[0];
|
|
143
|
+
const selectedId = selected?.pet_id ?? selected?.id;
|
|
144
|
+
if (selectedId != null && selectedId !== '') {
|
|
145
|
+
backendDetail = await client.getWikiPet(ctx, selectedId);
|
|
146
|
+
if (!backendDetail)
|
|
147
|
+
backendDetail = selected;
|
|
148
|
+
backendProfile = await client.getWikiPetProfile(ctx, selectedId);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (backendDetail) {
|
|
152
|
+
const compatibleByGroup = {};
|
|
153
|
+
const backendPet = { ...backendDetail, ...(backendProfile || {}) };
|
|
154
|
+
const eggGroups = Array.isArray(backendPet?.egg_groups) ? backendPet.egg_groups : [];
|
|
155
|
+
const eggGroupIds = eggGroups.length
|
|
156
|
+
? eggGroups.map((group) => group?.id).filter((id) => id != null && id !== '')
|
|
157
|
+
: Array.isArray(backendPet?.egg_group_ids) ? backendPet.egg_group_ids : [];
|
|
158
|
+
for (const groupId of eggGroupIds) {
|
|
159
|
+
const groupResults = await client.listWikiPets(ctx, '', 1, 31, { egg_group_id: groupId });
|
|
160
|
+
compatibleByGroup[String(groupId)] = Array.isArray(groupResults?.items) ? groupResults.items : [];
|
|
161
|
+
}
|
|
162
|
+
const data = eggService.buildSearchDataFromWiki(backendPet, compatibleByGroup);
|
|
163
|
+
data.commandHint = '数据来自新版 Wiki;接口不可用时自动回退本地查蛋';
|
|
164
|
+
data.copyright = 'Koishi & WeGame 洛克王国插件';
|
|
165
|
+
await sendEggImage(deps, session, 'searcheggs', data, eggService.buildSearchTextFromWiki(data));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
138
168
|
const sr = eggService.search(name);
|
|
139
169
|
if (sr.matchType === 'multi') {
|
|
140
170
|
const candidates = searchResultCandidates(sr);
|
package/lib/commands/merchant.js
CHANGED
|
@@ -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
|
-
|
|
642
|
+
runCheckWithJitter();
|
|
634
643
|
}, 60000);
|
|
635
644
|
}
|
|
636
645
|
else {
|
|
637
646
|
ctx.setInterval(async () => {
|
|
638
|
-
|
|
647
|
+
runCheckWithJitter();
|
|
639
648
|
}, config.merchantCheckInterval);
|
|
640
649
|
}
|
|
641
650
|
}
|
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');
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
});
|
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
|
}
|