koishi-plugin-rocom 1.0.10 → 1.0.11

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.
@@ -0,0 +1,72 @@
1
+ type ActivityReward = {
2
+ kind: string;
3
+ name: string;
4
+ };
5
+ type ActivityItem = {
6
+ id: string;
7
+ name: string;
8
+ desc: string;
9
+ description: string;
10
+ cover: string;
11
+ start_ts: number;
12
+ end_ts: number;
13
+ start: string;
14
+ end: string;
15
+ statusText: string;
16
+ statusClass: string;
17
+ is_unlimited: boolean;
18
+ is_perm: boolean;
19
+ rewards: ActivityReward[];
20
+ rewards_text: string;
21
+ sort: number;
22
+ time_label: string;
23
+ hide_start?: boolean;
24
+ left_pct?: number;
25
+ width_pct?: number;
26
+ theme?: string;
27
+ };
28
+ export declare class ActivitiesService {
29
+ extractActivities(payload: any): ActivityItem[];
30
+ buildRenderData(payload: any): {
31
+ title: string;
32
+ subtitle: string;
33
+ activity_count: number;
34
+ activities: ActivityItem[];
35
+ lanes: {
36
+ theme: string;
37
+ id: string;
38
+ name: string;
39
+ desc: string;
40
+ description: string;
41
+ cover: string;
42
+ start_ts: number;
43
+ end_ts: number;
44
+ start: string;
45
+ end: string;
46
+ statusText: string;
47
+ statusClass: string;
48
+ is_unlimited: boolean;
49
+ is_perm: boolean;
50
+ rewards: ActivityReward[];
51
+ rewards_text: string;
52
+ sort: number;
53
+ time_label: string;
54
+ hide_start?: boolean;
55
+ left_pct?: number;
56
+ width_pct?: number;
57
+ }[][];
58
+ axis_dates: {
59
+ label: string;
60
+ left_pct: number;
61
+ }[];
62
+ now_line: {
63
+ label: string;
64
+ left_pct: number;
65
+ };
66
+ empty: boolean;
67
+ commandHint: string;
68
+ copyright: string;
69
+ };
70
+ buildFallbackText(payload: any): string;
71
+ }
72
+ export {};
@@ -0,0 +1,293 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ActivitiesService = void 0;
4
+ const CHINA_TIMEZONE = 'Asia/Shanghai';
5
+ const DAY_MS = 24 * 60 * 60 * 1000;
6
+ const ACTIVITY_THEMES = ['gold', 'green', 'brown'];
7
+ const LOOKBACK_DAYS = 10;
8
+ const MAX_LOOKAHEAD_DAYS = 50;
9
+ const TRAILING_DAYS_AFTER_LAST_ACTIVITY = 3;
10
+ const MIN_LOOKAHEAD_DAYS = 7;
11
+ const chinaDateFormatter = new Intl.DateTimeFormat('zh-CN', {
12
+ timeZone: CHINA_TIMEZONE,
13
+ month: '2-digit',
14
+ day: '2-digit',
15
+ });
16
+ const chinaDateTimeFormatter = new Intl.DateTimeFormat('zh-CN', {
17
+ timeZone: CHINA_TIMEZONE,
18
+ month: '2-digit',
19
+ day: '2-digit',
20
+ hour: '2-digit',
21
+ minute: '2-digit',
22
+ hour12: false,
23
+ });
24
+ const chinaDatePartsFormatter = new Intl.DateTimeFormat('zh-CN', {
25
+ timeZone: CHINA_TIMEZONE,
26
+ year: 'numeric',
27
+ month: '2-digit',
28
+ day: '2-digit',
29
+ });
30
+ function trimText(value) {
31
+ return String(value ?? '').trim();
32
+ }
33
+ function isPlainObject(value) {
34
+ return !!value && typeof value === 'object' && !Array.isArray(value);
35
+ }
36
+ function clamp(value, min, max) {
37
+ return Math.max(min, Math.min(max, value));
38
+ }
39
+ function formatDateText(timestampMs, withTime = false) {
40
+ const numeric = Number(timestampMs);
41
+ if (!Number.isFinite(numeric) || numeric <= 0)
42
+ return '--';
43
+ const formatter = withTime ? chinaDateTimeFormatter : chinaDateFormatter;
44
+ return formatter.format(new Date(numeric)).replace(/\//g, '.');
45
+ }
46
+ function getChinaDateParts(date = new Date()) {
47
+ const parts = {};
48
+ for (const item of chinaDatePartsFormatter.formatToParts(date)) {
49
+ if (item.type !== 'literal')
50
+ parts[item.type] = Number(item.value);
51
+ }
52
+ return parts;
53
+ }
54
+ function chinaDateToTimestampMs(year, month, day, hour = 0, minute = 0, second = 0) {
55
+ return Date.UTC(year, month - 1, day, hour - 8, minute, second);
56
+ }
57
+ function parseChinaDateText(value = '', endOfDay = false) {
58
+ const text = trimText(value);
59
+ if (!text)
60
+ return 0;
61
+ const localMatch = text.match(/^(\d{4})-(\d{1,2})-(\d{1,2})(?:[ T](\d{1,2}):(\d{1,2})(?::(\d{1,2}))?)?$/);
62
+ if (localMatch) {
63
+ const [, year, month, day, hour, minute, second] = localMatch;
64
+ const hasTime = hour !== undefined;
65
+ return chinaDateToTimestampMs(Number(year), Number(month), Number(day), hasTime ? Number(hour) : endOfDay ? 23 : 0, hasTime ? Number(minute) : endOfDay ? 59 : 0, hasTime ? Number(second || 0) : endOfDay ? 59 : 0);
66
+ }
67
+ const parsed = Date.parse(text);
68
+ return Number.isFinite(parsed) ? parsed : 0;
69
+ }
70
+ function parseActivityTimestampMs(value, fallbackDate = '', endOfDay = false) {
71
+ const numeric = Number(value);
72
+ if (Number.isFinite(numeric) && numeric > 0) {
73
+ return numeric > 10000000000 ? numeric : numeric * 1000;
74
+ }
75
+ return parseChinaDateText(trimText(value) || fallbackDate, endOfDay);
76
+ }
77
+ function extractActivitySource(payload) {
78
+ if (!isPlainObject(payload))
79
+ return [];
80
+ for (const key of ['activityCalendar', 'calendar', 'otherActivities', 'other_activities', 'activities', 'list', 'items']) {
81
+ if (Array.isArray(payload[key]))
82
+ return payload[key];
83
+ }
84
+ if (isPlainObject(payload.data))
85
+ return extractActivitySource(payload.data);
86
+ return [];
87
+ }
88
+ function rewardName(item) {
89
+ if (isPlainObject(item)) {
90
+ return trimText(item.name || item.goods_name || item.pet_name || item.title);
91
+ }
92
+ return trimText(item);
93
+ }
94
+ function extractRewards(activity) {
95
+ const rewards = [];
96
+ for (const [kind, key] of [
97
+ ['prop', 'get_props'],
98
+ ['prop', 'get_extra_props'],
99
+ ['pet', 'get_pets'],
100
+ ['reward', 'rewards'],
101
+ ]) {
102
+ const value = Array.isArray(activity?.[key]) ? activity[key] : [];
103
+ for (const item of value) {
104
+ const name = rewardName(item);
105
+ if (name)
106
+ rewards.push({ kind, name });
107
+ }
108
+ }
109
+ return rewards;
110
+ }
111
+ function buildRewardText(rewards) {
112
+ return rewards.length
113
+ ? rewards.slice(0, 6).map(item => item.name).join('、')
114
+ : '暂无奖励信息';
115
+ }
116
+ function buildActivityTimeLabel(activity) {
117
+ if (activity.is_perm)
118
+ return `${activity.start} 开启`;
119
+ if (activity.hide_start)
120
+ return `截止 ${activity.end}`;
121
+ return `${activity.start} - ${activity.end}`;
122
+ }
123
+ function packLanes(items) {
124
+ const lanes = [];
125
+ for (const item of items) {
126
+ const lane = lanes.find(current => item.start_ts >= current[current.length - 1].end_ts + DAY_MS);
127
+ if (lane) {
128
+ lane.push(item);
129
+ }
130
+ else {
131
+ lanes.push([item]);
132
+ }
133
+ }
134
+ return lanes;
135
+ }
136
+ class ActivitiesService {
137
+ extractActivities(payload) {
138
+ const source = extractActivitySource(payload);
139
+ const nowMs = Date.now();
140
+ return source
141
+ .filter(item => isPlainObject(item) && !item.is_deleted)
142
+ .map((item) => {
143
+ let startTs = parseActivityTimestampMs(item.start_time || item.startAt || item.start_at || item.start_ts, item.start_date || '');
144
+ let endTs = parseActivityTimestampMs(item.end_time || item.endAt || item.end_at || item.end_ts, item.end_date || '', true);
145
+ const isUnlimited = Boolean(item.is_unlimited);
146
+ if (!startTs && !endTs && !isUnlimited)
147
+ return null;
148
+ if (isUnlimited && !endTs) {
149
+ endTs = startTs ? startTs + 365 * DAY_MS : nowMs + 365 * DAY_MS;
150
+ }
151
+ if (!startTs)
152
+ startTs = nowMs;
153
+ if (!endTs || endTs <= startTs)
154
+ endTs = startTs + DAY_MS;
155
+ const isPermanent = isUnlimited || endTs - startTs >= 300 * DAY_MS;
156
+ const rewards = extractRewards(item);
157
+ let statusText = '进行中';
158
+ let statusClass = 'active';
159
+ if (nowMs < startTs) {
160
+ statusText = '未开始';
161
+ statusClass = 'upcoming';
162
+ }
163
+ else if (nowMs > endTs && !isUnlimited) {
164
+ statusText = '已结束';
165
+ statusClass = 'ended';
166
+ }
167
+ else if (isPermanent) {
168
+ statusText = '常驻';
169
+ statusClass = 'permanent';
170
+ }
171
+ return {
172
+ id: trimText(item._id || item.id),
173
+ name: trimText(item.name || item.title) || '未命名活动',
174
+ desc: trimText(item.description || item.desc) || '活动',
175
+ description: trimText(item.description || item.desc),
176
+ cover: trimText(item.cover_url || item.cover || item.pic),
177
+ start_ts: startTs,
178
+ end_ts: endTs,
179
+ start: formatDateText(startTs, true),
180
+ end: formatDateText(endTs, true),
181
+ statusText,
182
+ statusClass,
183
+ is_unlimited: isUnlimited,
184
+ is_perm: isPermanent,
185
+ rewards,
186
+ rewards_text: buildRewardText(rewards),
187
+ sort: Number(item.sort) || 999,
188
+ time_label: formatDateText(startTs) === formatDateText(endTs)
189
+ ? formatDateText(startTs)
190
+ : `${formatDateText(startTs)} ~ ${formatDateText(endTs)}`,
191
+ };
192
+ })
193
+ .filter((item) => !!item)
194
+ .sort((a, b) => Number(a.is_perm) - Number(b.is_perm) || a.start_ts - b.start_ts || a.sort - b.sort);
195
+ }
196
+ buildRenderData(payload) {
197
+ const activities = this.extractActivities(payload);
198
+ const now = new Date();
199
+ const nowMs = now.getTime();
200
+ const today = getChinaDateParts(now);
201
+ const todayMidnightMs = chinaDateToTimestampMs(today.year, today.month, today.day);
202
+ const minTs = todayMidnightMs - LOOKBACK_DAYS * DAY_MS;
203
+ const defaultMaxTs = todayMidnightMs + MAX_LOOKAHEAD_DAYS * DAY_MS;
204
+ const minFutureMaxTs = todayMidnightMs + MIN_LOOKAHEAD_DAYS * DAY_MS;
205
+ const lastActivityEndTs = activities
206
+ .filter(activity => !activity.is_perm)
207
+ .reduce((max, activity) => Math.max(max, activity.end_ts), 0);
208
+ const maxTs = lastActivityEndTs
209
+ ? Math.min(defaultMaxTs, Math.max(minFutureMaxTs, lastActivityEndTs + TRAILING_DAYS_AFTER_LAST_ACTIVITY * DAY_MS))
210
+ : defaultMaxTs;
211
+ const totalDuration = Math.max(maxTs - minTs, 1);
212
+ const normalItems = [];
213
+ const permanentItems = [];
214
+ const keyDates = new Set();
215
+ for (const activity of activities) {
216
+ const item = { ...activity };
217
+ let leftPct = (item.start_ts - minTs) / totalDuration * 100;
218
+ let rightPct = (item.end_ts - minTs) / totalDuration * 100;
219
+ if (item.is_perm)
220
+ rightPct = 100;
221
+ leftPct = clamp(leftPct, 0, 100);
222
+ rightPct = clamp(rightPct, 0, 100);
223
+ let widthPct = Math.max(12.5, rightPct - leftPct);
224
+ if (leftPct + widthPct > 100) {
225
+ leftPct = Math.max(0, 100 - widthPct);
226
+ }
227
+ item.left_pct = Number(leftPct.toFixed(3));
228
+ item.width_pct = Number(widthPct.toFixed(3));
229
+ item.hide_start = item.start_ts < minTs;
230
+ item.time_label = buildActivityTimeLabel(item);
231
+ if (item.is_perm) {
232
+ permanentItems.push(item);
233
+ }
234
+ else {
235
+ normalItems.push(item);
236
+ if (minTs <= item.start_ts && item.start_ts <= maxTs)
237
+ keyDates.add(item.start_ts);
238
+ }
239
+ }
240
+ const lanes = [...packLanes(normalItems), ...packLanes(permanentItems)]
241
+ .map((lane, laneIndex) => lane.map((item, itemIndex) => ({
242
+ ...item,
243
+ theme: ACTIVITY_THEMES[(laneIndex + itemIndex) % ACTIVITY_THEMES.length],
244
+ })));
245
+ const axisDates = [];
246
+ let lastTs = 0;
247
+ for (const ts of [...keyDates].sort((a, b) => a - b)) {
248
+ if (ts - lastTs < 4 * DAY_MS)
249
+ continue;
250
+ lastTs = ts;
251
+ axisDates.push({
252
+ label: formatDateText(ts),
253
+ left_pct: Number(((ts - minTs) / totalDuration * 100).toFixed(3)),
254
+ });
255
+ }
256
+ const nowPct = (nowMs - minTs) / totalDuration * 100;
257
+ const nowLine = nowPct >= 0 && nowPct <= 100
258
+ ? { label: 'TODAY', left_pct: Number(nowPct.toFixed(3)) }
259
+ : null;
260
+ return {
261
+ title: '洛克活动日历',
262
+ subtitle: `显示 ${formatDateText(nowMs)} 前 ${LOOKBACK_DAYS} 天至 ${formatDateText(maxTs)} 活动`,
263
+ activity_count: activities.length,
264
+ activities,
265
+ lanes,
266
+ axis_dates: axisDates,
267
+ now_line: nowLine,
268
+ empty: activities.length === 0,
269
+ commandHint: '发送 洛克日历 / 洛克活动',
270
+ copyright: 'Koishi & WeGame Roco Kingdom Plugin',
271
+ };
272
+ }
273
+ buildFallbackText(payload) {
274
+ const activities = this.extractActivities(payload);
275
+ if (!activities.length) {
276
+ return ['洛克活动日历', '', '暂无进行中的活动。'].join('\n');
277
+ }
278
+ const lines = ['洛克活动日历', `当前共 ${activities.length} 个活动`, ''];
279
+ activities.forEach((activity, index) => {
280
+ lines.push(`${index + 1}. ${activity.name}`);
281
+ lines.push(`状态:${activity.statusText}`);
282
+ lines.push(`时间:${activity.time_label}`);
283
+ if (activity.description)
284
+ lines.push(`说明:${activity.description}`);
285
+ if (activity.rewards.length)
286
+ lines.push(`奖励:${activity.rewards.map(item => item.name).join('、')}`);
287
+ if (index !== activities.length - 1)
288
+ lines.push('');
289
+ });
290
+ return lines.join('\n');
291
+ }
292
+ }
293
+ exports.ActivitiesService = ActivitiesService;
package/lib/client.d.ts CHANGED
@@ -20,6 +20,7 @@ export declare class RocomClient {
20
20
  private logRequestFailureDetails;
21
21
  private get;
22
22
  private post;
23
+ private scopedParams;
23
24
  private delete;
24
25
  private requestWithStatus;
25
26
  private requestIngameWithFallback;
@@ -32,6 +33,8 @@ export declare class RocomClient {
32
33
  createBinding(ctx: Context, fwToken: string, userIdentifier: string): Promise<any>;
33
34
  refreshBinding(ctx: Context, bindingId: string, userIdentifier: string): Promise<any>;
34
35
  deleteBinding(ctx: Context, bindingId: string, userIdentifier: string): Promise<boolean>;
36
+ getAccounts(ctx: Context, userIdentifier?: string, accountType?: number): Promise<any>;
37
+ bindUid(ctx: Context, uid: string, userIdentifier?: string): Promise<any>;
35
38
  getRole(ctx: Context, fwToken: string, accountType?: number, userIdentifier?: string): Promise<any>;
36
39
  getEvaluation(ctx: Context, fwToken: string, userIdentifier?: string): Promise<any>;
37
40
  getLastError(defaultMessage?: string): string;
@@ -47,10 +50,38 @@ export declare class RocomClient {
47
50
  getLineupList(ctx: Context, fwToken: string, pageNo?: number, category?: string, userIdentifier?: string): Promise<any>;
48
51
  getExchangePosters(ctx: Context, fwToken: string, pageNo?: number, userIdentifier?: string): Promise<any>;
49
52
  getMerchantInfo(ctx: Context, refresh?: boolean): Promise<any>;
50
- queryPetSize(ctx: Context, diameter: number, weight: number): Promise<any>;
53
+ queryPetSize(ctx: Context, diameter: number, weight: number, sameRideEgg?: boolean, userIdentifier?: string): Promise<any>;
54
+ getActivitiesInfo(ctx: Context, refresh?: boolean, userIdentifier?: string): Promise<any>;
55
+ syncConfig(ctx: Context, userIdentifier?: string): Promise<any>;
56
+ getAnnouncementList(ctx: Context, params?: {
57
+ category_id?: number | string;
58
+ page?: number;
59
+ limit?: number;
60
+ order?: string;
61
+ }, userIdentifier?: string): Promise<any>;
62
+ getLatestAnnouncement(ctx: Context, params?: {
63
+ category_id?: number | string;
64
+ order?: string;
65
+ }, userIdentifier?: string): Promise<any>;
66
+ getAnnouncementDetail(ctx: Context, threadId: number | string, userIdentifier?: string): Promise<any>;
67
+ getEggSearch(ctx: Context, height: number, weight: number, pageNo?: number, pageSize?: number, userIdentifier?: string): Promise<any>;
68
+ getEggGroups(ctx: Context, userIdentifier?: string): Promise<any>;
69
+ getEggGroupPets(ctx: Context, groupIds: string | number[], matchMode?: 'any' | 'all', pageNo?: number, pageSize?: number, userIdentifier?: string): Promise<any>;
70
+ getEggPetGroups(ctx: Context, query: string, limit?: number, userIdentifier?: string): Promise<any>;
71
+ getEggExchanges(ctx: Context, params?: Record<string, any>, userIdentifier?: string): Promise<any>;
72
+ postEggExchange(ctx: Context, data: Record<string, any>, userIdentifier?: string): Promise<any>;
73
+ getMyEggExchanges(ctx: Context, params?: Record<string, any>, userIdentifier?: string): Promise<any>;
74
+ getEggExchangeReviewStatus(ctx: Context, postId: string | number, userIdentifier?: string): Promise<any>;
75
+ closeEggExchange(ctx: Context, postId: string | number, closeReason?: string, userIdentifier?: string): Promise<any>;
76
+ createEggExchangeSubscription(ctx: Context, filters: Record<string, any>, userIdentifier?: string): Promise<any>;
77
+ getEggExchangeSubscriptions(ctx: Context, userIdentifier?: string): Promise<any>;
78
+ deleteEggExchangeSubscription(ctx: Context, subscriptionId: string | number, userIdentifier?: string): Promise<any>;
79
+ getEggExchangeEvents(ctx: Context, subscriptionId: string | number, afterEventId?: string, limit?: number, userIdentifier?: string): Promise<any>;
51
80
  ingameHomeInfo(ctx: Context, uid: string, waitMs?: number): Promise<any>;
52
81
  ingameMerchantInfo(ctx: Context, shopId: string | number): Promise<any>;
53
82
  getFriendship(ctx: Context, fwToken: string, userIds: string, userIdentifier?: string): Promise<any>;
83
+ getStudentState(ctx: Context, fwToken: string, accountType?: number, userIdentifier?: string): Promise<any>;
84
+ getStudentPerks(ctx: Context, fwToken: string, area?: number, accountType?: number, userIdentifier?: string): Promise<any>;
54
85
  searchWikiPet(ctx: Context, query: string, limit?: number): Promise<any>;
55
86
  searchWikiSkill(ctx: Context, query: string, limit?: number): Promise<any>;
56
87
  getWikiPetDetail(ctx: Context, options: {
package/lib/client.js CHANGED
@@ -244,13 +244,19 @@ class RocomClient {
244
244
  return null;
245
245
  }
246
246
  }
247
- async delete(ctx, path, headers) {
247
+ scopedParams(params = {}, userIdentifier = '') {
248
+ const result = { ...params };
249
+ if (userIdentifier)
250
+ result.user_identifier = this.sanitizeUid(userIdentifier);
251
+ return result;
252
+ }
253
+ async delete(ctx, path, headers, params) {
248
254
  try {
249
- const resp = await ctx.http("DELETE", `${this.baseUrl}${path}`, { headers, timeout: this.timeout });
255
+ const resp = await ctx.http("DELETE", `${this.baseUrl}${path}`, { headers, params, timeout: this.timeout });
250
256
  if (resp?.code !== 0) {
251
257
  this.setLastError(resp?.message || '接口返回异常');
252
258
  logger.warn(path + ' error: ' + (resp?.message || 'unknown'));
253
- this.logRequestFailureDetails('DELETE', path, headers, undefined, undefined, resp);
259
+ this.logRequestFailureDetails('DELETE', path, headers, params, undefined, resp);
254
260
  return null;
255
261
  }
256
262
  return resp?.data ?? {};
@@ -258,7 +264,7 @@ class RocomClient {
258
264
  catch (e) {
259
265
  const message = this.formatHttpError(e);
260
266
  this.setLastError(message);
261
- this.logRequestFailureDetails('DELETE', path, headers, undefined, undefined, e);
267
+ this.logRequestFailureDetails('DELETE', path, headers, params, undefined, e);
262
268
  const err = e;
263
269
  if (err?.response) {
264
270
  logger.warn('DELETE ' + path + ' failed: ' + message);
@@ -384,6 +390,21 @@ class RocomClient {
384
390
  const res = await this.delete(ctx, `/api/v1/user/bindings/${bindingId}`, this.wegameHeaders('', userIdentifier));
385
391
  return res !== null;
386
392
  }
393
+ async getAccounts(ctx, userIdentifier = '', accountType) {
394
+ const params = this.scopedParams({}, userIdentifier);
395
+ if (accountType !== undefined)
396
+ params.account_type = accountType;
397
+ return this.get(ctx, '/api/v1/games/rocom/accounts', this.wegameHeaders('', userIdentifier, 'bot', 'koishi'), params);
398
+ }
399
+ async bindUid(ctx, uid, userIdentifier = '') {
400
+ const sanitizedUid = this.sanitizeUid(uid);
401
+ if (!/^\d+$/.test(sanitizedUid)) {
402
+ this.setLastError('UID 必须为纯数字');
403
+ return null;
404
+ }
405
+ const params = this.scopedParams({ client_type: 'bot', client_id: 'koishi' }, userIdentifier);
406
+ return this.post(ctx, '/api/v1/games/rocom/uid/bind', this.wegameHeaders('', userIdentifier, 'bot', 'koishi'), { uid: sanitizedUid }, params);
407
+ }
387
408
  // 洛克王国游戏数据接口
388
409
  async getRole(ctx, fwToken, accountType, userIdentifier = '') {
389
410
  const params = {};
@@ -477,8 +498,69 @@ class RocomClient {
477
498
  async getMerchantInfo(ctx, refresh = false) {
478
499
  return this.get(ctx, '/api/v1/games/rocom/merchant/info', this.wegameHeaders(), { refresh: refresh ? 'true' : 'false' });
479
500
  }
480
- async queryPetSize(ctx, diameter, weight) {
481
- return this.get(ctx, '/api/v1/games/rocom/pet/size-query', this.wegameHeaders(), { diameter, weight });
501
+ async queryPetSize(ctx, diameter, weight, sameRideEgg = false, userIdentifier = '') {
502
+ const params = this.scopedParams({ diameter, weight }, userIdentifier);
503
+ if (sameRideEgg)
504
+ params.sameRideEgg = 1;
505
+ return this.get(ctx, '/api/v1/games/rocom/pet/size-query', this.wegameHeaders('', userIdentifier, 'bot', 'koishi'), params);
506
+ }
507
+ async getActivitiesInfo(ctx, refresh = false, userIdentifier = '') {
508
+ return this.get(ctx, '/api/v1/games/rocom/activities/info', this.wegameHeaders('', userIdentifier, 'bot', 'koishi'), this.scopedParams({ refresh: refresh ? 'true' : 'false' }, userIdentifier));
509
+ }
510
+ async syncConfig(ctx, userIdentifier = '') {
511
+ return this.post(ctx, '/api/v1/games/rocom/config/sync', this.wegameHeaders('', userIdentifier, 'bot', 'koishi'), {}, this.scopedParams({}, userIdentifier));
512
+ }
513
+ async getAnnouncementList(ctx, params = {}, userIdentifier = '') {
514
+ return this.get(ctx, '/api/v1/games/rocom/announcement/list', this.wegameHeaders('', userIdentifier, 'bot', 'koishi'), this.scopedParams(params, userIdentifier));
515
+ }
516
+ async getLatestAnnouncement(ctx, params = {}, userIdentifier = '') {
517
+ return this.get(ctx, '/api/v1/games/rocom/announcement/latest', this.wegameHeaders('', userIdentifier, 'bot', 'koishi'), this.scopedParams(params, userIdentifier));
518
+ }
519
+ async getAnnouncementDetail(ctx, threadId, userIdentifier = '') {
520
+ return this.get(ctx, '/api/v1/games/rocom/announcement/detail', this.wegameHeaders('', userIdentifier, 'bot', 'koishi'), this.scopedParams({ thread_id: threadId }, userIdentifier));
521
+ }
522
+ async getEggSearch(ctx, height, weight, pageNo = 1, pageSize = 20, userIdentifier = '') {
523
+ return this.get(ctx, '/api/v1/games/rocom/egg/search', this.wegameHeaders('', userIdentifier, 'bot', 'koishi'), this.scopedParams({ height, weight, page_no: pageNo, page_size: pageSize }, userIdentifier));
524
+ }
525
+ async getEggGroups(ctx, userIdentifier = '') {
526
+ return this.get(ctx, '/api/v1/games/rocom/egg/groups', this.wegameHeaders('', userIdentifier, 'bot', 'koishi'), this.scopedParams({}, userIdentifier));
527
+ }
528
+ async getEggGroupPets(ctx, groupIds, matchMode = 'any', pageNo = 1, pageSize = 20, userIdentifier = '') {
529
+ const normalizedGroupIds = Array.isArray(groupIds) ? groupIds.join(',') : String(groupIds || '');
530
+ return this.get(ctx, '/api/v1/games/rocom/egg/group-pets', this.wegameHeaders('', userIdentifier, 'bot', 'koishi'), this.scopedParams({ group_ids: normalizedGroupIds, match_mode: matchMode, page_no: pageNo, page_size: pageSize }, userIdentifier));
531
+ }
532
+ async getEggPetGroups(ctx, query, limit = 20, userIdentifier = '') {
533
+ return this.get(ctx, '/api/v1/games/rocom/egg/pet-groups', this.wegameHeaders('', userIdentifier, 'bot', 'koishi'), this.scopedParams({ q: query, limit }, userIdentifier));
534
+ }
535
+ async getEggExchanges(ctx, params = {}, userIdentifier = '') {
536
+ return this.get(ctx, '/api/v1/games/rocom/community/egg-exchanges', this.wegameHeaders('', userIdentifier, 'bot', 'koishi'), this.scopedParams(params, userIdentifier));
537
+ }
538
+ async postEggExchange(ctx, data, userIdentifier = '') {
539
+ return this.post(ctx, '/api/v1/games/rocom/community/egg-exchanges', this.wegameHeaders('', userIdentifier, 'bot', 'koishi'), data, this.scopedParams({}, userIdentifier));
540
+ }
541
+ async getMyEggExchanges(ctx, params = {}, userIdentifier = '') {
542
+ return this.get(ctx, '/api/v1/games/rocom/community/egg-exchanges/my', this.wegameHeaders('', userIdentifier, 'bot', 'koishi'), this.scopedParams(params, userIdentifier));
543
+ }
544
+ async getEggExchangeReviewStatus(ctx, postId, userIdentifier = '') {
545
+ return this.get(ctx, `/api/v1/games/rocom/community/egg-exchanges/${encodeURIComponent(String(postId))}/review-status`, this.wegameHeaders('', userIdentifier, 'bot', 'koishi'), this.scopedParams({}, userIdentifier));
546
+ }
547
+ async closeEggExchange(ctx, postId, closeReason = 'cancel', userIdentifier = '') {
548
+ return this.post(ctx, `/api/v1/games/rocom/community/egg-exchanges/${encodeURIComponent(String(postId))}/close`, this.wegameHeaders('', userIdentifier, 'bot', 'koishi'), { close_reason: closeReason }, this.scopedParams({}, userIdentifier));
549
+ }
550
+ async createEggExchangeSubscription(ctx, filters, userIdentifier = '') {
551
+ return this.post(ctx, '/api/v1/games/rocom/community/egg-exchange-subscriptions', this.wegameHeaders('', userIdentifier, 'bot', 'koishi'), { filters }, this.scopedParams({}, userIdentifier));
552
+ }
553
+ async getEggExchangeSubscriptions(ctx, userIdentifier = '') {
554
+ return this.get(ctx, '/api/v1/games/rocom/community/egg-exchange-subscriptions', this.wegameHeaders('', userIdentifier, 'bot', 'koishi'), this.scopedParams({}, userIdentifier));
555
+ }
556
+ async deleteEggExchangeSubscription(ctx, subscriptionId, userIdentifier = '') {
557
+ return this.delete(ctx, `/api/v1/games/rocom/community/egg-exchange-subscriptions/${encodeURIComponent(String(subscriptionId))}`, this.wegameHeaders('', userIdentifier, 'bot', 'koishi'), this.scopedParams({}, userIdentifier));
558
+ }
559
+ async getEggExchangeEvents(ctx, subscriptionId, afterEventId = '', limit = 50, userIdentifier = '') {
560
+ const params = { subscription_id: subscriptionId, limit };
561
+ if (afterEventId)
562
+ params.after_event_id = afterEventId;
563
+ return this.get(ctx, '/api/v1/games/rocom/community/egg-exchange-events', this.wegameHeaders('', userIdentifier, 'bot', 'koishi'), this.scopedParams(params, userIdentifier));
482
564
  }
483
565
  async ingameHomeInfo(ctx, uid, waitMs = 5000) {
484
566
  const sanitizedUid = this.sanitizeUid(uid);
@@ -516,6 +598,20 @@ class RocomClient {
516
598
  async getFriendship(ctx, fwToken, userIds, userIdentifier = '') {
517
599
  return this.get(ctx, '/api/v1/games/rocom/social/friendship', this.rocomHeaders(fwToken, userIdentifier), { user_ids: userIds });
518
600
  }
601
+ async getStudentState(ctx, fwToken, accountType, userIdentifier = '') {
602
+ const params = {};
603
+ if (accountType !== undefined)
604
+ params.account_type = accountType;
605
+ return this.get(ctx, '/api/v1/games/rocom/activity/student-state', this.rocomHeaders(fwToken, userIdentifier), params);
606
+ }
607
+ async getStudentPerks(ctx, fwToken, area, accountType, userIdentifier = '') {
608
+ const params = {};
609
+ if (area !== undefined)
610
+ params.area = area;
611
+ if (accountType !== undefined)
612
+ params.account_type = accountType;
613
+ return this.get(ctx, '/api/v1/games/rocom/activity/perks', this.rocomHeaders(fwToken, userIdentifier), params);
614
+ }
519
615
  async searchWikiPet(ctx, query, limit = 10) {
520
616
  return this.get(ctx, '/api/v1/games/rocom/pet/list', this.wegameHeaders(), { q: query, page_size: limit });
521
617
  }
@@ -41,6 +41,7 @@ function formatLoginType(loginType) {
41
41
  qq: 'QQ',
42
42
  wechat: '微信',
43
43
  manual: '手动导入',
44
+ uid: 'UID 绑定',
44
45
  };
45
46
  return typeMap[loginType] || loginType || '未知';
46
47
  }
@@ -191,6 +192,49 @@ function register(deps) {
191
192
  const png = await deps.renderer.renderHtml(ctx, 'bind-list', data);
192
193
  await (0, send_image_1.sendImageWithFallback)(session, png, fallbackLines.join('\n'), 'account:bind-list', deps.config);
193
194
  });
195
+ ctx.command('洛克').subcommand('.绑定UID <uid:string>', '绑定洛克 UID')
196
+ .alias('洛克绑定UID')
197
+ .alias('绑定UID')
198
+ .action(async ({ session }, uid) => {
199
+ const userId = session.userId;
200
+ const targetUid = String(uid || '').trim();
201
+ if (!/^\d{4,20}$/.test(targetUid))
202
+ return '用法:洛克.绑定UID <UID>,UID 必须为 4 到 20 位数字。';
203
+ const res = await client.bindUid(ctx, targetUid, userId);
204
+ if (!res)
205
+ return `绑定 UID 失败:${client.getLastErrorBrief()}`;
206
+ const bindingPayload = res?.binding || {};
207
+ const bindingId = String(bindingPayload?.id || bindingPayload?.binding_id || `uid:${targetUid}`).trim();
208
+ const frameworkToken = String(res?.frameworkToken || res?.framework_token || '').trim();
209
+ const binding = {
210
+ binding_id: bindingId,
211
+ login_type: 'uid',
212
+ role_id: targetUid,
213
+ nickname: `UID ${targetUid}`,
214
+ bind_time: Date.now(),
215
+ is_primary: true,
216
+ };
217
+ const existing = userMgr.getUserBindings(userId)
218
+ .filter(item => item.binding_id !== bindingId && item.role_id !== targetUid)
219
+ .map(item => ({ ...item, is_primary: false }));
220
+ userMgr.saveUserBindings(userId, [...existing, binding]);
221
+ if (frameworkToken) {
222
+ await (0, role_token_1.upsertRoleToken)(ctx, {
223
+ userId,
224
+ fwt: frameworkToken,
225
+ bindingId,
226
+ roleId: targetUid,
227
+ loginType: 'uid',
228
+ });
229
+ }
230
+ const source = String(bindingPayload?.source || '').trim();
231
+ return [
232
+ 'UID 绑定成功。',
233
+ `UID:${targetUid}`,
234
+ source ? `来源:${source}` : '',
235
+ frameworkToken ? '已生成凭证,后续 ingame 查询会优先使用该账号。' : '已保存为本地主账号,可用于默认 UID 查询。',
236
+ ].filter(Boolean).join('\n');
237
+ });
194
238
  ctx.command('洛克').subcommand('.切换 <index:number>', '切换主账号')
195
239
  .alias('洛克切换')
196
240
  .action(async ({ session }, index) => {
@@ -0,0 +1,12 @@
1
+ import { PluginDeps } from '../types';
2
+ export declare function createCommunityHandlers(deps: PluginDeps): {
3
+ queryExchangeList: ({ session }: any, page?: number) => Promise<string>;
4
+ postExchange: ({ session }: any, args?: string) => Promise<string>;
5
+ queryMyPosts: ({ session }: any) => Promise<string>;
6
+ closePost: ({ session }: any, postIdArg: string, reason?: string) => Promise<string>;
7
+ reviewStatus: ({ session }: any, postIdArg: string) => Promise<string>;
8
+ subscribeEgg: ({ session }: any, filtersText?: string) => Promise<string>;
9
+ listSubscriptions: ({ session }: any) => Promise<string>;
10
+ unsubscribeEgg: ({ session }: any, subscriptionId?: string) => Promise<string>;
11
+ queryEvents: ({ session }: any, subscriptionId: string, afterEventId?: string) => Promise<string>;
12
+ };