koishi-plugin-cs2-update-log 1.0.0 → 2.0.1
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 +75 -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,16 @@ 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
|
+
processEntities: false,
|
|
52
|
+
trimValues: true,
|
|
53
|
+
});
|
|
48
54
|
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;
|
|
55
|
+
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;
|
|
56
|
+
const updateSectionInlinePattern = /\[\s*(?:MAPS|GAMEPLAY|MISC|AUDIO|ITEMS|WORKSHOP|PREMIER|GRAPHICS|ANIMATION|UI|SOUND|INPUT|NETWORKING|MATCHMAKING|ARMORY|ENGINE|MAP SCRIPTING)\s*\]/i;
|
|
57
|
+
const sectionHeadingPattern = /^\s*\[\s*([A-Z][A-Z0-9 &'/-]{1,48})\s*\]\s*$/gim;
|
|
51
58
|
function apply(ctx, config) {
|
|
52
59
|
const logger = ctx.logger(loggerName);
|
|
53
60
|
const statePath = resolveStatePath(ctx, config.stateFile);
|
|
@@ -95,22 +102,19 @@ function apply(ctx, config) {
|
|
|
95
102
|
}
|
|
96
103
|
async function fetchNews() {
|
|
97
104
|
try {
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
appid: APP_ID,
|
|
101
|
-
count: config.count,
|
|
102
|
-
maxlength: 0,
|
|
103
|
-
format: 'json',
|
|
104
|
-
feeds: FEED_NAME,
|
|
105
|
-
},
|
|
105
|
+
const xml = await ctx.http.get(STEAM_RSS_URL, {
|
|
106
|
+
responseType: 'text',
|
|
106
107
|
});
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
return items
|
|
108
|
+
const parsed = rssParser.parse(xml);
|
|
109
|
+
const rawItems = parsed.rss?.channel?.item;
|
|
110
|
+
const items = Array.isArray(rawItems) ? rawItems : rawItems ? [rawItems] : [];
|
|
111
|
+
return items
|
|
112
|
+
.map(parseRssItem)
|
|
113
|
+
.filter((item) => !!item?.gid && !!item.title)
|
|
114
|
+
.slice(0, config.count);
|
|
111
115
|
}
|
|
112
116
|
catch (error) {
|
|
113
|
-
logger.error('拉取 Steam CS2
|
|
117
|
+
logger.error('拉取 Steam CS2 RSS 失败:%s', formatError(error));
|
|
114
118
|
return [];
|
|
115
119
|
}
|
|
116
120
|
}
|
|
@@ -155,7 +159,7 @@ function apply(ctx, config) {
|
|
|
155
159
|
}
|
|
156
160
|
async function pushNews(news) {
|
|
157
161
|
const link = getNewsLink(news.item);
|
|
158
|
-
const baseMarkdown =
|
|
162
|
+
const baseMarkdown = steamContentToMarkdown(news.item.content);
|
|
159
163
|
const translated = await maybeTranslate(news.item.title, baseMarkdown);
|
|
160
164
|
const displayTitle = translated.title || news.item.title;
|
|
161
165
|
const displayMarkdown = translated.markdown || baseMarkdown;
|
|
@@ -308,7 +312,7 @@ function apply(ctx, config) {
|
|
|
308
312
|
}, Math.max(5, config.interval) * 1000);
|
|
309
313
|
}
|
|
310
314
|
function classifyNews(item) {
|
|
311
|
-
const body = decodeHtmlEntities(item.
|
|
315
|
+
const body = decodeHtmlEntities(item.content || '');
|
|
312
316
|
const isUpdate = updateTitlePattern.test(item.title)
|
|
313
317
|
|| updateSectionPattern.test(body)
|
|
314
318
|
|| updateSectionInlinePattern.test(body);
|
|
@@ -318,13 +322,61 @@ function classifyNews(item) {
|
|
|
318
322
|
category: isUpdate ? 'update' : 'announcement',
|
|
319
323
|
};
|
|
320
324
|
}
|
|
321
|
-
function
|
|
325
|
+
function parseRssItem(item) {
|
|
326
|
+
const title = decodeHtmlEntities(readXmlText(item.title));
|
|
327
|
+
const url = readXmlText(item.link);
|
|
328
|
+
const guid = readXmlText(item.guid);
|
|
329
|
+
const gid = extractNewsGid(guid) || extractNewsGid(url);
|
|
330
|
+
if (!gid || !title)
|
|
331
|
+
return null;
|
|
332
|
+
const pubDate = readXmlText(item.pubDate);
|
|
333
|
+
const dateValue = Date.parse(pubDate);
|
|
334
|
+
return {
|
|
335
|
+
gid,
|
|
336
|
+
title,
|
|
337
|
+
url,
|
|
338
|
+
author: 'Valve',
|
|
339
|
+
content: readXmlText(item.description),
|
|
340
|
+
date: Number.isFinite(dateValue) ? Math.floor(dateValue / 1000) : 0,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
function readXmlText(value) {
|
|
344
|
+
if (value == null)
|
|
345
|
+
return '';
|
|
346
|
+
if (typeof value === 'string' || typeof value === 'number')
|
|
347
|
+
return String(value).trim();
|
|
348
|
+
return String(value['#text'] ?? '').trim();
|
|
349
|
+
}
|
|
350
|
+
function extractNewsGid(value) {
|
|
351
|
+
return value.match(/\/view\/(\d+)/)?.[1] || '';
|
|
352
|
+
}
|
|
353
|
+
function steamContentToMarkdown(input) {
|
|
322
354
|
let output = decodeHtmlEntities(input || '')
|
|
323
|
-
.replace(/\r\n?/g, '\n')
|
|
355
|
+
.replace(/\r\n?/g, '\n')
|
|
356
|
+
.replace(/\\([\[\]])/g, '$1');
|
|
324
357
|
output = output
|
|
358
|
+
.replace(/<a\b[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi, '[$2]($1)')
|
|
359
|
+
.replace(/<div\b[^>]*class=["'][^"']*bb_h1[^"']*["'][^>]*>([\s\S]*?)<\/div>/gi, (_, text) => `\n# ${text.trim()}\n`)
|
|
360
|
+
.replace(/<div\b[^>]*class=["'][^"']*bb_h2[^"']*["'][^>]*>([\s\S]*?)<\/div>/gi, (_, text) => `\n## ${text.trim()}\n`)
|
|
361
|
+
.replace(/<div\b[^>]*class=["'][^"']*bb_h3[^"']*["'][^>]*>([\s\S]*?)<\/div>/gi, (_, text) => `\n### ${text.trim()}\n`)
|
|
362
|
+
.replace(/<div\b[^>]*class=["'][^"']*bb_h[45][^"']*["'][^>]*>([\s\S]*?)<\/div>/gi, (_, text) => `\n### ${text.trim()}\n`)
|
|
363
|
+
.replace(/<p\b[^>]*class=["'][^"']*bb_paragraph[^"']*["'][^>]*>([\s\S]*?)<\/p>/gi, (_, text) => `\n${text.trim()}\n`)
|
|
364
|
+
.replace(/<li\b[^>]*>\s*/gi, '\n- ')
|
|
365
|
+
.replace(/<\/li>/gi, '\n')
|
|
366
|
+
.replace(/<\/?(?:ul|ol)\b[^>]*>/gi, '\n')
|
|
367
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
368
|
+
.replace(/<img\b[^>]*>/gi, '')
|
|
369
|
+
.replace(/<strong\b[^>]*>([\s\S]*?)<\/strong>/gi, '**$1**')
|
|
370
|
+
.replace(/<b\b[^>]*>([\s\S]*?)<\/b>/gi, '**$1**')
|
|
371
|
+
.replace(/<em\b[^>]*>([\s\S]*?)<\/em>/gi, '*$1*')
|
|
372
|
+
.replace(/<i\b[^>]*>([\s\S]*?)<\/i>/gi, '*$1*')
|
|
373
|
+
.replace(/<code\b[^>]*>([\s\S]*?)<\/code>/gi, '`$1`')
|
|
374
|
+
.replace(/<pre\b[^>]*>([\s\S]*?)<\/pre>/gi, (_, code) => `\n\`\`\`\n${code.trim()}\n\`\`\`\n`)
|
|
375
|
+
.replace(/<[^>]+>/g, '')
|
|
325
376
|
.replace(/\[h1\]([\s\S]*?)\[\/h1\]/gi, (_, text) => `\n# ${text.trim()}\n`)
|
|
326
377
|
.replace(/\[h2\]([\s\S]*?)\[\/h2\]/gi, (_, text) => `\n## ${text.trim()}\n`)
|
|
327
378
|
.replace(/\[h3\]([\s\S]*?)\[\/h3\]/gi, (_, text) => `\n### ${text.trim()}\n`)
|
|
379
|
+
.replace(/\[h[45]\]([\s\S]*?)\[\/h[45]\]/gi, (_, text) => `\n### ${text.trim()}\n`)
|
|
328
380
|
.replace(/\[b\]([\s\S]*?)\[\/b\]/gi, '**$1**')
|
|
329
381
|
.replace(/\[strong\]([\s\S]*?)\[\/strong\]/gi, '**$1**')
|
|
330
382
|
.replace(/\[i\]([\s\S]*?)\[\/i\]/gi, '*$1*')
|
|
@@ -342,8 +394,8 @@ function steamBbcodeToMarkdown(input) {
|
|
|
342
394
|
.replace(/<br\s*\/?>/gi, '\n')
|
|
343
395
|
.replace(/<\/p>\s*<p>/gi, '\n\n')
|
|
344
396
|
.replace(/<\/?p>/gi, '\n');
|
|
345
|
-
output = output.replace(
|
|
346
|
-
|
|
397
|
+
output = output.replace(sectionHeadingPattern, (_, section) => `\n## [${String(section).toUpperCase()}]\n`);
|
|
398
|
+
sectionHeadingPattern.lastIndex = 0;
|
|
347
399
|
return output
|
|
348
400
|
.replace(/\n{3,}/g, '\n\n')
|
|
349
401
|
.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.1",
|
|
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": {
|