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
@@ -0,0 +1,148 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.numberOrNull = void 0;
4
+ exports.livePrice = livePrice;
5
+ exports.merchantTimestampMs = merchantTimestampMs;
6
+ exports.normalizeLiveMerchant = normalizeLiveMerchant;
7
+ exports.normalizeLegacyMerchant = normalizeLegacyMerchant;
8
+ exports.isLiveMerchantPayload = isLiveMerchantPayload;
9
+ exports.normalizeMerchant = normalizeMerchant;
10
+ const client_1 = require("./client");
11
+ const numberOrNull = (v) => {
12
+ if (v === undefined || v === null || v === '')
13
+ return null;
14
+ const n = Number(v);
15
+ return Number.isFinite(n) ? n : null;
16
+ };
17
+ exports.numberOrNull = numberOrNull;
18
+ // 价格可能直接是数字,也可能是 { real, origin } 或 { amount }(上游 v4.1.0)。
19
+ function livePrice(value) {
20
+ const selected = value && typeof value === 'object'
21
+ ? value.real ?? value.origin : value;
22
+ return (0, exports.numberOrNull)(selected && typeof selected === 'object'
23
+ ? selected.amount : selected);
24
+ }
25
+ // 秒/毫秒时间戳统一为毫秒;0/空值视为无时间。
26
+ function merchantTimestampMs(value) {
27
+ if (value === null || value === undefined || value === '' || value === 0 || value === '0')
28
+ return null;
29
+ const timestamp = Number(value);
30
+ if (!Number.isFinite(timestamp))
31
+ return null;
32
+ return Math.abs(timestamp) < 100_000_000_000 ? timestamp * 1000 : timestamp;
33
+ }
34
+ function mappingById(payload) {
35
+ const mapping = payload?._goods_mapping ?? payload?.goods_mapping;
36
+ const list = Array.isArray(mapping) ? mapping : [];
37
+ const map = new Map();
38
+ for (const item of list) {
39
+ if ((0, client_1.isObject)(item) && item.goods_id !== undefined && item.goods_id !== null) {
40
+ map.set(String(item.goods_id), item);
41
+ }
42
+ }
43
+ return map;
44
+ }
45
+ function liveGoodsName(meta, item, goodsId) {
46
+ const name = String(meta?.goods_name || meta?.name || item?.name || item?.goods_name || '').trim();
47
+ if (name)
48
+ return name;
49
+ return goodsId !== undefined && goodsId !== null && goodsId !== '' ? `商品 ${goodsId}` : '未知商品';
50
+ }
51
+ function liveGoodsIcon(meta, item) {
52
+ return String(item?.icon_url || item?.iconUrl || meta?.icon_url || meta?.icon || '').trim();
53
+ }
54
+ // 实时商人 goods 结构:按 mapping 补齐名称/图标,递归 sub_goods,按“商品 ID + 时间窗口 + 父商品”去重。
55
+ function normalizeLiveMerchant(payload, nowMs = Date.now()) {
56
+ const map = mappingById(payload);
57
+ const items = [];
58
+ const seen = new Set();
59
+ const add = (raw, parentId) => {
60
+ if (!(0, client_1.isObject)(raw))
61
+ return;
62
+ const goodsId = raw.goods_id ?? raw.id;
63
+ const meta = map.get(String(goodsId)) || {};
64
+ const startsAt = merchantTimestampMs(raw.start_time ?? payload?.start_time);
65
+ const endsAt = merchantTimestampMs(raw.disable_time ?? raw.end_time);
66
+ const key = `${String(goodsId)}|${startsAt ?? ''}|${endsAt ?? ''}|${parentId ?? ''}`;
67
+ if (seen.has(key))
68
+ return;
69
+ seen.add(key);
70
+ items.push({
71
+ id: String(goodsId ?? ''),
72
+ parentId,
73
+ name: liveGoodsName(meta, raw, goodsId),
74
+ icon: liveGoodsIcon(meta, raw),
75
+ price: livePrice(raw.price) ?? (0, exports.numberOrNull)(meta.price),
76
+ limit: (0, exports.numberOrNull)(raw.limit_buy_num ?? raw.buy_limit_num ?? meta.buy_limit_num),
77
+ startsAt,
78
+ endsAt,
79
+ active: (startsAt === null || nowMs >= startsAt) && (endsAt === null || nowMs < endsAt),
80
+ category: parentId ? '子商品' : '商品',
81
+ source: 'live',
82
+ });
83
+ const children = Array.isArray(raw.sub_goods) ? raw.sub_goods : [];
84
+ for (const child of children)
85
+ add(child, String(goodsId ?? ''));
86
+ };
87
+ const goods = Array.isArray(payload?.goods) ? payload.goods : [];
88
+ for (const raw of goods)
89
+ add(raw);
90
+ return items;
91
+ }
92
+ // 旧接口:merchantActivities 下 products/get_props/get_extra_props/get_pets 合并,random_goods 补价格/限购。
93
+ function normalizeLegacyMerchant(payload, nowMs = Date.now()) {
94
+ const activities = payload?.merchantActivities || payload?.merchant_activities || [];
95
+ const activity = activities[0] || {};
96
+ const randomGoods = Array.isArray(payload?.random_goods) ? payload.random_goods : [];
97
+ const metaByName = new Map();
98
+ for (const item of randomGoods) {
99
+ const name = String(item?.goods_name || item?.name || '').trim();
100
+ if (name)
101
+ metaByName.set(name, item);
102
+ }
103
+ const buckets = [
104
+ ['道具', activity.get_props || []],
105
+ ['额外道具', activity.get_extra_props || []],
106
+ ['精灵', activity.get_pets || []],
107
+ ['商品', activity.products || []],
108
+ ['商品', activity.product_list || []],
109
+ ];
110
+ const items = [];
111
+ const seen = new Set();
112
+ for (const [category, list] of buckets) {
113
+ for (const raw of Array.isArray(list) ? list : []) {
114
+ if (!(0, client_1.isObject)(raw))
115
+ continue;
116
+ const name = String(raw.name || raw.goods_name || '未知商品').trim() || '未知商品';
117
+ const startsAt = merchantTimestampMs(raw.start_time) ?? merchantTimestampMs(activity.start_time);
118
+ const endsAt = merchantTimestampMs(raw.end_time) ?? merchantTimestampMs(activity.end_time);
119
+ const key = `${raw.id ?? ''}|${name}|${startsAt ?? 0}|${endsAt ?? 'inf'}`;
120
+ if (seen.has(key))
121
+ continue;
122
+ seen.add(key);
123
+ const meta = metaByName.get(name) || {};
124
+ items.push({
125
+ id: String(raw.id ?? ''),
126
+ name,
127
+ icon: String(raw.icon_url || raw.iconUrl || meta.icon_url || meta.icon || '').trim(),
128
+ price: (0, exports.numberOrNull)(raw.price) ?? (0, exports.numberOrNull)(meta.price),
129
+ limit: (0, exports.numberOrNull)(raw.buy_limit_num ?? raw.limit_buy_num) ?? (0, exports.numberOrNull)(meta.buy_limit_num),
130
+ startsAt,
131
+ endsAt,
132
+ active: (startsAt === null || nowMs >= startsAt) && (endsAt === null || nowMs < endsAt),
133
+ category,
134
+ source: 'legacy',
135
+ });
136
+ }
137
+ }
138
+ return items;
139
+ }
140
+ function isLiveMerchantPayload(payload) {
141
+ return (0, client_1.isObject)(payload) && Array.isArray(payload.goods);
142
+ }
143
+ function normalizeMerchant(payload, nowMs = Date.now()) {
144
+ if (isLiveMerchantPayload(payload)) {
145
+ return { source: 'live', items: normalizeLiveMerchant(payload, nowMs) };
146
+ }
147
+ return { source: 'legacy', items: normalizeLegacyMerchant(payload, nowMs) };
148
+ }
@@ -0,0 +1,16 @@
1
+ import type { PluginConfig } from './types';
2
+ export type GroupRole = 'owner' | 'admin' | 'member' | 'unknown';
3
+ export interface SubscriptionPermissionInput {
4
+ privateChat: boolean;
5
+ privateAllowed: boolean;
6
+ groupRole: GroupRole;
7
+ groupAdminEnabled: boolean;
8
+ botAdminEnabled: boolean;
9
+ whitelisted: boolean;
10
+ authority: number;
11
+ botAdminAuthority: number;
12
+ }
13
+ export declare function mayManageSubscription(p: SubscriptionPermissionInput): boolean;
14
+ export declare function groupRoleFromSession(session: any): GroupRole;
15
+ export declare function isDirectSession(session: any): boolean;
16
+ export declare function canManageSubscription(config: PluginConfig, session: any, privateAllowed?: boolean): boolean;
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mayManageSubscription = mayManageSubscription;
4
+ exports.groupRoleFromSession = groupRoleFromSession;
5
+ exports.isDirectSession = isDirectSession;
6
+ exports.canManageSubscription = canManageSubscription;
7
+ // 统一订阅权限策略:群主/管理员与 Bot 管理员按独立开关生效。
8
+ function mayManageSubscription(p) {
9
+ if (p.privateChat)
10
+ return p.privateAllowed;
11
+ if (p.botAdminEnabled && (p.whitelisted || p.authority >= p.botAdminAuthority))
12
+ return true;
13
+ return p.groupAdminEnabled && (p.groupRole === 'owner' || p.groupRole === 'admin');
14
+ }
15
+ // 从平台会话中读取可信角色;只接受适配器规范化的字符串角色。
16
+ function groupRoleFromSession(session) {
17
+ const collect = (value, output) => {
18
+ if (value === undefined || value === null)
19
+ return;
20
+ if (Array.isArray(value)) {
21
+ for (const item of value)
22
+ collect(item, output);
23
+ return;
24
+ }
25
+ output.push(String(value));
26
+ };
27
+ const roles = [];
28
+ const member = session?.event?.member || session?.member;
29
+ collect(member?.roles, roles);
30
+ collect(member?.role, roles);
31
+ const sender = session?.event?.sender || session?.sender;
32
+ collect(sender?.role, roles);
33
+ const author = session?.event?.author || session?.author;
34
+ collect(author?.member_role, roles);
35
+ collect(author?.memberRole, roles);
36
+ collect(member?.member_role, roles);
37
+ // OneBot 原始事件由适配器提供;不从消息正文读取角色。
38
+ collect(session?.onebot?.sender?.role, roles);
39
+ const normalized = roles.map(role => role.trim().toLowerCase());
40
+ if (normalized.some(role => ['owner', '群主', '创建者'].includes(role)))
41
+ return 'owner';
42
+ if (normalized.some(role => ['admin', 'administrator', '管理员'].includes(role)))
43
+ return 'admin';
44
+ if (normalized.some(role => ['member', '普通成员'].includes(role)))
45
+ return 'member';
46
+ return 'unknown';
47
+ }
48
+ function isDirectSession(session) {
49
+ if (typeof session?.isDirect === 'boolean')
50
+ return session.isDirect;
51
+ const type = session?.event?.channel?.type;
52
+ if (type !== undefined)
53
+ return type === 1 || type === 'DIRECT';
54
+ return !session?.guildId;
55
+ }
56
+ function canManageSubscription(config, session, privateAllowed = true) {
57
+ return mayManageSubscription({
58
+ privateChat: isDirectSession(session), privateAllowed,
59
+ groupRole: groupRoleFromSession(session),
60
+ groupAdminEnabled: config.subscriptionGroupAdminEnabled !== false,
61
+ botAdminEnabled: config.subscriptionBotAdminEnabled !== false,
62
+ whitelisted: config.adminUserIds.includes(session?.userId || ''),
63
+ authority: Number(session?.user?.authority ?? 0) || 0,
64
+ botAdminAuthority: Math.max(1, Number(config.subscriptionBotAdminAuthority) || 4),
65
+ });
66
+ }
@@ -24,6 +24,7 @@ exports.petDataSkills = petDataSkills;
24
24
  exports.petDataCardItems = petDataCardItems;
