koishi-plugin-rocom 1.0.10 → 1.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +21 -5
- package/lib/commands/query.js +39 -0
- 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 +5300 -143
- 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.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
|
@@ -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
|
|
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
|
-
},
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
}
|
package/lib/commands/query.js
CHANGED
|
@@ -1153,6 +1153,45 @@ function buildFriendshipRenderData(payload, userIds) {
|
|
|
1153
1153
|
copyright: 'Koishi & WeGame Locke Kingdom Plugin',
|
|
1154
1154
|
};
|
|
1155
1155
|
}
|
|
1156
|
+
function buildStudentRenderData(statePayload, perksPayload, area, accountType) {
|
|
1157
|
+
const school = statePayload?.school || statePayload?.school_name || '未返回';
|
|
1158
|
+
const certified = String(statePayload?.certified) === '1';
|
|
1159
|
+
const cards = perksPayload?.cards || [];
|
|
1160
|
+
return {
|
|
1161
|
+
title: '洛克学生',
|
|
1162
|
+
subtitle: `大区:${area} 账号类型:${accountTypeText(accountType)}`,
|
|
1163
|
+
heroTitle: '学生信息总览',
|
|
1164
|
+
heroValue: certified ? '已通过' : '未认证',
|
|
1165
|
+
heroSubvalue: school,
|
|
1166
|
+
summaryCards: [
|
|
1167
|
+
{ label: '认证状态', value: certified ? '已认证' : '未认证' },
|
|
1168
|
+
{ label: '学校', value: school },
|
|
1169
|
+
{ label: '奖励数量', value: String(cards.length) },
|
|
1170
|
+
],
|
|
1171
|
+
stateItems: [
|
|
1172
|
+
{ label: '学生认证', value: certified ? '是' : '否' },
|
|
1173
|
+
{ label: '游戏内认证', value: String(statePayload?.game_certified) === '1' ? '是' : '否' },
|
|
1174
|
+
{ label: '学校', value: school },
|
|
1175
|
+
{ label: '上游状态', value: statePayload?.result?.error_message || 'WG_COMM_SUCC' },
|
|
1176
|
+
{ label: '上游错误码', value: stringifyInspectValue(statePayload?.result?.error_code || 0) },
|
|
1177
|
+
],
|
|
1178
|
+
perkCards: cards.map((card) => ({
|
|
1179
|
+
name: card.name || `奖励 #${card.id || '-'}`,
|
|
1180
|
+
count: card.count || 0,
|
|
1181
|
+
desc: card.desc || '暂无说明',
|
|
1182
|
+
icon: card.icon || '',
|
|
1183
|
+
id: stringifyInspectValue(card.id),
|
|
1184
|
+
stateText: `状态码 ${stringifyInspectValue(card.state)}`,
|
|
1185
|
+
})),
|
|
1186
|
+
detailItems: Object.entries(perksPayload || {})
|
|
1187
|
+
.filter(([key, value]) => !['cards', 'result'].includes(key) && (value === null || typeof value !== 'object'))
|
|
1188
|
+
.map(([key, value]) => ({ label: key.replace(/_/g, ' '), value: stringifyInspectValue(value) })),
|
|
1189
|
+
stateResult: statePayload?.result?.error_message || 'WG_COMM_SUCC',
|
|
1190
|
+
perksResult: perksPayload?.result?.error_message || 'WG_COMM_SUCC',
|
|
1191
|
+
commandHint: '洛克.学生 [area] [account_type]',
|
|
1192
|
+
copyright: 'Koishi & WeGame Locke Kingdom Plugin',
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1156
1195
|
function sessionTarget(session) {
|
|
1157
1196
|
return {
|
|
1158
1197
|
platform: session?.platform || session?.bot?.platform || '',
|
|
@@ -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(/ /g, ' ')
|
|
18
|
+
.replace(/&/g, '&')
|
|
19
|
+
.replace(/</g, '<')
|
|
20
|
+
.replace(/>/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
|
+
}
|
package/lib/egg-service.d.ts
CHANGED
|
@@ -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
|
}
|