koishi-plugin-rocom 1.0.10 → 1.0.12

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) {
@@ -15,6 +15,49 @@ const TEXT = {
15
15
  defaultSource: '\u9ed8\u8ba4',
16
16
  customSource: '\u81ea\u5b9a\u4e49',
17
17
  };
18
+ const CATEGORY_ORDER = ['normal', 'round', 'weekend'];
19
+ const CATEGORY_LABELS = {
20
+ normal: '热销商品',
21
+ round: '常规商品',
22
+ weekend: '周末限定',
23
+ };
24
+ const CHINA_TIMEZONE = 'Asia/Shanghai';
25
+ const chinaPartsFormatter = new Intl.DateTimeFormat('zh-CN', {
26
+ timeZone: CHINA_TIMEZONE,
27
+ year: 'numeric',
28
+ month: '2-digit',
29
+ day: '2-digit',
30
+ hour: '2-digit',
31
+ minute: '2-digit',
32
+ hour12: false,
33
+ });
34
+ function getChinaParts(timestampMs) {
35
+ const parts = {};
36
+ for (const item of chinaPartsFormatter.formatToParts(new Date(timestampMs))) {
37
+ if (item.type !== 'literal')
38
+ parts[item.type] = item.value;
39
+ }
40
+ return {
41
+ hour: Number(parts.hour || '0'),
42
+ minute: Number(parts.minute || '0'),
43
+ };
44
+ }
45
+ function classifyMerchantItem(item) {
46
+ const start = normalizeTimestamp(item?.start_time);
47
+ const end = normalizeTimestamp(item?.end_time);
48
+ if (!start || !end)
49
+ return 'normal';
50
+ const durationDays = (end - start) / (1000 * 60 * 60 * 24);
51
+ if (durationDays >= 2)
52
+ return 'weekend';
53
+ const startParts = getChinaParts(start);
54
+ const endParts = getChinaParts(end);
55
+ const startHour = startParts.hour + startParts.minute / 60;
56
+ const endHour = endParts.hour + endParts.minute / 60;
57
+ if (startHour <= 8 && endHour >= 23.5)
58
+ return 'normal';
59
+ return 'round';
60
+ }
18
61
  function normalizeTimestamp(value) {
19
62
  if (value === null || value === undefined || value === '')
20
63
  return null;
@@ -54,13 +97,34 @@ function getMerchantActivity(res) {
54
97
  }
55
98
  function getActiveProducts(res) {
56
99
  const activity = getMerchantActivity(res);
57
- const products = activity?.products || activity?.product_list || activity?.get_props || [];
58
- return products.filter((p) => {
59
- const now = Date.now();
60
- const start = normalizeTimestamp(p.start_time) ?? 0;
61
- const end = normalizeTimestamp(p.end_time) ?? Infinity;
62
- return now >= start && now < end;
63
- });
100
+ const groups = [];
101
+ if (Array.isArray(activity?.products))
102
+ groups.push(activity.products);
103
+ if (Array.isArray(activity?.product_list))
104
+ groups.push(activity.product_list);
105
+ if (Array.isArray(activity?.get_props))
106
+ groups.push(activity.get_props);
107
+ if (Array.isArray(activity?.get_extra_props))
108
+ groups.push(activity.get_extra_props);
109
+ if (Array.isArray(activity?.get_pets))
110
+ groups.push(activity.get_pets);
111
+ const merged = [];
112
+ const seen = new Set();
113
+ const now = Date.now();
114
+ for (const list of groups) {
115
+ for (const item of list) {
116
+ const start = normalizeTimestamp(item?.start_time) ?? 0;
117
+ const end = normalizeTimestamp(item?.end_time) ?? Infinity;
118
+ if (now < start || now >= end)
119
+ continue;
120
+ const key = `${item?.id ?? ''}|${item?.name ?? ''}|${start}|${end === Infinity ? 'inf' : end}`;
121
+ if (seen.has(key))
122
+ continue;
123
+ seen.add(key);
124
+ merged.push(item);
125
+ }
126
+ }
127
+ return merged;
64
128
  }
65
129
  function getCurrentMerchantRound() {
66
130
  const now = new Date();
@@ -132,23 +196,56 @@ function buildMerchantRenderPayload(res) {
132
196
  const products = getActiveProducts(res);
133
197
  const roundInfo = getCurrentMerchantRound();
134
198
  const activity = getMerchantActivity(res);
199
+ const renderedProducts = products.map((p) => ({
200
+ name: p.name || TEXT.unknown,
201
+ image: p.icon_url || '',
202
+ time_label: formatProductWindow(p),
203
+ category: classifyMerchantItem(p),
204
+ }));
205
+ const categoryMap = {
206
+ normal: [],
207
+ round: [],
208
+ weekend: [],
209
+ };
210
+ for (const product of renderedProducts) {
211
+ categoryMap[product.category].push(product);
212
+ }
213
+ const categories = CATEGORY_ORDER
214
+ .filter(key => categoryMap[key].length > 0)
215
+ .map(key => ({
216
+ key,
217
+ label: CATEGORY_LABELS[key],
218
+ products: categoryMap[key],
219
+ }));
135
220
  const data = {
136
221
  background: '',
137
222
  title: activity.name || TEXT.merchant,
138
223
  subtitle: activity.start_date || '\u6bcf\u65e5 08:00 / 12:00 / 16:00 / 20:00 \u5237\u65b0',
139
224
  titleIcon: true,
140
- product_count: products.length,
225
+ product_count: renderedProducts.length,
141
226
  round_info: roundInfo,
142
- products: products.map((p) => ({
143
- name: p.name || TEXT.unknown,
144
- image: p.icon_url || '',
145
- time_label: formatProductWindow(p),
146
- })),
227
+ categories,
228
+ products: renderedProducts,
147
229
  };
148
- const fallback = products.length
149
- ? `\u8fdc\u884c\u5546\u4eba\u5f53\u524d\u5546\u54c1\uff1a${products.map((p) => p.name || TEXT.unknown).join('\u3001')}\n\u8f6e\u6b21\uff1a${roundInfo.current || TEXT.notOpen}\n\u5269\u4f59\uff1a${roundInfo.countdown}`
150
- : '\u5f53\u524d\u8fdc\u884c\u5546\u4eba\u6682\u65e0\u5546\u54c1\u3002';
151
- return { products, roundInfo, data, fallback };
230
+ const fallbackLines = [];
231
+ if (renderedProducts.length) {
232
+ fallbackLines.push(`\u8fdc\u884c\u5546\u4eba\u5f53\u524d\u5546\u54c1\uff08\u5171 ${renderedProducts.length} \u4ef6\uff09`);
233
+ fallbackLines.push(`\u8f6e\u6b21\uff1a${roundInfo.current || TEXT.notOpen}\uff0c\u5269\u4f59\uff1a${roundInfo.countdown}`);
234
+ fallbackLines.push('');
235
+ for (const cat of categories) {
236
+ fallbackLines.push(`\u3010${cat.label}\u3011`);
237
+ cat.products.forEach((product, index) => {
238
+ const tail = product.time_label ? ` (${product.time_label})` : '';
239
+ fallbackLines.push(` ${index + 1}. ${product.name}${tail}`);
240
+ });
241
+ fallbackLines.push('');
242
+ }
243
+ }
244
+ else {
245
+ fallbackLines.push('\u5f53\u524d\u8fdc\u884c\u5546\u4eba\u6682\u65e0\u5546\u54c1\u3002');
246
+ }
247
+ const fallback = fallbackLines.join('\n').trimEnd();
248
+ return { products: renderedProducts, roundInfo, data, fallback };
152
249
  }
153
250
  async function checkMerchantSubscriptions(deps) {
154
251
  const { ctx, client, merchantSubMgr, renderer, config } = deps;
@@ -158,7 +255,7 @@ async function checkMerchantSubscriptions(deps) {
158
255
  const { products, roundInfo, data, fallback } = buildMerchantRenderPayload(res);
159
256
  const productNames = products.map((p) => p.name || '').filter(Boolean);
160
257
  const rendered = await renderer.renderHtml(ctx, 'yuanxing-shangren', data);
161
- const renderedPng = rendered ? (0, send_image_1.compressPngImage)(rendered, config) : null;
258
+ const renderedImage = rendered ? (0, send_image_1.compressPngImage)(rendered, config) : null;
162
259
  const subs = merchantSubMgr.getAll();
163
260
  let matchedCount = 0;
164
261
  let pushedCount = 0;
@@ -195,7 +292,7 @@ async function checkMerchantSubscriptions(deps) {
195
292
  channelId,
196
293
  guildId: sub.group_id || '',
197
294
  userId: sub.user_id || '',
198
- }, renderedPng, fallbackText, !!sub.mention_all);
295
+ }, renderedImage, fallbackText, !!sub.mention_all);
199
296
  if (!sent)
200
297
  continue;
201
298
  pushedCount++;
@@ -271,8 +368,24 @@ function register(deps) {
271
368
  return `远行商人订阅检查完成:订阅 ${result.subscriptions} 条,命中 ${result.matched} 条,推送 ${result.pushed} 条。`;
272
369
  });
273
370
  if (config.merchantSubscriptionEnabled) {
274
- ctx.setInterval(async () => {
275
- await checkMerchantSubscriptions(deps);
276
- }, config.merchantCheckInterval);
371
+ if (config.merchantCheckMode === 'times' && config.merchantCheckTimes.length > 0) {
372
+ let lastMerchantCheckKey = '';
373
+ ctx.setInterval(async () => {
374
+ const now = new Date();
375
+ const timeStr = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
376
+ if (!config.merchantCheckTimes.includes(timeStr))
377
+ return;
378
+ const checkKey = `${now.toDateString()}-${timeStr}`;
379
+ if (checkKey === lastMerchantCheckKey)
380
+ return;
381
+ lastMerchantCheckKey = checkKey;
382
+ await checkMerchantSubscriptions(deps);
383
+ }, 60000);
384
+ }
385
+ else {
386
+ ctx.setInterval(async () => {
387
+ await checkMerchantSubscriptions(deps);
388
+ }, config.merchantCheckInterval);
389
+ }
277
390
  }
278
391
  }