koishi-plugin-cs2-update-log 2.1.2 → 2.2.2
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 +2 -1
- package/assets/brand-logo.png +0 -0
- package/assets/github-qrcode.png +0 -0
- package/lib/index.js +129 -12
- package/package.json +7 -1
package/README.md
CHANGED
|
@@ -14,13 +14,14 @@ Koishi QQ 机器人插件,用于轮询 CS2 官方 Steam 公告流,按 `gid`
|
|
|
14
14
|
- 或正文包含 `[MAPS]`、`[GAMEPLAY]`、`[MISC]`、`[AUDIO]`、`[ITEMS]`、`[WORKSHOP]`、`[PREMIER]` 等更新分区
|
|
15
15
|
- 支持纯文本推送或 Puppeteer 深色长图卡片推送。
|
|
16
16
|
- 支持 OpenAI-compatible Chat Completions API 翻译。
|
|
17
|
+
- 内置短时运行时缓存:RSS 列表缓存 15 秒,新闻内容、AI 翻译结果、长图 PNG 和卡片静态资源每 5 分钟清理一次,减少连续测试/重复推送的开销。
|
|
17
18
|
- 提供 `cs2log.check`、`cs2log.push` 与 `cs2log.test` 命令。
|
|
18
19
|
|
|
19
20
|
## 配置要点
|
|
20
21
|
|
|
21
22
|
- `targets` 必须配置目标 QQ 群,其中 `channelId` 一般填写群号。
|
|
22
23
|
- `picture` 开启后需要安装并启用 Puppeteer 服务插件,例如 `@koishijs/plugin-puppeteer`。
|
|
23
|
-
- `trans` 开启后需要填写 `translateApiKey`;默认接口是 OpenAI Chat Completions 格式,可按你的服务商修改 `translateApiEndpoint` 和 `translateModel`。
|
|
24
|
+
- `trans` 开启后需要填写 `translateApiKey`;默认接口是 OpenAI Chat Completions 格式,可按你的服务商修改 `translateApiEndpoint` 和 `translateModel`。AI 翻译请求超时时间为 90 秒。
|
|
24
25
|
|
|
25
26
|
## 命令
|
|
26
27
|
|
|
Binary file
|
|
Binary file
|
package/lib/index.js
CHANGED
|
@@ -8,6 +8,7 @@ exports.apply = apply;
|
|
|
8
8
|
const koishi_1 = require("koishi");
|
|
9
9
|
const markdown_it_1 = __importDefault(require("markdown-it"));
|
|
10
10
|
const fast_xml_parser_1 = require("fast-xml-parser");
|
|
11
|
+
const node_crypto_1 = require("node:crypto");
|
|
11
12
|
const node_fs_1 = require("node:fs");
|
|
12
13
|
const node_path_1 = __importDefault(require("node:path"));
|
|
13
14
|
exports.name = 'cs2-update-log';
|
|
@@ -18,6 +19,12 @@ exports.inject = {
|
|
|
18
19
|
const APP_ID = 730;
|
|
19
20
|
const STEAM_RSS_URL = `https://store.steampowered.com/feeds/news/app/${APP_ID}/`;
|
|
20
21
|
const STATE_LIMIT = 1000;
|
|
22
|
+
const RSS_LIST_CACHE_TTL_MS = 15 * 1000;
|
|
23
|
+
const RUNTIME_CACHE_CLEAR_INTERVAL_MS = 5 * 60 * 1000;
|
|
24
|
+
const TRANSLATE_TIMEOUT_MS = 90 * 1000;
|
|
25
|
+
const ASSET_DIR = node_path_1.default.resolve(__dirname, '..', 'assets');
|
|
26
|
+
const BRAND_LOGO_PATH = node_path_1.default.join(ASSET_DIR, 'brand-logo.png');
|
|
27
|
+
const GITHUB_QRCODE_PATH = node_path_1.default.join(ASSET_DIR, 'github-qrcode.png');
|
|
21
28
|
const loggerName = 'cs2-update-log';
|
|
22
29
|
exports.Config = koishi_1.Schema.object({
|
|
23
30
|
interval: koishi_1.Schema.number().min(5).step(1).default(30).description('轮询间隔,单位:秒。'),
|
|
@@ -59,8 +66,25 @@ function apply(ctx, config) {
|
|
|
59
66
|
const logger = ctx.logger(loggerName);
|
|
60
67
|
const statePath = resolveStatePath(ctx, config.stateFile);
|
|
61
68
|
const knownGids = new Set();
|
|
69
|
+
const cache = {
|
|
70
|
+
rssFetchedAt: 0,
|
|
71
|
+
rssItems: [],
|
|
72
|
+
newsByGid: new Map(),
|
|
73
|
+
translations: new Map(),
|
|
74
|
+
cardImages: new Map(),
|
|
75
|
+
assetDataUris: new Map(),
|
|
76
|
+
};
|
|
62
77
|
let stateInitialized = false;
|
|
63
78
|
let polling = false;
|
|
79
|
+
function clearRuntimeCache(reason) {
|
|
80
|
+
cache.rssFetchedAt = 0;
|
|
81
|
+
cache.rssItems = [];
|
|
82
|
+
cache.newsByGid.clear();
|
|
83
|
+
cache.translations.clear();
|
|
84
|
+
cache.cardImages.clear();
|
|
85
|
+
cache.assetDataUris.clear();
|
|
86
|
+
logger.info('已清理运行时缓存:%s', reason);
|
|
87
|
+
}
|
|
64
88
|
async function loadState() {
|
|
65
89
|
try {
|
|
66
90
|
const raw = await node_fs_1.promises.readFile(statePath, 'utf8');
|
|
@@ -102,6 +126,10 @@ function apply(ctx, config) {
|
|
|
102
126
|
}
|
|
103
127
|
async function fetchNews() {
|
|
104
128
|
try {
|
|
129
|
+
if (cache.rssItems.length && Date.now() - cache.rssFetchedAt < RSS_LIST_CACHE_TTL_MS) {
|
|
130
|
+
logger.debug('使用 RSS 短时缓存:items=%d', cache.rssItems.length);
|
|
131
|
+
return cache.rssItems.slice(0, config.count);
|
|
132
|
+
}
|
|
105
133
|
const xml = await ctx.http.get(STEAM_RSS_URL, {
|
|
106
134
|
headers: {
|
|
107
135
|
Accept: 'application/rss+xml, application/xml, text/xml;q=0.9, */*;q=0.8',
|
|
@@ -113,10 +141,20 @@ function apply(ctx, config) {
|
|
|
113
141
|
const parsed = rssParser.parse(xml);
|
|
114
142
|
const rawItems = parsed.rss?.channel?.item;
|
|
115
143
|
const items = Array.isArray(rawItems) ? rawItems : rawItems ? [rawItems] : [];
|
|
116
|
-
|
|
144
|
+
const parsedItems = items
|
|
117
145
|
.map(parseRssItem)
|
|
118
|
-
.filter((item) => !!item?.gid && !!item.title)
|
|
119
|
-
|
|
146
|
+
.filter((item) => !!item?.gid && !!item.title);
|
|
147
|
+
const cachedItems = parsedItems.map((item) => {
|
|
148
|
+
const cached = cache.newsByGid.get(item.gid);
|
|
149
|
+
if (cached && cached.title === item.title && cached.content === item.content && cached.url === item.url && cached.date === item.date) {
|
|
150
|
+
return cached;
|
|
151
|
+
}
|
|
152
|
+
cache.newsByGid.set(item.gid, item);
|
|
153
|
+
return item;
|
|
154
|
+
});
|
|
155
|
+
cache.rssFetchedAt = Date.now();
|
|
156
|
+
cache.rssItems = cachedItems;
|
|
157
|
+
return cachedItems.slice(0, config.count);
|
|
120
158
|
}
|
|
121
159
|
catch (error) {
|
|
122
160
|
logger.error('拉取 Steam CS2 RSS 失败:url=%s\n%s', STEAM_RSS_URL, formatError(error));
|
|
@@ -199,7 +237,15 @@ function apply(ctx, config) {
|
|
|
199
237
|
return buildTextMessage(news, title, bodyMarkdown, link);
|
|
200
238
|
}
|
|
201
239
|
try {
|
|
202
|
-
const
|
|
240
|
+
const cacheKey = hashCacheKey('card-image', news.item.gid, news.category, config.brandName, config.siteName, title, bodyMarkdown);
|
|
241
|
+
let png = cache.cardImages.get(cacheKey);
|
|
242
|
+
if (!png) {
|
|
243
|
+
png = await renderCard(puppeteer, news, title, bodyMarkdown);
|
|
244
|
+
cache.cardImages.set(cacheKey, png);
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
logger.debug('使用长图缓存:gid=%s title=%s', news.item.gid, news.item.title);
|
|
248
|
+
}
|
|
203
249
|
const image = koishi_1.h.image(png, 'image/png');
|
|
204
250
|
return config.appendLink ? [image, '\n', link] : image;
|
|
205
251
|
}
|
|
@@ -209,7 +255,8 @@ function apply(ctx, config) {
|
|
|
209
255
|
}
|
|
210
256
|
}
|
|
211
257
|
async function renderCard(puppeteer, news, title, bodyMarkdown) {
|
|
212
|
-
const
|
|
258
|
+
const assets = await loadCardAssets();
|
|
259
|
+
const html = buildCardHtml(news, title, bodyMarkdown, config, assets);
|
|
213
260
|
const page = await puppeteer.page();
|
|
214
261
|
try {
|
|
215
262
|
if (page.setViewport) {
|
|
@@ -242,6 +289,12 @@ function apply(ctx, config) {
|
|
|
242
289
|
logger.warn('已开启 AI 翻译但未填写 translateApiKey,将推送原文。');
|
|
243
290
|
return { title, markdown: bodyMarkdown };
|
|
244
291
|
}
|
|
292
|
+
const cacheKey = hashCacheKey('translation', config.translateApiEndpoint, config.translateModel, config.translatePrompt, title, bodyMarkdown);
|
|
293
|
+
const cached = cache.translations.get(cacheKey);
|
|
294
|
+
if (cached) {
|
|
295
|
+
logger.debug('使用 AI 翻译缓存:title=%s', title);
|
|
296
|
+
return cached;
|
|
297
|
+
}
|
|
245
298
|
try {
|
|
246
299
|
const response = await ctx.http.post(config.translateApiEndpoint, {
|
|
247
300
|
model: config.translateModel,
|
|
@@ -270,22 +323,49 @@ function apply(ctx, config) {
|
|
|
270
323
|
Authorization: `Bearer ${config.translateApiKey}`,
|
|
271
324
|
'Content-Type': 'application/json',
|
|
272
325
|
},
|
|
273
|
-
timeout:
|
|
326
|
+
timeout: TRANSLATE_TIMEOUT_MS,
|
|
274
327
|
});
|
|
275
328
|
const content = response?.choices?.[0]?.message?.content?.trim();
|
|
276
329
|
if (!content)
|
|
277
330
|
throw new Error('empty translation response');
|
|
278
331
|
const parsed = parseJsonObject(content);
|
|
279
|
-
|
|
332
|
+
const translated = {
|
|
280
333
|
title: parsed.title || title,
|
|
281
334
|
markdown: parsed.markdown || bodyMarkdown,
|
|
282
335
|
};
|
|
336
|
+
cache.translations.set(cacheKey, translated);
|
|
337
|
+
return translated;
|
|
283
338
|
}
|
|
284
339
|
catch (error) {
|
|
285
340
|
logger.error('AI 翻译失败,将推送原文:%s', formatError(error));
|
|
286
341
|
return { title, markdown: bodyMarkdown };
|
|
287
342
|
}
|
|
288
343
|
}
|
|
344
|
+
async function loadCardAssets() {
|
|
345
|
+
const [brandLogoDataUri, githubQrDataUri] = await Promise.all([
|
|
346
|
+
loadAssetDataUri('brand-logo', BRAND_LOGO_PATH, 'image/png'),
|
|
347
|
+
loadAssetDataUri('github-qrcode', GITHUB_QRCODE_PATH, 'image/png'),
|
|
348
|
+
]);
|
|
349
|
+
return {
|
|
350
|
+
brandLogoDataUri,
|
|
351
|
+
githubQrDataUri,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
async function loadAssetDataUri(cacheKey, filePath, mimeType) {
|
|
355
|
+
const cached = cache.assetDataUris.get(cacheKey);
|
|
356
|
+
if (cached)
|
|
357
|
+
return cached;
|
|
358
|
+
try {
|
|
359
|
+
const buffer = await node_fs_1.promises.readFile(filePath);
|
|
360
|
+
const dataUri = `data:${mimeType};base64,${buffer.toString('base64')}`;
|
|
361
|
+
cache.assetDataUris.set(cacheKey, dataUri);
|
|
362
|
+
return dataUri;
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
logger.error('读取卡片资源失败:path=%s\n%s', filePath, formatError(error));
|
|
366
|
+
return '';
|
|
367
|
+
}
|
|
368
|
+
}
|
|
289
369
|
ctx.command('cs2log.check', '查看最近 5 条 CS2 官方公告分类结果')
|
|
290
370
|
.action(async () => {
|
|
291
371
|
const items = await fetchNews();
|
|
@@ -332,6 +412,9 @@ function apply(ctx, config) {
|
|
|
332
412
|
ctx.setInterval(() => {
|
|
333
413
|
void pollAndPush('timer');
|
|
334
414
|
}, Math.max(5, config.interval) * 1000);
|
|
415
|
+
ctx.setInterval(() => {
|
|
416
|
+
clearRuntimeCache('short cache interval');
|
|
417
|
+
}, RUNTIME_CACHE_CLEAR_INTERVAL_MS);
|
|
335
418
|
}
|
|
336
419
|
function classifyNews(item) {
|
|
337
420
|
const body = decodeHtmlEntities(item.content || '');
|
|
@@ -422,6 +505,14 @@ function steamContentToMarkdown(input) {
|
|
|
422
505
|
.replace(/\n{3,}/g, '\n\n')
|
|
423
506
|
.trim();
|
|
424
507
|
}
|
|
508
|
+
function hashCacheKey(...parts) {
|
|
509
|
+
const hash = (0, node_crypto_1.createHash)('sha256');
|
|
510
|
+
for (const part of parts) {
|
|
511
|
+
hash.update(part);
|
|
512
|
+
hash.update('\0');
|
|
513
|
+
}
|
|
514
|
+
return hash.digest('hex');
|
|
515
|
+
}
|
|
425
516
|
function buildTextMessage(news, title, bodyMarkdown, link) {
|
|
426
517
|
const categoryName = news.category === 'update' ? 'CS2 官方更新日志' : 'CS2 官方公告';
|
|
427
518
|
const parts = [
|
|
@@ -438,7 +529,7 @@ function buildTextMessage(news, title, bodyMarkdown, link) {
|
|
|
438
529
|
parts.push(`原文:${link}`);
|
|
439
530
|
return parts.join('\n');
|
|
440
531
|
}
|
|
441
|
-
function buildCardHtml(news, title, bodyMarkdown, config) {
|
|
532
|
+
function buildCardHtml(news, title, bodyMarkdown, config, assets) {
|
|
442
533
|
const categoryName = news.category === 'update' ? 'CS2 官方更新日志' : 'CS2 官方公告';
|
|
443
534
|
const categoryTag = news.category === 'update' ? 'UPDATE LOG' : 'ANNOUNCEMENT';
|
|
444
535
|
const rendered = markdown.render(bodyMarkdown);
|
|
@@ -502,6 +593,13 @@ body {
|
|
|
502
593
|
border: 1px solid rgba(255, 255, 255, 0.14);
|
|
503
594
|
box-shadow: inset 0 0 0 5px rgba(255, 255, 255, 0.03);
|
|
504
595
|
font-size: 26px;
|
|
596
|
+
overflow: hidden;
|
|
597
|
+
}
|
|
598
|
+
.avatar img {
|
|
599
|
+
display: block;
|
|
600
|
+
width: 100%;
|
|
601
|
+
height: 100%;
|
|
602
|
+
object-fit: cover;
|
|
505
603
|
}
|
|
506
604
|
.brand-title {
|
|
507
605
|
font-size: 21px;
|
|
@@ -642,6 +740,13 @@ body {
|
|
|
642
740
|
font-weight: 900;
|
|
643
741
|
color: #ffffff;
|
|
644
742
|
}
|
|
743
|
+
.site-wrap {
|
|
744
|
+
display: flex;
|
|
745
|
+
align-items: center;
|
|
746
|
+
justify-content: flex-end;
|
|
747
|
+
gap: 14px;
|
|
748
|
+
min-width: 230px;
|
|
749
|
+
}
|
|
645
750
|
.site {
|
|
646
751
|
text-align: right;
|
|
647
752
|
min-width: 160px;
|
|
@@ -656,13 +761,22 @@ body {
|
|
|
656
761
|
color: #78b7ff;
|
|
657
762
|
font-size: 14px;
|
|
658
763
|
}
|
|
764
|
+
.qr-code {
|
|
765
|
+
width: 62px;
|
|
766
|
+
height: 62px;
|
|
767
|
+
flex: 0 0 auto;
|
|
768
|
+
padding: 5px;
|
|
769
|
+
border-radius: 8px;
|
|
770
|
+
background: rgba(255, 255, 255, 0.94);
|
|
771
|
+
border: 1px solid rgba(255, 255, 255, 0.22);
|
|
772
|
+
}
|
|
659
773
|
</style>
|
|
660
774
|
</head>
|
|
661
775
|
<body>
|
|
662
776
|
<article id="card">
|
|
663
777
|
<header class="topbar">
|
|
664
778
|
<div class="brand">
|
|
665
|
-
<div class="avatar"
|
|
779
|
+
<div class="avatar">${assets.brandLogoDataUri ? `<img src="${assets.brandLogoDataUri}" alt="">` : 'CS'}</div>
|
|
666
780
|
<div>
|
|
667
781
|
<div class="brand-title">${escapeHtml(config.brandName)}</div>
|
|
668
782
|
<div class="brand-subtitle">CS2 更新日志工具</div>
|
|
@@ -683,9 +797,12 @@ body {
|
|
|
683
797
|
<div class="published-label">PUBLISHED AT</div>
|
|
684
798
|
<div class="published-time">${escapeHtml(publishedAt)}</div>
|
|
685
799
|
</div>
|
|
686
|
-
<div class="site">
|
|
687
|
-
<div class="site
|
|
688
|
-
|
|
800
|
+
<div class="site-wrap">
|
|
801
|
+
<div class="site">
|
|
802
|
+
<div class="site-name">${escapeHtml(config.siteName)}</div>
|
|
803
|
+
<div class="author">${escapeHtml(author)}</div>
|
|
804
|
+
</div>
|
|
805
|
+
${assets.githubQrDataUri ? `<img class="qr-code" src="${assets.githubQrDataUri}" alt="">` : ''}
|
|
689
806
|
</div>
|
|
690
807
|
</footer>
|
|
691
808
|
</article>
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-cs2-update-log",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.2",
|
|
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",
|
|
7
7
|
"files": [
|
|
8
|
+
"assets",
|
|
8
9
|
"lib",
|
|
9
10
|
"README.md"
|
|
10
11
|
],
|
|
@@ -22,6 +23,11 @@
|
|
|
22
23
|
],
|
|
23
24
|
"author": "BestBcz",
|
|
24
25
|
"license": "MIT",
|
|
26
|
+
"homepage": "https://github.com/BestBcz/koishi-cs2-update-log",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/BestBcz/koishi-cs2-update-log.git"
|
|
30
|
+
},
|
|
25
31
|
"peerDependencies": {
|
|
26
32
|
"@koishijs/plugin-puppeteer": "^3.2.0",
|
|
27
33
|
"koishi": "^4.18.0"
|