koishi-plugin-rocom 1.0.9 → 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,323 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createCommunityHandlers = createCommunityHandlers;
4
+ const koishi_1 = require("koishi");
5
+ const logger = new koishi_1.Logger('rocom-community');
6
+ function trimText(value) {
7
+ return String(value ?? '').trim();
8
+ }
9
+ function formatPostTime(value) {
10
+ const text = trimText(value);
11
+ if (!text)
12
+ return '未知时间';
13
+ const date = new Date(text);
14
+ if (Number.isNaN(date.getTime()))
15
+ return text;
16
+ const pad = (num) => String(num).padStart(2, '0');
17
+ return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
18
+ }
19
+ function pageNumber(value, defaultValue = 1) {
20
+ const num = Number(value);
21
+ if (!Number.isFinite(num) || num < 1)
22
+ return defaultValue;
23
+ return Math.min(50, Math.floor(num));
24
+ }
25
+ function normalizeItems(payload) {
26
+ if (Array.isArray(payload?.items))
27
+ return payload.items;
28
+ if (Array.isArray(payload?.posts))
29
+ return payload.posts;
30
+ if (Array.isArray(payload?.data?.items))
31
+ return payload.data.items;
32
+ return [];
33
+ }
34
+ function postId(item) {
35
+ return trimText(item?.post_id || item?.id || item?.postId);
36
+ }
37
+ function buildExchangeListText(data, pageNo) {
38
+ const items = normalizeItems(data);
39
+ if (!items.length)
40
+ return pageNo > 1 ? '该页没有更多换蛋帖了。' : '当前换蛋广场暂无帖子。';
41
+ const lines = ['换蛋广场'];
42
+ for (const [index, item] of items.entries()) {
43
+ const pinned = trimText(item?.pinned_until) ? '[置顶]' : '';
44
+ const roleId = trimText(item?.id) || '-';
45
+ const have = trimText(item?.have_text) || '未填写';
46
+ const want = trimText(item?.want_text) || '未填写';
47
+ const note = trimText(item?.want_note);
48
+ const remark = trimText(item?.remark);
49
+ const id = postId(item);
50
+ lines.push(`${index + 1}. ${pinned}[${id || roleId}] 我有:${have} -> 想要:${want}`);
51
+ if (note)
52
+ lines.push(` 补充:${note}`);
53
+ if (remark)
54
+ lines.push(` 备注:${remark}`);
55
+ lines.push(` 学号:${roleId} | ${formatPostTime(item?.created_at)}`);
56
+ }
57
+ const total = Number(data?.total);
58
+ const totalPages = Number(data?.total_pages);
59
+ const current = Number(data?.page_no) || pageNo;
60
+ if (Number.isFinite(totalPages) && totalPages > 1) {
61
+ lines.push(`第 ${current}/${totalPages} 页,共 ${Number.isFinite(total) ? total : items.length} 条`);
62
+ }
63
+ lines.push('翻页:换蛋广场 <页码>');
64
+ return lines.join('\n');
65
+ }
66
+ function statusLabel(value) {
67
+ const map = {
68
+ active: '生效中',
69
+ closed: '已关闭',
70
+ deleted: '已删除',
71
+ };
72
+ return map[value] || value || '未知';
73
+ }
74
+ function reviewLabel(value) {
75
+ const map = {
76
+ pending: '待审核',
77
+ manual_pending: '人工审核中',
78
+ approved: '已通过',
79
+ rejected: '已拒绝',
80
+ };
81
+ return map[value] || value || '未知';
82
+ }
83
+ function buildMyPostsText(data) {
84
+ const items = normalizeItems(data);
85
+ if (!items.length)
86
+ return '你当前没有换蛋帖。';
87
+ const lines = ['我的换蛋帖'];
88
+ for (const [index, item] of items.entries()) {
89
+ const have = trimText(item?.have_text) || '未填写';
90
+ const want = trimText(item?.want_text) || '未填写';
91
+ const id = postId(item);
92
+ lines.push(`${index + 1}. [${id || '-'}] 我有:${have} -> 想要:${want}`);
93
+ lines.push(` 状态:${statusLabel(item?.status)} | 审核:${reviewLabel(item?.review_status)} | ${formatPostTime(item?.created_at)}`);
94
+ }
95
+ lines.push('关闭帖子:关闭换蛋帖 <帖子ID> [traded/cancel]');
96
+ return lines.join('\n');
97
+ }
98
+ function parsePostArgs(raw) {
99
+ const text = trimText(raw);
100
+ const match = text.match(/^(\d{4,20})\s+(.+)$/);
101
+ if (!match) {
102
+ throw new Error('格式:发布换蛋帖 <学号> <我有>|<想要>|[补充]|[备注]');
103
+ }
104
+ const parts = match[2].split(/[||]/).map(part => trimText(part)).filter(Boolean);
105
+ if (parts.length < 2) {
106
+ throw new Error('请用 | 分隔“我有”和“想要”,例如:发布换蛋帖 470557585 雪怪果实|上岸蛙|固执|全天在线');
107
+ }
108
+ return {
109
+ id: match[1],
110
+ have_text: parts[0],
111
+ want_text: parts[1],
112
+ want_note: parts[2] || '',
113
+ remark: parts.slice(3).join(' | '),
114
+ };
115
+ }
116
+ function parseSubscribeFilters(raw) {
117
+ const filters = {};
118
+ const keyMap = {
119
+ '想要': 'want_text',
120
+ '我有': 'have_text',
121
+ '补充': 'want_note',
122
+ '学号': 'id',
123
+ '搜索': 'q',
124
+ };
125
+ for (const token of trimText(raw).split(/\s+/).filter(Boolean)) {
126
+ const match = token.match(/^(.+?)[::](.+)$/);
127
+ if (match) {
128
+ const key = keyMap[trimText(match[1])] || trimText(match[1]);
129
+ const value = trimText(match[2]);
130
+ if (key && value)
131
+ filters[key] = value;
132
+ continue;
133
+ }
134
+ filters.want_text = filters.want_text ? `${filters.want_text} ${token}` : token;
135
+ }
136
+ return filters;
137
+ }
138
+ function buildSubscriptionsText(data) {
139
+ const subscriptions = Array.isArray(data?.subscriptions) ? data.subscriptions : [];
140
+ if (!subscriptions.length)
141
+ return '当前没有换蛋订阅。';
142
+ const lines = ['换蛋订阅列表'];
143
+ subscriptions.forEach((subscription, index) => {
144
+ const id = trimText(subscription?.subscription_id);
145
+ const filters = subscription?.filters && typeof subscription.filters === 'object'
146
+ ? Object.entries(subscription.filters).map(([key, value]) => `${key}=${value}`).join('、')
147
+ : '无筛选';
148
+ lines.push(`${index + 1}. [${id || '-'}] ${filters}`);
149
+ lines.push(` 状态:${subscription?.status || '未知'} | ${formatPostTime(subscription?.updated_at || subscription?.created_at)}`);
150
+ });
151
+ return lines.join('\n');
152
+ }
153
+ function buildEventsText(data) {
154
+ const items = Array.isArray(data?.items) ? data.items : [];
155
+ if (!items.length)
156
+ return `暂未拉取到新的换蛋事件。next_event_id=${data?.next_event_id ?? 0}`;
157
+ const lines = [`换蛋订阅事件 next_event_id=${data?.next_event_id ?? '-'}`];
158
+ for (const item of items) {
159
+ const post = item?.post || {};
160
+ lines.push(`[${item?.event_id ?? '-'}] ${formatPostTime(item?.created_at)} 我有:${trimText(post?.have_text) || '未填写'} -> 想要:${trimText(post?.want_text) || '未填写'}`);
161
+ if (post?.want_note)
162
+ lines.push(` 补充:${post.want_note}`);
163
+ if (postId(post))
164
+ lines.push(` 帖子ID:${postId(post)}`);
165
+ }
166
+ if (data?.has_more)
167
+ lines.push('还有更多事件,可继续使用 next_event_id 拉取。');
168
+ return lines.join('\n');
169
+ }
170
+ function ensureGroupAdmin(session, adminUserIds) {
171
+ if (!session?.guildId)
172
+ return '';
173
+ if (adminUserIds.includes(session?.userId || ''))
174
+ return '';
175
+ return '群聊中仅 Bot 管理员可以管理换蛋订阅。';
176
+ }
177
+ function createCommunityHandlers(deps) {
178
+ const { ctx, client, config } = deps;
179
+ const queryExchangeList = async ({ session }, page = 1) => {
180
+ const pageNo = pageNumber(page);
181
+ const data = await client.getEggExchanges(ctx, { page_no: pageNo, page_size: 10 }, session?.userId || '');
182
+ if (!data)
183
+ return `换蛋广场查询失败:${client.getLastErrorBrief()}`;
184
+ return buildExchangeListText(data, pageNo);
185
+ };
186
+ const postExchange = async ({ session }, args = '') => {
187
+ try {
188
+ const payload = parsePostArgs(args);
189
+ const data = await client.postEggExchange(ctx, payload, session?.userId || '');
190
+ if (!data)
191
+ return `发布换蛋帖失败:${client.getLastErrorBrief()}`;
192
+ const id = trimText(data?.post_id || data?.id || data?.post?.post_id);
193
+ const similarPosts = Array.isArray(data?.similar_posts) ? data.similar_posts : [];
194
+ const lines = [
195
+ '换蛋帖发布成功,帖子已进入审核。',
196
+ `学号:${payload.id}`,
197
+ `我有:${payload.have_text}`,
198
+ `想要:${payload.want_text}`,
199
+ ];
200
+ if (payload.want_note)
201
+ lines.push(`补充:${payload.want_note}`);
202
+ if (payload.remark)
203
+ lines.push(`备注:${payload.remark}`);
204
+ if (id)
205
+ lines.push(`帖子ID:${id}`);
206
+ if (similarPosts.length) {
207
+ lines.push('相似帖子:');
208
+ similarPosts.slice(0, 3).forEach((post) => {
209
+ lines.push(` [${postId(post) || '-'}] 我有:${trimText(post?.have_text)} -> 想要:${trimText(post?.want_text)}`);
210
+ });
211
+ }
212
+ return lines.join('\n');
213
+ }
214
+ catch (error) {
215
+ logger.warn(`发布换蛋帖失败: ${error}`);
216
+ return `发布换蛋帖失败:${error?.message || error}`;
217
+ }
218
+ };
219
+ const queryMyPosts = async ({ session }) => {
220
+ const data = await client.getMyEggExchanges(ctx, { page_no: 1, page_size: 20 }, session?.userId || '');
221
+ if (!data)
222
+ return `查询我的换蛋帖失败:${client.getLastErrorBrief()}`;
223
+ return buildMyPostsText(data);
224
+ };
225
+ const closePost = async ({ session }, postIdArg, reason = 'cancel') => {
226
+ const id = trimText(postIdArg);
227
+ if (!id)
228
+ return '请提供帖子 ID。用法:关闭换蛋帖 <帖子ID> [traded/cancel]';
229
+ const normalizedReason = reason === 'traded' ? 'traded' : 'cancel';
230
+ const data = await client.closeEggExchange(ctx, id, normalizedReason, session?.userId || '');
231
+ if (!data)
232
+ return `关闭换蛋帖失败:${client.getLastErrorBrief()}`;
233
+ return `换蛋帖 ${id} 已关闭,原因:${normalizedReason === 'traded' ? '成交关闭' : '取消关闭'}。`;
234
+ };
235
+ const reviewStatus = async ({ session }, postIdArg) => {
236
+ const id = trimText(postIdArg);
237
+ if (!id)
238
+ return '请提供帖子 ID。用法:换蛋审核 <帖子ID>';
239
+ const data = await client.getEggExchangeReviewStatus(ctx, id, session?.userId || '');
240
+ if (!data)
241
+ return `查询换蛋审核状态失败:${client.getLastErrorBrief()}`;
242
+ const post = data?.post || data;
243
+ return [
244
+ `帖子ID:${id}`,
245
+ `状态:${statusLabel(post?.status)}`,
246
+ `审核:${reviewLabel(post?.review_status)}`,
247
+ post?.review_reason ? `原因:${post.review_reason}` : '',
248
+ ].filter(Boolean).join('\n');
249
+ };
250
+ const subscribeEgg = async ({ session }, filtersText = '') => {
251
+ const adminError = ensureGroupAdmin(session, config.adminUserIds);
252
+ if (adminError)
253
+ return adminError;
254
+ const filters = parseSubscribeFilters(filtersText);
255
+ if (!Object.keys(filters).length) {
256
+ return '用法:订阅换蛋 想要:上岸蛙 补充:固执\n支持筛选键:想要、我有、补充、学号、搜索';
257
+ }
258
+ const data = await client.createEggExchangeSubscription(ctx, filters, session?.userId || '');
259
+ if (!data)
260
+ return `订阅换蛋失败:${client.getLastErrorBrief()}`;
261
+ const subscription = data?.subscription || data;
262
+ const id = trimText(subscription?.subscription_id);
263
+ const filterText = Object.entries(filters).map(([key, value]) => `${key}=${value}`).join('、');
264
+ return [
265
+ '换蛋订阅创建成功。',
266
+ `筛选条件:${filterText}`,
267
+ id ? `订阅ID:${id}` : '',
268
+ '可使用“换蛋事件 <订阅ID>”手动拉取新通过帖子。',
269
+ ].filter(Boolean).join('\n');
270
+ };
271
+ const listSubscriptions = async ({ session }) => {
272
+ const data = await client.getEggExchangeSubscriptions(ctx, session?.userId || '');
273
+ if (!data)
274
+ return `查询换蛋订阅失败:${client.getLastErrorBrief()}`;
275
+ return buildSubscriptionsText(data);
276
+ };
277
+ const unsubscribeEgg = async ({ session }, subscriptionId = '') => {
278
+ const adminError = ensureGroupAdmin(session, config.adminUserIds);
279
+ if (adminError)
280
+ return adminError;
281
+ const id = trimText(subscriptionId);
282
+ if (id) {
283
+ const ok = await client.deleteEggExchangeSubscription(ctx, id, session?.userId || '');
284
+ return ok ? `已取消换蛋订阅 ${id}。` : `取消换蛋订阅失败:${client.getLastErrorBrief()}`;
285
+ }
286
+ const data = await client.getEggExchangeSubscriptions(ctx, session?.userId || '');
287
+ if (!data)
288
+ return `查询换蛋订阅失败:${client.getLastErrorBrief()}`;
289
+ const subscriptions = Array.isArray(data?.subscriptions) ? data.subscriptions : [];
290
+ if (!subscriptions.length)
291
+ return '当前没有换蛋订阅。';
292
+ let deleted = 0;
293
+ for (const subscription of subscriptions) {
294
+ const subId = trimText(subscription?.subscription_id);
295
+ if (!subId)
296
+ continue;
297
+ const ok = await client.deleteEggExchangeSubscription(ctx, subId, session?.userId || '');
298
+ if (ok)
299
+ deleted++;
300
+ }
301
+ return deleted ? `已取消 ${deleted} 个换蛋订阅。` : '没有成功取消任何换蛋订阅。';
302
+ };
303
+ const queryEvents = async ({ session }, subscriptionId, afterEventId = '') => {
304
+ const id = trimText(subscriptionId);
305
+ if (!id)
306
+ return '请提供订阅 ID。用法:换蛋事件 <订阅ID> [after_event_id]';
307
+ const data = await client.getEggExchangeEvents(ctx, id, afterEventId, 20, session?.userId || '');
308
+ if (!data)
309
+ return `拉取换蛋事件失败:${client.getLastErrorBrief()}`;
310
+ return buildEventsText(data);
311
+ };
312
+ return {
313
+ queryExchangeList,
314
+ postExchange,
315
+ queryMyPosts,
316
+ closePost,
317
+ reviewStatus,
318
+ subscribeEgg,
319
+ listSubscriptions,
320
+ unsubscribeEgg,
321
+ queryEvents,
322
+ };
323
+ }
@@ -110,10 +110,18 @@ function register(deps) {
110
110
  let data = null;
111
111
  let fallback = '';
112
112
  if (height != null && weight != null) {
113
- const backendResults = await client.queryPetSize(ctx, heightMeters ?? height / 100, weight);
114
- if (backendResults) {
115
- data = eggService.buildSizeSearchDataFromApi(height, weight, backendResults, heightDisplay);
116
- fallback = eggService.buildSizeSearchTextFromApi(height, weight, backendResults, heightDisplay);
113
+ const heightInMeters = heightMeters ?? height / 100;
114
+ const eggSearchResults = await client.getEggSearch(ctx, heightInMeters, weight, 1, 20, session?.userId || '');
115
+ if (eggSearchResults) {
116
+ data = eggService.buildEggSearchData(heightInMeters, weight, eggSearchResults, heightDisplay);
117
+ fallback = eggService.buildEggSearchText(heightInMeters, weight, eggSearchResults, heightDisplay);
118
+ }
119
+ if (!data) {
120
+ const backendResults = await client.queryPetSize(ctx, heightInMeters, weight, false, session?.userId || '');
121
+ if (backendResults) {
122
+ data = eggService.buildSizeSearchDataFromApi(height, weight, backendResults, heightDisplay);
123
+ fallback = eggService.buildSizeSearchTextFromApi(height, weight, backendResults, heightDisplay);
124
+ }
117
125
  }
118
126
  }
119
127
  if (!data) {
@@ -158,7 +158,7 @@ async function checkMerchantSubscriptions(deps) {
158
158
  const { products, roundInfo, data, fallback } = buildMerchantRenderPayload(res);
159
159
  const productNames = products.map((p) => p.name || '').filter(Boolean);
160
160
  const rendered = await renderer.renderHtml(ctx, 'yuanxing-shangren', data);
161
- const renderedPng = rendered ? (0, send_image_1.compressPngImage)(rendered, config) : null;
161
+ const renderedImage = rendered ? (0, send_image_1.compressPngImage)(rendered, config) : null;
162
162
  const subs = merchantSubMgr.getAll();
163
163
  let matchedCount = 0;
164
164
  let pushedCount = 0;
@@ -195,7 +195,7 @@ async function checkMerchantSubscriptions(deps) {
195
195
  channelId,
196
196
  guildId: sub.group_id || '',
197
197
  userId: sub.user_id || '',
198
- }, renderedPng, fallbackText, !!sub.mention_all);
198
+ }, renderedImage, fallbackText, !!sub.mention_all);
199
199
  if (!sent)
200
200
  continue;
201
201
  pushedCount++;
@@ -271,8 +271,24 @@ function register(deps) {
271
271
  return `远行商人订阅检查完成:订阅 ${result.subscriptions} 条,命中 ${result.matched} 条,推送 ${result.pushed} 条。`;
272
272
  });
