koishi-plugin-rocom 1.0.18 → 1.0.19

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 (57) hide show
  1. package/lib/announcement-service.d.ts +21 -0
  2. package/lib/announcement-service.js +63 -0
  3. package/lib/async-control.d.ts +3 -0
  4. package/lib/async-control.js +48 -0
  5. package/lib/client.d.ts +48 -7
  6. package/lib/client.js +244 -125
  7. package/lib/commands/egg.js +66 -4
  8. package/lib/commands/merchant.js +144 -146
  9. package/lib/commands/query.js +148 -117
  10. package/lib/commands/ranking.d.ts +2 -0
  11. package/lib/commands/ranking.js +31 -0
  12. package/lib/commands/share-code.d.ts +2 -0
  13. package/lib/commands/share-code.js +66 -0
  14. package/lib/commands/tools.js +47 -59
  15. package/lib/egg-service.d.ts +82 -8
  16. package/lib/egg-service.js +235 -19
  17. package/lib/img//345/274/202/350/211/262.png +0 -0
  18. package/lib/img//347/202/253/345/275/251.png +0 -0
  19. package/lib/index.d.ts +5 -0
  20. package/lib/index.js +16 -3
  21. package/lib/merchant-clock.d.ts +22 -0
  22. package/lib/merchant-clock.js +95 -0
  23. package/lib/merchant-service.d.ts +24 -0
  24. package/lib/merchant-service.js +148 -0
  25. package/lib/permissions.d.ts +16 -0
  26. package/lib/permissions.js +66 -0
  27. package/lib/pet-data-service.js +7 -9
  28. package/lib/pet-size.d.ts +13 -0
  29. package/lib/pet-size.js +54 -0
  30. package/lib/player-service.d.ts +37 -0
  31. package/lib/player-service.js +201 -0
  32. package/lib/ranking-service.d.ts +46 -0
  33. package/lib/ranking-service.js +137 -0
  34. package/lib/render-templates/announcement/detail.html +8 -0
  35. package/lib/render-templates/announcement/list.html +5 -0
  36. package/lib/render-templates/announcement/style.css +1 -0
  37. package/lib/render-templates/pet-ranking/index.html +99 -0
  38. package/lib/render-templates/pet-ranking/style.css +338 -0
  39. package/lib/render-templates/player-search/index.html +7 -0
  40. package/lib/render-templates/searcheggs/index.html +1 -1
  41. package/lib/render-templates/searcheggs/size.html +70 -68
  42. package/lib/render-templates/share-code-team/index.html +105 -0
  43. package/lib/render-templates/share-code-team/style.css +345 -0
  44. package/lib/render.d.ts +15 -2
  45. package/lib/render.js +120 -37
  46. package/lib/send-image.d.ts +1 -1
  47. package/lib/send-image.js +12 -1
  48. package/lib/share-code-service.d.ts +32 -0
  49. package/lib/share-code-service.js +184 -0
  50. package/lib/subscription-runner.d.ts +6 -0
  51. package/lib/subscription-runner.js +21 -0
  52. package/lib/subscription-send.d.ts +4 -2
  53. package/lib/subscription-send.js +45 -20
  54. package/lib/types.d.ts +5 -0
  55. package/lib/user.d.ts +5 -0
  56. package/package.json +4 -1
  57. package/readme.md +145 -124
@@ -9,63 +9,18 @@ const account_1 = require("./account");
9
9
  const send_image_1 = require("../send-image");
10
10
  const subscription_send_1 = require("../subscription-send");
11
11
  const pet_data_service_1 = require("../pet-data-service");
12
+ const player_service_1 = require("../player-service");
13
+ const merchant_service_1 = require("../merchant-service");
14
+ const permissions_1 = require("../permissions");
15
+ const render_1 = require("../render");
16
+ const subscription_runner_1 = require("../subscription-runner");
12
17
  const node_fs_1 = __importDefault(require("node:fs"));
13
18
  const node_path_1 = __importDefault(require("node:path"));
14
19
  const logger = new koishi_1.Logger('rocom-query');
