koishi-plugin-cs2-update-log 2.2.2 → 2.3.0
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/README.md +4 -3
- package/lib/index.js +107 -32
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -6,15 +6,16 @@ Koishi QQ 机器人插件,用于轮询 CS2 官方 Steam 公告流,按 `gid`
|
|
|
6
6
|
|
|
7
7
|
## 功能
|
|
8
8
|
|
|
9
|
-
- 使用 Steam Store RSS
|
|
10
|
-
- 默认每
|
|
9
|
+
- 使用 Valve Fastly RSS、Steam Web API 与 Store RSS 拉取 AppID `730` 的官方公告流。
|
|
10
|
+
- 默认每 15 秒轮询一次。
|
|
11
11
|
- 首次启动默认只记录历史 `gid`,不推送历史内容。
|
|
12
12
|
- 自动分类:
|
|
13
13
|
- 标题包含 `Counter-Strike 2 Update` / `Release Notes`
|
|
14
14
|
- 或正文包含 `[MAPS]`、`[GAMEPLAY]`、`[MISC]`、`[AUDIO]`、`[ITEMS]`、`[WORKSHOP]`、`[PREMIER]` 等更新分区
|
|
15
15
|
- 支持纯文本推送或 Puppeteer 深色长图卡片推送。
|
|
16
16
|
- 支持 OpenAI-compatible Chat Completions API 翻译。
|
|
17
|
-
-
|
|
17
|
+
- 优先并发请求 Valve Fastly RSS 与 Steam Web API,并在两者都不可用时回退到 Store RSS;Fastly RSS 使用 30 秒缓存时间桶,兼顾新公告发现速度和源站压力。
|
|
18
|
+
- 内置短时运行时缓存:RSS 列表缓存 3 秒,新闻内容、AI 翻译结果、长图 PNG 和卡片静态资源每 5 分钟清理一次,减少连续测试/重复推送的开销。
|
|
18
19
|
- 提供 `cs2log.check`、`cs2log.push` 与 `cs2log.test` 命令。
|
|
19
20
|
|
|
20
21
|
## 配置要点
|
package/lib/index.js
CHANGED
|
@@ -17,9 +17,13 @@ exports.inject = {
|
|
|
17
17
|
optional: ['puppeteer'],
|
|
18
18
|
};
|
|
19
19
|
const APP_ID = 730;
|
|
20
|
-
const
|
|
20
|
+
const STEAM_FASTLY_RSS_URL = `https://store.fastly.steamstatic.com/feeds/news/app/${APP_ID}/`;
|
|
21
|
+
const STEAM_STORE_RSS_URL = `https://store.steampowered.com/feeds/news/app/${APP_ID}/`;
|
|
22
|
+
const STEAM_NEWS_API_URL = 'https://api.steampowered.com/ISteamNews/GetNewsForApp/v2/';
|
|
21
23
|
const STATE_LIMIT = 1000;
|
|
22
|
-
const RSS_LIST_CACHE_TTL_MS =
|
|
24
|
+
const RSS_LIST_CACHE_TTL_MS = 3 * 1000;
|
|
25
|
+
const RSS_CACHE_BUCKET_MS = 30 * 1000;
|
|
26
|
+
const STEAM_REQUEST_TIMEOUT_MS = 8 * 1000;
|
|
23
27
|
const RUNTIME_CACHE_CLEAR_INTERVAL_MS = 5 * 60 * 1000;
|
|
24
28
|
const TRANSLATE_TIMEOUT_MS = 90 * 1000;
|
|
25
29
|
const ASSET_DIR = node_path_1.default.resolve(__dirname, '..', 'assets');
|
|
@@ -27,7 +31,7 @@ const BRAND_LOGO_PATH = node_path_1.default.join(ASSET_DIR, 'brand-logo.png');
|
|
|
27
31
|
const GITHUB_QRCODE_PATH = node_path_1.default.join(ASSET_DIR, 'github-qrcode.png');
|
|
28
32
|
const loggerName = 'cs2-update-log';
|
|
29
33
|
exports.Config = koishi_1.Schema.object({
|
|
30
|
-
interval: koishi_1.Schema.number().min(5).step(1).default(
|
|
34
|
+
interval: koishi_1.Schema.number().min(5).step(1).default(15).description('轮询间隔,单位:秒。'),
|
|
31
35
|
count: koishi_1.Schema.number().min(1).max(100).step(1).default(20).description('每次从 Steam 拉取的新闻数量。'),
|
|
32
36
|
stateFile: koishi_1.Schema.string().default('.koishi-cs2-update-log.json').description('本地 gid 判重文件路径。相对路径会基于 Koishi 启动目录解析。'),
|
|
33
37
|
pushOnFirstRun: koishi_1.Schema.boolean().default(false).description('首次启动时是否推送历史内容。默认关闭,避免刷屏。'),
|
|
@@ -37,8 +41,8 @@ exports.Config = koishi_1.Schema.object({
|
|
|
37
41
|
channelId: koishi_1.Schema.string().required().description('目标频道或 QQ 群号。'),
|
|
38
42
|
guildId: koishi_1.Schema.string().description('可选群组 ID。标准 bot.sendMessage 只使用 channelId,此字段用于记录配置语义。'),
|
|
39
43
|
})).default([]).description('推送目标列表。'),
|
|
40
|
-
brandName: koishi_1.Schema.string().default('
|
|
41
|
-
siteName: koishi_1.Schema.string().default('
|
|
44
|
+
brandName: koishi_1.Schema.string().default('CS2 update').description('图片顶部品牌名。'),
|
|
45
|
+
siteName: koishi_1.Schema.string().default('Github仓库').description('图片底部站点名。'),
|
|
42
46
|
picture: koishi_1.Schema.boolean().default(true).description('是否以 Puppeteer 截图长图形式推送。关闭后推送纯文本。'),
|
|
43
47
|
appendLink: koishi_1.Schema.boolean().default(true).description('图片或文本后是否附带 Steam 原文链接。'),
|
|
44
48
|
trans: koishi_1.Schema.boolean().default(false).description('是否启用 AI 翻译。关闭时推送 Steam 返回的原文。'),
|
|
@@ -130,13 +134,48 @@ function apply(ctx, config) {
|
|
|
130
134
|
logger.debug('使用 RSS 短时缓存:items=%d', cache.rssItems.length);
|
|
131
135
|
return cache.rssItems.slice(0, config.count);
|
|
132
136
|
}
|
|
133
|
-
const
|
|
137
|
+
const cacheBucket = Math.floor(Date.now() / RSS_CACHE_BUCKET_MS);
|
|
138
|
+
const fastlyUrl = `${STEAM_FASTLY_RSS_URL}?l=english&_=${cacheBucket}`;
|
|
139
|
+
const apiUrl = `${STEAM_NEWS_API_URL}?appid=${APP_ID}&count=${config.count}&maxlength=0&format=json`;
|
|
140
|
+
let items;
|
|
141
|
+
try {
|
|
142
|
+
items = await Promise.any([
|
|
143
|
+
fetchRssSource('Fastly RSS', fastlyUrl),
|
|
144
|
+
fetchApiSource('Steam Web API', apiUrl),
|
|
145
|
+
]);
|
|
146
|
+
}
|
|
147
|
+
catch (primaryError) {
|
|
148
|
+
const storeUrl = `${STEAM_STORE_RSS_URL}?l=english&_=${cacheBucket}`;
|
|
149
|
+
logger.warn('Fastly RSS 与 Steam Web API 均不可用,回退到 Store RSS:%s', formatAggregateError(primaryError));
|
|
150
|
+
items = await fetchRssSource('Store RSS', storeUrl);
|
|
151
|
+
}
|
|
152
|
+
const cachedItems = items.map((item) => {
|
|
153
|
+
const cached = cache.newsByGid.get(item.gid);
|
|
154
|
+
if (cached && cached.title === item.title && cached.content === item.content && cached.url === item.url && cached.date === item.date) {
|
|
155
|
+
return cached;
|
|
156
|
+
}
|
|
157
|
+
cache.newsByGid.set(item.gid, item);
|
|
158
|
+
return item;
|
|
159
|
+
});
|
|
160
|
+
cache.rssFetchedAt = Date.now();
|
|
161
|
+
cache.rssItems = cachedItems;
|
|
162
|
+
return cachedItems.slice(0, config.count);
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
logger.error('拉取 Steam CS2 官方新闻失败:\n%s', formatError(error));
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
async function fetchRssSource(source, url) {
|
|
170
|
+
const startedAt = Date.now();
|
|
171
|
+
try {
|
|
172
|
+
const xml = await ctx.http.get(url, {
|
|
134
173
|
headers: {
|
|
135
174
|
Accept: 'application/rss+xml, application/xml, text/xml;q=0.9, */*;q=0.8',
|
|
136
|
-
'User-Agent': 'koishi-plugin-cs2-update-log/2.
|
|
175
|
+
'User-Agent': 'koishi-plugin-cs2-update-log/2.2',
|
|
137
176
|
},
|
|
138
177
|
responseType: 'text',
|
|
139
|
-
timeout:
|
|
178
|
+
timeout: STEAM_REQUEST_TIMEOUT_MS,
|
|
140
179
|
});
|
|
141
180
|
const parsed = rssParser.parse(xml);
|
|
142
181
|
const rawItems = parsed.rss?.channel?.item;
|
|
@@ -144,21 +183,37 @@ function apply(ctx, config) {
|
|
|
144
183
|
const parsedItems = items
|
|
145
184
|
.map(parseRssItem)
|
|
146
185
|
.filter((item) => !!item?.gid && !!item.title);
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
186
|
+
if (!parsedItems.length)
|
|
187
|
+
throw new Error('返回内容中没有有效新闻');
|
|
188
|
+
logger.debug('%s 拉取成功:items=%d duration=%dms', source, parsedItems.length, Date.now() - startedAt);
|
|
189
|
+
return parsedItems;
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
logger.debug('%s 拉取失败:duration=%dms url=%s\n%s', source, Date.now() - startedAt, url, formatError(error));
|
|
193
|
+
throw new Error(`${source} 拉取失败:${formatError(error)}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
async function fetchApiSource(source, url) {
|
|
197
|
+
const startedAt = Date.now();
|
|
198
|
+
try {
|
|
199
|
+
const response = await ctx.http.get(url, {
|
|
200
|
+
headers: {
|
|
201
|
+
Accept: 'application/json',
|
|
202
|
+
'User-Agent': 'koishi-plugin-cs2-update-log/2.2',
|
|
203
|
+
},
|
|
204
|
+
timeout: STEAM_REQUEST_TIMEOUT_MS,
|
|
154
205
|
});
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
206
|
+
const parsedItems = (response.appnews?.newsitems || [])
|
|
207
|
+
.map(parseSteamApiItem)
|
|
208
|
+
.filter((item) => !!item?.gid && !!item.title);
|
|
209
|
+
if (!parsedItems.length)
|
|
210
|
+
throw new Error('返回内容中没有有效新闻');
|
|
211
|
+
logger.debug('%s 拉取成功:items=%d duration=%dms', source, parsedItems.length, Date.now() - startedAt);
|
|
212
|
+
return parsedItems;
|
|
158
213
|
}
|
|
159
214
|
catch (error) {
|
|
160
|
-
logger.
|
|
161
|
-
|
|
215
|
+
logger.debug('%s 拉取失败:duration=%dms url=%s\n%s', source, Date.now() - startedAt, url, formatError(error));
|
|
216
|
+
throw new Error(`${source} 拉取失败:${formatError(error)}`);
|
|
162
217
|
}
|
|
163
218
|
}
|
|
164
219
|
async function pollAndPush(source) {
|
|
@@ -211,24 +266,24 @@ function apply(ctx, config) {
|
|
|
211
266
|
: buildTextMessage(news, displayTitle, displayMarkdown, link);
|
|
212
267
|
}
|
|
213
268
|
async function pushNews(news) {
|
|
214
|
-
const content = await buildNewsContent(news);
|
|
215
269
|
if (!config.targets.length) {
|
|
216
270
|
logger.warn('未配置推送目标,已跳过:%s', news.item.title);
|
|
217
271
|
return;
|
|
218
272
|
}
|
|
219
|
-
|
|
273
|
+
const content = await buildNewsContent(news);
|
|
274
|
+
await Promise.all(config.targets.map(async (target) => {
|
|
220
275
|
try {
|
|
221
276
|
const bot = findTargetBot(ctx, target);
|
|
222
277
|
if (!bot) {
|
|
223
278
|
logger.warn('找不到推送机器人 platform=%s selfId=%s', target.platform, target.selfId || '*');
|
|
224
|
-
|
|
279
|
+
return;
|
|
225
280
|
}
|
|
226
281
|
await bot.sendMessage(target.channelId, content);
|
|
227
282
|
}
|
|
228
283
|
catch (error) {
|
|
229
284
|
logger.error('推送到 channelId=%s 失败:%s', target.channelId, formatError(error));
|
|
230
285
|
}
|
|
231
|
-
}
|
|
286
|
+
}));
|
|
232
287
|
}
|
|
233
288
|
async function renderOrFallbackText(news, title, bodyMarkdown, link) {
|
|
234
289
|
const puppeteer = ctx.puppeteer;
|
|
@@ -267,7 +322,7 @@ function apply(ctx, config) {
|
|
|
267
322
|
});
|
|
268
323
|
}
|
|
269
324
|
await page.setContent(html, {
|
|
270
|
-
waitUntil: '
|
|
325
|
+
waitUntil: 'load',
|
|
271
326
|
});
|
|
272
327
|
const card = await page.$('#card');
|
|
273
328
|
if (!card)
|
|
@@ -440,11 +495,25 @@ function parseRssItem(item) {
|
|
|
440
495
|
gid,
|
|
441
496
|
title,
|
|
442
497
|
url,
|
|
443
|
-
author: '
|
|
498
|
+
author: 'BestBcz',
|
|
444
499
|
content: readXmlText(item.description),
|
|
445
500
|
date: Number.isFinite(dateValue) ? Math.floor(dateValue / 1000) : 0,
|
|
446
501
|
};
|
|
447
502
|
}
|
|
503
|
+
function parseSteamApiItem(item) {
|
|
504
|
+
const gid = String(item.gid || '').trim();
|
|
505
|
+
const title = decodeHtmlEntities(String(item.title || '').trim());
|
|
506
|
+
if (!gid || !title)
|
|
507
|
+
return null;
|
|
508
|
+
return {
|
|
509
|
+
gid,
|
|
510
|
+
title,
|
|
511
|
+
url: String(item.url || '').trim(),
|
|
512
|
+
author: String(item.author || 'Valve').trim(),
|
|
513
|
+
content: String(item.contents || ''),
|
|
514
|
+
date: Number.isFinite(item.date) ? Number(item.date) : 0,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
448
517
|
function readXmlText(value) {
|
|
449
518
|
if (value == null)
|
|
450
519
|
return '';
|
|
@@ -762,13 +831,13 @@ body {
|
|
|
762
831
|
font-size: 14px;
|
|
763
832
|
}
|
|
764
833
|
.qr-code {
|
|
765
|
-
|
|
766
|
-
|
|
834
|
+
display: block;
|
|
835
|
+
width: 64px;
|
|
836
|
+
height: 64px;
|
|
767
837
|
flex: 0 0 auto;
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
border: 1px solid rgba(255, 255, 255, 0.22);
|
|
838
|
+
border-radius: 5px;
|
|
839
|
+
background: #ffffff;
|
|
840
|
+
object-fit: cover;
|
|
772
841
|
}
|
|
773
842
|
</style>
|
|
774
843
|
</head>
|
|
@@ -917,6 +986,12 @@ function formatError(error) {
|
|
|
917
986
|
appendErrorDetails(lines, error);
|
|
918
987
|
return lines.join('\n');
|
|
919
988
|
}
|
|
989
|
+
function formatAggregateError(error) {
|
|
990
|
+
if (error instanceof AggregateError) {
|
|
991
|
+
return error.errors.map((item) => formatError(item)).join(' | ');
|
|
992
|
+
}
|
|
993
|
+
return formatError(error);
|
|
994
|
+
}
|
|
920
995
|
function appendErrorDetails(lines, error, label = 'error', depth = 0, seen = new Set()) {
|
|
921
996
|
if (error == null || typeof error !== 'object') {
|
|
922
997
|
lines.push(`${label}: ${String(error)}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-cs2-update-log",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Koishi plugin for monitoring CS2 official Steam announcements and update logs.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
},
|
|
51
51
|
"koishi": {
|
|
52
52
|
"description": {
|
|
53
|
-
"zh": "
|
|
54
|
-
"en": "Monitor official CS2
|
|
53
|
+
"zh": "✨监控CS2更新日志和官方公告,并自动推送到指定QQ群⭐",
|
|
54
|
+
"en": "Monitor official CS2 announcements and update logs."
|
|
55
55
|
},
|
|
56
56
|
"service": {
|
|
57
57
|
"required": [
|