273
273
  if (config.merchantSubscriptionEnabled) {
274
- ctx.setInterval(async () => {
275
- await checkMerchantSubscriptions(deps);
276
- }, config.merchantCheckInterval);
274
+ if (config.merchantCheckMode === 'times' && config.merchantCheckTimes.length > 0) {
275
+ let lastMerchantCheckKey = '';
276
+ ctx.setInterval(async () => {
277
+ const now = new Date();
278
+ const timeStr = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
279
+ if (!config.merchantCheckTimes.includes(timeStr))
280
+ return;
281
+ const checkKey = `${now.toDateString()}-${timeStr}`;
282
+ if (checkKey === lastMerchantCheckKey)
283
+ return;
284
+ lastMerchantCheckKey = checkKey;
285
+ await checkMerchantSubscriptions(deps);
286
+ }, 60000);
287
+ }
288
+ else {
289
+ ctx.setInterval(async () => {
290
+ await checkMerchantSubscriptions(deps);
291
+ }, config.merchantCheckInterval);
292
+ }
277
293
  }
278
294
  }
@@ -1866,23 +1866,6 @@ function register(deps) {
1866
1866
  return `好友关系查询失败:${client.getLastErrorBrief()}`;
1867
1867
  await sendImage(deps, session, 'friendship', buildFriendshipRenderData(res, userIds), `【好友关系】${userIds}`);
1868
1868
  });
