koishi-plugin-rocom 1.0.18 → 1.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/announcement-service.d.ts +21 -0
- package/lib/announcement-service.js +63 -0
- package/lib/async-control.d.ts +3 -0
- package/lib/async-control.js +48 -0
- package/lib/client.d.ts +48 -7
- package/lib/client.js +244 -125
- package/lib/commands/egg.js +66 -4
- package/lib/commands/merchant.js +144 -146
- package/lib/commands/query.js +148 -117
- package/lib/commands/ranking.d.ts +2 -0
- package/lib/commands/ranking.js +31 -0
- package/lib/commands/share-code.d.ts +2 -0
- package/lib/commands/share-code.js +66 -0
- package/lib/commands/tools.js +47 -59
- package/lib/egg-service.d.ts +82 -8
- package/lib/egg-service.js +235 -19
- package/lib/img//345/274/202/350/211/262.png +0 -0
- package/lib/img//347/202/253/345/275/251.png +0 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.js +16 -3
- package/lib/merchant-clock.d.ts +22 -0
- package/lib/merchant-clock.js +95 -0
- package/lib/merchant-service.d.ts +24 -0
- package/lib/merchant-service.js +148 -0
- package/lib/permissions.d.ts +16 -0
- package/lib/permissions.js +66 -0
- package/lib/pet-data-service.js +7 -9
- package/lib/pet-size.d.ts +13 -0
- package/lib/pet-size.js +54 -0
- package/lib/player-service.d.ts +37 -0
- package/lib/player-service.js +201 -0
- package/lib/ranking-service.d.ts +46 -0
- package/lib/ranking-service.js +137 -0
- package/lib/render-templates/announcement/detail.html +8 -0
- package/lib/render-templates/announcement/list.html +5 -0
- package/lib/render-templates/announcement/style.css +1 -0
- package/lib/render-templates/pet-ranking/index.html +99 -0
- package/lib/render-templates/pet-ranking/style.css +338 -0
- package/lib/render-templates/player-search/index.html +7 -0
- package/lib/render-templates/searcheggs/index.html +1 -1
- package/lib/render-templates/searcheggs/size.html +70 -68
- package/lib/render-templates/share-code-team/index.html +105 -0
- package/lib/render-templates/share-code-team/style.css +345 -0
- package/lib/render.d.ts +15 -2
- package/lib/render.js +120 -37
- package/lib/send-image.d.ts +1 -1
- package/lib/send-image.js +12 -1
- package/lib/share-code-service.d.ts +32 -0
- package/lib/share-code-service.js +184 -0
- package/lib/subscription-runner.d.ts +6 -0
- package/lib/subscription-runner.js +21 -0
- package/lib/subscription-send.d.ts +4 -2
- package/lib/subscription-send.js +45 -20
- package/lib/types.d.ts +5 -0
- package/lib/user.d.ts +5 -0
- package/package.json +4 -1
- package/readme.md +145 -124
package/lib/commands/egg.js
CHANGED
|
@@ -111,10 +111,19 @@ function register(deps) {
|
|
|
111
111
|
let fallback = '';
|
|
112
112
|
if (height != null && weight != null) {
|
|
113
113
|
const heightInMeters = heightMeters ?? height / 100;
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
114
|
+
const userId = session?.userId || '';
|
|
115
|
+
// 后端查蛋优先,失败后回退 Wiki 尺寸接口,再回退本地引擎。成功空集不回退。
|
|
116
|
+
const eggApiResults = await client.searchEggBySize(ctx, heightInMeters, weight, 1, 30, userId);
|
|
117
|
+
if (eggApiResults) {
|
|
118
|
+
data = eggService.buildSizeSearchDataFromApi(height, weight, eggApiResults, heightDisplay);
|
|
119
|
+
fallback = eggService.buildSizeSearchTextFromApi(height, weight, eggApiResults, heightDisplay);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
const wikiResults = await client.queryPetSize(ctx, heightInMeters, weight, 'magic', 1, 30, userId);
|
|
123
|
+
if (wikiResults) {
|
|
124
|
+
data = eggService.buildSizeSearchDataFromApi(height, weight, wikiResults, heightDisplay);
|
|
125
|
+
fallback = eggService.buildSizeSearchTextFromApi(height, weight, wikiResults, heightDisplay);
|
|
126
|
+
}
|
|
118
127
|
}
|
|
119
128
|
}
|
|
120
129
|
if (!data) {
|
|
@@ -128,6 +137,59 @@ function register(deps) {
|
|
|
128
137
|
const name = nameParts.join(' ');
|
|
129
138
|
if (!name)
|
|
130
139
|
return '请输入精灵名称。用法:洛克.查蛋 <精灵名>';
|
|
140
|
+
// 后端查蛋优先(上游 v3.8.0):pet-groups → group-pets,接口不可用才回退 Wiki。
|
|
141
|
+
const userId = session?.userId || '';
|
|
142
|
+
const eggCandidatesRes = await client.getEggPetGroups(ctx, name, 20, userId);
|
|
143
|
+
if (eggCandidatesRes !== null) {
|
|
144
|
+
const eggCandidates = Array.isArray(eggCandidatesRes)
|
|
145
|
+
? eggCandidatesRes
|
|
146
|
+
: (Array.isArray(eggCandidatesRes?.items) ? eggCandidatesRes.items : []);
|
|
147
|
+
if (!eggCandidates.length)
|
|
148
|
+
return `未找到名为「${name}」的精灵,请检查名称后重试。`;
|
|
149
|
+
let selected = eggCandidates.find((item) => {
|
|
150
|
+
const itemName = String(item?.name || '').trim();
|
|
151
|
+
const itemForm = String(item?.form || '').trim();
|
|
152
|
+
const display = itemForm && !itemName.includes(itemForm) ? `${itemName}(${itemForm})` : itemName;
|
|
153
|
+
return String(item?.id || '') === name
|
|
154
|
+
|| itemName === name
|
|
155
|
+
|| display === name
|
|
156
|
+
|| (itemForm && `${itemName}${itemForm}` === name);
|
|
157
|
+
});
|
|
158
|
+
if (!selected && eggCandidates.length === 1)
|
|
159
|
+
selected = eggCandidates[0];
|
|
160
|
+
if (!selected) {
|
|
161
|
+
const data = eggService.buildCandidatesFromEggApi(name, eggCandidates);
|
|
162
|
+
const candidateText = [
|
|
163
|
+
`找到多个查蛋候选(${eggCandidates.length}),请使用更精确名称:`,
|
|
164
|
+
...eggCandidates.slice(0, 10).map((item, index) => `${index + 1}. ${item?.name || '未知精灵'} #${item?.id || '-'}`),
|
|
165
|
+
].join('\n');
|
|
166
|
+
await sendEggImage(deps, session, 'searcheggs/candidates', data, candidateText);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const compatibleByGroup = {};
|
|
170
|
+
const groupIds = [];
|
|
171
|
+
for (const group of selected?.egg_groups || []) {
|
|
172
|
+
if (!group || typeof group !== 'object')
|
|
173
|
+
continue;
|
|
174
|
+
const groupId = group.group_id || group.id;
|
|
175
|
+
if (groupId)
|
|
176
|
+
groupIds.push(groupId);
|
|
177
|
+
}
|
|
178
|
+
if (groupIds.length) {
|
|
179
|
+
compatibleByGroup['__all__'] = await client.getEggGroupPets(ctx, groupIds, 'any', 1, 1, userId) || {};
|
|
180
|
+
}
|
|
181
|
+
for (const group of selected?.egg_groups || []) {
|
|
182
|
+
if (!group || typeof group !== 'object')
|
|
183
|
+
continue;
|
|
184
|
+
const groupId = group.group_id || group.id;
|
|
185
|
+
if (!groupId)
|
|
186
|
+
continue;
|
|
187
|
+
compatibleByGroup[String(groupId)] = await client.getEggGroupPets(ctx, [groupId], 'any', 1, 60, userId) || {};
|
|
188
|
+
}
|
|
189
|
+
const data = eggService.buildSearchDataFromEggApi(selected, compatibleByGroup);
|
|
190
|
+
await sendEggImage(deps, session, 'searcheggs', data, eggService.buildSearchTextFromWiki(data));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
131
193
|
let backendDetail = null;
|
|
132
194
|
let backendProfile = null;
|
|
133
195
|
const backendList = await client.listWikiPets(ctx, name, 1, 10);
|
package/lib/commands/merchant.js
CHANGED
|
@@ -4,6 +4,12 @@ exports.register = register;
|
|
|
4
4
|
const koishi_1 = require("koishi");
|
|
5
5
|
const send_image_1 = require("../send-image");
|
|
6
6
|
const subscription_send_1 = require("../subscription-send");
|
|
7
|
+
const merchant_service_1 = require("../merchant-service");
|
|
8
|
+
const merchant_clock_1 = require("../merchant-clock");
|
|
9
|
+
const permissions_1 = require("../permissions");
|
|
10
|
+
const subscription_runner_1 = require("../subscription-runner");
|
|
11
|
+
const node_async_hooks_1 = require("node:async_hooks");
|
|
12
|
+
const async_control_1 = require("../async-control");
|
|
7
13
|
const logger = new koishi_1.Logger('rocom-merchant');
|
|
8
14
|
const TEXT = {
|
|
9
15
|
merchant: '\u8fdc\u884c\u5546\u4eba',
|
|
@@ -28,31 +34,21 @@ const ROUND_WINDOWS = [
|
|
|
28
34
|
{ id: 3, label: '16:00-20:00', startHour: 16, endHour: 20 },
|
|
29
35
|
{ id: 4, label: '20:00-24:00', startHour: 20, endHour: 24 },
|
|
30
36
|
];
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
month: '2-digit',
|
|
36
|
-
day: '2-digit',
|
|
37
|
-
hour: '2-digit',
|
|
38
|
-
minute: '2-digit',
|
|
39
|
-
second: '2-digit',
|
|
40
|
-
hour12: false,
|
|
41
|
-
});
|
|
37
|
+
const defaultClock = new merchant_clock_1.MerchantClock('Asia/Shanghai');
|
|
38
|
+
// Scoped to each synchronous view build; plugin instances never overwrite another clock.
|
|
39
|
+
const clockScope = new node_async_hooks_1.AsyncLocalStorage();
|
|
40
|
+
const currentClock = () => clockScope.getStore() || defaultClock;
|
|
42
41
|
function getChinaParts(input = Date.now()) {
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
if (item.type !== 'literal')
|
|
47
|
-
parts[item.type] = item.value;
|
|
48
|
-
}
|
|
42
|
+
const ms = input instanceof Date ? input.getTime() : input;
|
|
43
|
+
const parts = currentClock().parts(ms);
|
|
44
|
+
const [year, month, day] = parts.date.split('-').map(Number);
|
|
49
45
|
return {
|
|
50
|
-
year:
|
|
51
|
-
month:
|
|
52
|
-
day:
|
|
53
|
-
hour:
|
|
54
|
-
minute:
|
|
55
|
-
second:
|
|
46
|
+
year: year || 0,
|
|
47
|
+
month: month || 0,
|
|
48
|
+
day: day || 0,
|
|
49
|
+
hour: parts.hour,
|
|
50
|
+
minute: parts.minute,
|
|
51
|
+
second: parts.second,
|
|
56
52
|
};
|
|
57
53
|
}
|
|
58
54
|
function padNumber(value) {
|
|
@@ -62,10 +58,6 @@ function getChinaDateText(input = Date.now()) {
|
|
|
62
58
|
const parts = getChinaParts(input);
|
|
63
59
|
return `${parts.year}-${padNumber(parts.month)}-${padNumber(parts.day)}`;
|
|
64
60
|
}
|
|
65
|
-
function getChinaDayStartMs(input = Date.now()) {
|
|
66
|
-
const parts = getChinaParts(input);
|
|
67
|
-
return new Date(`${parts.year}-${padNumber(parts.month)}-${padNumber(parts.day)}T00:00:00+08:00`).getTime();
|
|
68
|
-
}
|
|
69
61
|
function classifyMerchantItem(item) {
|
|
70
62
|
const start = normalizeTimestamp(item?.start_time);
|
|
71
63
|
const end = normalizeTimestamp(item?.end_time);
|
|
@@ -115,34 +107,6 @@ function getMerchantActivity(res) {
|
|
|
115
107
|
const activities = res?.merchantActivities || res?.merchant_activities || [];
|
|
116
108
|
return activities[0] || {};
|
|
117
109
|
}
|
|
118
|
-
function getMerchantProducts(res) {
|
|
119
|
-
const activity = getMerchantActivity(res);
|
|
120
|
-
const groups = [];
|
|
121
|
-
if (Array.isArray(activity?.products))
|
|
122
|
-
groups.push(activity.products);
|
|
123
|
-
if (Array.isArray(activity?.product_list))
|
|
124
|
-
groups.push(activity.product_list);
|
|
125
|
-
if (Array.isArray(activity?.get_props))
|
|
126
|
-
groups.push(activity.get_props);
|
|
127
|
-
if (Array.isArray(activity?.get_extra_props))
|
|
128
|
-
groups.push(activity.get_extra_props);
|
|
129
|
-
if (Array.isArray(activity?.get_pets))
|
|
130
|
-
groups.push(activity.get_pets);
|
|
131
|
-
const merged = [];
|
|
132
|
-
const seen = new Set();
|
|
133
|
-
for (const list of groups) {
|
|
134
|
-
for (const item of list) {
|
|
135
|
-
const start = normalizeTimestamp(item?.start_time) ?? 0;
|
|
136
|
-
const end = normalizeTimestamp(item?.end_time) ?? Infinity;
|
|
137
|
-
const key = `${item?.id ?? ''}|${item?.name ?? ''}|${start}|${end === Infinity ? 'inf' : end}`;
|
|
138
|
-
if (seen.has(key))
|
|
139
|
-
continue;
|
|
140
|
-
seen.add(key);
|
|
141
|
-
merged.push(item);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return merged;
|
|
145
|
-
}
|
|
146
110
|
function isMerchantItemActive(item, now = Date.now()) {
|
|
147
111
|
const nowMs = now instanceof Date ? now.getTime() : now;
|
|
148
112
|
const start = normalizeTimestamp(item?.start_time);
|
|
@@ -154,8 +118,7 @@ function isMerchantItemToday(item, now = Date.now()) {
|
|
|
154
118
|
const end = normalizeTimestamp(item?.end_time);
|
|
155
119
|
if (start === null || end === null)
|
|
156
120
|
return true;
|
|
157
|
-
const startOfDay =
|
|
158
|
-
const endOfDay = startOfDay + 24 * 60 * 60 * 1000;
|
|
121
|
+
const { start: startOfDay, end: endOfDay } = currentClock().dayBounds(now instanceof Date ? now.getTime() : now);
|
|
159
122
|
return start < endOfDay && end > startOfDay;
|
|
160
123
|
}
|
|
161
124
|
function getRoundForItem(item, now = Date.now()) {
|
|
@@ -220,61 +183,49 @@ function parseMerchantSubscriptionArgs(args, defaultItems) {
|
|
|
220
183
|
}
|
|
221
184
|
function getSubscriptionTarget(session) {
|
|
222
185
|
const platform = session.platform || session.bot?.platform || '';
|
|
223
|
-
const privateChat =
|
|
186
|
+
const privateChat = (0, permissions_1.isDirectSession)(session);
|
|
224
187
|
const channelId = session.channelId || session.guildId || session.userId || '';
|
|
225
|
-
const key = privateChat ? `private_${session.userId}` : session.guildId;
|
|
188
|
+
const key = privateChat ? `private_${session.userId}` : (session.guildId || channelId);
|
|
226
189
|
return { key, platform, channelId, privateChat };
|
|
227
190
|
}
|
|
228
191
|
function isBotAdmin(session, adminUserIds) {
|
|
229
192
|
return adminUserIds.includes(session?.userId || '');
|
|
230
193
|
}
|
|
194
|
+
// 订阅管理权限:群主/管理员与 Bot 管理员按独立开关生效(上游 v4.0)。
|
|
195
|
+
function canManageGroupSubscription(deps, session) {
|
|
196
|
+
return (0, permissions_1.canManageSubscription)(deps.config, session);
|
|
197
|
+
}
|
|
231
198
|
function sameStringArray(left, right) {
|
|
232
199
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
233
200
|
}
|
|
234
|
-
function getRandomGoodsMaps(res) {
|
|
235
|
-
const priceMap = new Map();
|
|
236
|
-
const limitMap = new Map();
|
|
237
|
-
const randomGoods = Array.isArray(res?.random_goods)
|
|
238
|
-
? res.random_goods
|
|
239
|
-
: Array.isArray(res?.randomGoods)
|
|
240
|
-
? res.randomGoods
|
|
241
|
-
: [];
|
|
242
|
-
for (const item of randomGoods) {
|
|
243
|
-
const name = String(item?.goods_name || item?.name || '').trim();
|
|
244
|
-
if (!name)
|
|
245
|
-
continue;
|
|
246
|
-
if (item?.price !== undefined && item?.price !== null && item.price !== '')
|
|
247
|
-
priceMap.set(name, item.price);
|
|
248
|
-
if (item?.buy_limit_num !== undefined && item?.buy_limit_num !== null && item.buy_limit_num !== '') {
|
|
249
|
-
limitMap.set(name, item.buy_limit_num);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
return { priceMap, limitMap };
|
|
253
|
-
}
|
|
254
201
|
function normalizeMerchantProducts(res, now = new Date()) {
|
|
255
|
-
|
|
256
|
-
|
|
202
|
+
const nowMs = now.getTime();
|
|
203
|
+
const { items } = (0, merchant_service_1.normalizeMerchant)(res, nowMs);
|
|
204
|
+
return items.map((item) => {
|
|
205
|
+
const source = { start_time: item.startsAt, end_time: item.endsAt };
|
|
257
206
|
return {
|
|
258
|
-
name,
|
|
259
|
-
image:
|
|
260
|
-
time_label: formatProductWindow(
|
|
261
|
-
category: classifyMerchantItem(
|
|
262
|
-
round_id: getRoundForItem(
|
|
263
|
-
is_active:
|
|
264
|
-
start_time:
|
|
265
|
-
end_time:
|
|
207
|
+
name: item.name,
|
|
208
|
+
image: item.icon,
|
|
209
|
+
time_label: formatProductWindow(source),
|
|
210
|
+
category: classifyMerchantItem(source),
|
|
211
|
+
round_id: getRoundForItem(source, now),
|
|
212
|
+
is_active: item.active,
|
|
213
|
+
start_time: item.startsAt,
|
|
214
|
+
end_time: item.endsAt,
|
|
215
|
+
price: item.price,
|
|
216
|
+
limit: item.limit,
|
|
217
|
+
item_category: item.category,
|
|
266
218
|
};
|
|
267
219
|
});
|
|
268
220
|
}
|
|
269
|
-
function buildMerchantCardItems(products,
|
|
270
|
-
const { priceMap, limitMap } = getRandomGoodsMaps(res);
|
|
221
|
+
function buildMerchantCardItems(products, options) {
|
|
271
222
|
const catOrder = { round: 0, normal: 1, weekend: 2 };
|
|
272
223
|
const startY = 592;
|
|
273
224
|
const cardHeight = 308;
|
|
274
225
|
const gap = 43;
|
|
275
226
|
const goodsAll = products.map((product) => {
|
|
276
|
-
const limit =
|
|
277
|
-
const limitText = limit === undefined || limit === null
|
|
227
|
+
const limit = product.limit;
|
|
228
|
+
const limitText = limit === undefined || limit === null ? '--' : String(limit);
|
|
278
229
|
const isHot = product.category !== 'round';
|
|
279
230
|
const isEnded = options.includeEnded ? !product.is_active : false;
|
|
280
231
|
const roundPrefix = product.round_id ? `第${product.round_id}轮·` : '';
|
|
@@ -286,7 +237,7 @@ function buildMerchantCardItems(products, res, options) {
|
|
|
286
237
|
return {
|
|
287
238
|
goods_name: product.name,
|
|
288
239
|
iconUrl: product.image,
|
|
289
|
-
price:
|
|
240
|
+
price: product.price ?? 0,
|
|
290
241
|
num: '',
|
|
291
242
|
category: product.category,
|
|
292
243
|
roundId: product.round_id || 0,
|
|
@@ -397,7 +348,7 @@ function buildMerchantRenderPayload(res, now = new Date()) {
|
|
|
397
348
|
const data = {
|
|
398
349
|
dateStr: getMerchantDateStr(now),
|
|
399
350
|
timeRange,
|
|
400
|
-
...buildMerchantCardItems(products,
|
|
351
|
+
...buildMerchantCardItems(products, { includeEnded: false }),
|
|
401
352
|
};
|
|
402
353
|
const fallback = buildMerchantFallbackText(TEXT.merchant, products, roundInfo);
|
|
403
354
|
return { products, roundInfo, data, fallback };
|
|
@@ -432,7 +383,7 @@ function buildTodayMerchantRenderPayload(res, now = new Date()) {
|
|
|
432
383
|
});
|
|
433
384
|
const data = {
|
|
434
385
|
dateStr: getMerchantDateStr(now),
|
|
435
|
-
...buildMerchantCardItems(products,
|
|
386
|
+
...buildMerchantCardItems(products, { includeEnded: true }),
|
|
436
387
|
};
|
|
437
388
|
const fallback = buildMerchantFallbackText(`今日远行商人 (${getChinaDateText(now)})`, products);
|
|
438
389
|
return { products, data, fallback };
|
|
@@ -472,36 +423,52 @@ function useLegacyMerchantUi(deps) {
|
|
|
472
423
|
return deps.config.merchantUiStyle === 'old';
|
|
473
424
|
}
|
|
474
425
|
function buildConfiguredMerchantRenderPayload(deps, res, now = new Date()) {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
426
|
+
return clockScope.run(new merchant_clock_1.MerchantClock(deps.config.merchantTimezone || 'Asia/Shanghai'), () => {
|
|
427
|
+
const payload = useLegacyMerchantUi(deps)
|
|
428
|
+
? buildLegacyMerchantRenderPayload(res, now)
|
|
429
|
+
: buildMerchantRenderPayload(res, now);
|
|
430
|
+
return {
|
|
431
|
+
...payload,
|
|
432
|
+
templateName: useLegacyMerchantUi(deps) ? 'yuanxing-shangren' : 'yuanxing-shangren/merchant',
|
|
433
|
+
};
|
|
434
|
+
});
|
|
482
435
|
}
|
|
483
436
|
function buildConfiguredTodayMerchantRenderPayload(deps, res, now = new Date()) {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
437
|
+
return clockScope.run(new merchant_clock_1.MerchantClock(deps.config.merchantTimezone || 'Asia/Shanghai'), () => {
|
|
438
|
+
const payload = useLegacyMerchantUi(deps)
|
|
439
|
+
? buildLegacyTodayMerchantRenderPayload(res, now)
|
|
440
|
+
: buildTodayMerchantRenderPayload(res, now);
|
|
441
|
+
return {
|
|
442
|
+
...payload,
|
|
443
|
+
templateName: useLegacyMerchantUi(deps) ? 'yuanxing-shangren' : 'yuanxing-shangren/today',
|
|
444
|
+
};
|
|
445
|
+
});
|
|
491
446
|
}
|
|
492
|
-
async function checkMerchantSubscriptions(deps) {
|
|
447
|
+
async function checkMerchantSubscriptions(deps, signal) {
|
|
493
448
|
const { ctx, client, merchantSubMgr, renderer, config } = deps;
|
|
494
|
-
const
|
|
449
|
+
const subs = merchantSubMgr.getAll();
|
|
450
|
+
if (!Object.keys(subs).length)
|
|
451
|
+
return { subscriptions: 0, matched: 0, pushed: 0, kind: 'no-subscriptions' };
|
|
452
|
+
if (signal?.aborted)
|
|
453
|
+
return { subscriptions: 0, matched: 0, pushed: 0, kind: 'cancelled' };
|
|
454
|
+
const res = await client.getMerchantInfo(ctx, true, { signal });
|
|
455
|
+
if (signal?.aborted)
|
|
456
|
+
return { subscriptions: 0, matched: 0, pushed: 0, kind: 'cancelled' };
|
|
495
457
|
if (!res)
|
|
496
|
-
return { subscriptions:
|
|
458
|
+
return { subscriptions: Object.keys(subs).length, matched: 0, pushed: 0, kind: 'error' };
|
|
497
459
|
const { products, roundInfo, data, fallback, templateName } = buildConfiguredMerchantRenderPayload(deps, res);
|
|
460
|
+
if (!products.length)
|
|
461
|
+
return { subscriptions: Object.keys(subs).length, matched: 0, pushed: 0, kind: 'empty' };
|
|
498
462
|
const productNames = products.map((p) => p.name || '').filter(Boolean);
|
|
499
|
-
const rendered = await renderer.renderHtml(ctx, templateName, data);
|
|
463
|
+
const rendered = await renderer.renderHtml(ctx, templateName, data, { signal });
|
|
500
464
|
const renderedImage = rendered ? (0, send_image_1.compressPngImage)(rendered, config) : null;
|
|
501
|
-
const subs = merchantSubMgr.getAll();
|
|
502
465
|
let matchedCount = 0;
|
|
503
466
|
let pushedCount = 0;
|
|
504
467
|
for (const [key, sub] of Object.entries(subs)) {
|
|
468
|
+
if (signal?.aborted)
|
|
469
|
+
break;
|
|
470
|
+
if (JSON.stringify(merchantSubMgr.getAll()[key]) !== JSON.stringify(sub))
|
|
471
|
+
continue;
|
|
505
472
|
const matchAll = !!sub.match_all;
|
|
506
473
|
const matched = matchAll
|
|
507
474
|
? productNames
|
|
@@ -534,8 +501,9 @@ async function checkMerchantSubscriptions(deps) {
|
|
|
534
501
|
channelId,
|
|
535
502
|
guildId: sub.group_id || '',
|
|
536
503
|
userId: sub.user_id || '',
|
|
537
|
-
|
|
538
|
-
|
|
504
|
+
selfId: sub.self_id || '',
|
|
505
|
+
}, renderedImage, fallbackText, !!sub.mention_all, signal);
|
|
506
|
+
if (!sent || signal?.aborted || JSON.stringify(merchantSubMgr.getAll()[key]) !== JSON.stringify(sub))
|
|
539
507
|
continue;
|
|
540
508
|
pushedCount++;
|
|
541
509
|
merchantSubMgr.upsert(key, {
|
|
@@ -544,12 +512,15 @@ async function checkMerchantSubscriptions(deps) {
|
|
|
544
512
|
last_matched_items: matched,
|
|
545
513
|
});
|
|
546
514
|
}
|
|
547
|
-
return { subscriptions: Object.keys(subs).length, matched: matchedCount, pushed: pushedCount };
|
|
515
|
+
return { subscriptions: Object.keys(subs).length, matched: matchedCount, pushed: pushedCount, kind: 'success' };
|
|
548
516
|
}
|
|
549
517
|
function register(deps) {
|
|
518
|
+
const merchantRunner = (0, subscription_runner_1.createRunner)(deps.ctx);
|
|
519
|
+
const merchantClock = new merchant_clock_1.MerchantClock(deps.config.merchantTimezone || 'Asia/Shanghai');
|
|
550
520
|
const { ctx, config, client, merchantSubMgr } = deps;
|
|
551
521
|
ctx.command(TEXT.merchant, '\u67e5\u770b\u8fdc\u884c\u5546\u4eba\u5546\u54c1')
|
|
552
522
|
.alias('yxsr')
|
|
523
|
+
.userFields(['authority'])
|
|
553
524
|
.action(async ({ session }) => {
|
|
554
525
|
const res = await client.getMerchantInfo(ctx, true);
|
|
555
526
|
if (!res)
|
|
@@ -559,6 +530,7 @@ function register(deps) {
|
|
|
559
530
|
await (0, send_image_1.sendImageWithFallback)(session, png, fallback, 'merchant:yuanxing-shangren', deps.config);
|
|
560
531
|
});
|
|
561
532
|
ctx.command(TEXT.todayMerchant, '查看今日远行商人全部商品')
|
|
533
|
+
.userFields(['authority'])
|
|
562
534
|
.action(async ({ session }) => {
|
|
563
535
|
const res = await client.getMerchantInfo(ctx, true);
|
|
564
536
|
if (!res)
|
|
@@ -568,10 +540,11 @@ function register(deps) {
|
|
|
568
540
|
await (0, send_image_1.sendImageWithFallback)(session, png, fallback, 'merchant:yuanxing-shangren:today', deps.config);
|
|
569
541
|
});
|
|
570
542
|
ctx.command(`${TEXT.subscribe} [args:text]`, '\u8ba2\u9605\u8fdc\u884c\u5546\u4eba\u5546\u54c1\u63d0\u9192')
|
|
543
|
+
.userFields(['authority'])
|
|
571
544
|
.action(async ({ session }, args) => {
|
|
572
545
|
const target = getSubscriptionTarget(session);
|
|
573
|
-
if (!target.privateChat && !
|
|
574
|
-
return '
|
|
546
|
+
if (!target.privateChat && !canManageGroupSubscription(deps, session))
|
|
547
|
+
return '此指令仅限群管理员或机器人管理员使用。';
|
|
575
548
|
if (target.privateChat && !config.merchantPrivateSubscriptionEnabled)
|
|
576
549
|
return '个人私聊订阅功能已被禁用,请联系机器人管理员。';
|
|
577
550
|
const parsed = parseMerchantSubscriptionArgs(args, config.merchantSubscriptionItems);
|
|
@@ -582,6 +555,7 @@ function register(deps) {
|
|
|
582
555
|
type: target.privateChat ? '个人订阅' : '群订阅',
|
|
583
556
|
channel_id: target.channelId,
|
|
584
557
|
platform: target.platform,
|
|
558
|
+
self_id: session.selfId || session.bot?.selfId || '',
|
|
585
559
|
items: parsed.items,
|
|
586
560
|
match_all: parsed.match_all,
|
|
587
561
|
mention_all: target.privateChat ? false : parsed.mention_all,
|
|
@@ -592,10 +566,11 @@ function register(deps) {
|
|
|
592
566
|
return `\u2705 \u5df2\u8ba2\u9605\u8fdc\u884c\u5546\u4eba\u5546\u54c1\uff1a${parsed.match_all ? '\u5168\u90e8\u5546\u54c1\uff08\u6bcf\u8f6e\u63a8\u9001\uff09' : parsed.items.join('\u3001')}\uff08${parsed.source}\uff09\uff1b${target.privateChat ? '个人订阅' : (parsed.mention_all ? '\u547d\u4e2d\u540e\u4f1a @\u5168\u4f53' : '\u547d\u4e2d\u540e\u4e0d @\u5168\u4f53')}`;
|
|
593
567
|
});
|
|
594
568
|
ctx.command(TEXT.viewSubscribe, '\u67e5\u770b\u5f53\u524d\u4f1a\u8bdd\u7684\u8fdc\u884c\u5546\u4eba\u8ba2\u9605')
|
|
569
|
+
.userFields(['authority'])
|
|
595
570
|
.action(async ({ session }) => {
|
|
596
571
|
const target = getSubscriptionTarget(session);
|
|
597
|
-
if (!target.privateChat && !
|
|
598
|
-
return '
|
|
572
|
+
if (!target.privateChat && !canManageGroupSubscription(deps, session))
|
|
573
|
+
return '此指令仅限群管理员或机器人管理员使用。';
|
|
599
574
|
const sub = merchantSubMgr.get(target.key);
|
|
600
575
|
const scopeName = target.privateChat ? '你' : '当前群组';
|
|
601
576
|
if (!sub)
|
|
@@ -604,48 +579,71 @@ function register(deps) {
|
|
|
604
579
|
return `${scopeName}订阅商品:${itemsText}\n提醒方式:${target.privateChat ? '私聊提醒' : (sub.mention_all ? '@全体' : '普通提醒')}`;
|
|
605
580
|
});
|
|
606
581
|
ctx.command(TEXT.unsubscribe, '\u53d6\u6d88\u8fdc\u884c\u5546\u4eba\u8ba2\u9605')
|
|
582
|
+
.userFields(['authority'])
|
|
607
583
|
.action(async ({ session }) => {
|
|
608
584
|
const target = getSubscriptionTarget(session);
|
|
609
|
-
if (!target.privateChat && !
|
|
610
|
-
return '
|
|
585
|
+
if (!target.privateChat && !canManageGroupSubscription(deps, session))
|
|
586
|
+
return '此指令仅限群管理员或机器人管理员使用。';
|
|
611
587
|
merchantSubMgr.delete(target.key);
|
|
612
588
|
return '\u2705 \u5df2\u53d6\u6d88\u8fdc\u884c\u5546\u4eba\u8ba2\u9605\u3002';
|
|
613
589
|
});
|
|
614
590
|
ctx.command('洛克').subcommand('.调试远行商人订阅', '立即执行一次远行商人订阅检查')
|
|
615
591
|
.alias('洛克调试远行商人订阅')
|
|
592
|
+
.userFields(['authority'])
|
|
616
593
|
.action(async ({ session }) => {
|
|
617
594
|
if (!isBotAdmin(session, config.adminUserIds))
|
|
618
595
|
return '此指令仅限管理员使用。';
|
|
619
|
-
const result = await checkMerchantSubscriptions(deps);
|
|
596
|
+
const result = await merchantRunner(signal => checkMerchantSubscriptions(deps, signal));
|
|
597
|
+
if (!result)
|
|
598
|
+
return '订阅检查正在运行或插件已停止。';
|
|
620
599
|
return `远行商人订阅检查完成:订阅 ${result.subscriptions} 条,命中 ${result.matched} 条,推送 ${result.pushed} 条。`;
|
|
621
600
|
});
|
|
601
|
+
const roundKey = () => {
|
|
602
|
+
const p = merchantClock.parts();
|
|
603
|
+
return p.date + ':' + Math.floor(p.hour / 4);
|
|
604
|
+
};
|
|
605
|
+
const runMerchantCheck = () => merchantRunner(async (signal) => {
|
|
606
|
+
const round = roundKey();
|
|
607
|
+
for (let retry = 0; retry <= 3; retry++) {
|
|
608
|
+
if (signal.aborted || (retry > 0 && roundKey() !== round))
|
|
609
|
+
return;
|
|
610
|
+
if (!Object.keys(merchantSubMgr.getAll()).length)
|
|
611
|
+
return;
|
|
612
|
+
const result = await checkMerchantSubscriptions(deps, signal);
|
|
613
|
+
if (result.kind !== 'empty' || retry === 3)
|
|
614
|
+
return;
|
|
615
|
+
await (0, async_control_1.delay)(240000 + Math.floor(Math.random() * 60001) - 30000, signal);
|
|
616
|
+
}
|
|
617
|
+
}).catch(err => { if (!merchantRunner.aborted())
|
|
618
|
+
logger.warn('远行商人订阅检查失败: ' + err); });
|
|
622
619
|
if (config.merchantSubscriptionEnabled) {
|
|
623
|
-
// 上游 v3.1.0:检查前加入 ±30s 随机延迟,降低刷新窗口和并发请求冲突。
|
|
624
|
-
const MERCHANT_JITTER_MS = 30000;
|
|
625
|
-
const runCheckWithJitter = () => {
|
|
626
|
-
const jitter = Math.floor(Math.random() * MERCHANT_JITTER_MS);
|
|
627
|
-
setTimeout(() => {
|
|
628
|
-
checkMerchantSubscriptions(deps).catch(err => logger.warn(`远行商人订阅检查失败: ${err}`));
|
|
629
|
-
}, jitter);
|
|
630
|
-
};
|
|
631
620
|
if (config.merchantCheckMode === 'times' && config.merchantCheckTimes.length > 0) {
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
621
|
+
const times = config.merchantCheckTimes.filter(value => /^([01]\d|2[0-3]):[0-5]\d$/.test(value));
|
|
622
|
+
let lastWallKey = '';
|
|
623
|
+
const schedule = (after = Date.now()) => {
|
|
624
|
+
if (merchantRunner.aborted())
|
|
625
|
+
return;
|
|
626
|
+
const target = merchantClock.nextTimeFor(times, after);
|
|
627
|
+
if (target === null)
|
|
637
628
|
return;
|
|
638
|
-
const
|
|
639
|
-
if (
|
|
629
|
+
const wallKey = merchantClock.date(target) + ':' + merchantClock.timeText(target);
|
|
630
|
+
if (wallKey === lastWallKey) {
|
|
631
|
+
schedule(target);
|
|
640
632
|
return;
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
633
|
+
}
|
|
634
|
+
const jitter = Math.floor(Math.random() * 60001) - 30000;
|
|
635
|
+
ctx.setTimeout(() => {
|
|
636
|
+
lastWallKey = wallKey;
|
|
637
|
+
void runMerchantCheck();
|
|
638
|
+
schedule(target);
|
|
639
|
+
}, Math.max(0, target + jitter - Date.now()));
|
|
640
|
+
};
|
|
641
|
+
schedule();
|
|
644
642
|
}
|
|
645
643
|
else {
|
|
646
|
-
ctx.setInterval(
|
|
647
|
-
|
|
648
|
-
}, config.merchantCheckInterval);
|
|
644
|
+
ctx.setInterval(() => {
|
|
645
|
+
ctx.setTimeout(() => { void runMerchantCheck(); }, Math.floor(Math.random() * 30001));
|
|
646
|
+
}, Math.max(30000, config.merchantCheckInterval || 300000));
|
|
649
647
|
}
|
|
650
648
|
}
|
|
651
649
|
}
|