koishi-plugin-leaf-60s-api 0.2.16
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/index.d.ts +25 -0
- package/lib/index.js +1373 -0
- package/package.json +28 -0
- package/readme.md +14 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,1373 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Config = exports.inject = exports.name = void 0;
|
|
4
|
+
exports.apply = apply;
|
|
5
|
+
const koishi_1 = require("koishi");
|
|
6
|
+
exports.name = 'leaf-60s-api';
|
|
7
|
+
exports.inject = ['database'];
|
|
8
|
+
exports.Config = koishi_1.Schema.intersect([
|
|
9
|
+
koishi_1.Schema.object({
|
|
10
|
+
apiBaseUrl: koishi_1.Schema.string().default('http://172.0.0.1:4399').description('60s 服务 URL(不含 /v2 路径)'),
|
|
11
|
+
cooldownTime: koishi_1.Schema.number().default(30).min(5).max(300).description('命令冷却时间(秒),用户手动触发命令的间隔限制'),
|
|
12
|
+
scheduleCooldown: koishi_1.Schema.number().default(86400).min(0).description('定时任务防重发冷却时间(秒),0表示不限制。默认86400秒=24小时'),
|
|
13
|
+
enableLog: koishi_1.Schema.boolean().default(true).description('启用日志记录'),
|
|
14
|
+
scheduleWhitelist: koishi_1.Schema.array(String).default([]).description('定时发送群组白名单频道ID列表(格式: platform:channelId,如 onebot:123456)'),
|
|
15
|
+
fuelDefaultRegion: koishi_1.Schema.string().default('广西').description('今日油价默认地区')
|
|
16
|
+
}).description('基础设置'),
|
|
17
|
+
koishi_1.Schema.object({
|
|
18
|
+
enableSchedule: koishi_1.Schema.boolean().default(false).description('启用定时发送新闻'),
|
|
19
|
+
scheduleTime: koishi_1.Schema.string().default('08:00').description('定时发送时间 (格式: HH:MM 或 HH:MM / Nd,如 08:00 或 08:00 / 1d,表示每天或每N天的固定时间)'),
|
|
20
|
+
useForward: koishi_1.Schema.boolean().default(false).description('是否使用合并转发(仅QQ平台效果最佳)')
|
|
21
|
+
}).description('每日新闻'),
|
|
22
|
+
koishi_1.Schema.object({
|
|
23
|
+
enableAiNewsSchedule: koishi_1.Schema.boolean().default(false).description('启用AI快报定时发送(仅当天)'),
|
|
24
|
+
aiNewsScheduleTime: koishi_1.Schema.string().default('22:00').description('AI快报定时发送时间 (格式: HH:MM 或 HH:MM / Nd,如 22:00 或 22:00 / 1d)'),
|
|
25
|
+
aiUseForward: koishi_1.Schema.boolean().default(false).description('AI快报是否使用合并转发(仅QQ平台效果最佳)')
|
|
26
|
+
}).description('AI快报'),
|
|
27
|
+
koishi_1.Schema.object({
|
|
28
|
+
enableMoyuSchedule: koishi_1.Schema.boolean().default(false).description('启用摸鱼日报定时发送'),
|
|
29
|
+
moyuScheduleTime: koishi_1.Schema.string().default('10:00').description('摸鱼日报定时发送时间 (格式: HH:MM 或 HH:MM / Nd,如 10:00 或 10:00 / 1d)'),
|
|
30
|
+
}).description('摸鱼日报'),
|
|
31
|
+
koishi_1.Schema.object({
|
|
32
|
+
enableGoldSchedule: koishi_1.Schema.boolean().default(false).description('启用今日金价定时发送'),
|
|
33
|
+
goldScheduleTime: koishi_1.Schema.string().default('09:00').description('今日金价定时发送时间 (格式: HH:MM 或 HH:MM / Nd,如 09:00 或 09:00 / 1d)'),
|
|
34
|
+
}).description('今日金价'),
|
|
35
|
+
koishi_1.Schema.object({
|
|
36
|
+
enableFuelSchedule: koishi_1.Schema.boolean().default(false).description('启用今日油价定时发送'),
|
|
37
|
+
fuelScheduleTime: koishi_1.Schema.string().default('09:30').description('今日油价定时发送时间 (格式: HH:MM 或 HH:MM / Nd,如 09:30 或 09:30 / 1d)')
|
|
38
|
+
}).description('今日油价')
|
|
39
|
+
]);
|
|
40
|
+
function apply(ctx, config) {
|
|
41
|
+
const logger = ctx.logger('leaf-60s-api');
|
|
42
|
+
const normalizedApiBaseUrl = (config.apiBaseUrl || 'http://172.0.0.1:4399').replace(/\/$/, '');
|
|
43
|
+
const buildApiUrl = (path) => `${normalizedApiBaseUrl}${path}`;
|
|
44
|
+
const cooldowns = new Map();
|
|
45
|
+
let scheduleTimeout = null;
|
|
46
|
+
let aiNewsScheduleTimeout = null;
|
|
47
|
+
let moyuScheduleTimeout = null;
|
|
48
|
+
let goldScheduleTimeout = null;
|
|
49
|
+
let fuelScheduleTimeout = null;
|
|
50
|
+
// 记录上次发送时间戳,用于冷却控制(毫秒)
|
|
51
|
+
const lastSentTime = {
|
|
52
|
+
news: 0,
|
|
53
|
+
aiNews: 0,
|
|
54
|
+
moyu: 0,
|
|
55
|
+
gold: 0,
|
|
56
|
+
fuel: 0
|
|
57
|
+
};
|
|
58
|
+
// 检查是否在冷却时间内
|
|
59
|
+
function isInCooldown(type) {
|
|
60
|
+
// scheduleCooldown 为 0 表示不限制
|
|
61
|
+
if (config.scheduleCooldown <= 0) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
const lastTime = lastSentTime[type];
|
|
66
|
+
const cooldownMs = config.scheduleCooldown * 1000;
|
|
67
|
+
return now - lastTime < cooldownMs;
|
|
68
|
+
}
|
|
69
|
+
// 获取剩余冷却时间(秒)
|
|
70
|
+
function getRemainingCooldown(type) {
|
|
71
|
+
if (config.scheduleCooldown <= 0) {
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
const now = Date.now();
|
|
75
|
+
const lastTime = lastSentTime[type];
|
|
76
|
+
const cooldownMs = config.scheduleCooldown * 1000;
|
|
77
|
+
const remaining = Math.ceil((lastTime + cooldownMs - now) / 1000);
|
|
78
|
+
return Math.max(0, remaining);
|
|
79
|
+
}
|
|
80
|
+
// 获取今日日期字符串 YYYY-MM-DD
|
|
81
|
+
function getTodayString() {
|
|
82
|
+
const now = new Date();
|
|
83
|
+
const year = now.getFullYear();
|
|
84
|
+
const month = `${now.getMonth() + 1}`.padStart(2, '0');
|
|
85
|
+
const day = `${now.getDate()}`.padStart(2, '0');
|
|
86
|
+
return `${year}-${month}-${day}`;
|
|
87
|
+
}
|
|
88
|
+
async function resolveGroupScheduleChannels(whitelist, tag) {
|
|
89
|
+
try {
|
|
90
|
+
if (!whitelist.length) {
|
|
91
|
+
logInfo('60s API: 未配置定时发送白名单', { tag });
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
// 白名单兼容:
|
|
95
|
+
// - 支持 platform:channelId(推荐)
|
|
96
|
+
// - 兼容仅填写 channelId(例如 onebot 群号)
|
|
97
|
+
// - 兼容常见尾缀(例如 onebot:123456@group)
|
|
98
|
+
const normalize = (raw) => {
|
|
99
|
+
const value = String(raw || '').trim();
|
|
100
|
+
const withoutAt = value.includes('@') ? value.split('@')[0] : value;
|
|
101
|
+
const [platform, id] = withoutAt.includes(':') ? withoutAt.split(':', 2) : ['', withoutAt];
|
|
102
|
+
return {
|
|
103
|
+
raw: value,
|
|
104
|
+
withoutAt,
|
|
105
|
+
platform,
|
|
106
|
+
id,
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
const whitelistNormalized = whitelist.map(normalize).filter((item) => item.withoutAt);
|
|
110
|
+
const whitelistKeys = new Set();
|
|
111
|
+
whitelistNormalized.forEach((item) => {
|
|
112
|
+
whitelistKeys.add(item.raw);
|
|
113
|
+
whitelistKeys.add(item.withoutAt);
|
|
114
|
+
if (item.id)
|
|
115
|
+
whitelistKeys.add(item.id);
|
|
116
|
+
});
|
|
117
|
+
const assigned = await ctx.database.getAssignedChannels(['id', 'platform', 'guildId']);
|
|
118
|
+
logInfo('60s API: 获取到的频道列表', {
|
|
119
|
+
tag,
|
|
120
|
+
count: assigned.length,
|
|
121
|
+
channels: assigned.map((c) => ({ platform: c.platform, id: c.id, guildId: c.guildId })),
|
|
122
|
+
});
|
|
123
|
+
const groupChannels = assigned
|
|
124
|
+
.filter((channel) => !!channel.guildId)
|
|
125
|
+
.map((channel) => ({
|
|
126
|
+
platform: channel.platform,
|
|
127
|
+
id: channel.id,
|
|
128
|
+
key: `${channel.platform}:${channel.id}`,
|
|
129
|
+
}));
|
|
130
|
+
if (!groupChannels.length) {
|
|
131
|
+
logInfo('60s API: 没有可发送的群组频道', { tag });
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
logInfo('60s API: 群组频道列表', { tag, channels: groupChannels.map((c) => c.key) });
|
|
135
|
+
logInfo('60s API: 白名单配置(原始)', { tag, whitelist });
|
|
136
|
+
logInfo('60s API: 白名单配置(规范化)', {
|
|
137
|
+
tag,
|
|
138
|
+
whitelist: whitelistNormalized.map((w) => ({ raw: w.raw, withoutAt: w.withoutAt, id: w.id })),
|
|
139
|
+
});
|
|
140
|
+
const targets = groupChannels
|
|
141
|
+
.filter((c) => whitelistKeys.has(c.key) || whitelistKeys.has(c.id))
|
|
142
|
+
.map((c) => c.key);
|
|
143
|
+
if (!targets.length) {
|
|
144
|
+
logInfo('60s API: 白名单未命中任何已加入群组频道', {
|
|
145
|
+
tag,
|
|
146
|
+
groupChannels: groupChannels.map((c) => c.key),
|
|
147
|
+
whitelist,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
logInfo('60s API: 白名单匹配成功', { tag, targets });
|
|
152
|
+
}
|
|
153
|
+
return targets;
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
logError('60s API: 获取群组频道失败', { tag, error });
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// 日志函数
|
|
161
|
+
function logInfo(message, data) {
|
|
162
|
+
if (config.enableLog && logger) {
|
|
163
|
+
logger.info(message, data);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function logError(message, error) {
|
|
167
|
+
if (config.enableLog && logger) {
|
|
168
|
+
logger.error(message, error);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// 检查冷却时间
|
|
172
|
+
function checkCooldown(userId) {
|
|
173
|
+
const now = Date.now();
|
|
174
|
+
const lastTime = cooldowns.get(userId) || 0;
|
|
175
|
+
const timeLeft = Math.ceil((lastTime + config.cooldownTime * 1000 - now) / 1000);
|
|
176
|
+
if (timeLeft > 0) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
cooldowns.set(userId, now);
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
// 获取60秒新闻图片
|
|
183
|
+
async function get60sNewsImage() {
|
|
184
|
+
try {
|
|
185
|
+
logInfo('60s API: 开始获取新闻图片');
|
|
186
|
+
const response = await ctx.http.get(buildApiUrl('/v2/60s'), {
|
|
187
|
+
params: {
|
|
188
|
+
encoding: 'image'
|
|
189
|
+
},
|
|
190
|
+
timeout: 30000,
|
|
191
|
+
responseType: 'arraybuffer'
|
|
192
|
+
});
|
|
193
|
+
const buffer = Buffer.from(response);
|
|
194
|
+
logInfo('60s API: 获取新闻图片成功', {
|
|
195
|
+
size: buffer.length
|
|
196
|
+
});
|
|
197
|
+
return buffer;
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
logError('60s API: 获取新闻图片失败', error);
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// 获取历史上的今天
|
|
205
|
+
async function getTodayInHistory() {
|
|
206
|
+
try {
|
|
207
|
+
logInfo('60s API: 开始获取历史上的今天');
|
|
208
|
+
const response = await ctx.http.get(buildApiUrl('/v2/today-in-history'), {
|
|
209
|
+
params: {
|
|
210
|
+
encoding: 'json'
|
|
211
|
+
},
|
|
212
|
+
timeout: 30000
|
|
213
|
+
});
|
|
214
|
+
logInfo('60s API: 获取历史上的今天成功', {
|
|
215
|
+
code: response.code,
|
|
216
|
+
hasData: !!response.data,
|
|
217
|
+
itemsCount: response.data?.items?.length || 0
|
|
218
|
+
});
|
|
219
|
+
return response;
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
logError('60s API: 获取历史上的今天失败', error);
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// 获取知乎话题榜
|
|
227
|
+
async function getZhihuTrends() {
|
|
228
|
+
try {
|
|
229
|
+
logInfo('60s API: 开始获取知乎话题榜');
|
|
230
|
+
const response = await ctx.http.get(buildApiUrl('/v2/zhihu'), {
|
|
231
|
+
params: {
|
|
232
|
+
encoding: 'json'
|
|
233
|
+
},
|
|
234
|
+
timeout: 30000
|
|
235
|
+
});
|
|
236
|
+
logInfo('60s API: 获取知乎话题榜成功', {
|
|
237
|
+
code: response.code,
|
|
238
|
+
hasData: !!response.data,
|
|
239
|
+
topicsCount: response.data?.length || 0
|
|
240
|
+
});
|
|
241
|
+
return response;
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
logError('60s API: 获取知乎话题榜失败', error);
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// 获取AI快报
|
|
249
|
+
async function getAiNews(params) {
|
|
250
|
+
try {
|
|
251
|
+
logInfo('60s API: 开始获取AI快报', params);
|
|
252
|
+
const response = await ctx.http.get(buildApiUrl('/v2/ai-news'), {
|
|
253
|
+
params: {
|
|
254
|
+
date: params.date,
|
|
255
|
+
all: params.all,
|
|
256
|
+
encoding: params.encoding || 'text'
|
|
257
|
+
},
|
|
258
|
+
timeout: 30000
|
|
259
|
+
});
|
|
260
|
+
logInfo('60s API: 获取AI快报成功');
|
|
261
|
+
return response;
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
logError('60s API: 获取AI快报失败', error);
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
async function getMoyuDaily(encoding = 'text') {
|
|
269
|
+
try {
|
|
270
|
+
logInfo('60s API: 开始获取摸鱼日报', { encoding });
|
|
271
|
+
const response = await ctx.http.get(buildApiUrl('/v2/moyu'), {
|
|
272
|
+
params: {
|
|
273
|
+
encoding
|
|
274
|
+
},
|
|
275
|
+
timeout: 30000
|
|
276
|
+
});
|
|
277
|
+
logInfo('60s API: 获取摸鱼日报成功');
|
|
278
|
+
return response;
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
logError('60s API: 获取摸鱼日报失败', error);
|
|
282
|
+
throw error;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
async function getGoldPrice(encoding = 'text') {
|
|
286
|
+
try {
|
|
287
|
+
logInfo('60s API: 开始获取今日金价', { encoding });
|
|
288
|
+
const response = await ctx.http.get(buildApiUrl('/v2/gold-price'), {
|
|
289
|
+
params: {
|
|
290
|
+
encoding
|
|
291
|
+
},
|
|
292
|
+
timeout: 30000
|
|
293
|
+
});
|
|
294
|
+
logInfo('60s API: 获取今日金价成功');
|
|
295
|
+
return response;
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
logError('60s API: 获取今日金价失败', error);
|
|
299
|
+
throw error;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
async function getFuelPrice(params) {
|
|
303
|
+
try {
|
|
304
|
+
logInfo('60s API: 开始获取今日油价', params);
|
|
305
|
+
const response = await ctx.http.get(buildApiUrl('/v2/fuel-price'), {
|
|
306
|
+
params: {
|
|
307
|
+
region: params.region,
|
|
308
|
+
encoding: params.encoding || 'text'
|
|
309
|
+
},
|
|
310
|
+
timeout: 30000
|
|
311
|
+
});
|
|
312
|
+
logInfo('60s API: 获取今日油价成功');
|
|
313
|
+
return response;
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
logError('60s API: 获取今日油价失败', error);
|
|
317
|
+
throw error;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// 发送新闻到指定频道
|
|
321
|
+
async function sendNewsToChannels() {
|
|
322
|
+
const targetChannels = await resolveGroupScheduleChannels(config.scheduleWhitelist, 'news');
|
|
323
|
+
if (targetChannels.length === 0) {
|
|
324
|
+
logInfo('60s API: 没有目标频道,跳过新闻发送');
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
try {
|
|
328
|
+
const imageBuffer = await get60sNewsImage();
|
|
329
|
+
const imageMessage = koishi_1.h.image(imageBuffer, 'image/png');
|
|
330
|
+
for (const channelId of targetChannels) {
|
|
331
|
+
try {
|
|
332
|
+
await ctx.broadcast([channelId], imageMessage);
|
|
333
|
+
logInfo('60s API: 定时发送新闻成功', { channelId });
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
logError('60s API: 定时发送新闻到频道失败', { channelId, error });
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
logError('60s API: 定时发送新闻失败', error);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function formatAiNewsText(data) {
|
|
345
|
+
const items = normalizeAiNewsData(data).flatMap((item) => item.news);
|
|
346
|
+
return items.map((newsItem, index) => `${index + 1}. ${newsItem.title}\n${newsItem.link}`).join('\n\n');
|
|
347
|
+
}
|
|
348
|
+
function formatAiNewsMarkdown(data) {
|
|
349
|
+
const items = normalizeAiNewsData(data).flatMap((item) => item.news);
|
|
350
|
+
return items.map((newsItem) => `- [${newsItem.title}](${newsItem.link})`).join('\n');
|
|
351
|
+
}
|
|
352
|
+
function toTitleLinkData(data) {
|
|
353
|
+
return normalizeAiNewsData(data).map((item) => ({
|
|
354
|
+
date: item.date,
|
|
355
|
+
news: item.news.map((newsItem) => ({
|
|
356
|
+
title: newsItem.title,
|
|
357
|
+
detail: '',
|
|
358
|
+
link: newsItem.link,
|
|
359
|
+
source: '',
|
|
360
|
+
date: newsItem.date,
|
|
361
|
+
})),
|
|
362
|
+
}));
|
|
363
|
+
}
|
|
364
|
+
function formatDate(date) {
|
|
365
|
+
const year = date.getFullYear();
|
|
366
|
+
const month = `${date.getMonth() + 1}`.padStart(2, '0');
|
|
367
|
+
const day = `${date.getDate()}`.padStart(2, '0');
|
|
368
|
+
return `${year}-${month}-${day}`;
|
|
369
|
+
}
|
|
370
|
+
function normalizeAiNewsData(data) {
|
|
371
|
+
return Array.isArray(data) ? data : [data];
|
|
372
|
+
}
|
|
373
|
+
function getRecentAiNewsDates() {
|
|
374
|
+
const now = new Date();
|
|
375
|
+
const yesterday = new Date(now);
|
|
376
|
+
yesterday.setDate(now.getDate() - 1);
|
|
377
|
+
return [formatDate(now), formatDate(yesterday)];
|
|
378
|
+
}
|
|
379
|
+
async function fetchAiNewsByDates(dates) {
|
|
380
|
+
const responses = await Promise.all(dates.map((date) => getAiNews({ date, encoding: 'json' })));
|
|
381
|
+
const items = [];
|
|
382
|
+
responses.forEach((response, index) => {
|
|
383
|
+
if (response.code !== 200 || !response.data) {
|
|
384
|
+
logError('60s API: AI快报返回错误', { code: response.code, message: response.message, date: dates[index] });
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
items.push(...normalizeAiNewsData(response.data));
|
|
388
|
+
});
|
|
389
|
+
return items.sort((a, b) => b.date.localeCompare(a.date));
|
|
390
|
+
}
|
|
391
|
+
async function sendAiNewsToChannels() {
|
|
392
|
+
const targetChannels = await resolveGroupScheduleChannels(config.scheduleWhitelist, 'ai-news');
|
|
393
|
+
if (targetChannels.length === 0) {
|
|
394
|
+
logInfo('60s API: 没有目标频道,跳过AI快报发送');
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
try {
|
|
398
|
+
const response = await getAiNews({ encoding: 'json' });
|
|
399
|
+
if (response.code !== 200 || !response.data) {
|
|
400
|
+
logError('60s API: AI快报返回错误', { code: response.code, message: response.message });
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const message = formatAiNewsText(response.data);
|
|
404
|
+
for (const channelId of targetChannels) {
|
|
405
|
+
try {
|
|
406
|
+
const output = config.aiUseForward ? (0, koishi_1.h)('figure', {}, [message]) : message;
|
|
407
|
+
await ctx.broadcast([channelId], output);
|
|
408
|
+
logInfo('60s API: 定时发送AI快报成功', { channelId });
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
logError('60s API: 定时发送AI快报到频道失败', { channelId, error });
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
catch (error) {
|
|
416
|
+
logError('60s API: 定时发送AI快报失败', error);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function formatMoyuText(data) {
|
|
420
|
+
const lunar = `${data.date.lunar.yearCN}年${data.date.lunar.monthCN}${data.date.lunar.dayCN}`;
|
|
421
|
+
const festival = data.today.lunarFestivals.length ? `节日:${data.today.lunarFestivals.join('、')}` : '节日:无';
|
|
422
|
+
const holidayName = data.today.holidayName ? `假期:${data.today.holidayName}` : '假期:无';
|
|
423
|
+
const solarTerm = data.today.solarTerm ? `节气:${data.today.solarTerm}` : '节气:无';
|
|
424
|
+
const lines = [
|
|
425
|
+
`🍱 摸鱼日报 ${data.date.gregorian} ${data.date.weekday}`,
|
|
426
|
+
`农历:${lunar}`,
|
|
427
|
+
`${holidayName} | ${solarTerm} | ${festival}`,
|
|
428
|
+
`进度:周 ${data.progress.week.percentage}% / 月 ${data.progress.month.percentage}% / 年 ${data.progress.year.percentage}%`,
|
|
429
|
+
`倒计时:周末 ${data.countdown.toWeekEnd} 天 | 周五 ${data.countdown.toFriday} 天 | 月末 ${data.countdown.toMonthEnd} 天 | 年末 ${data.countdown.toYearEnd} 天`,
|
|
430
|
+
`下个周末:${data.nextWeekend.date}(${data.nextWeekend.weekday})还剩 ${data.nextWeekend.daysUntil} 天`,
|
|
431
|
+
`下个假期:${data.nextHoliday.name} ${data.nextHoliday.date}(${data.nextHoliday.until} 天后)`,
|
|
432
|
+
`摸鱼语录:${data.moyuQuote}`
|
|
433
|
+
];
|
|
434
|
+
return lines.join('\n');
|
|
435
|
+
}
|
|
436
|
+
async function sendMoyuToChannels() {
|
|
437
|
+
const targetChannels = await resolveGroupScheduleChannels(config.scheduleWhitelist, 'moyu');
|
|
438
|
+
if (targetChannels.length === 0) {
|
|
439
|
+
logInfo('60s API: 没有目标频道,跳过摸鱼日报发送');
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
try {
|
|
443
|
+
const response = await getMoyuDaily('json');
|
|
444
|
+
if (response.code !== 200 || !response.data) {
|
|
445
|
+
logError('60s API: 摸鱼日报返回错误', { code: response.code, message: response.message });
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const message = formatMoyuText(response.data);
|
|
449
|
+
for (const channelId of targetChannels) {
|
|
450
|
+
try {
|
|
451
|
+
await ctx.broadcast([channelId], message);
|
|
452
|
+
logInfo('60s API: 定时发送摸鱼日报成功', { channelId });
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
logError('60s API: 定时发送摸鱼日报到频道失败', { channelId, error });
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
catch (error) {
|
|
460
|
+
logError('60s API: 定时发送摸鱼日报失败', error);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function formatGoldText(data) {
|
|
464
|
+
const targetItems = [
|
|
465
|
+
{ key: '今日金价', label: '今日金价' },
|
|
466
|
+
{ key: '黄金_9999', label: '黄金9999' },
|
|
467
|
+
{ key: '黄金_T+D', label: '黄金T+D' },
|
|
468
|
+
{ key: '伦敦金', label: '伦敦金' },
|
|
469
|
+
{ key: '白银价格', label: '白银价格' },
|
|
470
|
+
{ key: '铂金价格', label: '铂金价格' },
|
|
471
|
+
{ key: '钯金价格', label: '钯金价格' },
|
|
472
|
+
];
|
|
473
|
+
const goldItem = data.metals.find(m => m.name === '今日金价');
|
|
474
|
+
const quoteTime = goldItem?.updated || data.date;
|
|
475
|
+
const header = `💰 今日金价 ${quoteTime}`;
|
|
476
|
+
const lines = [header];
|
|
477
|
+
for (const target of targetItems) {
|
|
478
|
+
const item = data.metals.find(m => m.name === target.key || m.name.includes(target.key));
|
|
479
|
+
if (item) {
|
|
480
|
+
lines.push(`${target.label}: ${item.today_price}${item.unit}`);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (lines.length <= 1) {
|
|
484
|
+
return `${header}\n暂无数据`;
|
|
485
|
+
}
|
|
486
|
+
return lines.join('\n');
|
|
487
|
+
}
|
|
488
|
+
function formatFuelText(data) {
|
|
489
|
+
const items = data.items.map((item, index) => {
|
|
490
|
+
return `${index + 1}. ${item.name} ${item.price_desc}`;
|
|
491
|
+
});
|
|
492
|
+
const lines = [
|
|
493
|
+
`⛽ 今日油价 ${data.region}`,
|
|
494
|
+
...items,
|
|
495
|
+
];
|
|
496
|
+
if (data.trend) {
|
|
497
|
+
const directionIcon = data.trend.direction === '上调' ? '📈' : data.trend.direction === '下调' ? '📉' : '➡️';
|
|
498
|
+
lines.push(`${directionIcon} 下次调价: ${data.trend.next_adjustment_date} 预计${data.trend.direction}`);
|
|
499
|
+
if (data.trend.change_liter_desc) {
|
|
500
|
+
lines.push(`预计变动: ${data.trend.change_liter_desc}`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
lines.push(`更新时间: ${data.updated}`);
|
|
504
|
+
return lines.join('\n');
|
|
505
|
+
}
|
|
506
|
+
async function sendGoldToChannels() {
|
|
507
|
+
const targetChannels = await resolveGroupScheduleChannels(config.scheduleWhitelist, 'gold');
|
|
508
|
+
if (targetChannels.length === 0) {
|
|
509
|
+
logInfo('60s API: 没有目标频道,跳过金价发送');
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
try {
|
|
513
|
+
const response = await getGoldPrice('json');
|
|
514
|
+
if (response.code !== 200 || !response.data) {
|
|
515
|
+
logError('60s API: 今日金价返回错误', { code: response.code, message: response.message });
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
const message = formatGoldText(response.data);
|
|
519
|
+
for (const channelId of targetChannels) {
|
|
520
|
+
try {
|
|
521
|
+
await ctx.broadcast([channelId], message);
|
|
522
|
+
logInfo('60s API: 定时发送今日金价成功', { channelId });
|
|
523
|
+
}
|
|
524
|
+
catch (error) {
|
|
525
|
+
logError('60s API: 定时发送今日金价到频道失败', { channelId, error });
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
catch (error) {
|
|
530
|
+
logError('60s API: 定时发送今日金价失败', error);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
async function sendFuelToChannels() {
|
|
534
|
+
const targetChannels = await resolveGroupScheduleChannels(config.scheduleWhitelist, 'fuel');
|
|
535
|
+
if (targetChannels.length === 0) {
|
|
536
|
+
logInfo('60s API: 没有目标频道,跳过油价发送');
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
try {
|
|
540
|
+
const response = await getFuelPrice({ region: config.fuelDefaultRegion, encoding: 'json' });
|
|
541
|
+
if (response.code !== 200 || !response.data) {
|
|
542
|
+
logError('60s API: 今日油价返回错误', { code: response.code, message: response.message });
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
const message = formatFuelText(response.data);
|
|
546
|
+
for (const channelId of targetChannels) {
|
|
547
|
+
try {
|
|
548
|
+
await ctx.broadcast([channelId], message);
|
|
549
|
+
logInfo('60s API: 定时发送今日油价成功', { channelId });
|
|
550
|
+
}
|
|
551
|
+
catch (error) {
|
|
552
|
+
logError('60s API: 定时发送今日油价到频道失败', { channelId, error });
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
catch (error) {
|
|
557
|
+
logError('60s API: 定时发送今日油价失败', error);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
// 计算到下一个指定时间的毫秒数
|
|
561
|
+
// 支持格式: HH:MM 或 HH:MM / Nd (如 08:00 或 08:00 / 1d)
|
|
562
|
+
// 返回 null 表示时间格式错误,应该跳过今天
|
|
563
|
+
function getMsUntilNextTime(timeStr) {
|
|
564
|
+
const trimmedTimeStr = timeStr.trim();
|
|
565
|
+
// 解析时间格式,支持 HH:MM 或 HH:MM / Nd
|
|
566
|
+
// 格式: HH:MM / Nd 表示每 N 天的 HH:MM 执行
|
|
567
|
+
const fullPattern = /^([0-1]?\d|2[0-3]):([0-5]\d)\s*(?:\/\s*(\d+)\s*d)?$/;
|
|
568
|
+
const match = trimmedTimeStr.match(fullPattern);
|
|
569
|
+
if (!match) {
|
|
570
|
+
logError('60s API: 时间格式无效,跳过今天', { timeStr });
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
const hours = parseInt(match[1], 10);
|
|
574
|
+
const minutes = parseInt(match[2], 10);
|
|
575
|
+
const daysInterval = match[3] ? parseInt(match[3], 10) : 1; // 默认为每天
|
|
576
|
+
const now = new Date();
|
|
577
|
+
const target = new Date();
|
|
578
|
+
target.setHours(hours, minutes, 0, 0);
|
|
579
|
+
// 如果今天的时间已过,设置为下一天的间隔
|
|
580
|
+
if (target <= now) {
|
|
581
|
+
target.setDate(target.getDate() + daysInterval);
|
|
582
|
+
}
|
|
583
|
+
const msUntilNext = target.getTime() - now.getTime();
|
|
584
|
+
// 如果计算结果无效,跳过今天
|
|
585
|
+
if (!isFinite(msUntilNext) || msUntilNext <= 0) {
|
|
586
|
+
logError('60s API: 计算的时间无效,跳过今天', { timeStr, msUntilNext });
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
return msUntilNext;
|
|
590
|
+
}
|
|
591
|
+
// 检查指定时间是否在今天已过
|
|
592
|
+
// 支持格式: HH:MM 或 HH:MM / Nd
|
|
593
|
+
function isTimePassedToday(timeStr) {
|
|
594
|
+
const trimmedTimeStr = timeStr.trim();
|
|
595
|
+
// 解析时间格式,支持 HH:MM 或 HH:MM / Nd
|
|
596
|
+
const fullPattern = /^([0-1]?\d|2[0-3]):([0-5]\d)\s*(?:\/\s*(\d+)\s*d)?$/;
|
|
597
|
+
const match = trimmedTimeStr.match(fullPattern);
|
|
598
|
+
if (!match) {
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
const hours = parseInt(match[1], 10);
|
|
602
|
+
const minutes = parseInt(match[2], 10);
|
|
603
|
+
const now = new Date();
|
|
604
|
+
const target = new Date();
|
|
605
|
+
target.setHours(hours, minutes, 0, 0);
|
|
606
|
+
return target <= now;
|
|
607
|
+
}
|
|
608
|
+
// 设置定时任务 - 使用 setTimeout 递归实现精确的每日定时
|
|
609
|
+
function setupSchedule() {
|
|
610
|
+
if (!config.enableSchedule) {
|
|
611
|
+
logInfo('60s API: 定时发送新闻功能已禁用');
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
// 清除现有任务
|
|
615
|
+
if (scheduleTimeout) {
|
|
616
|
+
clearTimeout(scheduleTimeout);
|
|
617
|
+
scheduleTimeout = null;
|
|
618
|
+
}
|
|
619
|
+
try {
|
|
620
|
+
const today = getTodayString();
|
|
621
|
+
const remainingCooldown = getRemainingCooldown('news');
|
|
622
|
+
// 如果在冷却时间内,跳过本次发送
|
|
623
|
+
if (isInCooldown('news')) {
|
|
624
|
+
logInfo('60s API: 新闻发送冷却中,跳过本次发送', {
|
|
625
|
+
today,
|
|
626
|
+
remainingCooldownSec: remainingCooldown,
|
|
627
|
+
scheduleCooldown: config.scheduleCooldown
|
|
628
|
+
});
|
|
629
|
+
const msUntilNext = getMsUntilNextTime(config.scheduleTime);
|
|
630
|
+
if (msUntilNext === null) {
|
|
631
|
+
scheduleTimeout = setTimeout(() => {
|
|
632
|
+
setupSchedule();
|
|
633
|
+
}, 24 * 60 * 60 * 1000);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
scheduleTimeout = setTimeout(() => {
|
|
637
|
+
setupSchedule();
|
|
638
|
+
}, msUntilNext);
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const msUntilNext = getMsUntilNextTime(config.scheduleTime);
|
|
642
|
+
// 时间格式错误,跳过今天
|
|
643
|
+
if (msUntilNext === null) {
|
|
644
|
+
logError('60s API: 新闻定时任务时间格式错误,跳过今天', { scheduleTime: config.scheduleTime });
|
|
645
|
+
scheduleTimeout = setTimeout(() => {
|
|
646
|
+
setupSchedule();
|
|
647
|
+
}, 24 * 60 * 60 * 1000);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
logInfo('60s API: 新闻定时任务已设置', {
|
|
651
|
+
scheduleTime: config.scheduleTime,
|
|
652
|
+
msUntilNext: msUntilNext,
|
|
653
|
+
nextRun: new Date(Date.now() + msUntilNext).toLocaleString(),
|
|
654
|
+
whitelist: config.scheduleWhitelist,
|
|
655
|
+
scheduleCooldown: config.scheduleCooldown
|
|
656
|
+
});
|
|
657
|
+
scheduleTimeout = setTimeout(async () => {
|
|
658
|
+
// 再次检查是否在冷却时间内
|
|
659
|
+
if (isInCooldown('news')) {
|
|
660
|
+
logInfo('60s API: 新闻发送冷却中,跳过本次发送', {
|
|
661
|
+
remainingCooldownSec: getRemainingCooldown('news')
|
|
662
|
+
});
|
|
663
|
+
setupSchedule();
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
await sendNewsToChannels();
|
|
667
|
+
lastSentTime.news = Date.now();
|
|
668
|
+
setupSchedule();
|
|
669
|
+
}, msUntilNext);
|
|
670
|
+
}
|
|
671
|
+
catch (error) {
|
|
672
|
+
logError('60s API: 设置新闻定时任务失败', error);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
function setupAiNewsSchedule() {
|
|
676
|
+
if (!config.enableAiNewsSchedule) {
|
|
677
|
+
logInfo('60s API: AI快报定时发送功能已禁用');
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
if (aiNewsScheduleTimeout) {
|
|
681
|
+
clearTimeout(aiNewsScheduleTimeout);
|
|
682
|
+
aiNewsScheduleTimeout = null;
|
|
683
|
+
}
|
|
684
|
+
try {
|
|
685
|
+
const today = getTodayString();
|
|
686
|
+
const remainingCooldown = getRemainingCooldown('aiNews');
|
|
687
|
+
// 如果在冷却时间内,跳过本次发送
|
|
688
|
+
if (isInCooldown('aiNews')) {
|
|
689
|
+
logInfo('60s API: AI快报发送冷却中,跳过本次发送', {
|
|
690
|
+
today,
|
|
691
|
+
remainingCooldownSec: remainingCooldown,
|
|
692
|
+
scheduleCooldown: config.scheduleCooldown
|
|
693
|
+
});
|
|
694
|
+
const msUntilNext = getMsUntilNextTime(config.aiNewsScheduleTime);
|
|
695
|
+
if (msUntilNext === null) {
|
|
696
|
+
aiNewsScheduleTimeout = setTimeout(() => {
|
|
697
|
+
setupAiNewsSchedule();
|
|
698
|
+
}, 24 * 60 * 60 * 1000);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
aiNewsScheduleTimeout = setTimeout(() => {
|
|
702
|
+
setupAiNewsSchedule();
|
|
703
|
+
}, msUntilNext);
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
const msUntilNext = getMsUntilNextTime(config.aiNewsScheduleTime);
|
|
707
|
+
// 时间格式错误,跳过今天
|
|
708
|
+
if (msUntilNext === null) {
|
|
709
|
+
logError('60s API: AI快报定时任务时间格式错误,跳过今天', { scheduleTime: config.aiNewsScheduleTime });
|
|
710
|
+
aiNewsScheduleTimeout = setTimeout(() => {
|
|
711
|
+
setupAiNewsSchedule();
|
|
712
|
+
}, 24 * 60 * 60 * 1000);
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
logInfo('60s API: AI快报定时任务已设置', {
|
|
716
|
+
scheduleTime: config.aiNewsScheduleTime,
|
|
717
|
+
msUntilNext: msUntilNext,
|
|
718
|
+
nextRun: new Date(Date.now() + msUntilNext).toLocaleString(),
|
|
719
|
+
whitelist: config.scheduleWhitelist,
|
|
720
|
+
scheduleCooldown: config.scheduleCooldown
|
|
721
|
+
});
|
|
722
|
+
aiNewsScheduleTimeout = setTimeout(async () => {
|
|
723
|
+
// 再次检查是否在冷却时间内
|
|
724
|
+
if (isInCooldown('aiNews')) {
|
|
725
|
+
logInfo('60s API: AI快报发送冷却中,跳过本次发送', {
|
|
726
|
+
remainingCooldownSec: getRemainingCooldown('aiNews')
|
|
727
|
+
});
|
|
728
|
+
setupAiNewsSchedule();
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
await sendAiNewsToChannels();
|
|
732
|
+
lastSentTime.aiNews = Date.now();
|
|
733
|
+
setupAiNewsSchedule();
|
|
734
|
+
}, msUntilNext);
|
|
735
|
+
}
|
|
736
|
+
catch (error) {
|
|
737
|
+
logError('60s API: 设置AI快报定时任务失败', error);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
function setupMoyuSchedule() {
|
|
741
|
+
if (!config.enableMoyuSchedule) {
|
|
742
|
+
logInfo('60s API: 摸鱼日报定时发送功能已禁用');
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
if (moyuScheduleTimeout) {
|
|
746
|
+
clearTimeout(moyuScheduleTimeout);
|
|
747
|
+
moyuScheduleTimeout = null;
|
|
748
|
+
}
|
|
749
|
+
try {
|
|
750
|
+
const today = getTodayString();
|
|
751
|
+
const remainingCooldown = getRemainingCooldown('moyu');
|
|
752
|
+
// 如果在冷却时间内,跳过本次发送
|
|
753
|
+
if (isInCooldown('moyu')) {
|
|
754
|
+
logInfo('60s API: 摸鱼日报发送冷却中,跳过本次发送', {
|
|
755
|
+
today,
|
|
756
|
+
remainingCooldownSec: remainingCooldown,
|
|
757
|
+
scheduleCooldown: config.scheduleCooldown
|
|
758
|
+
});
|
|
759
|
+
const msUntilNext = getMsUntilNextTime(config.moyuScheduleTime);
|
|
760
|
+
if (msUntilNext === null) {
|
|
761
|
+
moyuScheduleTimeout = setTimeout(() => {
|
|
762
|
+
setupMoyuSchedule();
|
|
763
|
+
}, 24 * 60 * 60 * 1000);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
moyuScheduleTimeout = setTimeout(() => {
|
|
767
|
+
setupMoyuSchedule();
|
|
768
|
+
}, msUntilNext);
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
const msUntilNext = getMsUntilNextTime(config.moyuScheduleTime);
|
|
772
|
+
// 时间格式错误,跳过今天
|
|
773
|
+
if (msUntilNext === null) {
|
|
774
|
+
logError('60s API: 摸鱼日报定时任务时间格式错误,跳过今天', { scheduleTime: config.moyuScheduleTime });
|
|
775
|
+
moyuScheduleTimeout = setTimeout(() => {
|
|
776
|
+
setupMoyuSchedule();
|
|
777
|
+
}, 24 * 60 * 60 * 1000);
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
logInfo('60s API: 摸鱼日报定时任务已设置', {
|
|
781
|
+
scheduleTime: config.moyuScheduleTime,
|
|
782
|
+
msUntilNext: msUntilNext,
|
|
783
|
+
nextRun: new Date(Date.now() + msUntilNext).toLocaleString(),
|
|
784
|
+
whitelist: config.scheduleWhitelist,
|
|
785
|
+
scheduleCooldown: config.scheduleCooldown
|
|
786
|
+
});
|
|
787
|
+
moyuScheduleTimeout = setTimeout(async () => {
|
|
788
|
+
// 再次检查是否在冷却时间内
|
|
789
|
+
if (isInCooldown('moyu')) {
|
|
790
|
+
logInfo('60s API: 摸鱼日报发送冷却中,跳过本次发送', {
|
|
791
|
+
remainingCooldownSec: getRemainingCooldown('moyu')
|
|
792
|
+
});
|
|
793
|
+
setupMoyuSchedule();
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
await sendMoyuToChannels();
|
|
797
|
+
lastSentTime.moyu = Date.now();
|
|
798
|
+
setupMoyuSchedule();
|
|
799
|
+
}, msUntilNext);
|
|
800
|
+
}
|
|
801
|
+
catch (error) {
|
|
802
|
+
logError('60s API: 设置摸鱼日报定时任务失败', error);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
function setupGoldSchedule() {
|
|
806
|
+
if (!config.enableGoldSchedule) {
|
|
807
|
+
logInfo('60s API: 今日金价定时发送功能已禁用');
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
if (goldScheduleTimeout) {
|
|
811
|
+
clearTimeout(goldScheduleTimeout);
|
|
812
|
+
goldScheduleTimeout = null;
|
|
813
|
+
}
|
|
814
|
+
try {
|
|
815
|
+
const today = getTodayString();
|
|
816
|
+
const remainingCooldown = getRemainingCooldown('gold');
|
|
817
|
+
// 如果在冷却时间内,跳过本次发送
|
|
818
|
+
if (isInCooldown('gold')) {
|
|
819
|
+
logInfo('60s API: 今日金价发送冷却中,跳过本次发送', {
|
|
820
|
+
today,
|
|
821
|
+
remainingCooldownSec: remainingCooldown,
|
|
822
|
+
scheduleCooldown: config.scheduleCooldown
|
|
823
|
+
});
|
|
824
|
+
const msUntilNext = getMsUntilNextTime(config.goldScheduleTime);
|
|
825
|
+
if (msUntilNext === null) {
|
|
826
|
+
goldScheduleTimeout = setTimeout(() => {
|
|
827
|
+
setupGoldSchedule();
|
|
828
|
+
}, 24 * 60 * 60 * 1000);
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
goldScheduleTimeout = setTimeout(() => {
|
|
832
|
+
setupGoldSchedule();
|
|
833
|
+
}, msUntilNext);
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
const msUntilNext = getMsUntilNextTime(config.goldScheduleTime);
|
|
837
|
+
// 时间格式错误,跳过今天
|
|
838
|
+
if (msUntilNext === null) {
|
|
839
|
+
logError('60s API: 今日金价定时任务时间格式错误,跳过今天', { scheduleTime: config.goldScheduleTime });
|
|
840
|
+
goldScheduleTimeout = setTimeout(() => {
|
|
841
|
+
setupGoldSchedule();
|
|
842
|
+
}, 24 * 60 * 60 * 1000);
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
logInfo('60s API: 今日金价定时任务已设置', {
|
|
846
|
+
scheduleTime: config.goldScheduleTime,
|
|
847
|
+
msUntilNext: msUntilNext,
|
|
848
|
+
nextRun: new Date(Date.now() + msUntilNext).toLocaleString(),
|
|
849
|
+
whitelist: config.scheduleWhitelist,
|
|
850
|
+
scheduleCooldown: config.scheduleCooldown
|
|
851
|
+
});
|
|
852
|
+
goldScheduleTimeout = setTimeout(async () => {
|
|
853
|
+
// 再次检查是否在冷却时间内
|
|
854
|
+
if (isInCooldown('gold')) {
|
|
855
|
+
logInfo('60s API: 今日金价发送冷却中,跳过本次发送', {
|
|
856
|
+
remainingCooldownSec: getRemainingCooldown('gold')
|
|
857
|
+
});
|
|
858
|
+
setupGoldSchedule();
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
await sendGoldToChannels();
|
|
862
|
+
lastSentTime.gold = Date.now();
|
|
863
|
+
setupGoldSchedule();
|
|
864
|
+
}, msUntilNext);
|
|
865
|
+
}
|
|
866
|
+
catch (error) {
|
|
867
|
+
logError('60s API: 设置今日金价定时任务失败', error);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
function setupFuelSchedule() {
|
|
871
|
+
if (!config.enableFuelSchedule) {
|
|
872
|
+
logInfo('60s API: 今日油价定时发送功能已禁用');
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
if (fuelScheduleTimeout) {
|
|
876
|
+
clearTimeout(fuelScheduleTimeout);
|
|
877
|
+
fuelScheduleTimeout = null;
|
|
878
|
+
}
|
|
879
|
+
try {
|
|
880
|
+
const today = getTodayString();
|
|
881
|
+
const remainingCooldown = getRemainingCooldown('fuel');
|
|
882
|
+
// 如果在冷却时间内,跳过本次发送
|
|
883
|
+
if (isInCooldown('fuel')) {
|
|
884
|
+
logInfo('60s API: 今日油价发送冷却中,跳过本次发送', {
|
|
885
|
+
today,
|
|
886
|
+
remainingCooldownSec: remainingCooldown,
|
|
887
|
+
scheduleCooldown: config.scheduleCooldown
|
|
888
|
+
});
|
|
889
|
+
const msUntilNext = getMsUntilNextTime(config.fuelScheduleTime);
|
|
890
|
+
if (msUntilNext === null) {
|
|
891
|
+
fuelScheduleTimeout = setTimeout(() => {
|
|
892
|
+
setupFuelSchedule();
|
|
893
|
+
}, 24 * 60 * 60 * 1000);
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
fuelScheduleTimeout = setTimeout(() => {
|
|
897
|
+
setupFuelSchedule();
|
|
898
|
+
}, msUntilNext);
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
const msUntilNext = getMsUntilNextTime(config.fuelScheduleTime);
|
|
902
|
+
// 时间格式错误,跳过今天
|
|
903
|
+
if (msUntilNext === null) {
|
|
904
|
+
logError('60s API: 今日油价定时任务时间格式错误,跳过今天', { scheduleTime: config.fuelScheduleTime });
|
|
905
|
+
fuelScheduleTimeout = setTimeout(() => {
|
|
906
|
+
setupFuelSchedule();
|
|
907
|
+
}, 24 * 60 * 60 * 1000);
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
logInfo('60s API: 今日油价定时任务已设置', {
|
|
911
|
+
scheduleTime: config.fuelScheduleTime,
|
|
912
|
+
msUntilNext: msUntilNext,
|
|
913
|
+
nextRun: new Date(Date.now() + msUntilNext).toLocaleString(),
|
|
914
|
+
whitelist: config.scheduleWhitelist,
|
|
915
|
+
scheduleCooldown: config.scheduleCooldown
|
|
916
|
+
});
|
|
917
|
+
fuelScheduleTimeout = setTimeout(async () => {
|
|
918
|
+
// 再次检查是否在冷却时间内
|
|
919
|
+
if (isInCooldown('fuel')) {
|
|
920
|
+
logInfo('60s API: 今日油价发送冷却中,跳过本次发送', {
|
|
921
|
+
remainingCooldownSec: getRemainingCooldown('fuel')
|
|
922
|
+
});
|
|
923
|
+
setupFuelSchedule();
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
await sendFuelToChannels();
|
|
927
|
+
lastSentTime.fuel = Date.now();
|
|
928
|
+
setupFuelSchedule();
|
|
929
|
+
}, msUntilNext);
|
|
930
|
+
}
|
|
931
|
+
catch (error) {
|
|
932
|
+
logError('60s API: 设置今日油价定时任务失败', error);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
// 设置新闻指令 (图片格式)
|
|
936
|
+
ctx.command('新闻', '获取60秒新闻图片')
|
|
937
|
+
.action(async (argv) => {
|
|
938
|
+
const userId = argv.session.userId;
|
|
939
|
+
// 检查冷却时间
|
|
940
|
+
if (!checkCooldown(userId)) {
|
|
941
|
+
const now = Date.now();
|
|
942
|
+
const lastTime = cooldowns.get(userId) || 0;
|
|
943
|
+
const timeLeft = Math.ceil((lastTime + config.cooldownTime * 1000 - now) / 1000);
|
|
944
|
+
return `请等待 ${timeLeft} 秒后再试`;
|
|
945
|
+
}
|
|
946
|
+
try {
|
|
947
|
+
logInfo('60s API: 用户请求新闻图片', { userId });
|
|
948
|
+
// 获取新闻图片
|
|
949
|
+
const imageBuffer = await get60sNewsImage();
|
|
950
|
+
// 发送图片
|
|
951
|
+
const imageMessage = koishi_1.h.image(imageBuffer, 'image/png');
|
|
952
|
+
await argv.session.send(imageMessage);
|
|
953
|
+
logInfo('60s API: 成功发送新闻图片', {
|
|
954
|
+
size: imageBuffer.length,
|
|
955
|
+
userId: userId
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
catch (error) {
|
|
959
|
+
logError('60s API: 处理新闻图片请求失败', {
|
|
960
|
+
error: error,
|
|
961
|
+
errorMessage: error?.message || '未知错误',
|
|
962
|
+
userId: userId
|
|
963
|
+
});
|
|
964
|
+
return '获取新闻图片失败,请稍后重试';
|
|
965
|
+
}
|
|
966
|
+
});
|
|
967
|
+
// 设置历史上的今天指令
|
|
968
|
+
ctx.command('历史上的今天', '获取历史上的今天')
|
|
969
|
+
.action(async (argv) => {
|
|
970
|
+
const userId = argv.session.userId;
|
|
971
|
+
// 检查冷却时间
|
|
972
|
+
if (!checkCooldown(userId)) {
|
|
973
|
+
const now = Date.now();
|
|
974
|
+
const lastTime = cooldowns.get(userId) || 0;
|
|
975
|
+
const timeLeft = Math.ceil((lastTime + config.cooldownTime * 1000 - now) / 1000);
|
|
976
|
+
return `请等待 ${timeLeft} 秒后再试`;
|
|
977
|
+
}
|
|
978
|
+
try {
|
|
979
|
+
logInfo('60s API: 用户请求历史上的今天', { userId });
|
|
980
|
+
// 获取历史上的今天数据
|
|
981
|
+
const response = await getTodayInHistory();
|
|
982
|
+
if (response.code !== 200) {
|
|
983
|
+
logError('60s API: 返回错误', {
|
|
984
|
+
code: response.code,
|
|
985
|
+
message: response.message
|
|
986
|
+
});
|
|
987
|
+
return `获取历史上的今天失败: ${response.message || '未知错误'}`;
|
|
988
|
+
}
|
|
989
|
+
if (!response.data || !response.data.items || response.data.items.length === 0) {
|
|
990
|
+
logError('60s API: 返回数据为空');
|
|
991
|
+
return '获取历史上的今天失败: 未获取到历史事件数据';
|
|
992
|
+
}
|
|
993
|
+
// 构建消息内容
|
|
994
|
+
const { date, items } = response.data;
|
|
995
|
+
if (config.useForward && argv.session.platform === 'onebot') {
|
|
996
|
+
// 使用合并转发
|
|
997
|
+
const forwardElements = [
|
|
998
|
+
`📅 ${date} 历史上的今天`,
|
|
999
|
+
...items.map((item, index) => {
|
|
1000
|
+
const typeIcon = {
|
|
1001
|
+
'birth': '👶',
|
|
1002
|
+
'event': '📅',
|
|
1003
|
+
'death': '💀'
|
|
1004
|
+
}[item.event_type] || '📅';
|
|
1005
|
+
return `${index + 1}. ${typeIcon} ${item.year}年 - ${item.title}\n${item.description}`;
|
|
1006
|
+
})
|
|
1007
|
+
];
|
|
1008
|
+
const forwardMessage = (0, koishi_1.h)("figure", {}, forwardElements);
|
|
1009
|
+
await argv.session.send(forwardMessage);
|
|
1010
|
+
}
|
|
1011
|
+
else {
|
|
1012
|
+
// 普通文本发送
|
|
1013
|
+
let message = `📅 ${date} 历史上的今天\n\n`;
|
|
1014
|
+
items.forEach((item, index) => {
|
|
1015
|
+
const typeIcon = {
|
|
1016
|
+
'birth': '👶',
|
|
1017
|
+
'event': '📅',
|
|
1018
|
+
'death': '💀'
|
|
1019
|
+
}[item.event_type] || '📅';
|
|
1020
|
+
message += `${index + 1}. ${typeIcon} ${item.year}年 - ${item.title}\n${item.description}\n\n`;
|
|
1021
|
+
});
|
|
1022
|
+
await argv.session.send(message);
|
|
1023
|
+
}
|
|
1024
|
+
logInfo('60s API: 成功发送历史上的今天', {
|
|
1025
|
+
date: date,
|
|
1026
|
+
itemsCount: items.length,
|
|
1027
|
+
userId: userId
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
catch (error) {
|
|
1031
|
+
logError('60s API: 处理历史上的今天请求失败', {
|
|
1032
|
+
error: error,
|
|
1033
|
+
errorMessage: error?.message || '未知错误',
|
|
1034
|
+
userId: userId
|
|
1035
|
+
});
|
|
1036
|
+
return '获取历史上的今天失败,请稍后重试';
|
|
1037
|
+
}
|
|
1038
|
+
});
|
|
1039
|
+
// 设置知乎话题榜指令
|
|
1040
|
+
ctx.command('知乎话题榜', '获取知乎话题榜')
|
|
1041
|
+
.action(async (argv) => {
|
|
1042
|
+
const userId = argv.session.userId;
|
|
1043
|
+
// 检查冷却时间
|
|
1044
|
+
if (!checkCooldown(userId)) {
|
|
1045
|
+
const now = Date.now();
|
|
1046
|
+
const lastTime = cooldowns.get(userId) || 0;
|
|
1047
|
+
const timeLeft = Math.ceil((lastTime + config.cooldownTime * 1000 - now) / 1000);
|
|
1048
|
+
return `请等待 ${timeLeft} 秒后再试`;
|
|
1049
|
+
}
|
|
1050
|
+
try {
|
|
1051
|
+
logInfo('60s API: 用户请求知乎话题榜', { userId });
|
|
1052
|
+
// 获取知乎话题榜数据
|
|
1053
|
+
const response = await getZhihuTrends();
|
|
1054
|
+
if (response.code !== 200) {
|
|
1055
|
+
logError('60s API: 返回错误', {
|
|
1056
|
+
code: response.code,
|
|
1057
|
+
message: response.message
|
|
1058
|
+
});
|
|
1059
|
+
return `获取知乎话题榜失败: ${response.message || '未知错误'}`;
|
|
1060
|
+
}
|
|
1061
|
+
if (!response.data || response.data.length === 0) {
|
|
1062
|
+
logError('60s API: 返回数据为空');
|
|
1063
|
+
return '获取知乎话题榜失败: 未获取到话题数据';
|
|
1064
|
+
}
|
|
1065
|
+
// 构建消息内容
|
|
1066
|
+
const topics = response.data;
|
|
1067
|
+
if (config.useForward && argv.session.platform === 'onebot') {
|
|
1068
|
+
// 使用合并转发
|
|
1069
|
+
const forwardElements = [
|
|
1070
|
+
`🔥 知乎话题榜`,
|
|
1071
|
+
...topics.map((topic, index) => {
|
|
1072
|
+
return `${index + 1}. ${topic.title}\n${topic.detail}\n🔗 ${topic.link}`;
|
|
1073
|
+
})
|
|
1074
|
+
];
|
|
1075
|
+
const forwardMessage = (0, koishi_1.h)("figure", {}, forwardElements);
|
|
1076
|
+
await argv.session.send(forwardMessage);
|
|
1077
|
+
}
|
|
1078
|
+
else {
|
|
1079
|
+
// 普通文本发送 - 限制长度避免消息过长
|
|
1080
|
+
let message = `🔥 知乎话题榜\n\n`;
|
|
1081
|
+
const maxTopics = 10; // 限制最多显示10个话题
|
|
1082
|
+
const topicsToShow = topics.slice(0, maxTopics);
|
|
1083
|
+
topicsToShow.forEach((topic, index) => {
|
|
1084
|
+
// 截断过长的详情
|
|
1085
|
+
const shortDetail = topic.detail.length > 200 ? topic.detail.substring(0, 200) + '...' : topic.detail;
|
|
1086
|
+
message += `${index + 1}. ${topic.title}\n${shortDetail}\n🔗 ${topic.link}\n\n`;
|
|
1087
|
+
});
|
|
1088
|
+
if (topics.length > maxTopics) {
|
|
1089
|
+
message += `\n... 还有 ${topics.length - maxTopics} 个话题,完整内容请使用合并转发模式`;
|
|
1090
|
+
}
|
|
1091
|
+
await argv.session.send(message);
|
|
1092
|
+
}
|
|
1093
|
+
logInfo('60s API: 成功发送知乎话题榜', {
|
|
1094
|
+
topicsCount: topics.length,
|
|
1095
|
+
userId: userId
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
catch (error) {
|
|
1099
|
+
logError('60s API: 处理知乎话题榜请求失败', {
|
|
1100
|
+
error: error,
|
|
1101
|
+
errorMessage: error?.message || '未知错误',
|
|
1102
|
+
userId: userId
|
|
1103
|
+
});
|
|
1104
|
+
return '获取知乎话题榜失败,请稍后重试';
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1107
|
+
ctx.command('AI快报 [date]', '获取AI资讯快报')
|
|
1108
|
+
.option('all', '-a 获取所有日期')
|
|
1109
|
+
.option('encoding', '-e <encoding> 编码方式 text/json/markdown')
|
|
1110
|
+
.action(async (argv, date) => {
|
|
1111
|
+
const userId = argv.session.userId;
|
|
1112
|
+
if (!checkCooldown(userId)) {
|
|
1113
|
+
const now = Date.now();
|
|
1114
|
+
const lastTime = cooldowns.get(userId) || 0;
|
|
1115
|
+
const timeLeft = Math.ceil((lastTime + config.cooldownTime * 1000 - now) / 1000);
|
|
1116
|
+
return `请等待 ${timeLeft} 秒后再试`;
|
|
1117
|
+
}
|
|
1118
|
+
try {
|
|
1119
|
+
logInfo('60s API: 用户请求AI快报', { userId, date, all: argv.options.all, encoding: argv.options.encoding });
|
|
1120
|
+
const encoding = argv.options.encoding || 'text';
|
|
1121
|
+
const all = argv.options.all ? '1' : undefined;
|
|
1122
|
+
// 优先使用位置参数 date,如果未提供则获取最近两天
|
|
1123
|
+
const targetDate = date?.trim();
|
|
1124
|
+
// 如果没有指定日期且不是获取所有日期,默认获取最近两天
|
|
1125
|
+
if (!targetDate && !all) {
|
|
1126
|
+
const dates = getRecentAiNewsDates();
|
|
1127
|
+
const data = await fetchAiNewsByDates(dates);
|
|
1128
|
+
if (!data.length)
|
|
1129
|
+
return '未获取到近两天的AI快报数据';
|
|
1130
|
+
if (encoding === 'json') {
|
|
1131
|
+
await argv.session.send(JSON.stringify(toTitleLinkData(data), null, 2));
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
if (encoding === 'markdown') {
|
|
1135
|
+
await argv.session.send(formatAiNewsMarkdown(data));
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
const message = formatAiNewsText(data);
|
|
1139
|
+
if (config.aiUseForward && argv.session.platform === 'onebot') {
|
|
1140
|
+
await argv.session.send((0, koishi_1.h)('figure', {}, [message]));
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
await argv.session.send(message);
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
// 获取指定日期或所有日期的数据
|
|
1147
|
+
const response = await getAiNews({ date: targetDate, all, encoding: 'json' });
|
|
1148
|
+
if (response.code !== 200 || !response.data) {
|
|
1149
|
+
logError('60s API: AI快报返回错误', { code: response.code, message: response.message });
|
|
1150
|
+
return `获取AI快报失败: ${response.message || '未知错误'}`;
|
|
1151
|
+
}
|
|
1152
|
+
// 根据编码格式返回
|
|
1153
|
+
if (encoding === 'json') {
|
|
1154
|
+
await argv.session.send(JSON.stringify(toTitleLinkData(response.data), null, 2));
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
if (encoding === 'markdown') {
|
|
1158
|
+
await argv.session.send(formatAiNewsMarkdown(response.data));
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
// 默认文本格式
|
|
1162
|
+
const message = formatAiNewsText(response.data);
|
|
1163
|
+
if (config.aiUseForward && argv.session.platform === 'onebot') {
|
|
1164
|
+
await argv.session.send((0, koishi_1.h)('figure', {}, [message]));
|
|
1165
|
+
}
|
|
1166
|
+
else {
|
|
1167
|
+
await argv.session.send(message);
|
|
1168
|
+
}
|
|
1169
|
+
logInfo('60s API: 成功发送AI快报', {
|
|
1170
|
+
userId: userId,
|
|
1171
|
+
date: targetDate || 'recent',
|
|
1172
|
+
all: all || '0'
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
catch (error) {
|
|
1176
|
+
logError('60s API: 处理AI快报请求失败', {
|
|
1177
|
+
error: error,
|
|
1178
|
+
errorMessage: error?.message || '未知错误',
|
|
1179
|
+
userId: userId
|
|
1180
|
+
});
|
|
1181
|
+
return '获取AI快报失败,请稍后重试';
|
|
1182
|
+
}
|
|
1183
|
+
});
|
|
1184
|
+
ctx.command('摸鱼', '获取摸鱼日报')
|
|
1185
|
+
.option('encoding', '-e <encoding> 编码方式 text/json/markdown')
|
|
1186
|
+
.action(async (argv) => {
|
|
1187
|
+
const userId = argv.session.userId;
|
|
1188
|
+
if (!checkCooldown(userId)) {
|
|
1189
|
+
const now = Date.now();
|
|
1190
|
+
const lastTime = cooldowns.get(userId) || 0;
|
|
1191
|
+
const timeLeft = Math.ceil((lastTime + config.cooldownTime * 1000 - now) / 1000);
|
|
1192
|
+
return `请等待 ${timeLeft} 秒后再试`;
|
|
1193
|
+
}
|
|
1194
|
+
try {
|
|
1195
|
+
logInfo('60s API: 用户请求摸鱼日报', { userId });
|
|
1196
|
+
const encoding = argv.options.encoding || 'text';
|
|
1197
|
+
if (encoding === 'markdown') {
|
|
1198
|
+
const response = await getMoyuDaily('markdown');
|
|
1199
|
+
await argv.session.send(response);
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
if (encoding === 'json') {
|
|
1203
|
+
const response = await getMoyuDaily('json');
|
|
1204
|
+
if (response.code !== 200 || !response.data) {
|
|
1205
|
+
logError('60s API: 摸鱼日报返回错误', { code: response.code, message: response.message });
|
|
1206
|
+
return `获取摸鱼日报失败: ${response.message || '未知错误'}`;
|
|
1207
|
+
}
|
|
1208
|
+
await argv.session.send(JSON.stringify(response.data, null, 2));
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
const response = await getMoyuDaily('json');
|
|
1212
|
+
if (response.code !== 200 || !response.data) {
|
|
1213
|
+
logError('60s API: 摸鱼日报返回错误', { code: response.code, message: response.message });
|
|
1214
|
+
return `获取摸鱼日报失败: ${response.message || '未知错误'}`;
|
|
1215
|
+
}
|
|
1216
|
+
const message = formatMoyuText(response.data);
|
|
1217
|
+
await argv.session.send(message);
|
|
1218
|
+
logInfo('60s API: 成功发送摸鱼日报', {
|
|
1219
|
+
userId: userId
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
catch (error) {
|
|
1223
|
+
logError('60s API: 处理摸鱼日报请求失败', {
|
|
1224
|
+
error: error,
|
|
1225
|
+
errorMessage: error?.message || '未知错误',
|
|
1226
|
+
userId: userId
|
|
1227
|
+
});
|
|
1228
|
+
return '获取摸鱼日报失败,请稍后重试';
|
|
1229
|
+
}
|
|
1230
|
+
});
|
|
1231
|
+
ctx.command('金价', '获取今日金价')
|
|
1232
|
+
.option('encoding', '-e <encoding> 编码方式 text/json/markdown')
|
|
1233
|
+
.action(async (argv) => {
|
|
1234
|
+
const userId = argv.session.userId;
|
|
1235
|
+
if (!checkCooldown(userId)) {
|
|
1236
|
+
const now = Date.now();
|
|
1237
|
+
const lastTime = cooldowns.get(userId) || 0;
|
|
1238
|
+
const timeLeft = Math.ceil((lastTime + config.cooldownTime * 1000 - now) / 1000);
|
|
1239
|
+
return `请等待 ${timeLeft} 秒后再试`;
|
|
1240
|
+
}
|
|
1241
|
+
try {
|
|
1242
|
+
logInfo('60s API: 用户请求今日金价', { userId });
|
|
1243
|
+
const encoding = argv.options.encoding || 'text';
|
|
1244
|
+
if (encoding === 'markdown') {
|
|
1245
|
+
const response = await getGoldPrice('markdown');
|
|
1246
|
+
await argv.session.send(response);
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
if (encoding === 'json') {
|
|
1250
|
+
const response = await getGoldPrice('json');
|
|
1251
|
+
if (response.code !== 200 || !response.data) {
|
|
1252
|
+
logError('60s API: 今日金价返回错误', { code: response.code, message: response.message });
|
|
1253
|
+
return `获取今日金价失败: ${response.message || '未知错误'}`;
|
|
1254
|
+
}
|
|
1255
|
+
await argv.session.send(JSON.stringify(response.data, null, 2));
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
const response = await getGoldPrice('json');
|
|
1259
|
+
if (response.code !== 200 || !response.data) {
|
|
1260
|
+
logError('60s API: 今日金价返回错误', { code: response.code, message: response.message });
|
|
1261
|
+
return `获取今日金价失败: ${response.message || '未知错误'}`;
|
|
1262
|
+
}
|
|
1263
|
+
const message = formatGoldText(response.data);
|
|
1264
|
+
await argv.session.send(message);
|
|
1265
|
+
logInfo('60s API: 成功发送今日金价', { userId: userId });
|
|
1266
|
+
}
|
|
1267
|
+
catch (error) {
|
|
1268
|
+
logError('60s API: 处理今日金价请求失败', {
|
|
1269
|
+
error: error,
|
|
1270
|
+
errorMessage: error?.message || '未知错误',
|
|
1271
|
+
userId: userId
|
|
1272
|
+
});
|
|
1273
|
+
return '获取今日金价失败,请稍后重试';
|
|
1274
|
+
}
|
|
1275
|
+
});
|
|
1276
|
+
// 中间件:支持 "XX油价" 格式,提取地区并转发到油价指令
|
|
1277
|
+
ctx.middleware((session, next) => {
|
|
1278
|
+
const content = session.content?.trim();
|
|
1279
|
+
if (!content)
|
|
1280
|
+
return next();
|
|
1281
|
+
const match = content.match(/^(.+)油价$/);
|
|
1282
|
+
if (match && match[1] && match[1] !== '油') {
|
|
1283
|
+
const region = match[1];
|
|
1284
|
+
return session.execute(`油价 ${region}`);
|
|
1285
|
+
}
|
|
1286
|
+
return next();
|
|
1287
|
+
});
|
|
1288
|
+
ctx.command('油价 [region]', '获取今日油价')
|
|
1289
|
+
.option('region', '-r <region> 地区')
|
|
1290
|
+
.option('encoding', '-e <encoding> 编码方式 text/json/markdown')
|
|
1291
|
+
.action(async (argv, region) => {
|
|
1292
|
+
const userId = argv.session.userId;
|
|
1293
|
+
if (!checkCooldown(userId)) {
|
|
1294
|
+
const now = Date.now();
|
|
1295
|
+
const lastTime = cooldowns.get(userId) || 0;
|
|
1296
|
+
const timeLeft = Math.ceil((lastTime + config.cooldownTime * 1000 - now) / 1000);
|
|
1297
|
+
return `请等待 ${timeLeft} 秒后再试`;
|
|
1298
|
+
}
|
|
1299
|
+
try {
|
|
1300
|
+
logInfo('60s API: 用户请求今日油价', { userId });
|
|
1301
|
+
const encoding = argv.options.encoding || 'text';
|
|
1302
|
+
const targetRegion = argv.options.region || region || config.fuelDefaultRegion;
|
|
1303
|
+
if (encoding === 'markdown') {
|
|
1304
|
+
const response = await getFuelPrice({ region: targetRegion, encoding: 'markdown' });
|
|
1305
|
+
await argv.session.send(response);
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
if (encoding === 'json') {
|
|
1309
|
+
const response = await getFuelPrice({ region: targetRegion, encoding: 'json' });
|
|
1310
|
+
if (response.code !== 200 || !response.data) {
|
|
1311
|
+
logError('60s API: 今日油价返回错误', { code: response.code, message: response.message });
|
|
1312
|
+
return `获取今日油价失败: ${response.message || '未知错误'}`;
|
|
1313
|
+
}
|
|
1314
|
+
await argv.session.send(JSON.stringify(response.data, null, 2));
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
const response = await getFuelPrice({ region: targetRegion, encoding: 'json' });
|
|
1318
|
+
if (response.code !== 200 || !response.data) {
|
|
1319
|
+
logError('60s API: 今日油价返回错误', { code: response.code, message: response.message });
|
|
1320
|
+
return `获取今日油价失败: ${response.message || '未知错误'}`;
|
|
1321
|
+
}
|
|
1322
|
+
const message = formatFuelText(response.data);
|
|
1323
|
+
await argv.session.send(message);
|
|
1324
|
+
logInfo('60s API: 成功发送今日油价', { userId: userId, region: targetRegion });
|
|
1325
|
+
}
|
|
1326
|
+
catch (error) {
|
|
1327
|
+
logError('60s API: 处理今日油价请求失败', {
|
|
1328
|
+
error: error,
|
|
1329
|
+
errorMessage: error?.message || '未知错误',
|
|
1330
|
+
userId: userId
|
|
1331
|
+
});
|
|
1332
|
+
return '获取今日油价失败,请稍后重试';
|
|
1333
|
+
}
|
|
1334
|
+
});
|
|
1335
|
+
// 插件启动时初始化定时任务
|
|
1336
|
+
ctx.on('ready', async () => {
|
|
1337
|
+
setupSchedule();
|
|
1338
|
+
setupAiNewsSchedule();
|
|
1339
|
+
setupMoyuSchedule();
|
|
1340
|
+
setupGoldSchedule();
|
|
1341
|
+
setupFuelSchedule();
|
|
1342
|
+
});
|
|
1343
|
+
// 插件卸载时清理资源
|
|
1344
|
+
ctx.on('dispose', () => {
|
|
1345
|
+
cooldowns.clear();
|
|
1346
|
+
if (scheduleTimeout) {
|
|
1347
|
+
clearTimeout(scheduleTimeout);
|
|
1348
|
+
scheduleTimeout = null;
|
|
1349
|
+
}
|
|
1350
|
+
if (aiNewsScheduleTimeout) {
|
|
1351
|
+
clearTimeout(aiNewsScheduleTimeout);
|
|
1352
|
+
aiNewsScheduleTimeout = null;
|
|
1353
|
+
}
|
|
1354
|
+
if (moyuScheduleTimeout) {
|
|
1355
|
+
clearTimeout(moyuScheduleTimeout);
|
|
1356
|
+
moyuScheduleTimeout = null;
|
|
1357
|
+
}
|
|
1358
|
+
if (goldScheduleTimeout) {
|
|
1359
|
+
clearTimeout(goldScheduleTimeout);
|
|
1360
|
+
goldScheduleTimeout = null;
|
|
1361
|
+
}
|
|
1362
|
+
if (fuelScheduleTimeout) {
|
|
1363
|
+
clearTimeout(fuelScheduleTimeout);
|
|
1364
|
+
fuelScheduleTimeout = null;
|
|
1365
|
+
}
|
|
1366
|
+
// 重置发送记录
|
|
1367
|
+
lastSentTime.news = 0;
|
|
1368
|
+
lastSentTime.aiNews = 0;
|
|
1369
|
+
lastSentTime.moyu = 0;
|
|
1370
|
+
lastSentTime.gold = 0;
|
|
1371
|
+
lastSentTime.fuel = 0;
|
|
1372
|
+
});
|
|
1373
|
+
}
|