1869
- ctx.command('洛克').subcommand('.学生 [area:number] [accountType:number]', '查询学生认证状态与学生活动福利')
1870
- .alias('洛克学生')
1871
- .action(async ({ session }, area = 101, accountType = 0) => {
1872
- const fwToken = await (0, account_1.getPrimaryToken)(deps, session.userId);
1873
- if (!fwToken)
1874
- return (0, account_1.notLoggedInHint)();
1875
- const userIdentifier = session.userId;
1876
- const [stateRes, perksRes] = await Promise.all([
1877
- client.getStudentState(ctx, fwToken, accountType, userIdentifier),
1878
- client.getStudentPerks(ctx, fwToken, area, accountType, userIdentifier),
1879
- ]);
1880
- if (!stateRes)
1881
- return `学生认证状态查询失败:${client.getLastErrorBrief()}`;
1882
- if (!perksRes)
1883
- return `学生活动福利查询失败:${client.getLastErrorBrief()}`;
1884
- await sendImage(deps, session, 'student', buildStudentRenderData(stateRes, perksRes, area, accountType), '【洛克学生】认证与福利信息');
1885
- });
1886
1869
  ctx.command('订阅家园菜园 [uid:string]', '订阅指定 UID 的家园菜园成熟提醒')
1887
1870
  .action(async ({ session }, uid = '') => subscribeHome(deps, session, uid, 'garden'));
