koishi-plugin-steaminfo-xiaoheihe 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 WhiteBr1ck
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/lib/index.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ import { Context, Schema } from 'koishi';
2
+ export declare const name = "steaminfo-xiaoheihe";
3
+ export declare const using: string[];
4
+ export interface Config {
5
+ cookies: string;
6
+ waitTimeout: number;
7
+ renderDelay: number;
8
+ deviceScaleFactor: number;
9
+ imageType: 'jpeg' | 'png';
10
+ imageQuality: number;
11
+ showGameTitle: boolean;
12
+ showOnlineCount: boolean;
13
+ debug: boolean;
14
+ }
15
+ export declare const Config: Schema<Config>;
16
+ export declare function apply(ctx: Context, config: Config): void;
package/lib/index.js ADDED
@@ -0,0 +1,197 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
6
+ var __export = (target, all) => {
7
+ for (var name2 in all)
8
+ __defProp(target, name2, { get: all[name2], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ Config: () => Config,
24
+ apply: () => apply,
25
+ name: () => name,
26
+ using: () => using
27
+ });
28
+ module.exports = __toCommonJS(src_exports);
29
+ var import_koishi = require("koishi");
30
+ var name = "steaminfo-xiaoheihe";
31
+ var using = ["puppeteer"];
32
+ var Config = import_koishi.Schema.intersect([
33
+ import_koishi.Schema.object({
34
+ cookies: import_koishi.Schema.string().description("【选填】【建议填写】登录小黑盒后获取的Cookie。获取方式请查看readme。").role("secret")
35
+ }).description("身份验证"),
36
+ import_koishi.Schema.object({
37
+ waitTimeout: import_koishi.Schema.number().description("页面加载的超时时间(毫秒)。").default(6e4),
38
+ renderDelay: import_koishi.Schema.number().description("在内容框架出现后,额外等待渲染的时间(毫秒)。").default(5e3),
39
+ deviceScaleFactor: import_koishi.Schema.number().min(1).max(3).step(0.5).description("截图清晰度(设备缩放因子)。设置为 2 可让文字更加清晰。").default(2),
40
+ imageType: import_koishi.Schema.union(["jpeg", "png"]).description("截图的图片格式。PNG 无损但文件较大,JPEG 有损但文件较小。").default("jpeg"),
41
+ imageQuality: import_koishi.Schema.number().min(1).max(100).description("图片质量,仅在格式为 jpeg 时生效。").default(95)
42
+ }).description("高级设置"),
43
+ import_koishi.Schema.object({
44
+ showGameTitle: import_koishi.Schema.boolean().description("是否在截图前显示“游戏名”文本。").default(true),
45
+ showOnlineCount: import_koishi.Schema.boolean().description("是否在截图前显示“当前在线”文本。").default(true)
46
+ }).description("显示设置"),
47
+ import_koishi.Schema.object({
48
+ debug: import_koishi.Schema.boolean().description("启用后,将在控制台输出详细的调试日志。").default(false)
49
+ }).description("调试")
50
+ ]);
51
+ function apply(ctx, config) {
52
+ const logger = ctx.logger("xiaoheihe");
53
+ const log = /* @__PURE__ */ __name((message) => {
54
+ if (config.debug) {
55
+ logger.info(message);
56
+ }
57
+ }, "log");
58
+ ctx.command("xiaoheihe <game:text>", "小黑盒游戏页面截图").alias("小黑盒").action(async ({ session }, game) => {
59
+ if (!game) return "请输入要搜索的游戏名称。";
60
+ let page;
61
+ let tempMessageId;
62
+ try {
63
+ const tempMessage = [
64
+ import_koishi.segment.quote(session.messageId),
65
+ "请求已收到,正在为您生成游戏截图,请稍候..."
66
+ ];
67
+ const sentMessageIds = await session.send(tempMessage);
68
+ tempMessageId = sentMessageIds[0];
69
+ log(`收到截图请求,游戏名称: "${game}"。临时消息ID: ${tempMessageId}`);
70
+ page = await ctx.puppeteer.page();
71
+ await page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
72
+ if (config.cookies) {
73
+ const cookies = config.cookies.split(";").map((pair) => {
74
+ const parts = pair.trim().split("=");
75
+ const name2 = parts.shift();
76
+ const value = parts.join("=");
77
+ return { name: name2, value, domain: ".xiaoheihe.cn" };
78
+ });
79
+ await page.setCookie(...cookies);
80
+ log(`成功注入 ${cookies.length} 个 Cookie。`);
81
+ } else {
82
+ log("未配置Cookie,以游客身份访问。");
83
+ }
84
+ await page.setViewport({ width: 1280, height: 800, deviceScaleFactor: config.deviceScaleFactor });
85
+ log(`视口已设置为 1280x800,缩放因子为 ${config.deviceScaleFactor}。`);
86
+ const searchUrl = `https://www.xiaoheihe.cn/app/search?q=${encodeURIComponent(game)}`;
87
+ log(`准备导航到搜索页面: ${searchUrl}`);
88
+ await page.goto(searchUrl, { waitUntil: "load", timeout: config.waitTimeout });
89
+ const gameLinkSelector = 'a[href*="/app/topic/game/"]';
90
+ let gamePageHref;
91
+ try {
92
+ log(`将使用选择器 "${gameLinkSelector}" 寻找游戏链接...`);
93
+ await page.waitForSelector(gameLinkSelector, { timeout: 1e4 });
94
+ gamePageHref = await page.$eval(gameLinkSelector, (el) => el.getAttribute("href"));
95
+ } catch (e) {
96
+ if (config.debug) logger.warn("在搜索页未能找到游戏链接,可能是Cookie失效、游戏不存在或页面结构更新。");
97
+ const screenshot = await page.screenshot({ fullPage: true });
98
+ await session.send(["未能找到游戏专题链接。这是当前页面的截图:", import_koishi.segment.image(screenshot)]);
99
+ return;
100
+ }
101
+ const finalUrl = `https://www.xiaoheihe.cn${gamePageHref}`;
102
+ log(`已获取详情页链接,正在导航到: ${finalUrl}`);
103
+ await page.goto(finalUrl, { waitUntil: "load", timeout: config.waitTimeout });
104
+ const mainContentSelector = ".game-detail-page-detail";
105
+ log(`等待核心内容 "${mainContentSelector}" 出现...`);
106
+ await page.waitForSelector(mainContentSelector, { timeout: config.waitTimeout });
107
+ log("核心内容容器已出现!");
108
+ let extractedTitle = game;
109
+ let onlineInfo = "获取失败";
110
+ try {
111
+ const titleSelector = ".game-name p.name";
112
+ const onlineNumberSelector = ".data-list .data-item:first-child .editor p";
113
+ const onlineUnitSelector = ".data-list .data-item:first-child .editor p + p";
114
+ const onlineLabelSelector = ".data-list .data-item:first-child > .p2";
115
+ log("等待标题和数据项出现...");
116
+ await Promise.all([
117
+ page.waitForSelector(titleSelector, { timeout: 1e4 }),
118
+ page.waitForSelector(onlineNumberSelector, { timeout: 1e4 }),
119
+ page.waitForSelector(onlineLabelSelector, { timeout: 1e4 })
120
+ ]);
121
+ log("标题和数据项均已出现,开始提取...");
122
+ const title = await page.$eval(titleSelector, (el) => el.textContent.trim());
123
+ const number = await page.$eval(onlineNumberSelector, (el) => el.textContent.trim());
124
+ const label = await page.$eval(onlineLabelSelector, (el) => el.textContent.trim());
125
+ let unit = "";
126
+ try {
127
+ unit = await page.$eval(onlineUnitSelector, (el) => el.textContent.trim());
128
+ } catch {
129
+ }
130
+ extractedTitle = title;
131
+ onlineInfo = `${label}:${number}${unit}`;
132
+ log(`成功提取到标题: "${extractedTitle}"`);
133
+ log(`成功提取到在线信息: "${onlineInfo}"`);
134
+ } catch {
135
+ log(`无法从页面提取标题或在线人数,将使用用户输入的游戏名。`);
136
+ }
137
+ log(`额外等待 ${config.renderDelay} 毫秒以确保内容渲染完成...`);
138
+ await new Promise((resolve) => setTimeout(resolve, config.renderDelay));
139
+ const selectorsToHide = [
140
+ ".game-detail-section-comment",
141
+ ".game-detail-section-similar-games",
142
+ ".publish-score-wrapper"
143
+ ];
144
+ const selectorToModify = ".game-detail-section-footer";
145
+ log(`准备隐藏 ${selectorsToHide.length} 个元素,并修正 1 个悬浮元素的位置...`);
146
+ await page.evaluate((toHide, toModify) => {
147
+ for (const selector of toHide) {
148
+ const element2 = document.querySelector(selector);
149
+ if (element2) element2.style.display = "none";
150
+ }
151
+ const floatingElement = document.querySelector(toModify);
152
+ if (floatingElement) floatingElement.style.position = "static";
153
+ }, selectorsToHide, selectorToModify);
154
+ const element = await page.$(mainContentSelector);
155
+ if (!element) throw new Error("无法定位到已等待的核心内容元素");
156
+ log(`正在执行最终的精准截图... 格式: ${config.imageType}`);
157
+ const imageBuffer = await element.screenshot({ type: config.imageType, quality: config.imageType === "jpeg" ? config.imageQuality : void 0 });
158
+ log("截图成功!");
159
+ const message = [];
160
+ const textLines = [];
161
+ if (config.showGameTitle) textLines.push(`游戏名:${extractedTitle}`);
162
+ if (config.showOnlineCount && onlineInfo !== "获取失败") textLines.push(onlineInfo);
163
+ const textContent = textLines.join("\n");
164
+ if (textContent) message.push(textContent);
165
+ message.push(import_koishi.segment.image(imageBuffer));
166
+ await session.send(message);
167
+ } catch (error) {
168
+ logger.error("截图过程中发生严重错误:", error);
169
+ let errorMsg = `截图失败,请检查控制台错误日志。`;
170
+ if (error.name === "TimeoutError") {
171
+ errorMsg = `截图失败,页面加载超时。可能是小黑盒服务器繁忙或您的网络不稳定。`;
172
+ }
173
+ await session.send(errorMsg);
174
+ } finally {
175
+ if (page) {
176
+ await page.close();
177
+ log("浏览器页面已关闭。");
178
+ }
179
+ if (tempMessageId) {
180
+ try {
181
+ await session.bot.deleteMessage(session.channelId, tempMessageId);
182
+ log(`临时消息 ${tempMessageId} 已撤回。`);
183
+ } catch (e) {
184
+ logger.warn(`撤回临时消息失败: ${e.message}`);
185
+ }
186
+ }
187
+ }
188
+ });
189
+ }
190
+ __name(apply, "apply");
191
+ // Annotate the CommonJS export names for ESM import in node:
192
+ 0 && (module.exports = {
193
+ Config,
194
+ apply,
195
+ name,
196
+ using
197
+ });
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "koishi-plugin-steaminfo-xiaoheihe",
3
+ "version": "0.1.0",
4
+ "description": "通过小黑盒网站搜索并生成截图获取 steam 游戏信息",
5
+ "main": "lib/index.js",
6
+ "typings": "lib/index.d.ts",
7
+ "files": [
8
+ "lib",
9
+ "dist"
10
+ ],
11
+ "author": "WhiteBr1ck",
12
+ "license": "MIT",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/WhiteBr1ck/koishi-plugin-steaminfo-xiaoheihe.git"
16
+ },
17
+ "bugs": {
18
+ "url": "https://github.com/WhiteBr1ck/koishi-plugin-steaminfo-xiaoheihe/issues"
19
+ },
20
+ "homepage": "https://github.com/WhiteBr1ck/koishi-plugin-steaminfo-xiaoheihe#readme",
21
+ "keywords": [
22
+ "koishi",
23
+ "plugin",
24
+ "xiaoheihe",
25
+ "heybox",
26
+ "game",
27
+ "screenshot",
28
+ "小黑盒",
29
+ "游戏",
30
+ "截图"
31
+ ],
32
+ "peerDependencies": {
33
+ "koishi": "^4.18.7"
34
+ }
35
+ }
package/readme.md ADDED
@@ -0,0 +1,82 @@
1
+ # koishi-plugin-steaminfo-xiaoheihe
2
+
3
+ [![npm](https://img.shields.io/npm/v/koishi-plugin-steaminfo-xiaoheihe?style=flat-square)](https://www.npmjs.com/package/koishi-plugin-steaminfo-xiaoheihe)
4
+
5
+ 一个为 [Koishi](https://koishi.chat/) 设计的插件,允许用户通过指令搜索小黑盒上的内容,并返回一个包含游戏信息的截图。
6
+
7
+ ## ✨ 功能
8
+
9
+ - 对小黑盒游戏详情页进行截图。
10
+ - 提取游戏标题和在线人数。
11
+ - 支持配置小黑盒 Cookie 以访问需要登录才能查看的内容。
12
+
13
+ ## 🚀 安装
14
+
15
+ **前往 Koishi 插件市场,搜索 `steaminfo-xiaoheihe` 并安装。**
16
+
17
+
18
+ ## 📝 使用
19
+
20
+ 启用插件后,向机器人发送指令即可:
21
+
22
+ - **`小黑盒 <游戏名称>`**
23
+ - 别名: **`xiaoheihe <游戏名称>`**
24
+
25
+ **示例:**
26
+
27
+ ```
28
+ 小黑盒 三角洲行动
29
+ ```
30
+
31
+ 机器人将会回复包含该游戏信息的高清截图。
32
+
33
+ ## ⚙️ 配置项
34
+
35
+ 所有配置项均可在插件的设置界面中进行修改。
36
+
37
+ ### 身份验证
38
+
39
+ - **cookies**: (选填,但建议填写) 登录小黑盒后获取的Cookie字符串。提供后可以正常使用搜索功能,并访问需要登录才能查看的内容。
40
+
41
+ ### 高级设置
42
+
43
+ - **waitTimeout**: 页面加载的超时时间(毫秒)。
44
+ - **renderDelay**: 在内容框架出现后,额外等待渲染的时间(毫秒),用于确保图片等动态内容加载完成。
45
+ - **deviceScaleFactor**: 截图清晰度。设置为 `2` 可获得更清晰的“视网膜”截图效果。
46
+ - **imageType**: 截图的图片格式。`jpeg` 文件较小,`png` 画质无损但文件较大。
47
+ - **imageQuality**: 图片质量,仅在格式为 `jpeg` 时生效。
48
+
49
+ ### 显示设置
50
+
51
+ - **showGameTitle**: 是否在截图前显示“游戏名”文本。
52
+ - **showOnlineCount**: 是否在截图前显示“当前在线”文本。
53
+
54
+ ### 调试
55
+
56
+ - **debug**: 启用后,将在控制台输出详细的运行日志,方便排查问题。
57
+
58
+ ---
59
+
60
+ ### 如何获取小黑盒 Cookie?
61
+
62
+ 1. 在你的电脑浏览器(推荐 Chrome 或 Edge)中,访问并登录 [小黑盒官网](https://www.xiaoheihe.cn/)。
63
+ 2. 登录成功后,在当前页面按 `F12` 键,打开“开发者工具”。
64
+ 3. 切换到“网络” (Network) 面板。
65
+ 4. 按 `F5` 刷新一下页面,此时面板中会显示很多网络请求。
66
+ 5. 在请求列表中,随便点击一个发往 `www.xiaoheihe.cn` 的请求。比如 [![示例](https://i.postimg.cc/Z5yQ77X5/1.png)](https://postimg.cc/Hrd3j2pq)
67
+ 6. 在右侧新出现的窗口中,找到“请求标头” (Request Headers) 部分。
68
+ 7. 找到名为 `cookie:` 的一行,右键点击它,选择“复制值” (Copy value)。
69
+ 8. 将复制的整段字符串粘贴到本插件配置项的 `cookies` 输入框中即可。
70
+
71
+ ## ⚠️ 免责声明
72
+
73
+ - 本插件通过模拟浏览器操作访问公开的网页信息,所有数据均来自小黑盒 (xiaoheihe.cn)。
74
+ - 本插件仅供学习和技术交流使用,用户应自觉遵守相关法律法规及网站的用户协议。
75
+ - 因滥用本插件或因小黑盒网站结构变更导致的任何问题,开发者不承担任何责任。
76
+ - 请勿将本插件用于任何商业或非法用途。
77
+
78
+ ## 📄 License
79
+
80
+ 本插件使用 [MIT License](https://github.com/WhiteBr1ck/koishi-plugin-steaminfo-xiaoheihe/blob/main/LICENSE) 授权。
81
+
82
+ © 2025, WhiteBr1ck.