25
25
  exports.petDataExtractItems = petDataExtractItems;
26
26
  exports.buildPetDataRenderData = buildPetDataRenderData;
27
+ const pet_size_1 = require("./pet-size");
27
28
  const CN_TIME_ZONE = 'Asia/Shanghai';
28
29
  function petDataDisplay(value, defaultText = '--') {
29
30
  if (value === undefined || value === null || value === '')
@@ -105,12 +106,14 @@ function petDataKgCompact(grams) {
105
106
  const text = (number / 1000).toFixed(2).replace(/0+$/, '').replace(/\.$/, '');
106
107
  return `${text}kg`;
107
108
  }
108
- // 上游 v3.7.0:接入 Wiki 精灵体重范围,前 5% 标小块头、后 5% 标大块头。
109
+ // 上游 v3.7.0:接入 Wiki 精灵体重范围,与查蛋共用同一判定。
109
110
  function petDataWeightSizeInfo(pet, wikiPet) {
110
111
  const bodySize = wikiPet && typeof wikiPet === 'object' ? wikiPet.body_size : null;
111
112
  const weightRange = bodySize && typeof bodySize === 'object' ? bodySize.weight : null;
112
113
  if (!weightRange || typeof weightRange !== 'object')
113
114
  return {};
115
+ if ([pet?.weight, weightRange.min_g, weightRange.max_g].some(value => value == null || value === ''))
116
+ return {};
114
117
  const current = Number(pet?.weight);
115
118
  const low = Number(weightRange.min_g);
116
119
  const high = Number(weightRange.max_g);
@@ -118,15 +121,10 @@ function petDataWeightSizeInfo(pet, wikiPet) {
118
121
  return {};
119
122
  if (high <= low)
120
123
  return {};
121
- const span = high - low;
122
- const smallCut = low + span * 0.05;
123
- const largeCut = high - span * 0.05;
124
+ const size = (0, pet_size_1.classifyWeight)(current / 1000, low / 1000, high / 1000);
124
125
  const rangeText = `${petDataKgCompact(low)}-${petDataKgCompact(high)}`;
125
- if (current <= smallCut) {
126
- return { label: '小块头', className: 'size-small', hint: `小块头 · ≤${petDataKgCompact(smallCut)} · 范围 ${rangeText}` };
127
- }
128
- if (current >= largeCut) {
129
- return { label: '大块头', className: 'size-large', hint: `大块头 · ≥${petDataKgCompact(largeCut)} · 范围 ${rangeText}` };
126
+ if (size) {
127
+ return { label: size.label, className: size.css, hint: `${size.label} · ${size.css === 'size-small' ? '≤' : '≥'}${petDataKgCompact(size.threshold * 1000)} · 范围 ${rangeText}` };
130
128
  }
131
129
  return { label: '', className: '', hint: `范围 ${rangeText}` };
132
130
  }
@@ -0,0 +1,13 @@
1
+ export interface WeightClass {
2
+ label: '小块头' | '大块头';
3
+ css: 'size-small' | 'size-large';
4
+ threshold: number;
5
+ }
6
+ export declare function classifyWeight(weightKg: number, minKg: number, maxKg: number): WeightClass | null;
7
+ export interface SizeVariant {
8
+ size_variant: string;
9
+ size_variant_label: string;
10
+ size_variant_class: string;
11
+ size_variant_hint: string;
12
+ }
13
+ export declare function sizeVariantPayload(queryWeight: unknown, minKg: unknown, maxKg: unknown, rangeText?: string): SizeVariant;
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.classifyWeight = classifyWeight;
4
+ exports.sizeVariantPayload = sizeVariantPayload;
5
+ // 体重范围下端 5% / 上端 5% 判定大小块头;使用未四舍五入的数值比较。
6
+ function classifyWeight(weightKg, minKg, maxKg) {
7
+ if (![weightKg, minKg, maxKg].every(Number.isFinite) || maxKg <= minKg)
8
+ return null;
9
+ if (weightKg < minKg || weightKg > maxKg)
10
+ return null;
11
+ const span = maxKg - minKg;
12
+ const small = minKg + span * 0.05;
13
+ const large = maxKg - span * 0.05;
14
+ if (weightKg <= small)
15
+ return { label: '小块头', css: 'size-small', threshold: small };
16
+ if (weightKg >= large)
17
+ return { label: '大块头', css: 'size-large', threshold: large };
18
+ return null;
19
+ }
20
+ function num(value) {
21
+ if (value === null || value === undefined || value === '')
22
+ return null;
23
+ const parsed = Number(value);
24
+ return Number.isFinite(parsed) ? parsed : null;
25
+ }
26
+ function formatNumber(value, digits = 3) {
27
+ return String(Number(value.toFixed(digits)));
28
+ }
29
+ const EMPTY_VARIANT = {
30
+ size_variant: '',
31
+ size_variant_label: '',
32
+ size_variant_class: '',
33
+ size_variant_hint: '',
34
+ };
35
+ function sizeVariantPayload(queryWeight, minKg, maxKg, rangeText = '') {
36
+ const query = num(queryWeight);
37
+ const low = num(minKg);
38
+ const high = num(maxKg);
39
+ if (query === null || low === null || high === null || high <= low)
40
+ return EMPTY_VARIANT;
41
+ const cls = classifyWeight(query, low, high);
42
+ if (!cls)
43
+ return EMPTY_VARIANT;
44
+ const limit = formatNumber(cls.threshold);
45
+ const range = rangeText || `${formatNumber(low)}-${formatNumber(high)}kg`;
46
+ return {
47
+ size_variant: cls.css === 'size-small' ? 'small' : 'large',
48
+ size_variant_label: cls.label,
49
+ size_variant_class: cls.css,
50
+ size_variant_hint: cls.label === '小块头'
51
+ ? `小块头区间:≤ ${limit} kg(体重范围 ${range} 的前 5%)`
52
+ : `大块头区间:≥ ${limit} kg(体重范围 ${range} 的后 5%)`,
53
+ };
54
+ }
@@ -0,0 +1,37 @@
1
+ export interface PlayerRow {
2
+ field?: string;
3
+ label?: string;
4
+ value?: unknown;
5
+ }
6
+ export type IngamePlayerPayload = {
7
+ rows?: PlayerRow[];
8
+ notes?: unknown[];
9
+ title?: string;
10
+ player_info?: Record<string, any>;
11
+ player_card_brief_info?: Record<string, any>;
12
+ [key: string]: any;
13
+ };
14
+ export declare function resourceUrl(value: unknown, apiBaseUrl: string): string;
15
+ export declare function playerRows(payload: any, uid: string): PlayerRow[];
16
+ /** 玩家时间统一按北京时间展示,不依赖 Bot 所在服务器的时区。 */
17
+ export declare function formatPlayerTime(value: unknown): string;
18
+ export declare function cleanPlayerFieldValue(field: string, value: unknown): string;
19
+ export interface ParsedPlayer {
20
+ title: string;
21
+ nickname: string;
22
+ uid: string;
23
+ level: string;
24
+ signature: string;
25
+ rowMap: Record<string, string>;
26
+ labelMap: Record<string, string>;
27
+ }
28
+ export declare function parseIngamePlayerPayload(payload: IngamePlayerPayload | null | undefined, uid: string): ParsedPlayer;
29
+ export declare function playerField(parsed: ParsedPlayer | null, field: string, defaultValue?: string): string;
30
+ export interface PlayerView {
31
+ parsed: ParsedPlayer;
32
+ cardImageUrl: string;
33
+ }
34
+ export declare function buildPlayerView(searchPayload: any, cardPayload: any, uid: string, apiBaseUrl: string): PlayerView;
35
+ export declare function mergePlayerPayloads(searchPayload: any, cardPayload: any): any;
36
+ export declare function playerPayloadUid(payload: any): string;
37
+ export declare function buildPlayerText(payload: any, uid: string): string;
@@ -0,0 +1,201 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resourceUrl = resourceUrl;
4
+ exports.playerRows = playerRows;
5
+ exports.formatPlayerTime = formatPlayerTime;
6
+ exports.cleanPlayerFieldValue = cleanPlayerFieldValue;
7
+ exports.parseIngamePlayerPayload = parseIngamePlayerPayload;
8
+ exports.playerField = playerField;
9
+ exports.buildPlayerView = buildPlayerView;
10
+ exports.mergePlayerPayloads = mergePlayerPayloads;
11
+ exports.playerPayloadUid = playerPayloadUid;
12
+ exports.buildPlayerText = buildPlayerText;
13
+ const client_1 = require("./client");
14
+ // 相对资源地址转绝对地址(上游 v4.1.0);非字符串/非法协议返回空串,避免渲染裂图。
15
+ function resourceUrl(value, apiBaseUrl) {
16
+ if (typeof value !== 'string' || !value.trim())
17
+ return '';
18
+ // `relative/` 是后端资源标记,不属于资源路径。
19
+ const text = value.trim().replace(/^relative\//, '');
20
+ if (!text)
21
+ return '';
22
+ if (/^data:/i.test(text))
23
+ return text;
24
+ try {
25
+ const url = new URL(text, apiBaseUrl.replace(/\/$/, '') + '/');
26
+ return ['https:', 'http:'].includes(url.protocol) ? url.href : '';
27
+ }
28
+ catch {
29
+ return '';
30
+ }
31
+ }
32
+ // 将 v4.1.0 玩家嵌套结构与旧 rows 统一成 IngamePlayerRow[] 目标展示。
33
+ // 值为 0/false 需要保留,不能用 `value || ''` 清空。
34
+ function playerRows(payload, uid) {
35
+ const legacy = Array.isArray(payload?.rows) ? payload.rows : [];
36
+ const p = payload?.player_info ?? {};
37
+ const c = payload?.player_card_brief_info ?? {};
38
+ const stats = c.card_pet_info ?? {};
39
+ const home = p.home_info ?? {};
40
+ const visit = p.visit_info ?? {};
41
+ const appearance = c.card_appearance_info ?? {};
42
+ const business = c.business_card_info ?? {};
43
+ const fields = [
44
+ ['uin', 'UID', p.uin ?? p.uid ?? legacy.find(row => row.field === 'uin')?.value ?? uid],
45
+ ['name', '昵称', p.name],
46
+ ['level', '等级', p.level],
47
+ ['online', '在线状态', p.online ?? p.is_online ?? payload?.online],
48
+ ['sex', '性别', p.sex ?? p.gender],
49
+ ['gender', '性别', p.gender ?? p.sex],
50
+ ['signature', '个性签名', p.signature ?? c.card_signature],
51
+ ['home_name', '家园名称', home.home_name ?? home.name],
52
+ ['home_level', '家园等级', home.home_level],
53
+ ['room_level', '房间等级', home.room_level],
54
+ ['home_experience', '家园经验', home.home_experience],
55
+ ['home_comfort_level', '舒适度', home.home_comfort_level ?? home.comfort_level],
56
+ ['visitor_num', '访客数量', visit.visitor_num],
57
+ ['world_level', '世界等级', p.world_level],
58
+ ['card_handbook_collect_num', '图鉴收集', p.card_handbook_collect_num ?? c.card_handbook_collect_num],
59
+ ['last_logout_time', '最后离线', p.last_logout_time],
60
+ ['collected_shining_pet_count', '异色收集', stats.collected_shining_pet_count],
61
+ ['collected_glass_pet_count', '炫彩收集', stats.collected_glass_pet_count],
62
+ ['card_skin_selected', '名片皮肤', appearance.card_skin_selected ?? p.card_skin_selected ?? business.card_skin_selected],
63
+ ['card_icon_selected', '名片头像', c.card_icon_selected ?? p.card_icon_selected ?? appearance.card_icon_selected ?? business.card_icon_selected],
64
+ ['card_bussiness_card_url', '名片图片', business.cur_card_url ?? c.cur_card_url ?? p.card_bussiness_card_url],
65
+ ];
66
+ const mapped = fields
67
+ .filter(([, , v]) => v !== undefined && v !== null)
68
+ .map(([field, label, value]) => ({ field, label, value }));
69
+ return [...new Map([...legacy, ...mapped].map(row => [row.field, row])).values()];
70
+ }
71
+ /** 玩家时间统一按北京时间展示,不依赖 Bot 所在服务器的时区。 */
72
+ function formatPlayerTime(value) {
73
+ if (typeof value !== 'string' && typeof value !== 'number')
74
+ return '未知';
75
+ const text = String(value).trim();
76
+ if (!text)
77
+ return '未知';
78
+ let timestamp;
79
+ if (/^[+-]?\d+(?:\.\d+)?$/.test(text)) {
80
+ const numeric = Number(text);
81
+ if (!Number.isFinite(numeric) || numeric <= 0)
82
+ return '未知';
83
+ timestamp = numeric < 100_000_000_000 ? numeric * 1000 : numeric;
84
+ }
85
+ else {
86
+ // 已格式化的无时区文本视作北京时间;带 Z/偏移的 ISO 日期按原偏移解析。
87
+ const match = /^(\d{4})[-/](\d{2})[-/](\d{2})(?:[ T](\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?))?(Z|[+-]\d{2}:?\d{2})?(?:(北京时间))?$/i.exec(text);
88
+ if (!match)
89
+ return '未知';
90
+ timestamp = Date.parse(`${match[1]}-${match[2]}-${match[3]}T${match[4] || '00:00:00'}${match[5] || '+08:00'}`);
91
+ }
92
+ const beijing = new Date(timestamp + 8 * 3600_000);
93
+ if (!Number.isFinite(beijing.getTime()))
94
+ return '未知';
95
+ return `${beijing.toISOString().slice(0, 19).replace('T', ' ')}(北京时间)`;
96
+ }
97
+ function cleanPlayerFieldValue(field, value) {
98
+ if (field === 'last_logout_time')
99
+ return formatPlayerTime(value);
100
+ const text = String(value ?? '').trim().replace(/^'+|'+$/g, '');
101
+ if (!text || ['<0B>', '<0b>', '<0B >', '<0b >'].includes(text))
102
+ return '未设置';
103
+ if (['is_online', 'online', 'chat_top_unlock', 'is_friend', 'is_black', 'is_black_role', 'is_chat_node_unlock'].includes(field)) {
104
+ return ['1', 'true', 'True', '是'].includes(text) ? '是' : '否';
105
+ }
106
+ if (['sex', 'gender'].includes(field)) {
107
+ return { '0': '未知', '1': '男', '2': '女' }[text] || text;
108
+ }
109
+ if (field === 'friend_type') {
110
+ return { '0': '默认', '1': '特殊' }[text] || text;
111
+ }
112
+ if (field === 'battle_state') {
113
+ return { '0': '空闲', '1': '对战中' }[text] || text;
114
+ }
115
+ return text;
116
+ }
117
+ function parseIngamePlayerPayload(payload, uid) {
118
+ const rows = playerRows(payload, uid);
119
+ const rowMap = {};
120
+ const labelMap = {};
121
+ for (const row of rows) {
122
+ const field = String(row.field || '');
123
+ if (!field)
124
+ continue;
125
+ rowMap[field] = String(row.value ?? '');
126
+ labelMap[field] = String(row.label || field);
127
+ }
128
+ const playerUid = cleanPlayerFieldValue('uin', rowMap.uin || uid);
129
+ const signature = cleanPlayerFieldValue('signature', rowMap.signature || '');
130
+ return {
131
+ title: String(payload?.title || '玩家搜索'),
132
+ nickname: cleanPlayerFieldValue('name', rowMap.name || '-'),
133
+ uid: playerUid,
134
+ level: cleanPlayerFieldValue('level', rowMap.level || '-'),
135
+ signature: signature === '未设置' ? '' : signature,
136
+ rowMap,
137
+ labelMap,
138
+ };
139
+ }
140
+ function playerField(parsed, field, defaultValue = '未设置') {
141
+ if (!parsed)
142
+ return defaultValue;
143
+ const raw = parsed.rowMap[field];
144
+ if (field === 'last_logout_time')
145
+ return formatPlayerTime(raw);
146
+ if (raw == null || raw === '')
147
+ return defaultValue;
148
+ const value = cleanPlayerFieldValue(field, raw);
149
+ return value && value !== '-' && value !== '未设置' ? value : defaultValue;
150
+ }
151
+ // 合并搜索结果与名片结果;名片失败时保留基础信息(上游 v4.1.0)。
152
+ function buildPlayerView(searchPayload, cardPayload, uid, apiBaseUrl) {
153
+ const merged = mergePlayerPayloads(searchPayload, cardPayload);
154
+ const parsed = parseIngamePlayerPayload(merged, uid);
155
+ const cardRaw = playerField(parsed, 'card_bussiness_card_url', '');
156
+ return {
157
+ parsed,
158
+ cardImageUrl: cardRaw ? resourceUrl(cardRaw, apiBaseUrl) : '',
159
+ };
160
+ }
161
+ function mergePlayerPayloads(searchPayload, cardPayload) {
162
+ const result = { ...(searchPayload || {}) };
163
+ const searchUid = playerPayloadUid(searchPayload);
164
+ const cardUid = playerPayloadUid(cardPayload);
165
+ if (searchUid && cardUid && searchUid !== cardUid)
166
+ return result;
167
+ if (!(0, client_1.isObject)(cardPayload))
168
+ return result;
169
+ const searchInfo = (0, client_1.isObject)(result.player_info) ? result.player_info : {};
170
+ const cardInfo = (0, client_1.isObject)(cardPayload.player_info) ? cardPayload.player_info : {};
171
+ if (Object.keys(cardInfo).length)
172
+ result.player_info = { ...searchInfo, ...cardInfo };
173
+ for (const key of ['home_info', 'visit_info', 'battle_brief_info', 'pos_info']) {
174
+ if ((0, client_1.isObject)(searchInfo[key]) && (0, client_1.isObject)(cardInfo[key])) {
175
+ result.player_info[key] = { ...searchInfo[key], ...cardInfo[key] };
176
+ }
177
+ }
178
+ if ((0, client_1.isObject)(cardPayload.player_card_brief_info)) {
179
+ const base = (0, client_1.isObject)(result.player_card_brief_info) ? result.player_card_brief_info : {};
180
+ result.player_card_brief_info = { ...base, ...cardPayload.player_card_brief_info };
181
+ for (const key of ['card_pet_info', 'business_card_info', 'card_appearance_info']) {
182
+ if ((0, client_1.isObject)(base[key]) && (0, client_1.isObject)(cardPayload.player_card_brief_info[key])) {
183
+ result.player_card_brief_info[key] = { ...base[key], ...cardPayload.player_card_brief_info[key] };
184
+ }
185
+ }
186
+ }
187
+ if (Array.isArray(result.rows) && result.rows.length && !(0, client_1.isObject)(cardPayload.player_card_brief_info)) {
188
+ return result;
189
+ }
190
+ return result;
191
+ }
192
+ function playerPayloadUid(payload) {
193
+ return String(payload?.player_info?.uin ?? payload?.player_info?.uid ?? payload?.uin ?? payload?.uid
194
+ ?? payload?.rows?.find((row) => row.field === 'uin')?.value ?? '');
195
+ }
196
+ function buildPlayerText(payload, uid) {
197
+ const p = parseIngamePlayerPayload(payload, uid);
198
+ return ['洛克玩家 ' + p.nickname + ' · UID ' + p.uid,
199
+ ...Object.entries(p.rowMap).filter(([key]) => !['uin', 'name'].includes(key))
200
+ .map(([key, value]) => (p.labelMap[key] || key) + ':' + cleanPlayerFieldValue(key, value))].join('\n');
201
+ }
@@ -0,0 +1,46 @@
1
+ export type RankingKind = 'shining' | 'glass';
2
+ interface RankingMeta {
3
+ title: string;
4
+ subtitle: string;
5
+ label: string;
6
+ otherLabel: string;
7
+ countField: string;
8
+ otherCountField: string;
9
+ theme: string;
10
+ }
11
+ export declare const RANKING_META: Record<RankingKind, RankingMeta>;
12
+ export interface RankingArgs {
13
+ uid: string;
14
+ limit: number;
15
+ }
16
+ export declare function parseRankingArgs(text: string, primaryUid?: string): RankingArgs;
17
+ export declare function formatRankingTime(value: unknown): string;
18
+ export declare function buildRankingView(payload: any, rankType: RankingKind, baseUrl: string, requestedUid?: string, now?: Date): {
19
+ total: number;
20
+ items: {
21
+ rank: number;
22
+ podiumClass: string;
23
+ playerName: string;
24
+ avatar: string;
25
+ signature: string;
26
+ primaryCount: number;
27
+ secondaryCount: number;
28
+ sampleCount: number;
29
+ lastSeen: string;
30
+ }[];
31
+ current: any;
32
+ requestedUid: string;
33
+ shownCount: number;
34
+ updatedAt: string;
35
+ commandHint: string;
36
+ copyright: string;
37
+ title: string;
38
+ subtitle: string;
39
+ label: string;
40
+ otherLabel: string;
41
+ countField: string;
42
+ otherCountField: string;
43
+ theme: string;
44
+ };
45
+ export declare function buildRankingText(data: any): string;
46
+ export {};