15
- async function sendImage(deps, session, templateName, data, fallback) {
16
- const png = await deps.renderer.renderHtml(deps.ctx, templateName, data);
20
+ async function sendImage(deps, session, templateName, data, fallback, options) {
21
+ const png = await deps.renderer.renderPages(deps.ctx, templateName, data, options);
17
22
  await (0, send_image_1.sendImageWithFallback)(session, png, fallback, `query:${templateName}`, deps.config);
18
23
  }
19
- function cleanPlayerFieldValue(field, value) {
20
- const text = String(value ?? '').trim().replace(/^'+|'+$/g, '');
21
- if (!text || ['<0B>', '<0b>', '<0B >', '<0b >'].includes(text))
22
- return '未设置';
23
- if (['is_online', 'online', 'chat_top_unlock', 'is_friend', 'is_black', 'is_black_role', 'is_chat_node_unlock'].includes(field)) {
24
- return ['1', 'true', 'True', '是'].includes(text) ? '是' : '否';
25
- }
26
- if (['sex', 'gender'].includes(field)) {
27
- return { '0': '未知', '1': '男', '2': '女' }[text] || text;
28
- }
29
- if (field === 'friend_type') {
30
- return { '0': '默认', '1': '特殊' }[text] || text;
31
- }
32
- if (field === 'battle_state') {
33
- return { '0': '空闲', '1': '对战中' }[text] || text;
34
- }
35
- return text;
36
- }
37
- function parseIngamePlayerPayload(payload, uid) {
38
- const rows = payload?.rows || [];
39
- const rowMap = {};
40
- const labelMap = {};
41
- for (const row of rows) {
42
- const field = String(row.field || '');
43
- if (!field)
44
- continue;
45
- rowMap[field] = String(row.value ?? '');
46
- labelMap[field] = String(row.label || field);
47
- }
48
- const playerUid = cleanPlayerFieldValue('uin', rowMap.uin || uid);
49
- const signature = cleanPlayerFieldValue('signature', rowMap.signature || '');
50
- return {
51
- title: String(payload?.title || '玩家搜索'),
52
- nickname: cleanPlayerFieldValue('name', rowMap.name || '-'),
53
- uid: playerUid,
54
- level: cleanPlayerFieldValue('level', rowMap.level || '-'),
55
- signature: signature === '未设置' ? '' : signature,
56
- rowMap,
57
- labelMap,
58
- };
59
- }
60
- function playerField(parsed, field, defaultValue = '未设置') {
61
- if (!parsed)
62
- return defaultValue;
63
- const raw = parsed.rowMap[field];
64
- if (raw == null || raw === '')
65
- return defaultValue;
66
- const value = cleanPlayerFieldValue(field, raw);
67
- return value && value !== '-' && value !== '未设置' ? value : defaultValue;
68
- }
69
24
  function tryParseJson(text) {
70
25
  try {
71
26
  return JSON.parse(text);
@@ -1112,8 +1067,8 @@ function buildHomeRenderData(deps, res, uid) {
1112
1067
  updatedAt: new Date(createdAt ? createdAt * 1000 : Date.now()).toLocaleString('zh-CN'),
1113
1068
  };
1114
1069
  }
1115
- function buildPlayerSearchRenderData(payload, uid) {
1116
- const parsed = parseIngamePlayerPayload(payload, uid);
1070
+ function buildPlayerSearchRenderData(payload, uid, cardImageUrl = '') {
1071
+ const parsed = (0, player_service_1.parseIngamePlayerPayload)(payload, uid);
1117
1072
  const pack = (title, pairs) => {
1118
1073
  const items = pairs
1119
1074
  .filter(([, value]) => value && value !== '-' && value !== '未设置')
@@ -1123,27 +1078,27 @@ function buildPlayerSearchRenderData(payload, uid) {
1123
1078
  const sections = [
1124
1079
  pack('核心档案', [
1125
1080
  ['等级', parsed.level],
1126
- ['在线状态', playerField(parsed, 'online', playerField(parsed, 'is_online'))],
1127
- ['性别', playerField(parsed, 'gender', playerField(parsed, 'sex'))],
1128
- ['世界等级', playerField(parsed, 'world_level')],
1129
- ['图鉴收集', playerField(parsed, 'card_handbook_collect_num')],
1130
- ['最后离线', playerField(parsed, 'last_logout_time')],
1081
+ ['在线状态', (0, player_service_1.playerField)(parsed, 'online', (0, player_service_1.playerField)(parsed, 'is_online'))],
1082
+ ['性别', (0, player_service_1.playerField)(parsed, 'gender', (0, player_service_1.playerField)(parsed, 'sex'))],
1083
+ ['世界等级', (0, player_service_1.playerField)(parsed, 'world_level')],
1084
+ ['图鉴收集', (0, player_service_1.playerField)(parsed, 'card_handbook_collect_num')],
1085
+ ['最后离线', (0, player_service_1.playerField)(parsed, 'last_logout_time')],
1131
1086
  ]),
1132
1087
  pack('家园信息', [
1133
- ['家园名称', playerField(parsed, 'home_name')],
1134
- ['家园等级', playerField(parsed, 'home_level')],
1135
- ['家园经验', playerField(parsed, 'home_experience')],
1136
- ['舒适度', playerField(parsed, 'home_comfort_level')],
1137
- ['访客数量', playerField(parsed, 'visitor_num')],
1088
+ ['家园名称', (0, player_service_1.playerField)(parsed, 'home_name')],
1089
+ ['家园等级', (0, player_service_1.playerField)(parsed, 'home_level')],
1090
+ ['家园经验', (0, player_service_1.playerField)(parsed, 'home_experience')],
1091
+ ['舒适度', (0, player_service_1.playerField)(parsed, 'home_comfort_level')],
1092
+ ['访客数量', (0, player_service_1.playerField)(parsed, 'visitor_num')],
1138
1093
  ]),
1139
1094
  ].filter(Boolean);
1140
1095
  const summaryCards = [
1141
1096
  { label: '等级', value: parsed.level },
1142
- { label: '在线状态', value: playerField(parsed, 'online', playerField(parsed, 'is_online')) },
1143
- { label: '世界等级', value: playerField(parsed, 'world_level') },
1144
- { label: '图鉴收集', value: playerField(parsed, 'card_handbook_collect_num') },
1145
- { label: '家园等级', value: playerField(parsed, 'home_level') },
1146
- { label: '舒适度', value: playerField(parsed, 'home_comfort_level') },
1097
+ { label: '在线状态', value: (0, player_service_1.playerField)(parsed, 'online', (0, player_service_1.playerField)(parsed, 'is_online')) },
1098
+ { label: '世界等级', value: (0, player_service_1.playerField)(parsed, 'world_level') },
1099
+ { label: '图鉴收集', value: (0, player_service_1.playerField)(parsed, 'card_handbook_collect_num') },
1100
+ { label: '家园等级', value: (0, player_service_1.playerField)(parsed, 'home_level') },
1101
+ { label: '舒适度', value: (0, player_service_1.playerField)(parsed, 'home_comfort_level') },
1147
1102
  ].filter(item => item.value && item.value !== '-');
1148
1103
  const signature = parsed.signature && parsed.signature !== '未设置' ? parsed.signature : '';
1149
1104
  return {
@@ -1155,7 +1110,10 @@ function buildPlayerSearchRenderData(payload, uid) {
1155
1110
  signature,
1156
1111
  showSignature: Boolean(signature),
1157
1112
  sections,
1158
- commandHint: '洛克.玩家 <UID>',
1113
+ cardImageUrl: cardImageUrl || '',
1114
+ collectedShining: (0, player_service_1.playerField)(parsed, 'collected_shining_pet_count', '未知'),
1115
+ collectedGlass: (0, player_service_1.playerField)(parsed, 'collected_glass_pet_count', '未知'),
1116
+ commandHint: '洛克.玩家 [UID]',
1159
1117
  copyright: 'Koishi & WeGame Locke Kingdom Plugin',
1160
1118
  };
1161
1119
  }
@@ -1163,7 +1121,27 @@ function buildShopRenderData(payload, shopId) {
1163
1121
  const sections = [];
1164
1122
  const detailItems = [];
1165
1123
  const summaryCards = [{ label: '商店 ID', value: shopId }];
1166
- if (Array.isArray(payload?.rows)) {
1124
+ const shop = payload?.shop || {};
1125
+ if (Array.isArray(payload?.goods)) {
1126
+ const items = (0, merchant_service_1.normalizeLiveMerchant)(payload);
1127
+ summaryCards.push({ label: '商品数量', value: String(items.length) });
1128
+ const shopName = String(shop.name || shop.title || '').trim();
1129
+ if (shopName)
1130
+ summaryCards.push({ label: '商店名称', value: shopName });
1131
+ sections.push({
1132
+ title: '商品列表',
1133
+ cards: items.map((item) => ({
1134
+ title: item.name,
1135
+ image: item.icon,
1136
+ meta: [
1137
+ item.price !== null ? { label: '价格', value: String(item.price) } : null,
1138
+ item.limit !== null ? { label: '限购', value: String(item.limit) } : null,
1139
+ item.active ? null : { label: '状态', value: '非在售' },
1140
+ ].filter(Boolean),
1141
+ })),
1142
+ });
1143
+ }
1144
+ else if (Array.isArray(payload?.rows)) {
1167
1145
  detailItems.push(...payload.rows.filter((row) => Number(row.level || 0) === 0).map((row) => ({
1168
1146
  label: row.label || row.field || '-',
1169
1147
  value: stringifyInspectValue(row.value),
@@ -1171,6 +1149,8 @@ function buildShopRenderData(payload, shopId) {
1171
1149
  }
1172
1150
  else {
1173
1151
  for (const [key, value] of Object.entries(payload || {})) {
1152
+ if (['_source', 'goods', 'shop', 'meta', '_goods_mapping', 'goods_mapping'].includes(key))
1153
+ continue;
1174
1154
  if (Array.isArray(value)) {
1175
1155
  summaryCards.push({ label: key, value: String(value.length) });
1176
1156
  sections.push({
@@ -1194,12 +1174,12 @@ function buildShopRenderData(payload, shopId) {
1194
1174
  title: '洛克商店',
1195
1175
  subtitle: `shop_id = ${shopId}`,
1196
1176
  heroTitle: '商店查询',
1197
- heroValue: detailItems.find(item => ['name', 'title', '名称', '标题'].includes(item.label))?.value || shopId,
1177
+ heroValue: String(shop.name || shop.title || '') || detailItems.find(item => ['name', 'title', '名称', '标题'].includes(item.label))?.value || shopId,
1198
1178
  heroSubvalue: `shop_id = ${shopId}`,
1199
1179
  summaryCards: summaryCards.slice(0, 3),
1200
1180
  sections,
1201
1181
  detailItems: detailItems.slice(0, 18),
1202
- commandHint: '洛克.商店 <shop_id>',
1182
+ commandHint: '洛克.商店 [shop_id]',
1203
1183
  copyright: 'Koishi & WeGame Locke Kingdom Plugin',
1204
1184
  };
1205
1185
  }
@@ -1274,7 +1254,7 @@ function sessionTarget(session) {
1274
1254
  return {
1275
1255
  platform: session?.platform || session?.bot?.platform || '',
1276
1256
  channelId: session?.channelId || session?.guildId || '',
1277
- userId: session?.guildId ? '' : (session?.userId || ''),
1257
+ userId: (0, permissions_1.isDirectSession)(session) ? (session?.userId || '') : '',
1278
1258
  };
1279
1259
  }
1280
1260
  function homeSubscriptionKey(session, uid, kind) {
@@ -1284,6 +1264,9 @@ function homeSubscriptionKey(session, uid, kind) {
1284
1264
  function isBotAdmin(session, adminUserIds) {
1285
1265
  return adminUserIds.includes(session?.userId || '');
1286
1266
  }
1267
+ function canManageGroupSubscription(deps, session) {
1268
+ return (0, permissions_1.canManageSubscription)(deps.config, session);
1269
+ }
1287
1270
  async function resolveHomeUid(deps, session, uid = '') {
1288
1271
  const targetUid = String(uid || '').trim();
1289
1272
  if (targetUid)
@@ -1312,22 +1295,27 @@ const HOME_SUB_TEXT = {
1312
1295
  };
1313
1296
  async function subscribeHome(deps, session, uid, kind) {
1314
1297
  const target = sessionTarget(session);
1315
- if (!target.userId && !isBotAdmin(session, deps.config.adminUserIds))
1316
- return '此指令仅限管理员使用。';
1298
+ if (!target.userId && !canManageGroupSubscription(deps, session))
1299
+ return '此指令仅限群管理员或机器人管理员使用。';
1317
1300
  const targetUid = await resolveHomeUid(deps, session, uid);
1318
1301
  if (!targetUid)
1319
1302
  return HOME_SUB_TEXT[kind].needUidHint;
1320
1303
  const key = homeSubscriptionKey(session, targetUid, kind);
1304
+ const own = deps.userMgr.getUserBindings(session?.userId || '').find(binding => binding.role_id === targetUid);
1305
+ const existing = deps.homeSubMgr.getAll()[key];
1321
1306
  deps.homeSubMgr.upsert(key, {
1322
1307
  key,
1323
1308
  kind,
1324
1309
  uid: targetUid,
1310
+ notify_user_id: own ? session.userId : '',
1311
+ nickname: own?.nickname || '',
1325
1312
  platform: target.platform,
1313
+ self_id: session?.selfId || session?.bot?.selfId || '',
1326
1314
  channel_id: target.channelId,
1327
1315
  guild_id: session?.guildId || '',
1328
1316
  user_id: target.userId,
1329
1317
  updated_by: session?.userId || '',
1330
- notify_state: {},
1318
+ notify_state: existing?.notify_state || {},
1331
1319
  updated_at: Math.floor(Date.now() / 1000),
1332
1320
  });
1333
1321
  return `已订阅 UID ${targetUid} ${HOME_SUB_TEXT[kind].subscribedHint}`;
@@ -1359,18 +1347,24 @@ function homeSubscriptionMessage(uid, kind, level, totalCount, readyItems, names
1359
1347
  names.length ? `已完成:${names.slice(0, 8).join('、')}` : '',
1360
1348
  ].filter(Boolean).join('\n');
1361
1349
  }
1362
- async function checkHomeSubscriptions(deps) {
1350
+ async function checkHomeSubscriptions(deps, signal) {
1363
1351
  const subs = deps.homeSubMgr.getAll();
1364
1352
  const cache = new Map();
1365
1353
  let checkedCount = 0;
1366
1354
  let pushedCount = 0;
1367
1355
  for (const [key, sub] of Object.entries(subs)) {
1356
+ if (signal?.aborted)
1357
+ break;
1368
1358
  if (!sub.uid || !['garden', 'inspiration', 'egg'].includes(sub.kind))
1369
1359
  continue;
1370
1360
  checkedCount++;
1371
1361
  if (!cache.has(sub.uid)) {
1372
- cache.set(sub.uid, await deps.client.ingameHomeInfo(deps.ctx, sub.uid, { timeoutMs: 30000 }));
1362
+ cache.set(sub.uid, await deps.client.ingameHomeInfo(deps.ctx, sub.uid, { timeoutMs: 30000, signal }));
1373
1363
  }
1364
+ if (signal?.aborted)
1365
+ break;
1366
+ if (JSON.stringify(deps.homeSubMgr.getAll()[key]) !== JSON.stringify(sub))
1367
+ continue;
1374
1368
  const res = cache.get(sub.uid);
1375
1369
  if (!res)
1376
1370
  continue;
@@ -1379,7 +1373,7 @@ async function checkHomeSubscriptions(deps) {
1379
1373
  const totalCount = items.length;
1380
1374
  if (totalCount <= 0)
1381
1375
  continue;
1382
- const notifyState = sub.notify_state || {};
1376
+ const notifyState = { ...sub.notify_state };
1383
1377
  const pushLevels = [];
1384
1378
  if (!readyItems.length) {
1385
1379
  notifyState.first = false;
@@ -1397,15 +1391,19 @@ async function checkHomeSubscriptions(deps) {
1397
1391
  deps.homeSubMgr.upsert(key, { ...sub, notify_state: notifyState });
1398
1392
  continue;
1399
1393
  }
1400
- const messages = pushLevels.map(level => homeSubscriptionMessage(sub.uid, sub.kind, level, totalCount, readyItems, names));
1394
+ const owned = deps.userMgr.getUserBindings(sub.updated_by).find(binding => binding.role_id === sub.uid);
1395
+ const label = owned?.nickname || sub.uid;
1396
+ const notifyUser = owned && sub.notify_user_id === sub.updated_by ? sub.notify_user_id : '';
1397
+ const messages = pushLevels.map(level => homeSubscriptionMessage(label, sub.kind, level, totalCount, readyItems, names));
1401
1398
  try {
1402
1399
  const sent = await (0, subscription_send_1.sendScheduledMessage)(deps.ctx, {
1403
1400
  platform: sub.platform,
1404
1401
  channelId: sub.channel_id || sub.guild_id || sub.user_id || '',
1405
1402
  guildId: sub.guild_id || '',
1406
1403
  userId: sub.user_id || '',
1407
- }, messages.join('\n\n'));
1408
- if (!sent)
1404
+ selfId: sub.self_id || '',
1405
+ }, !sub.user_id && notifyUser ? [koishi_1.h.at(notifyUser), koishi_1.h.text('\n' + messages.join('\n\n'))] : messages.join('\n\n'), signal);
1406
+ if (!sent || signal?.aborted || JSON.stringify(deps.homeSubMgr.getAll()[key]) !== JSON.stringify(sub))
1409
1407
  continue;
1410
1408
  }
1411
1409
  catch (e) {
@@ -1421,6 +1419,7 @@ async function checkHomeSubscriptions(deps) {
1421
1419
  }
1422
1420
  function register(deps) {
1423
1421
  const { ctx, client } = deps;
1422
+ const homeRunner = (0, subscription_runner_1.createRunner)(ctx);
1424
1423
  ctx.command('洛克').subcommand('.档案', '查看个人档案')
1425
1424
  .alias('洛克档案')
1426
1425
  .action(async ({ session }) => {
@@ -1444,28 +1443,30 @@ function register(deps) {
1444
1443
  const cl = collRes || {};
1445
1444
  const bo = boRes || {};
1446
1445
  const recentBattle = blRes?.battles?.[0];
1447
- const playerSearchRes = role?.id ? await client.ingamePlayerSearch(ctx, String(role.id)) : null;
1448
- const playerSearchData = parseIngamePlayerPayload(playerSearchRes, String(role.id || ''));
1446
+ const auth = { fwToken, userIdentifier };
1447
+ const playerSearchRes = role?.id ? await client.ingamePlayerSearch(ctx, String(role.id), { auth }) : null;
1448
+ const playerCardRes = role?.id ? await client.ingamePlayerCard(ctx, String(role.id), { auth }).catch(() => null) : null;
1449
+ const playerSearchData = (0, player_service_1.parseIngamePlayerPayload)((0, player_service_1.mergePlayerPayloads)(playerSearchRes, playerCardRes), String(role.id || ''));
1449
1450
  const profileSignature = playerSearchData?.signature || '';
1450
1451
  const profileHeadTags = playerSearchData ? [
1451
- { label: '在线', value: playerField(playerSearchData, 'online', '未知') },
1452
- { label: '性别', value: playerField(playerSearchData, 'gender', playerField(playerSearchData, 'sex', '未知')) },
1453
- { label: '世界等级', value: playerField(playerSearchData, 'world_level') },
1454
- { label: '家园等级', value: playerField(playerSearchData, 'home_level') },
1452
+ { label: '在线', value: (0, player_service_1.playerField)(playerSearchData, 'online', '未知') },
1453
+ { label: '性别', value: (0, player_service_1.playerField)(playerSearchData, 'gender', (0, player_service_1.playerField)(playerSearchData, 'sex', '未知')) },
1454
+ { label: '世界等级', value: (0, player_service_1.playerField)(playerSearchData, 'world_level') },
1455
+ { label: '家园等级', value: (0, player_service_1.playerField)(playerSearchData, 'home_level') },
1455
1456
  ].filter((item) => item.value && item.value !== '-' && item.value !== '未设置').slice(0, 4) : [];
1456
1457
  const profileHomeItems = playerSearchData ? [
1457
- { label: '家园名称', value: playerField(playerSearchData, 'home_name') },
1458
- { label: '家园等级', value: playerField(playerSearchData, 'home_level') },
1459
- { label: '家园经验', value: playerField(playerSearchData, 'home_experience') },
1460
- { label: '舒适度', value: playerField(playerSearchData, 'home_comfort_level') },
1461
- { label: '访客数量', value: playerField(playerSearchData, 'visitor_num') },
1458
+ { label: '家园名称', value: (0, player_service_1.playerField)(playerSearchData, 'home_name') },
1459
+ { label: '家园等级', value: (0, player_service_1.playerField)(playerSearchData, 'home_level') },
1460
+ { label: '家园经验', value: (0, player_service_1.playerField)(playerSearchData, 'home_experience') },
1461
+ { label: '舒适度', value: (0, player_service_1.playerField)(playerSearchData, 'home_comfort_level') },
1462
+ { label: '访客数量', value: (0, player_service_1.playerField)(playerSearchData, 'visitor_num') },
1462
1463
  ].filter((item) => item.value && item.value !== '-' && item.value !== '未设置') : [];
1463
1464
  const profileCardItems = playerSearchData ? [
1464
- { label: '名片皮肤', value: playerField(playerSearchData, 'card_skin_selected') },
1465
- { label: '名片头像', value: playerField(playerSearchData, 'card_icon_selected') },
1465
+ { label: '名片皮肤', value: (0, player_service_1.playerField)(playerSearchData, 'card_skin_selected') },
1466
+ { label: '名片头像', value: (0, player_service_1.playerField)(playerSearchData, 'card_icon_selected') },
1466
1467
  ].filter((item) => item.value && item.value !== '-' && item.value !== '未设置') : [];
1467
- const profileCardImage = playerSearchData ? playerField(playerSearchData, 'card_bussiness_card_url', '') : '';
1468
- const profileStatusText = playerSearchData ? playerField(playerSearchData, 'online', '未知') : '未知';
1468
+ const profileCardImage = playerSearchData ? (0, player_service_1.resourceUrl)((0, player_service_1.playerField)(playerSearchData, 'card_bussiness_card_url', ''), deps.config.apiBaseUrl) : '';
1469
+ const profileStatusText = playerSearchData ? (0, player_service_1.playerField)(playerSearchData, 'online', '未知') : '未知';
1469
1470
  const hasExtraProfileData = Boolean(profileSignature || profileHomeItems.length || profileCardItems.length || profileCardImage);
1470
1471
  let degraded = false;
1471
1472
  if (!sm) {
@@ -1838,15 +1839,33 @@ function register(deps) {
1838
1839
  };
1839
1840
  await sendImage(deps, session, 'exchange-hall', data, `【交换大厅】第${page}页`);
1840
1841
  });
1841
- ctx.command('洛克').subcommand('.玩家 <uid:string>', '通过 ingame 接口查询玩家基础资料')
1842
+ ctx.command('洛克').subcommand('.玩家 [uid:string]', '通过 ingame 接口查询玩家资料')
1842
1843
  .alias('洛克玩家')
1843
- .action(async ({ session }, uid) => {
1844
- if (!uid)
1845
- return '请提供玩家 UID。用法:洛克.玩家 <UID>';
1846
- const res = await client.ingamePlayerSearch(ctx, uid);
1847
- if (!res)
1844
+ .action(async ({ session }, uid = '') => {
1845
+ const userId = session.userId;
1846
+ const explicitUid = String(uid || '').trim();
1847
+ const binding = deps.userMgr.getPrimaryBinding(userId);
1848
+ const targetUid = explicitUid || String(binding?.role_id || '');
1849
+ const fwToken = await (0, account_1.getPrimaryToken)(deps, userId);
1850
+ if (!targetUid && !fwToken) {
1851
+ return '请提供玩家 UID,或先使用“洛克.QQ登录 / 洛克.微信登录”完成绑定。用法:洛克.玩家 [UID]';
1852
+ }
1853
+ const auth = { fwToken, userIdentifier: userId };
1854
+ const search = await client.ingamePlayerSearch(ctx, targetUid, { auth });
1855
+ if (!search)
1848
1856
  return `玩家搜索失败:${client.getLastErrorBrief()}`;
1849
- await sendImage(deps, session, 'player-search', buildPlayerSearchRenderData(res, uid), `【洛克玩家】UID ${uid}`);
1857
+ // 显式 UID 与搜索返回 UID 不一致时不合并名片,避免混用两个角色数据。
1858
+ const returnedUid = String(search?.player_info?.uin ?? search?.player_info?.uid ?? search?.uin ?? search?.uid ?? '');
1859
+ const uidConsistent = !explicitUid || !returnedUid || returnedUid === explicitUid;
1860
+ let card = uidConsistent
1861
+ ? await client.ingamePlayerCard(ctx, targetUid, { auth }).catch(() => null)
1862
+ : null;
1863
+ const cardUid = (0, player_service_1.playerPayloadUid)(card);
1864
+ if (cardUid && (targetUid || returnedUid) && cardUid !== (targetUid || returnedUid))
1865
+ card = null;
1866
+ const view = (0, player_service_1.buildPlayerView)(search, card, targetUid, deps.config.apiBaseUrl);
1867
+ const merged = (0, player_service_1.mergePlayerPayloads)(search, card);
1868
+ await sendImage(deps, session, 'player-search', buildPlayerSearchRenderData(merged, targetUid || view.parsed.uid, view.cardImageUrl), (0, player_service_1.buildPlayerText)(merged, targetUid || view.parsed.uid));
1850
1869
  });
1851
1870
  ctx.command('洛克').subcommand('.家园 [uid:string]', '通过 UID 查询家园菜园、守卫和室内精灵')
1852
1871
  .alias('洛克家园')
@@ -1856,10 +1875,12 @@ function register(deps) {
1856
1875
  const binding = deps.userMgr.getPrimaryBinding(session.userId);
1857
1876
  targetUid = String(binding?.role_id || '');
1858
1877
  }
1859
- if (!targetUid)
1878
+ const fwToken = await (0, account_1.getPrimaryToken)(deps, session.userId);
1879
+ if (!targetUid && !fwToken)
1860
1880
  return '请提供玩家 UID,或先完成绑定后使用 洛克.家园。';
1861
1881
  let queuedNotified = false;
1862
1882
  const res = await client.ingameHomeInfo(ctx, targetUid, {
1883
+ auth: { fwToken, userIdentifier: session.userId },
1863
1884
  waitMs: deps.config.homeQueryWaitMs,
1864
1885
  intervalMs: deps.config.homeQueryPollIntervalMs,
1865
1886
  timeoutMs: deps.config.homeQueryTimeoutMs,
@@ -1958,7 +1979,10 @@ function register(deps) {
1958
1979
  fallbackLines.push('');
1959
1980
  fallbackLines.push('若服务器带宽过小导致生成超时,请在配置项打开低带宽模式。');
1960
1981
  fallbackLines.push('低带宽模式开启后,家园详情将不再加载技能图标。');
1961
- await sendImage(deps, session, 'pet-data', data, fallbackLines.join('\n'));
1982
+ await sendImage(deps, session, 'pet-data', data, fallbackLines.join('\n'), {
1983
+ ...(lowBandwidth ? render_1.lowBandwidthRenderOptions : {}),
1984
+ timeoutMs: deps.config.renderTimeout || 30000,
1985
+ });
1962
1986
  });
1963
1987
  ctx.command('洛克').subcommand('.刷新面板 [uid:string]', '刷新指定 UID 的精灵面板缓存')
1964
1988
  .alias('洛克刷新面板')
@@ -2041,15 +2065,16 @@ function register(deps) {
2041
2065
  const fallback = panelPetDetailFallback(targetUid, selected, updatedAtText);
2042
2066
  await sendImage(deps, session, 'pet-panel-detail', data, fallback);
2043
2067
  });
2044
- ctx.command('洛克').subcommand('.商店 <shopId:string>', '通过 ingame 接口查询商店信息')
2068
+ ctx.command('洛克').subcommand('.商店 [shopId:string]', '通过 ingame 接口查询商店信息,默认商店 3009')
2045
2069
  .alias('洛克商店')
2046
- .action(async ({ session }, shopId) => {
2047
- if (!shopId)
2048
- return '请提供商店 ID。用法:洛克.商店 <shop_id>';
2049
- const res = await client.ingameMerchantInfo(ctx, shopId);
2070
+ .action(async ({ session }, shopId = '3009') => {
2071
+ const targetShopId = String(shopId || '3009').trim() || '3009';
2072
+ if (!/^\d+$/.test(targetShopId))
2073
+ return '商店 ID 必须是数字。用法:洛克.商店 [shop_id],默认 3009';
2074
+ const res = await client.ingameMerchantInfo(ctx, targetShopId);
2050
2075
  if (!res)
2051
2076
  return `商店查询失败:${client.getLastErrorBrief()}`;
2052
- await sendImage(deps, session, 'ingame-shop', buildShopRenderData(res, shopId), `【洛克商店】shop_id=${shopId}`);
2077
+ await sendImage(deps, session, 'ingame-shop', buildShopRenderData(res, targetShopId), `【洛克商店】shop_id=${targetShopId}`);
2053
2078
  });
2054
2079
  ctx.command('洛克').subcommand('.好友关系 <userIds:string>', '查询好友关系')
2055
2080
  .alias('洛克好友关系')
@@ -2065,16 +2090,20 @@ function register(deps) {
2065
2090
  await sendImage(deps, session, 'friendship', buildFriendshipRenderData(res, userIds), `【好友关系】${userIds}`);
2066
2091
  });
2067
2092
  ctx.command('订阅家园菜园 [uid:string]', '订阅指定 UID 的家园菜园成熟提醒')
2093
+ .userFields(['authority'])
2068
2094
  .action(async ({ session }, uid = '') => subscribeHome(deps, session, uid, 'garden'));
2069
2095
  ctx.command('订阅家园灵感 [uid:string]', '订阅指定 UID 的家园精灵灵感完成提醒')
2096
+ .userFields(['authority'])
2070
2097
  .action(async ({ session }, uid = '') => subscribeHome(deps, session, uid, 'inspiration'));
2071
2098
  ctx.command('订阅家园生蛋 [uid:string]', '订阅指定 UID 的家园精灵生蛋提醒')
2099
+ .userFields(['authority'])
2072
2100
  .action(async ({ session }, uid = '') => subscribeHome(deps, session, uid, 'egg'));
2073
2101
  ctx.command('取消订阅家园 [kind:string] [uid:string]', '取消当前会话的家园订阅')
2102
+ .userFields(['authority'])
2074
2103
  .action(async ({ session }, kind = '全部', uid = '') => {
2075
2104
  const target = sessionTarget(session);
2076
- if (!target.userId && !isBotAdmin(session, deps.config.adminUserIds))
2077
- return '此指令仅限管理员使用。';
2105
+ if (!target.userId && !canManageGroupSubscription(deps, session))
2106
+ return '此指令仅限群管理员或机器人管理员使用。';
2078
2107
  const kindMap = { '菜园': 'garden', '灵感': 'inspiration', '生蛋': 'egg', '全部': '', all: '', garden: 'garden', inspiration: 'inspiration', egg: 'egg' };
2079
2108
  const deleted = deps.homeSubMgr.deleteMatching(target, kindMap[String(kind || '全部')] ?? '', String(uid || '').trim());
2080
2109
  return deleted ? `已取消 ${deleted} 条家园订阅。` : '当前会话没有匹配的家园订阅。';
@@ -2084,10 +2113,12 @@ function register(deps) {
2084
2113
  .action(async ({ session }) => {
2085
2114
  if (!isBotAdmin(session, deps.config.adminUserIds))
2086
2115
  return '此指令仅限管理员使用。';
2087
- const result = await checkHomeSubscriptions(deps);
2116
+ const result = await homeRunner(signal => checkHomeSubscriptions(deps, signal));
2117
+ if (!result)
2118
+ return '订阅检查正在运行或插件已停止。';
2088
2119
  return `家园订阅检查完成:订阅 ${result.subscriptions} 条,检查 ${result.checked} 条,推送 ${result.pushed} 档提醒。`;
2089
2120
  });
2090
2121
  if (deps.config.homeSubscriptionEnabled) {
2091
- ctx.setInterval(() => checkHomeSubscriptions(deps).catch(err => logger.warn(`家园订阅检查失败: ${err}`)), Math.max(1, deps.config.homeSubscriptionIntervalMinutes || 5) * 60000);
2122
+ ctx.setInterval(() => homeRunner(signal => checkHomeSubscriptions(deps, signal)).catch(err => logger.warn(`家园订阅检查失败: ${err}`)), Math.max(1, deps.config.homeSubscriptionIntervalMinutes || 5) * 60000);
2092
2123
  }
2093
2124
  }
@@ -0,0 +1,2 @@
1
+ import { PluginDeps } from '../types';
2
+ export declare function register(deps: PluginDeps): void;
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.register = register;
4
+ const send_image_1 = require("../send-image");
5
+ const ranking_service_1 = require("../ranking-service");
6
+ function register(deps) {
7
+ const { ctx, client, userMgr, renderer, config } = deps;
8
+ for (const [label, kind] of [['异色', 'shining'], ['炫彩', 'glass']]) {
9
+ ctx.command(`${label}排行榜 [args:text]`, `查询${label}精灵收集排行榜`)
10
+ .alias(`${label}榜`)
11
+ .alias(`洛克${label}排行榜`)
12
+ .action(async ({ session }, args = '') => {
13
+ if (!session)
14
+ return;
15
+ const primaryUid = String(userMgr.getPrimaryBinding(session.userId)?.role_id || '');
16
+ let parsed;
17
+ try {
18
+ parsed = (0, ranking_service_1.parseRankingArgs)(args, primaryUid);
19
+ }
20
+ catch (error) {
21
+ return error.message;
22
+ }
23
+ const payload = await client.getPetCollectionRanking(ctx, kind, parsed.limit, parsed.uid, { userIdentifier: session.userId });
24
+ if (payload === null)
25
+ return `排行榜查询失败:${client.getLastErrorBrief()}`;
26
+ const data = (0, ranking_service_1.buildRankingView)(payload, kind, config.apiBaseUrl.replace(/\/$/, ''), parsed.uid);
27
+ const image = await renderer.renderPages(ctx, 'pet-ranking', data, { maxPageHeight: 3500 });
28
+ await (0, send_image_1.sendImageWithFallback)(session, image, (0, ranking_service_1.buildRankingText)(data), 'ranking', config);
29
+ });
30
+ }
31
+ }
@@ -0,0 +1,2 @@
1
+ import { PluginDeps } from '../types';
2
+ export declare function register(deps: PluginDeps): void;
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.register = register;
4
+ const send_image_1 = require("../send-image");
5
+ const share_code_service_1 = require("../share-code-service");
6
+ function register(deps) {
7
+ const { ctx, client, renderer, config } = deps;
8
+ const baseUrl = config.apiBaseUrl.replace(/\/$/, '');
9
+ const send = async (session, template, data, fallback) => {
10
+ const image = await renderer.renderPages(ctx, template, data);
11
+ await (0, send_image_1.sendImageWithFallback)(session, image, fallback, `share-code:${template}`, config);
12
+ };
13
+ const command = ctx.command('阵容码', '阵容分享码工具');
14
+ command.subcommand('解析 <input:text>', '解析分享码或链接')
15
+ .alias('阵容码解析')
16
+ .action(async ({ session }, input) => {
17
+ if (!session)
18
+ return;
19
+ let code = '';
20
+ try {
21
+ code = (0, share_code_service_1.extractShareCode)(input || '');
22
+ }
23
+ catch (error) {
24
+ return error.message;
25
+ }
26
+ if (!code)
27
+ return '请提供分享码或包含 shareData 的链接。用法:阵容码 解析 <分享码或链接>';
28
+ const payload = await client.parseShareCode(ctx, code, session.userId);
29
+ if (!payload)
30
+ return `分享码解析失败:${client.getLastErrorBrief()}`;
31
+ const data = (0, share_code_service_1.buildShareCodeView)(payload, 'parse', undefined, baseUrl);
32
+ if (!data.teams.length)
33
+ return '分享码中没有可展示的精灵数据。';
34
+ await send(session, 'share-code-team', data, (0, share_code_service_1.buildShareCodeText)(data));
35
+ });
36
+ command.subcommand('查询 <input:text>', '查询分享码历史记录')
37
+ .alias('阵容码查询')
38
+ .action(async ({ session }, input) => {
39
+ if (!session)
40
+ return;
41
+ let code = '';
42
+ try {
43
+ code = (0, share_code_service_1.extractShareCode)(input || '');
44
+ }
45
+ catch (error) {
46
+ return error.message;
47
+ }
48
+ if (!code)
49
+ return '请提供分享码。用法:阵容码 查询 <分享码>';
50
+ const res = await client.getShareCodeRecords(ctx, { shareCode: code, pageNo: 1, pageSize: 1 }, session.userId);
51
+ if (!res)
52
+ return `分享码记录查询失败:${client.getLastErrorBrief()}`;
53
+ const record = Array.isArray(res.items) ? res.items[0] : null;
54
+ if (!record)
55
+ return '未找到该分享码的解析记录,请先使用:阵容码 解析 <分享码或链接>';
56
+ let data = (0, share_code_service_1.buildShareCodeView)(record, 'record', record, baseUrl);
57
+ if (!data.teams.length) {
58
+ const parsed = await client.parseShareCode(ctx, code, session.userId);
59
+ if (parsed)
60
+ data = (0, share_code_service_1.buildShareCodeView)(parsed, 'record', record, baseUrl);
61
+ }
62
+ if (!data.teams.length)
63
+ return '已找到分享码记录,但无法还原阵容详情,请稍后重试。';
64
+ await send(session, 'share-code-team', data, (0, share_code_service_1.buildShareCodeText)(data));
65
+ });
66
+ }