1888
1871
  ctx.command('订阅家园灵感 [uid:string]', '订阅指定 UID 的家园精灵灵感完成提醒')
@@ -0,0 +1,2 @@
1
+ import { PluginDeps } from '../types';
2
+ export declare function register(deps: PluginDeps): void;
@@ -0,0 +1,164 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.register = register;
4
+ const koishi_1 = require("koishi");
5
+ const activities_service_1 = require("../activities-service");
6
+ const send_image_1 = require("../send-image");
7
+ const logger = new koishi_1.Logger('rocom-tools');
8
+ const activitiesService = new activities_service_1.ActivitiesService();
9
+ function trimText(value) {
10
+ return String(value ?? '').trim();
11
+ }
12
+ function stripHtml(value) {
13
+ return trimText(value)
14
+ .replace(/<br\s*\/?>/gi, '\n')
15
+ .replace(/<\/p>/gi, '\n')
16
+ .replace(/<[^>]+>/g, '')
17
+ .replace(/&nbsp;/g, ' ')
18
+ .replace(/&amp;/g, '&')
19
+ .replace(/&lt;/g, '<')
20
+ .replace(/&gt;/g, '>')
21
+ .replace(/\n{3,}/g, '\n\n')
22
+ .trim();
23
+ }
24
+ function formatDate(value) {
25
+ const text = trimText(value);
26
+ if (!text)
27
+ return '未知时间';
28
+ const numeric = Number(text);
29
+ const date = Number.isFinite(numeric)
30
+ ? new Date(numeric > 10000000000 ? numeric : numeric * 1000)
31
+ : new Date(text);
32
+ if (Number.isNaN(date.getTime()))
33
+ return text;
34
+ const pad = (num) => String(num).padStart(2, '0');
35
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
36
+ }
37
+ function pageNumber(value, defaultValue = 1) {
38
+ const num = Number(value);
39
+ if (!Number.isFinite(num) || num < 1)
40
+ return defaultValue;
41
+ return Math.min(50, Math.floor(num));
42
+ }
43
+ function firstArray(payload, keys) {
44
+ if (!payload || typeof payload !== 'object')
45
+ return [];
46
+ for (const key of keys) {
47
+ if (Array.isArray(payload[key]))
48
+ return payload[key];
49
+ }
50
+ if (payload.data && typeof payload.data === 'object')
51
+ return firstArray(payload.data, keys);
52
+ return [];
53
+ }
54
+ function announcementId(item) {
55
+ return trimText(item?.thread_id || item?.id);
56
+ }
57
+ function buildAnnouncementListText(data, page) {
58
+ const list = firstArray(data, ['list', 'items']);
59
+ if (!list.length)
60
+ return page > 1 ? '该页没有更多公告。' : '当前没有公告数据。';
61
+ const lines = ['洛克公告'];
62
+ for (const [index, item] of list.entries()) {
63
+ const title = trimText(item?.title) || '未命名公告';
64
+ const summary = trimText(item?.summary);
65
+ const id = announcementId(item);
66
+ const stick = Number(item?.isStick) === 1 ? '[置顶]' : '';
67
+ lines.push(`${index + 1}. ${stick}${title}${id ? ` #${id}` : ''}`);
68
+ if (summary)
69
+ lines.push(` ${summary.length > 70 ? `${summary.slice(0, 67)}...` : summary}`);
70
+ lines.push(` ${formatDate(item?.publishAt || item?.published_at || item?.createdAt)}`);
71
+ }
72
+ const current = Number(data?.page) || page;
73
+ if (data?.has_more || data?.next_page) {
74
+ lines.push(`当前第 ${current} 页,下一页:洛克.公告 ${data.next_page || current + 1}`);
75
+ }
76
+ lines.push('详情:洛克.公告详情 <公告ID>');
77
+ return lines.join('\n');
78
+ }
79
+ function buildAnnouncementDetailText(data) {
80
+ const item = data?.detail || data?.announcement || data;
81
+ if (!item || typeof item !== 'object')
82
+ return '公告详情为空。';
83
+ const title = trimText(item?.title) || '未命名公告';
84
+ const id = announcementId(item);
85
+ const summary = trimText(item?.summary);
86
+ const content = stripHtml(item?.content?.text || item?.text || item?.content);
87
+ const lines = [
88
+ `公告详情${id ? ` #${id}` : ''}`,
89
+ title,
90
+ `发布时间:${formatDate(item?.publishAt || item?.published_at || item?.createdAt)}`,
91
+ ];
92
+ if (summary)
93
+ lines.push(`摘要:${summary}`);
94
+ if (content) {
95
+ lines.push('');
96
+ lines.push(content.length > 900 ? `${content.slice(0, 900)}...` : content);
97
+ }
98
+ const images = Array.isArray(item?.content?.indexes)
99
+ ? item.content.indexes.flatMap((entry) => Array.isArray(entry?.imageUrl) ? entry.imageUrl : [])
100
+ : [];
101
+ if (images.length) {
102
+ lines.push('');
103
+ lines.push(`图片资源:${images.slice(0, 3).join('\n')}`);
104
+ }
105
+ return lines.join('\n');
106
+ }
107
+ function register(deps) {
108
+ const { ctx, client, config, renderer } = deps;
109
+ ctx.command('洛克').subcommand('.日历 [mode:string]', '查看洛克活动日历')
110
+ .alias('洛克日历')
111
+ .action(async ({ session }, mode = '') => {
112
+ const refresh = ['刷新', 'refresh', 'true', '1'].includes(String(mode || '').toLowerCase());
113
+ const data = await client.getActivitiesInfo(ctx, refresh, session?.userId || '');
114
+ if (!data)
115
+ return `活动日历查询失败:${client.getLastErrorBrief()}`;
116
+ const fallback = activitiesService.buildFallbackText(data);
117
+ if (!session?.send)
118
+ return fallback;
119
+ const image = await renderer.renderHtml(ctx, 'activities', activitiesService.buildRenderData(data));
120
+ await (0, send_image_1.sendImageWithFallback)(session, image, fallback, 'activities:calendar', config);
121
+ });
122
+ ctx.command('洛克').subcommand('.公告 [page:number]', '查看洛克公告列表')
123
+ .alias('洛克公告')
124
+ .action(async ({ session }, page = 1) => {
125
+ const currentPage = pageNumber(page);
126
+ const data = await client.getAnnouncementList(ctx, { category_id: 99, page: currentPage, limit: 10 }, session?.userId || '');
127
+ if (!data)
128
+ return `公告列表查询失败:${client.getLastErrorBrief()}`;
129
+ return buildAnnouncementListText(data, currentPage);
130
+ });
131
+ ctx.command('洛克').subcommand('.最新公告', '查看最新洛克公告')
132
+ .alias('洛克最新公告')
133
+ .action(async ({ session }) => {
134
+ const data = await client.getLatestAnnouncement(ctx, { category_id: 99 }, session?.userId || '');
135
+ if (!data)
136
+ return `最新公告查询失败:${client.getLastErrorBrief()}`;
137
+ return buildAnnouncementDetailText(data);
138
+ });
139
+ ctx.command('洛克').subcommand('.公告详情 <threadId:string>', '查看公告详情')
140
+ .alias('洛克公告详情')
141
+ .action(async ({ session }, threadId) => {
142
+ const id = trimText(threadId);
143
+ if (!/^\d+$/.test(id))
144
+ return '请提供公告 ID。用法:洛克.公告详情 <公告ID>';
145
+ const data = await client.getAnnouncementDetail(ctx, id, session?.userId || '');
146
+ if (!data)
147
+ return `公告详情查询失败:${client.getLastErrorBrief()}`;
148
+ return buildAnnouncementDetailText(data);
149
+ });
150
+ ctx.command('洛克').subcommand('.同步配置', '手动同步 RoCom 远端配置')
151
+ .alias('洛克同步配置')
152
+ .action(async ({ session }) => {
153
+ if (!config.adminUserIds.includes(session?.userId || ''))
154
+ return '此指令仅限管理员使用。';
155
+ const data = await client.syncConfig(ctx, session?.userId || '');
156
+ if (!data)
157
+ return `同步配置失败:${client.getLastErrorBrief()}`;
158
+ logger.info(`manual config sync requested by ${session?.userId || 'unknown'}`);
159
+ const skipped = Array.isArray(data?.skipped_resources) ? data.skipped_resources.length : 0;
160
+ return skipped
161
+ ? `配置同步完成,但有 ${skipped} 个资源跳过。`
162
+ : '配置同步完成。';
163
+ });
164
+ }
@@ -27,6 +27,8 @@ export declare class EggService {
27
27
  private rangeMatchScore;
28
28
  private formatPetCard;
29
29
  private formatSizeApiCard;
30
+ private formatEggSearchCard;
31
+ private formatEggSearchTextLine;
30
32
  private mergeCardsByName;
31
33
  private mergeSizeCard;
32
34
  private minValue;
@@ -47,6 +49,7 @@ export declare class EggService {
47
49
  range: any[];
48
50
  }, heightDisplay?: string): string;
49
51
  buildSizeSearchTextFromApi(height?: number, weight?: number, results?: any, heightDisplay?: string): string;
52
+ buildEggSearchText(heightMeters?: number, weight?: number, results?: any, heightDisplay?: string): string;
50
53
  buildSearchText(pet: any): string;
51
54
  buildCandidatesText(keyword: string, candidates: any[]): string;
52
55
  buildWantPetText(pet: any): string;
@@ -225,5 +228,14 @@ export declare class EggService {
225
228
  commandHint: string;
226
229
  copyright: string;
227
230
  };
231
+ buildEggSearchData(heightMeters?: number, weight?: number, results?: any, heightDisplay?: string): {
232
+ query_label: string;
233
+ perfect_matches: any;
234
+ range_matches: any[];
235
+ total_count: any;
236
+ has_results: boolean;
237
+ commandHint: string;
238
+ copyright: string;
239
+ };
228
240
  private buildEggDetails;
229
241
  }