koishi-plugin-cs2-update-log 1.0.0 → 2.0.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 +1 -1
- package/lib/index.js +74 -23
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Koishi QQ 机器人插件,用于轮询 CS2 官方 Steam 公告流,按 `gid`
|
|
|
6
6
|
|
|
7
7
|
## 功能
|
|
8
8
|
|
|
9
|
-
- 使用 Steam `
|
|
9
|
+
- 使用 Steam Store RSS `https://store.steampowered.com/feeds/news/app/730/` 拉取 AppID `730` 的官方公告流。
|
|
10
10
|
- 默认每 30 秒轮询一次。
|
|
11
11
|
- 首次启动默认只记录历史 `gid`,不推送历史内容。
|
|
12
12
|
- 自动分类:
|
package/lib/index.js
CHANGED
|
@@ -7,6 +7,7 @@ exports.Config = exports.inject = exports.name = void 0;
|
|
|
7
7
|
exports.apply = apply;
|
|
8
8
|
const koishi_1 = require("koishi");
|
|
9
9
|
const markdown_it_1 = __importDefault(require("markdown-it"));
|
|
10
|
+
const fast_xml_parser_1 = require("fast-xml-parser");
|
|
10
11
|
const node_fs_1 = require("node:fs");
|
|
11
12
|
const node_path_1 = __importDefault(require("node:path"));
|
|
12
13
|
exports.name = 'cs2-update-log';
|
|
@@ -15,8 +16,7 @@ exports.inject = {
|
|
|
15
16
|
optional: ['puppeteer'],
|
|
16
17
|
};
|
|
17
18
|
const APP_ID = 730;
|
|
18
|
-
const
|
|
19
|
-
const STEAM_NEWS_API = 'https://api.steampowered.com/ISteamNews/GetNewsForApp/v2/';
|
|
19
|
+
const STEAM_RSS_URL = `https://store.steampowered.com/feeds/news/app/${APP_ID}/`;
|
|
20
20
|
const STATE_LIMIT = 1000;
|
|
21
21
|
const loggerName = 'cs2-update-log';
|
|
22
22
|
exports.Config = koishi_1.Schema.object({
|
|
@@ -45,9 +45,15 @@ const markdown = new markdown_it_1.default({
|
|
|
45
45
|
linkify: true,
|
|
46
46
|
breaks: true,
|
|
47
47
|
});
|
|
48
|
+
const rssParser = new fast_xml_parser_1.XMLParser({
|
|
49
|
+
ignoreAttributes: false,
|
|
50
|
+
parseTagValue: false,
|
|
51
|
+
trimValues: true,
|
|
52
|
+
});
|
|
48
53
|
const updateTitlePattern = /\b(?:Counter-Strike 2 Update|Release Notes)\b/i;
|
|
49
|
-
const updateSectionPattern = /^\s*\[\s*(MAPS|GAMEPLAY|MISC|AUDIO|ITEMS|WORKSHOP|PREMIER|GRAPHICS|ANIMATION|UI|SOUND|INPUT|NETWORKING|MATCHMAKING)\s*\]\s*$/gim;
|
|
50
|
-
const updateSectionInlinePattern = /\[\s*(?:MAPS|GAMEPLAY|MISC|AUDIO|ITEMS|WORKSHOP|PREMIER|GRAPHICS|ANIMATION|UI|SOUND|INPUT|NETWORKING|MATCHMAKING)\s*\]/i;
|
|
54
|
+
const updateSectionPattern = /^\s*\[\s*(MAPS|GAMEPLAY|MISC|AUDIO|ITEMS|WORKSHOP|PREMIER|GRAPHICS|ANIMATION|UI|SOUND|INPUT|NETWORKING|MATCHMAKING|ARMORY|ENGINE|MAP SCRIPTING)\s*\]\s*$/gim;
|
|
55
|
+
const updateSectionInlinePattern = /\[\s*(?:MAPS|GAMEPLAY|MISC|AUDIO|ITEMS|WORKSHOP|PREMIER|GRAPHICS|ANIMATION|UI|SOUND|INPUT|NETWORKING|MATCHMAKING|ARMORY|ENGINE|MAP SCRIPTING)\s*\]/i;
|
|
56
|
+
const sectionHeadingPattern = /^\s*\[\s*([A-Z][A-Z0-9 &'/-]{1,48})\s*\]\s*$/gim;
|
|
51
57
|
function apply(ctx, config) {
|
|
52
58
|
const logger = ctx.logger(loggerName);
|
|
53
59
|
const statePath = resolveStatePath(ctx, config.stateFile);
|
|
@@ -95,22 +101,19 @@ function apply(ctx, config) {
|
|
|
95
101
|
}
|
|
96
102
|
async function fetchNews() {
|
|
97
103
|
try {
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
appid: APP_ID,
|
|
101
|
-
count: config.count,
|
|
102
|
-
maxlength: 0,
|
|
103
|
-
format: 'json',
|
|
104
|
-
feeds: FEED_NAME,
|
|
105
|
-
},
|
|
104
|
+
const xml = await ctx.http.get(STEAM_RSS_URL, {
|
|
105
|
+
responseType: 'text',
|
|
106
106
|
});
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
return items
|
|
107
|
+
const parsed = rssParser.parse(xml);
|
|
108
|
+
const rawItems = parsed.rss?.channel?.item;
|
|
109
|
+
const items = Array.isArray(rawItems) ? rawItems : rawItems ? [rawItems] : [];
|
|
110
|
+
return items
|
|
111
|
+
.map(parseRssItem)
|
|
112
|
+
.filter((item) => !!item?.gid && !!item.title)
|
|
113
|
+
.slice(0, config.count);
|
|
111
114
|
}
|
|
112
115
|
catch (error) {
|
|
113
|
-
logger.error('拉取 Steam CS2
|
|
116
|
+
logger.error('拉取 Steam CS2 RSS 失败:%s', formatError(error));
|
|
114
117
|
return [];
|
|
115
118
|
}
|
|
116
119
|
}
|
|
@@ -155,7 +158,7 @@ function apply(ctx, config) {
|
|
|
155
158
|
}
|
|
156
159
|
async function pushNews(news) {
|
|
157
160
|
const link = getNewsLink(news.item);
|
|
158
|
-
const baseMarkdown =
|
|
161
|
+
const baseMarkdown = steamContentToMarkdown(news.item.content);
|
|
159
162
|
const translated = await maybeTranslate(news.item.title, baseMarkdown);
|
|
160
163
|
const displayTitle = translated.title || news.item.title;
|
|
161
164
|
const displayMarkdown = translated.markdown || baseMarkdown;
|
|
@@ -308,7 +311,7 @@ function apply(ctx, config) {
|
|
|
308
311
|
}, Math.max(5, config.interval) * 1000);
|
|
309
312
|
}
|
|
310
313
|
function classifyNews(item) {
|
|
311
|
-
const body = decodeHtmlEntities(item.
|
|
314
|
+
const body = decodeHtmlEntities(item.content || '');
|
|
312
315
|
const isUpdate = updateTitlePattern.test(item.title)
|
|
313
316
|
|| updateSectionPattern.test(body)
|
|
314
317
|
|| updateSectionInlinePattern.test(body);
|
|
@@ -318,13 +321,61 @@ function classifyNews(item) {
|
|
|
318
321
|
category: isUpdate ? 'update' : 'announcement',
|
|
319
322
|
};
|
|
320
323
|
}
|
|
321
|
-
function
|
|
324
|
+
function parseRssItem(item) {
|
|
325
|
+
const title = readXmlText(item.title);
|
|
326
|
+
const url = readXmlText(item.link);
|
|
327
|
+
const guid = readXmlText(item.guid);
|
|
328
|
+
const gid = extractNewsGid(guid) || extractNewsGid(url);
|
|
329
|
+
if (!gid || !title)
|
|
330
|
+
return null;
|
|
331
|
+
const pubDate = readXmlText(item.pubDate);
|
|
332
|
+
const dateValue = Date.parse(pubDate);
|
|
333
|
+
return {
|
|
334
|
+
gid,
|
|
335
|
+
title,
|
|
336
|
+
url,
|
|
337
|
+
author: 'Valve',
|
|
338
|
+
content: readXmlText(item.description),
|
|
339
|
+
date: Number.isFinite(dateValue) ? Math.floor(dateValue / 1000) : 0,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
function readXmlText(value) {
|
|
343
|
+
if (value == null)
|
|
344
|
+
return '';
|
|
345
|
+
if (typeof value === 'string' || typeof value === 'number')
|
|
346
|
+
return String(value).trim();
|
|
347
|
+
return String(value['#text'] ?? '').trim();
|
|
348
|
+
}
|
|
349
|
+
function extractNewsGid(value) {
|
|
350
|
+
return value.match(/\/view\/(\d+)/)?.[1] || '';
|
|
351
|
+
}
|
|
352
|
+
function steamContentToMarkdown(input) {
|
|
322
353
|
let output = decodeHtmlEntities(input || '')
|
|
323
|
-
.replace(/\r\n?/g, '\n')
|
|
354
|
+
.replace(/\r\n?/g, '\n')
|
|
355
|
+
.replace(/\\([\[\]])/g, '$1');
|
|
324
356
|
output = output
|
|
357
|
+
.replace(/<a\b[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi, '[$2]($1)')
|
|
358
|
+
.replace(/<div\b[^>]*class=["'][^"']*bb_h1[^"']*["'][^>]*>([\s\S]*?)<\/div>/gi, (_, text) => `\n# ${text.trim()}\n`)
|
|
359
|
+
.replace(/<div\b[^>]*class=["'][^"']*bb_h2[^"']*["'][^>]*>([\s\S]*?)<\/div>/gi, (_, text) => `\n## ${text.trim()}\n`)
|
|
360
|
+
.replace(/<div\b[^>]*class=["'][^"']*bb_h3[^"']*["'][^>]*>([\s\S]*?)<\/div>/gi, (_, text) => `\n### ${text.trim()}\n`)
|
|
361
|
+
.replace(/<div\b[^>]*class=["'][^"']*bb_h[45][^"']*["'][^>]*>([\s\S]*?)<\/div>/gi, (_, text) => `\n### ${text.trim()}\n`)
|
|
362
|
+
.replace(/<p\b[^>]*class=["'][^"']*bb_paragraph[^"']*["'][^>]*>([\s\S]*?)<\/p>/gi, (_, text) => `\n${text.trim()}\n`)
|
|
363
|
+
.replace(/<li\b[^>]*>\s*/gi, '\n- ')
|
|
364
|
+
.replace(/<\/li>/gi, '\n')
|
|
365
|
+
.replace(/<\/?(?:ul|ol)\b[^>]*>/gi, '\n')
|
|
366
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
367
|
+
.replace(/<img\b[^>]*>/gi, '')
|
|
368
|
+
.replace(/<strong\b[^>]*>([\s\S]*?)<\/strong>/gi, '**$1**')
|
|
369
|
+
.replace(/<b\b[^>]*>([\s\S]*?)<\/b>/gi, '**$1**')
|
|
370
|
+
.replace(/<em\b[^>]*>([\s\S]*?)<\/em>/gi, '*$1*')
|
|
371
|
+
.replace(/<i\b[^>]*>([\s\S]*?)<\/i>/gi, '*$1*')
|
|
372
|
+
.replace(/<code\b[^>]*>([\s\S]*?)<\/code>/gi, '`$1`')
|
|
373
|
+
.replace(/<pre\b[^>]*>([\s\S]*?)<\/pre>/gi, (_, code) => `\n\`\`\`\n${code.trim()}\n\`\`\`\n`)
|
|
374
|
+
.replace(/<[^>]+>/g, '')
|
|
325
375
|
.replace(/\[h1\]([\s\S]*?)\[\/h1\]/gi, (_, text) => `\n# ${text.trim()}\n`)
|
|
326
376
|
.replace(/\[h2\]([\s\S]*?)\[\/h2\]/gi, (_, text) => `\n## ${text.trim()}\n`)
|
|
327
377
|
.replace(/\[h3\]([\s\S]*?)\[\/h3\]/gi, (_, text) => `\n### ${text.trim()}\n`)
|
|
378
|
+
.replace(/\[h[45]\]([\s\S]*?)\[\/h[45]\]/gi, (_, text) => `\n### ${text.trim()}\n`)
|
|
328
379
|
.replace(/\[b\]([\s\S]*?)\[\/b\]/gi, '**$1**')
|
|
329
380
|
.replace(/\[strong\]([\s\S]*?)\[\/strong\]/gi, '**$1**')
|
|
330
381
|
.replace(/\[i\]([\s\S]*?)\[\/i\]/gi, '*$1*')
|
|
@@ -342,8 +393,8 @@ function steamBbcodeToMarkdown(input) {
|
|
|
342
393
|
.replace(/<br\s*\/?>/gi, '\n')
|
|
343
394
|
.replace(/<\/p>\s*<p>/gi, '\n\n')
|
|
344
395
|
.replace(/<\/?p>/gi, '\n');
|
|
345
|
-
output = output.replace(
|
|
346
|
-
|
|
396
|
+
output = output.replace(sectionHeadingPattern, (_, section) => `\n## [${String(section).toUpperCase()}]\n`);
|
|
397
|
+
sectionHeadingPattern.lastIndex = 0;
|
|
347
398
|
return output
|
|
348
399
|
.replace(/\n{3,}/g, '\n\n')
|
|
349
400
|
.trim();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-cs2-update-log",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.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",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
}
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
+
"fast-xml-parser": "^4.5.3",
|
|
35
36
|
"markdown-it": "^14.1.0"
|
|
36
37
|
},
|
|
37
38
|
"devDependencies": {
|