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.
- package/lib/activities-service.d.ts +72 -0
- package/lib/activities-service.js +293 -0
- package/lib/client.d.ts +32 -1
- package/lib/client.js +102 -6
- package/lib/commands/account.js +44 -0
- package/lib/commands/community.d.ts +12 -0
- package/lib/commands/community.js +323 -0
- package/lib/commands/egg.js +12 -4
- package/lib/commands/merchant.js +135 -22
- package/lib/commands/query.js +112 -21
- package/lib/commands/tools.d.ts +2 -0
- package/lib/commands/tools.js +164 -0
- package/lib/egg-service.d.ts +12 -0
- package/lib/egg-service.js +76 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +5416 -144
- package/lib/render-templates/activities/index.html +77 -0
- package/lib/render-templates/activities/style.css +434 -0
- package/lib/render-templates/student/index.html +95 -0
- package/lib/render-templates/student/style.css +255 -0
- package/lib/render-templates/student-perks/index.html +78 -0
- package/lib/render-templates/student-perks/style.css +238 -0
- package/lib/render-templates/student-state/index.html +52 -0
- package/lib/render-templates/student-state/style.css +157 -0
- package/lib/render-templates/yuanxing-shangren/index.html +85 -28
- package/lib/render.js +17 -5
- package/lib/send-image.d.ts +1 -0
- package/lib/send-image.js +13 -1
- package/lib/subscription-send.js +2 -1
- package/lib/types.d.ts +2 -0
- package/package.json +1 -1
- package/readme.md +4 -8
|
@@ -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
|
+
}
|
package/lib/commands/egg.js
CHANGED
|
@@ -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
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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) {
|
package/lib/commands/merchant.js
CHANGED
|
@@ -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
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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:
|
|
225
|
+
product_count: renderedProducts.length,
|
|
141
226
|
round_info: roundInfo,
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
image: p.icon_url || '',
|
|
145
|
-
time_label: formatProductWindow(p),
|
|
146
|
-
})),
|
|
227
|
+
categories,
|
|
228
|
+
products: renderedProducts,
|
|
147
229
|
};
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
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
|
-
},
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
}
|