UNPKG

koishi-plugin-mc-tools

Version:

我的世界(Minecraft/MC)工具。支持查询MCWiki/MCMod/CurseForge/Modrinth、服务器信息、最新版本和玩家皮肤;推送MC更新通知,运行命令等

1,265 lines (1,261 loc) 101 kB
var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __esm = (fn, res) => function __init() { return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; }; var __export = (target, all) => { for (var name2 in all) __defProp(target, name2, { get: all[name2], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/mod.ts var mod_exports = {}; __export(mod_exports, { fetchModContent: () => fetchModContent, formatContent: () => formatContent, registerModCommands: () => registerModCommands, searchMod: () => searchMod }); function parseContent($, pageType, maxLength) { const sections = []; const relatedLinks = []; let totalLength = 0; const parseImage = /* @__PURE__ */ __name(($elem) => { const $img = $elem.find("img"); const src = $img.attr("data-src") || $img.attr("src"); return src ? import_koishi.h.image(src.startsWith("//") ? `https:${src}` : src).toString() : null; }, "parseImage"); const parseLink = /* @__PURE__ */ __name(($elem) => { const links = []; $elem.find('[id^="link_"]').each((_, elem) => { const $link = $(elem); const id = $link.attr("id"); if (!id) return; const scriptContent = $(`script:contains(${id})`).text(); const urlMatch = scriptContent.match(/content:"[^"]*?<strong>([^<]+)/); if (urlMatch?.[1]) { const url = urlMatch[1]; if (url.match(/\.(jpg|jpeg|png|gif|webp|bmp|tiff|tif)$/i)) return; let prefix = ""; let prevNode = $link[0].previousSibling; while (prevNode && prevNode.type === "text") { prefix = prevNode.data.trim() + " " + prefix; prevNode = prevNode.previousSibling; } prefix = prefix.trim(); const linkText = $link.text().trim(); const isUrl = linkText.match(/^https?:\/\//); const formattedLink = isUrl ? url : `[${linkText}](${url})`; links.push(prefix ? `${prefix} ${formattedLink}` : formattedLink); } }); return links.length > 0 ? links.join("\n") : null; }, "parseLink"); const parseText = /* @__PURE__ */ __name(($elem) => { const cleanedElem = $elem.clone(); cleanedElem.find("script, i.pstatus, .fastcopy").remove(); cleanedElem.find("a").each((_, link) => { const $link = $(link); const href = $link.attr("href"); const text2 = $link.text().trim(); if (href && text2 && !href.includes("javascript:") && !href.startsWith("#")) { let processedHref = href; if (href.startsWith("//")) { processedHref = `https:${href}`; } else if (href.startsWith("/")) { processedHref = `https://www.mcmod.cn${href}`; } let prefix = ""; let prevNode = link.previousSibling; while (prevNode && prevNode.type === "text") { prefix = prevNode.data.trim() + " " + prefix; prevNode.data = ""; prevNode = prevNode.previousSibling; } const isUrl = text2.match(/^https?:\/\//); const markdownLink = isUrl ? processedHref : `[${text2}](${processedHref})`; const linkText = prefix ? `${prefix} ${markdownLink}` : markdownLink; $link.replaceWith(linkText); } }); const text = cleanedElem.text().replace(/[\u0000-\u001F\u007F-\u009F\u200B-\u200D\uFEFF]|\[(\w+)\]|本帖最后由.+编辑|复制代码/g, "").replace(/\s+/g, " ").trim(); const isTitle = /* @__PURE__ */ __name(() => { if (text.length > 12) return false; const $parent = $elem.parent(); return $parent.find("strong").length || $parent.find("span.common-text-title").length; }, "isTitle"); return text && !text.includes("此链接会跳转到") && !text.includes("不要再提示我") ? isTitle() ? `『${text}』` : text : null; }, "parseText"); if (pageType === "item") { const itemName = $(".itemname .name h5").first().text().trim(); const title = itemName || $(".class-title h3").first().text().trim() + ($(".class-title h4").first().text().trim() ? ` (${$(".class-title h4").first().text().trim()})` : ""); sections.push(title); const $itemIcon = $(".item-info-table"); if ($itemIcon.length) { const image = parseImage($itemIcon); if (image) sections.push(image); } } else if (["mod", "modpack"].includes(pageType)) { const shortName = $(".short-name").first().text().trim(); const title = $(".class-title h3").first().text().trim(); const enTitle = $(".class-title h4").first().text().trim(); const modStatusLabels = $(`.class-official-group .class-status`).map((_, el) => $(el).text().trim()).get().concat($(`.class-official-group .class-source`).map((_, el) => $(el).text().trim()).get()); sections.push(`${shortName} ${enTitle} | ${title}${modStatusLabels.length ? ` (${modStatusLabels.join(" | ")})` : ""}`); const $coverImage = $(".class-cover-image"); if ($coverImage.length) { const imageResult = parseImage($coverImage); if (imageResult) sections.push(imageResult); } $(".class-info-left .col-lg-4").each((_, elem) => { const text = $(elem).text().trim().replace(/\s+/g, " ").replace(/:/g, ":"); if (text.includes("支持的MC版本")) return; const infoTypes = ["整合包类型", "运作方式", "打包方式", "运行环境"]; if (infoTypes.some((t) => text.includes(t))) { sections.push(text); } }); const versionSections = ["支持版本:"]; const processedLoaders = /* @__PURE__ */ new Set(); $(".mcver ul").each((_, ul) => { const $ul = $(ul); const loader = $ul.find("li:first").text().trim(); if (loader && !processedLoaders.has(loader)) { const versions = $ul.find("a").map((_2, el) => $(el).text().trim()).get().filter((v) => v.match(/^\d/)); if (versions.length) { const grouped = versions.reduce((groups, version) => { const mainVersion = version.match(/^(\d+\.\d+)/)?.[1] || version; groups.set(mainVersion, (groups.get(mainVersion) || []).concat(version)); return groups; }, /* @__PURE__ */ new Map()); const processedVersions = Array.from(grouped.entries()).sort(([a], [b]) => b.localeCompare(a, void 0, { numeric: true })).map(([main, group]) => group.length > 1 ? `${main}(${group.length}个版本)` : group[0]); versionSections.push(`${loader} ${processedVersions.join(", ")}`); processedLoaders.add(loader); } } }); if (versionSections.length > 1) { sections.push(...versionSections); } } else if (pageType === "bbs") { const title = $("#thread_subject").first().text().trim(); if (title) sections.push(title); } const contentSelector = { mod: ".common-text", modpack: ".common-text", post: "div.text", item: ".item-content.common-text", bbs: '[id^="postmessage_"]' }[pageType]; const $content = $(contentSelector).first(); $content.children().each((_, element) => { const $elem = $(element); const image = parseImage($elem); if (image) { sections.push(image); return; } const link = parseLink($elem); if (link) { sections.push(link); return; } const text = parseText($elem); if (text && (maxLength < 0 || totalLength < maxLength)) { sections.push(text); if (!text.startsWith("http")) { totalLength += text.length; } } }); const linkMap = /* @__PURE__ */ new Map(); $(".common-link-frame .list ul li").each((_, item) => { const $item = $(item); const $link = $item.find("a"); const url = $link.attr("href"); const rawType = $link.attr("data-original-title") || $item.find(".name").text().trim(); if (!url || !rawType) return; const [type, customName] = rawType.split(":").map((s) => s.trim()); const name2 = customName || type; let processedUrl = url; if (url.startsWith("//link.mcmod.cn/target/")) { try { const encodedPart = url.split("target/")[1]; processedUrl = Buffer.from(encodedPart, "base64").toString("utf-8"); } catch { processedUrl = url; } } else if (url.startsWith("//")) { processedUrl = `https:${url}`; } if (!linkMap.has(type)) { linkMap.set(type, { url: processedUrl, name: name2 }); } }); Array.from(linkMap.entries()).forEach(([type, { url, name: name2 }]) => { relatedLinks.push(`${type}${name2 !== type ? ` (${name2})` : ""}: ${url}`); }); return { sections: sections.filter((s, i, arr) => s.trim() && arr.indexOf(s) === i), links: relatedLinks }; } function formatContent(result, url, options = {}) { if (!result?.sections) { return `无法获取页面内容,请访问:${url}`; } const sections = result.sections.filter(Boolean).map( (section) => section.toString().trim().replace(/[\u0000-\u001F\u007F-\u009F\u200B-\u200D\uFEFF]/g, "") ); const title = sections[0]; const coverImage = sections[1]; const basicInfo = sections.filter((s) => ["运行环境", "整合包类型", "运作方式", "打包方式"].some((type) => s.includes(type))); const versionInfo = sections.filter((s) => s === "支持版本:" || ["行为包:", "Forge:", "Fabric:"].some((type) => s.includes(type))); const content = sections.filter( (s, index) => index > 1 && !["运行环境", "整合包类型", "运作方式", "打包方式"].some((type) => s.includes(type)) && !["支持版本:", "行为包:", "Forge:", "Fabric:"].some((type) => s.includes(type)) ).map((s, i, arr) => { const noLimit = options.linkCount === -1; if (i === arr.length - 1 && !s.startsWith("http") && !s.endsWith("...") && !options.linkCount && !noLimit) { return s + "..."; } return s; }); const images = sections.filter((s, index) => index > 1 && s.startsWith("http") && !s.includes(":")); const linkLimit = options.linkCount === -1 ? Infinity : options.linkCount || 0; const links = result.links?.length ? ["相关链接:", ...result.links.slice(0, linkLimit)] : []; const shouldShowImages = options.showImages === "always" || options.showImages === "noqq" && options.platform !== "qq"; const output = [ title, coverImage, ...basicInfo, ...versionInfo, ...links.length ? links : [], "简介:", ...content, ...shouldShowImages ? images : [], `详细内容: ${url}` ]; return output.filter((s) => s && s.length > 0).join("\n").trim() || `无法获取详细内容,请访问:${url}`; } async function fetchModContent(url, config) { try { const response = await import_axios.default.get(url, { timeout: config.Timeout < 0 ? 0 : config.Timeout * 1e3, headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" } }); const $ = cheerio.load(response.data); if ($(".class-404").length > 0) { throw new Error("该页面不存在或已被删除"); } const pageType = url.includes("/modpack/") ? "modpack" : url.includes("/post/") ? "post" : url.includes("/item/") ? "item" : url.includes("bbs.mcmod.cn") ? "bbs" : "mod"; return parseContent($, pageType, config.totalLength); } catch (error) { if (import_axios.default.isAxiosError(error)) { throw new Error(error.code === "ECONNABORTED" ? "请求超时,请稍后再试" : `内容获取失败:${error.message}`); } throw error; } } async function searchMod(keyword, config) { try { const response = await import_axios.default.get( `https://search.mcmod.cn/s?key=${encodeURIComponent(keyword)}`, { timeout: config.Timeout < 0 ? 0 : config.Timeout * 1e3 } ); const $ = cheerio.load(response.data); const results = []; $(".result-item").each((_, item) => { const $item = $(item); const titleEl = $item.find(".head a").last(); const title = titleEl.text().trim(); const url = titleEl.attr("href") || ""; let desc = ""; if (config.descLength !== 0) { desc = $item.find(".body").text().trim().replace(/\[.*?\]/g, "").trim(); if (config.descLength > 0 && desc.length > config.descLength) { desc = desc.slice(0, config.descLength) + "..."; } } const normalizedUrl = url.startsWith("http") ? url : `https://www.mcmod.cn${url}`; if (title && url) { results.push({ title, url: normalizedUrl, desc, source: "mcmod" }); } }); return results.slice(0, 10); } catch (error) { throw new Error(`搜索失败: ${error.message}`); } } function processGameVersions(versions) { if (!versions?.length) return []; const [stableVersions, snapshotCount] = versions.reduce(([stable, count], version) => { const isSnapshot = version.includes("exp") || version.includes("pre") || version.includes("rc") || /\d+w\d+[a-z]/.test(version); return isSnapshot ? [stable, count + 1] : [[...stable, version], count]; }, [[], 0]); const grouped = stableVersions.reduce((groups, version) => { const mainVersion = version.match(/^(\d+\.\d+)/)?.[1] || version; groups.set(mainVersion, (groups.get(mainVersion) || []).concat(version)); return groups; }, /* @__PURE__ */ new Map()); const result = Array.from(grouped.entries()).sort(([a], [b]) => b.localeCompare(a, void 0, { numeric: true })).map(([main, group]) => group.length > 1 ? `${main}(${group.length}个版本)` : group[0]); if (snapshotCount > 0) result.push(`+${snapshotCount}个快照版本`); return result; } async function fetchAPI(url, options = {}) { try { const response = await (0, import_axios.default)({ url, ...options, timeout: (options.timeout || 10) * 1e3 }); return response.data; } catch (error) { throw new Error(`请求失败: ${error.message}`); } } async function searchMods(keyword, source, config, cfApiKey, type) { const typeInfo = TypeMap.getTypeInfo(source, type); if (!typeInfo.valid) throw new Error(`无效的类型: ${type}`); if (source === "modrinth") { const data = await fetchAPI("https://api.modrinth.com/v2/search", { params: { query: keyword, limit: 10, facets: typeInfo.facets ? [typeInfo.facets] : void 0, offset: 0, index: "relevance" }, timeout: config.Timeout < 0 ? 0 : config.Timeout }); return data.hits.map((hit) => ({ source: "modrinth", id: hit.slug, type: hit.project_type, title: hit.title, description: hit.description, categories: hit.categories })); } else { const data = await fetchAPI("https://api.curseforge.com/v1/mods/search", { headers: { "x-api-key": cfApiKey }, params: { gameId: 432, searchFilter: keyword, classId: typeInfo.classId, pageSize: 10, sortField: 2, sortOrder: "desc" }, timeout: config.Timeout < 0 ? 0 : config.Timeout }); return data.data.map((r) => ({ source: "curseforge", id: r.id, type: TypeMap.curseforgeTypes[r.classId] || "未知", title: r.name, description: r.summary, categories: r.categories.map((c) => typeof c === "string" ? c : c.name) })); } } async function getModDetails(result, config, cfApiKey) { let details, displayType; if (result.source === "modrinth") { const data = await fetchAPI(`https://api.modrinth.com/v2/project/${result.id}`); details = { title: data.title, description: data.body, type: data.project_type, categories: data.categories, requirements: [ `客户端: ${data.client_side === "required" ? "必需" : data.client_side === "optional" ? "可选" : "无需"}`, `服务端: ${data.server_side === "required" ? "必需" : data.server_side === "optional" ? "可选" : "无需"}` ], loaders: data.loaders, versions: processGameVersions(data.game_versions), url: `https://modrinth.com/${data.project_type}/${data.slug}` }; displayType = TypeMap.getLocalizedType("modrinth", data.project_type); } else { const data = await fetchAPI(`https://api.curseforge.com/v1/mods/${result.id}`, { headers: { "x-api-key": cfApiKey } }); const descData = await fetchAPI(`https://api.curseforge.com/v1/mods/${result.id}/description`, { headers: { "x-api-key": cfApiKey } }); const allVersions = /* @__PURE__ */ new Set(); const loaders = /* @__PURE__ */ new Set(); data.data.latestFiles?.forEach((file) => { file.gameVersions?.forEach((version) => { if (version.includes("Forge") || version.includes("Fabric") || version.includes("NeoForge") || version.includes("Quilt")) { loaders.add(version.split("-")[0]); } else { allVersions.add(version); } }); }); details = { title: data.data.name, description: descData.data.replace(/<[^>]*>/g, "").replace(/\n\s*\n/g, "\n"), type: data.data.classId, categories: data.data.categories.map((c) => typeof c === "string" ? c : c.name), requirements: [], loaders: Array.from(loaders), versions: processGameVersions(Array.from(allVersions)), url: data.data.links.websiteUrl }; displayType = TypeMap.getLocalizedType("curseforge", data.data.classId); } let description = details.description; if (config.totalLength > 0 && description.length > config.totalLength) { description = description.slice(0, config.totalLength) + "..."; } const parts = [ `${displayType} | ${details.title}(${details.categories.join(", ")})`, details.requirements.length ? details.requirements.join(" | ") : "", details.loaders?.length ? `加载器: ${details.loaders.join(", ")}` : "", details.versions?.length ? `支持版本: ${details.versions.join(", ")}` : "", `详细介绍: ${description}`, `链接: ${details.url}` ]; return parts.filter(Boolean).join("\n"); } function registerModPlatformCommands(mcmod, config) { mcmod.subcommand(".mr <keyword> [type]", "查询 Modrinth").usage("mc.mod.mr <关键词> [类型] - 查询 Modrinth 内容\n可用类型:mod(模组), resourcepack(资源包), datapack(数据包), shader(光影), modpack(整合包), plugin(插件)").action(async ({}, keyword, type) => { if (!keyword) return "请输入要搜索的关键词"; try { const results = await searchMods(keyword, "modrinth", config, config.cfApi, type); if (!results.length) return "未找到相关内容"; return await getModDetails(results[0], config, config.cfApi); } catch (error) { return error.message; } }).subcommand(".find <keyword> [type]", "搜索 Modrinth").usage("mc.mod.findmr <关键词> [类型] - 搜索 Modrinth 项目\n可用类型:mod(模组), resourcepack(资源包), datapack(数据包), shader(光影), modpack(整合包), plugin(插件)").action(async ({ session }, keyword, type) => { if (!keyword) return "请输入要搜索的关键词"; try { const results = await searchMods(keyword, "modrinth", config, config.cfApi, type); if (!results.length) return "未找到相关项目"; const formattedResults = results.map((r, i) => { let description = r.description; if (config.descLength > 0 && description.length > config.descLength) { description = description.slice(0, config.descLength) + "..."; } return `${i + 1}. ${[ `${TypeMap.getLocalizedType(r.source, r.type)} | ${r.title}`, `分类: ${r.categories.join(", ")}`, `描述: ${description}` ].join("\n")}`; }).join("\n"); await session.send(`Modrinth 搜索结果: ${formattedResults} 请回复序号查看详细内容`); const response = await session.prompt(config.Timeout < 0 ? void 0 : config.Timeout * 1e3); if (!response) return "操作超时"; const index = parseInt(response) - 1; if (isNaN(index) || index < 0 || index >= results.length) return "请输入有效的序号"; return await getModDetails(results[index], config, config.cfApi); } catch (error) { return error.message; } }); mcmod.subcommand(".cf <keyword> [type]", "查询 CurseForge").usage("mc.mod.cf <关键词> [类型] - 查询 CurseForge 内容\n可用类型:mod(模组), resourcepack(资源包), modpack(整合包), shader(光影), datapack(数据包), world(地图), addon(附加包), plugin(插件)").action(async ({}, keyword, type) => { if (!keyword) return "请输入要搜索的关键词"; try { const results = await searchMods(keyword, "curseforge", config, config.cfApi, type); if (!results.length) return "未找到相关内容"; return await getModDetails(results[0], config, config.cfApi); } catch (error) { return error.message; } }).subcommand(".find <keyword> [type]", "搜索 CurseForge").usage("mc.mod.findcf <关键词> [类型] - 搜索 CurseForge 项目\n可用类型:mod(模组), resourcepack(资源包), modpack(整合包), shader(光影), datapack(数据包), world(地图), addon(附加包), plugin(插件)").action(async ({ session }, keyword, type) => { if (!keyword) return "请输入要搜索的关键词"; try { const results = await searchMods(keyword, "curseforge", config, config.cfApi, type); if (!results.length) return "未找到相关项目"; const formattedResults = results.map((r, i) => { const description = r.description.length > config.descLength ? r.description.slice(0, config.descLength) + "..." : r.description; return `${i + 1}. ${[ `${TypeMap.getLocalizedType(r.source, r.type)} | ${r.title}`, `分类: ${r.categories.join(", ")}`, `描述: ${description}` ].join("\n")}`; }).join("\n"); await session.send(`CurseForge 搜索结果: ${formattedResults} 请回复序号查看详细内容`); const response = await session.prompt(config.Timeout * 1e3); if (!response) return "操作超时"; const index = parseInt(response) - 1; if (isNaN(index) || index < 0 || index >= results.length) return "请输入有效的序号"; return await getModDetails(results[index], config, config.cfApi); } catch (error) { return error.message; } }); } function registerModCommands(ctx, parent, config) { const mcmod = parent.subcommand(".mod <keyword:text>", "查询 Minecraft 相关资源").usage("mc.mod <关键词> - 查询 MCMod\nmc.mod.find <关键词> - 搜索 MCMod\nmc.mod.shot <关键词> - 截图 MCMod 页面\nmc.mod.(find)mr <关键词> [类型] - 搜索 Modrinth\nmc.mod.(find)cf <关键词> [类型] - 搜索 CurseForge").action(async ({ session }, keyword) => { if (!keyword) return "请输入要查询的关键词"; try { const results = await searchMod(keyword, config); if (!results.length) return "未找到相关内容"; const content = await fetchModContent(results[0].url, config); return formatContent(content, results[0].url, { linkCount: config.linkCount, showImages: config.showImages, platform: session.platform }); } catch (error) { return error.message; } }); mcmod.subcommand(".find <keyword:text>", "搜索 MCMod").usage("mc.mod.find <关键词> - 搜索 MCMOD 页面").action(async ({ session }, keyword) => { const { search: search2 } = await Promise.resolve().then(() => (init_wiki(), wiki_exports)); return await search2({ keyword, source: "mcmod", session, config, ctx }); }); mcmod.subcommand(".shot <keyword:text>", "截图 MCMod 页面").usage("mc.mod.shot <关键词> - 搜索并获取指定页面截图").action(async ({ session }, keyword) => { if (!keyword) return "请输入要查询的关键词"; try { const results = await searchMod(keyword, config); if (!results.length) throw new Error("未找到相关内容"); const targetUrl = results[0].url; await session.send(`正在获取页面... 完整内容:${targetUrl}`); const result = await capture( targetUrl, ctx, { type: "mcmod" }, config ); return result.image; } catch (error) { return error.message; } }); registerModPlatformCommands(mcmod, config); } var import_koishi, import_axios, cheerio, TypeMap; var init_mod = __esm({ "src/mod.ts"() { import_koishi = require("koishi"); import_axios = __toESM(require("axios")); cheerio = __toESM(require("cheerio")); init_wiki(); TypeMap = { modrinthTypes: { "mod": "模组", "resourcepack": "资源包", "datapack": "数据包", "shader": "光影", "modpack": "整合包", "plugin": "插件" }, facets: { "mod": ["project_type:mod"], "resourcepack": ["project_type:resourcepack"], "datapack": ["project_type:datapack"], "shader": ["project_type:shader"], "modpack": ["project_type:modpack"], "plugin": ["project_type:plugin"] }, curseforgeTypes: { 6: "mod", 12: "resourcepack", 17: "modpack", 4471: "shader", 4546: "datapack", 4944: "world", 5141: "addon", 5232: "plugin" }, curseforgeTypeNames: { "mod": "模组/扩展", "resourcepack": "资源包/材质包", "modpack": "整合包", "shader": "光影包", "datapack": "数据包", "world": "地图存档", "addon": "附加内容", "plugin": "服务器插件" }, getTypeInfo(source, type) { if (!type) return { valid: true }; const types = source === "modrinth" ? Object.keys(this.modrinthTypes) : Object.values(this.curseforgeTypes); return { valid: types.includes(type), facets: source === "modrinth" ? [`project_type:${type}`] : void 0, classId: source === "curseforge" ? Number(Object.keys(this.curseforgeTypes).find((k) => this.curseforgeTypes[k] === type)) : void 0 }; }, getLocalizedType(source, typeKey) { return source === "modrinth" ? this.modrinthTypes[typeKey] || typeKey : this.curseforgeTypeNames[this.curseforgeTypes[typeKey]] || "未知"; } }; __name(parseContent, "parseContent"); __name(formatContent, "formatContent"); __name(fetchModContent, "fetchModContent"); __name(searchMod, "searchMod"); __name(processGameVersions, "processGameVersions"); __name(fetchAPI, "fetchAPI"); __name(searchMods, "searchMods"); __name(getModDetails, "getModDetails"); __name(registerModPlatformCommands, "registerModPlatformCommands"); __name(registerModCommands, "registerModCommands"); } }); // src/wiki.ts var wiki_exports = {}; __export(wiki_exports, { capture: () => capture, registerWikiCommands: () => registerWikiCommands, search: () => search }); function getLanguageVariant(languageCode) { if (!languageCode.startsWith("zh")) return ""; return languageCode === "zh" ? "zh-cn" : languageCode === "zh-hk" ? "zh-hk" : languageCode === "zh-tw" ? "zh-tw" : "zh-cn"; } function buildUrl(articleTitle, languageCode, includeLanguageVariant = false) { const cleanTitle = articleTitle.trim().replace(/[\u200B-\u200D\uFEFF]/g, ""); const wikiDomain = languageCode.startsWith("zh") ? "zh.minecraft.wiki" : languageCode === "en" ? "minecraft.wiki" : `${languageCode}.minecraft.wiki`; const languageVariant = getLanguageVariant(languageCode); try { const encodedTitle = encodeURIComponent(cleanTitle); const baseUrl = `https://${wikiDomain}/w/${encodedTitle}`; return includeLanguageVariant && languageVariant ? `${baseUrl}?variant=${languageVariant}` : baseUrl; } catch (error) { const safeTitle = cleanTitle.replace(/[^\w\s-]/g, "").replace(/\s+/g, "_"); const baseUrl = `https://${wikiDomain}/w/${safeTitle}`; return includeLanguageVariant && languageVariant ? `${baseUrl}?variant=${languageVariant}` : baseUrl; } } async function searchWiki(keyword, _config) { try { const searchUrl = buildUrl("api.php", "zh", true).replace("/w/", "/") + `&action=opensearch&search=${encodeURIComponent(keyword)}&limit=10`; const { data } = await import_axios2.default.get(searchUrl, { timeout: 3e4 }); const [_, titles, urls] = data; if (!titles?.length) return []; return titles.map((title, i) => ({ title, url: urls[i], source: "wiki" })); } catch (error) { throw new Error(`搜索出错:${error.message}`); } } async function fetchContent(articleUrl, languageCode, config) { try { const languageVariant = getLanguageVariant(languageCode); const requestUrl = articleUrl.includes("?") ? articleUrl : `${articleUrl}?variant=${languageVariant}`; const response = await import_axios2.default.get(requestUrl, { params: { uselang: languageCode, setlang: languageCode }, timeout: 1e4, headers: { "Accept-Language": `${languageCode},${languageCode}-*;q=0.9,en;q=0.8` } }); if (response.status !== 200) { throw new Error(`HTTP错误: ${response.status}`); } const $ = cheerio2.load(response.data); const title = $("#firstHeading").text().trim().replace(/[\u200B-\u200D\uFEFF]/g, ""); const sections = []; let currentSection = { content: [] }; $("#mw-content-text .mw-parser-output > *").each((_, element) => { const el = $(element); if (el.is("h2, h3, h4")) { if (currentSection.content.length && currentSection.content.join(" ").length >= 12) { sections.push(currentSection); } currentSection = { title: el.find(".mw-headline").text().trim().replace(/[\u200B-\u200D\uFEFF]/g, ""), content: [] }; } else if (el.is("p, ul, ol")) { const text = el.text().trim(); if (text && !text.startsWith("[") && !text.startsWith("跳转") && !el.hasClass("quote") && !el.hasClass("treeview")) { const cleanText = el.text().trim().replace(/[\u200B-\u200D\uFEFF]/g, "").replace(/\s+/g, " "); if (cleanText.length > 0) { currentSection.content.push(cleanText); } } } }); if (currentSection.content.length && currentSection.content.join(" ").length >= 12) { sections.push(currentSection); } if (!sections.length) { const cleanUrl2 = articleUrl.split("?")[0]; return { title, content: `${title}:本页面没有内容。`, url: cleanUrl2 }; } const formattedContent = sections.map((section, index) => { const sectionLimit = config.sectionLength < 0 ? Infinity : config.sectionLength; const sectionText = index === 0 ? section.content.join(" ") : section.content.join(" ").slice(0, sectionLimit); const truncated = sectionText.length >= sectionLimit && index > 0 && config.sectionLength >= 0 ? "..." : ""; return section.title ? `『${section.title}』${sectionText}${truncated}` : sectionText; }).join("\n"); const totalLimit = config.totalLength < 0 ? Infinity : config.totalLength; const limitedContent = totalLimit === Infinity ? formattedContent : formattedContent.slice(0, totalLimit); const cleanUrl = articleUrl.split("?")[0]; return { title, content: limitedContent.length >= totalLimit && config.totalLength >= 0 ? limitedContent + "..." : limitedContent, url: cleanUrl }; } catch (error) { throw new Error(`获取Wiki内容失败: ${error.message}`); } } async function processWikiRequest(keyword, userId, config, userLangs, mode = "text") { if (!keyword) return "请输入需要查询的关键词"; keyword = keyword.trim().replace(/[\u200B-\u200D\uFEFF]/g, ""); try { const lang = userLangs.get(userId) || config.Language; const results = await searchWiki(keyword); if (!results || !results.length) { return `未找到与"${keyword}"相关的内容。`; } if (mode === "search") { return { results, domain: lang === "en" ? "minecraft.wiki" : `${lang}.minecraft.wiki`, lang }; } const result = results[0]; const pageUrl = buildUrl(result.title, lang, true); const displayUrl = buildUrl(result.title, lang); if (mode === "image") { return { url: displayUrl, pageUrl }; } try { const { title, content } = await fetchContent(pageUrl, lang, config); const contentSliced = content.slice(0, config.totalLength); const ellipsis = contentSliced.length >= config.totalLength ? "..." : ""; return `『${title}』${contentSliced}${ellipsis} 详细内容:${displayUrl}`; } catch (error) { return `获取"${result.title}"的内容时发生错误: ${error.message}`; } } catch (error) { return `查询"${keyword}"时发生错误: ${error.message}`; } } async function search(params) { const { keyword, source, session, config, ctx, lang } = params; if (!keyword) return "请输入搜索关键词"; try { const searchFn = source === "wiki" ? searchWiki : (await Promise.resolve().then(() => (init_mod(), mod_exports))).searchMod; const results = await searchFn(keyword, config); if (!results.length) return "没有找到相关内容"; const message = formatSearchResults(results, source, config); await session.send(message); const timeout = config.Timeout < 0 ? void 0 : config.Timeout * 1e3; const response = await session.prompt(timeout); if (!response) return "等待超时,已取消操作"; return await processSelection({ response, results, source, config, ctx, lang, session }); } catch (error) { return error.message; } } function formatSearchResults(results, source, config) { const items = results.map((r, i) => { const base = `${i + 1}. ${r.title}`; const showDesc = source === "mcmod" && config.descLength !== 0 && r.desc; const desc = showDesc ? config.descLength > 0 ? ` ${r.desc.slice(0, config.descLength)}${r.desc.length > config.descLength ? "..." : ""}` : ` ${r.desc}` : ""; return `${base}${desc}`; }); return `${results[0].source === "wiki" ? "Wiki" : "MCMOD"} 搜索结果: ${items.join("\n")}输入序号查看详情(添加 -i 获取页面截图)`; } async function processSelection(params) { const { response, results, source, config, ctx, lang, session } = params; const [input, flag] = response.split("-"); const index = parseInt(input) - 1; if (isNaN(index) || index < 0 || index >= results.length) { return "请输入正确的序号"; } const result = results[index]; const isImage = flag?.trim() === "i"; try { if (isImage) { return await capture( source === "wiki" ? buildUrl(result.title, lang, true) : result.url, ctx, { type: source, lang }, config ).then((res) => res.image); } else { return await fetchWikiContent(result, source, config, lang, session); } } catch (error) { return `处理内容时出错 (${error?.message || String(error)}),请直接访问:${result.url}`; } } async function fetchWikiContent(result, source, config, lang, session) { if (source === "wiki") { const pageUrl = buildUrl(result.title, lang, true); const displayUrl = buildUrl(result.title, lang); const { title, content: content2 } = await fetchContent(pageUrl, lang, config); return `『${title}』${content2} 详细内容:${displayUrl}`; } const { fetchModContent: fetchModContent2, formatContent: formatContent2 } = await Promise.resolve().then(() => (init_mod(), mod_exports)); const content = await fetchModContent2(result.url, config); return formatContent2(content, result.url, { linkCount: config.linkCount, showImages: config.showImages, platform: session.platform }) || `内容获取失败,请访问:${result.url}`; } async function capture(url, ctx, options, config) { const context = await ctx.puppeteer.browser.createBrowserContext(); const page = await context.newPage(); try { await Promise.all([ page.setRequestInterception(true), page.setCacheEnabled(true), page.setJavaScriptEnabled(false), page.setExtraHTTPHeaders({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" }) ]); page.on("request", (request) => { const resourceType = request.resourceType(); const url2 = request.url().toLowerCase(); const allowedTypes = ["stylesheet", "image", "fetch", "xhr", "document"]; const allowedKeywords = [".svg", "canvas", "swiper", ".css", ".png", ".jpg", ".jpeg", "static", "assets"]; const shouldAllow = allowedTypes.includes(resourceType) || allowedKeywords.some((keyword) => url2.includes(keyword)); if (!shouldAllow && ["media", "font", "manifest", "script"].includes(resourceType)) { request.abort(); } else { request.continue(); } }); if (options.type === "wiki" && options.lang) { await page.setExtraHTTPHeaders({ "Accept-Language": `${options.lang},${options.lang}-*;q=0.9,en;q=0.8`, "Cookie": `language=${options.lang}; hl=${options.lang}; uselang=${options.lang}` }); } for (let i = 0; i < 2; i++) { try { await page.goto(url, { waitUntil: config.waitUntil, timeout: config.captureTimeout < 0 ? 0 : config.captureTimeout * 1e3 }); break; } catch (err) { if (i === 1) throw err; await new Promise((resolve) => setTimeout(resolve, 50)); } } if (options.type === "wiki") { await page.evaluate(() => { const content = document.querySelector("#mw-content-text .mw-parser-output"); const newBody = document.createElement("div"); newBody.id = "content"; if (content) newBody.appendChild(content.cloneNode(true)); document.body.innerHTML = ""; document.body.appendChild(newBody); }); } await page.evaluate((selectors) => { selectors.forEach((selector) => { document.querySelectorAll(selector).forEach((el) => el.remove()); }); }, CLEANUP_SELECTORS); const clipData = await page.evaluate((data) => { const { type, url: url2, maxHeight } = data; const selector = type === "wiki" ? "#content" : url2.includes("bbs.mcmod.cn") ? "#postlist" : url2.includes("/item/") ? ".col-lg-12.right" : ".col-lg-12.center"; const element = document.querySelector(selector); if (!element) return null; const rect = element.getBoundingClientRect(); return { x: 0, y: Math.max(0, Math.floor(rect.top)), width: 1080, height: maxHeight <= 0 ? Math.ceil(rect.height) : Math.min(maxHeight, Math.ceil(rect.height)) }; }, { type: options.type, url, maxHeight: config.maxHeight }); await page.setViewport({ width: clipData.width, height: clipData.height, deviceScaleFactor: 1, isMobile: false }); await new Promise((resolve) => setTimeout(resolve, 50)); const screenshot = await page.screenshot({ type: "jpeg", quality: 75, clip: clipData, omitBackground: true, optimizeForSpeed: true }); return { url, image: import_koishi2.h.image(screenshot, "image/jpeg") }; } catch (error) { throw new Error(`截图失败: ${error.message}`); } finally { await context.close(); } } function registerWikiCommands(ctx, parent, config, userLangs) { const mcwiki = parent.subcommand(".wiki <keyword:text>", "查询 Minecraft Wiki").usage("mc.wiki <关键词> - 查询 Wiki\nmc.wiki.find <关键词> - 搜索 Wiki\nmc.wiki.shot <关键词> - 截图 Wiki 页面").action(async ({ session }, keyword) => { try { return await processWikiRequest(keyword, session.userId, config, userLangs, "text"); } catch (error) { return error.message; } }); mcwiki.subcommand(".find <keyword:text>", "搜索 Wiki").usage("mc.wiki.find <关键词> - 搜索 Wiki 页面").action(async ({ session }, keyword) => { try { const result = await processWikiRequest(keyword, session.userId, config, userLangs, "search"); if (typeof result === "string") return result; return await search({ keyword, source: "wiki", session, config, ctx, lang: userLangs.get(session.userId) || config.Language }); } catch (error) { return error.message; } }); mcwiki.subcommand(".shot <keyword:text>", "截图 Wiki 页面").usage("mc.wiki.shot <关键词> - 搜索并获取指定页面截图").action(async ({ session }, keyword) => { try { const wikiResult = await processWikiRequest(keyword, session.userId, config, userLangs, "image"); if (typeof wikiResult === "string") return wikiResult; await session.send(`正在获取页面... 完整内容:${wikiResult.url}`); const result = await capture( wikiResult.pageUrl, ctx, { type: "wiki", lang: userLangs.get(session.userId) || config.Language }, config ); return result.image; } catch (error) { return error.message; } }); } var import_koishi2, cheerio2, import_axios2, CLEANUP_SELECTORS; var init_wiki = __esm({ "src/wiki.ts"() { import_koishi2 = require("koishi"); cheerio2 = __toESM(require("cheerio")); import_axios2 = __toESM(require("axios")); CLEANUP_SELECTORS = [ // Wiki 相关 ".mw-editsection", "#mw-navigation", "#footer", ".noprint", "#toc", ".navbox", "#siteNotice", "#contentSub", ".mw-indicators", ".sister-wiki", ".external", "script", "meta", "#mw-head", "#mw-head-base", "#mw-page-base", "#catlinks", ".printfooter", ".mw-jump-link", ".vector-toc", ".vector-menu", ".mw-cite-backlink", ".reference", ".treeview", ".file-display-header", // MCMOD 相关 "header", "footer", ".header-container", ".common-background", ".common-nav", ".common-menu-page", ".common-comment-block", ".comment-ad", ".ad-leftside", ".slidetips", ".item-table-tips", ".common-icon-text-frame", ".common-ad-frame", ".ad-class-page", ".class-rating-submit", ".common-icon-text.edit-history", // MCMOD 论坛相关 ".ad", ".under", "#scrolltop", ".po", "#f_pst", ".psth", ".sign", ".sd", "#append_parent", ".wrap-posts.total", ".rate", ".ratl", ".cm", ".modact" ]; __name(getLanguageVariant, "getLanguageVariant"); __name(buildUrl, "buildUrl"); __name(searchWiki, "searchWiki"); __name(fetchContent, "fetchContent"); __name(processWikiRequest, "processWikiRequest"); __name(search, "search"); __name(formatSearchResults, "formatSearchResults"); __name(processSelection, "processSelection"); __name(fetchWikiContent, "fetchWikiContent"); __name(capture, "capture"); __name(registerWikiCommands, "registerWikiCommands"); } }); // src/index.ts var src_exports = {}; __export(src_exports, { Config: () => Config, apply: () => apply, dispose: () => dispose, inject: () => inject, name: () => name, usage: () => usage }); module.exports = __toCommonJS(src_exports); var import_koishi4 = require("koishi"); init_wiki(); init_mod(); // src/tool.ts var import_koishi3 = require("koishi"); var import_axios3 = __toESM(require("axios")); var logger = new import_koishi3.Logger("mcver"); var API_SOURCES = { MOJANG: "https://launchermeta.mojang.com/mc/game/version_manifest.json", BMCLAPI: "https://bmclapi2.bangbang93.com/mc/game/version_manifest.json" }; function parseServerAddress(input, defaultServer) { const address = input || defaultServer; try { let host, port; if (address.includes("[")) { const match = address.match(/^\[([\da-fA-F:]+)\](?::(\d+))?$/); if (!match) throw new Error("无效的IPv6地址格式"); host = match[1]; port = match[2] ? parseInt(match[2], 10) : void 0; } else if (address.includes(":")) { const [hostPart, portPart] = address.split(":"); host = hostPart; port = parseInt(portPart, 10); } else { host = address; } if (["localhost", "0.0.0.0"].includes(host.toLowerCase())) { throw new Error("不允许连接到本地服务器"); } if (/^(\d{1,3}\.){3}\d{1,3}$/.test(host)) { const parts = host.split(".").map(Number); if (!parts.every((p) => p >= 0 && p <= 255)) { throw new Error("无效的IPv4地址格式"); } if (parts[0] === 127 || parts[0] === 10 || parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31 || parts[0] === 192 && parts[1] === 168 || parts[0] === 169 && parts[1] === 254 || parts[0] >= 224 || parts[0] === 0) { throw new Error("不允许连接到私有网络地址"); } } else if (/^[0-9a-fA-F:]+$/.test(host)) { const lowerIP = host.toLowerCase(); if (lowerIP === "::1" || lowerIP === "0:0:0:0:0:0:0:1" || /^fe80:/i.test(lowerIP) || /^f[cd][0-9a-f]{2}:/i.test(lowerIP) || lowerIP.startsWith("ff") || lowerIP === "::") { throw new Error("不允许连接到私有网络地址"); } } if (port !== void 0 && (isNaN(port) || port < 1 || port > 65535)) { throw new Error("端口号必须在1-65535之间"); } const type = port === 19132 || address.endsWith(":19132") ? "bedrock" : "java"; return { address, type }; } catch (error) { throw new Error(`地址格式错误:${error.message}`); } } __name(parseServerAddress, "parseServerAddress"); async function checkServerStatus(server, forceType, config) { try { const parsed = parseServerAddress(server, config?.default); const type = forceType || parsed.type; const apis = type === "java" ? config?.javaApis : config?.bedrockApis; if (!apis?.length) { throw new Error(`缺少 ${type} 版本查询 API 配置`); } const errors = []; for (const apiUrl of apis) { const actualUrl = apiUrl.replace("${address}", parsed.address); try { const response = await import_axios3.default.get(actualUrl, { headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" }, timeout: 1e4, validateStatus: null }); if (!response.data || response.status !== 200) { errors.push(`${actualUrl} 请求失败: ${response.data?.error || response.status}`); continue; } if (actualUrl.includes("mcsrvstat.us")) { return await transformMcsrvstatResponse(response.data); } const data = response.data; if (!data.online) { errors.push(`${actualUrl} 返回服务器离线`); continue; } return { online: true, host: data.host, port: data.port, ip_address: data.ip_address, eula_blocked: data.eula_blocked, retrieved_at: data.retrieved_at, version: { name_clean: data.version?.name_clean, name: data.version?.name }, players: { online: data.players?.online ?? null, max: data.players?.max ?? null, list: data.players?.list?.map((p) => p.name_clean) }, motd: data.motd?.clean, icon: data.icon, mods: data.mods, software: data.software, plugins: data.plugins, srv_record: data.srv_record, gamemode: data.gamemode, server_id: data.server_id, edition: data.edition }; } catch (error) { errors.push(`${actualUrl} 连接错误: ${error.message}`); } } return { online: false, host: parsed.address, port: parseInt(parsed.address.split(":")[1]) || (type === "java" ? 25565 : 19132), players: { online: null, max: null }, error: `所有 API 均请求失败: ${errors.join("\n")}` }; } catch (error) { return { online: false, host: server || "未知", port: 0, players: { online: null, max: null }, error: error.message || "服务器地址解析失败" }; } } __name(checkServerStatus, "checkServerStatus"); async function transformMcsrvstatResponse(data) { if (!data.online) { return { online: false, host: data.hostname || data.ip || "unknown", port: data.port || 0, players: { online: null, max: null } }; } return { online: true, host: data.hostname || data.ip, port: data.port, ip_address: data.ip, retrieved_at: data.debug?.cachetime * 1e3, version: { name_clean: data.version, name: data.protocol?.name }, players: { online: data.players?.online ?? 0, max: data.players?.max ?? 0, list: data.players?.list?.map((p) => p.name) }, motd: data.motd?.clean?.[0] || data.motd?.raw?.[0], icon: data.icon, mods: data.mods, software: data.software, plugins: data.plugins, gamemode: data.gamemode, server_id: data.serverid, eula_blocked: data.eula_blocked }; } __name(transformMcsrvstatResponse, "transformMcsrvstatResponse"); function formatServerStatus(status, config) { if (!status.online) { return status.error || "服务器离线 - 连接失败"; } const lines = []; config.showIP && status.ip_address && lines.push(`IP: ${status.ip_address}`); config.showIP && status.srv_record && lines.push(`SRV: ${status.srv_record.host}:${status.srv_record.port}`); status.icon?.startsWith("data:image/png;base64,") && config.showIcon && lines.push(import_koishi3.h.image(status.icon).toString()); status.motd && lines.push(status.motd); lines.push([ status.version?.name_clean || "未知", `${status.players?.online ?? 0}/${status.players?.max ?? 0}`, status.retrieved_at && `${Date.now() - status.retrieved_at}ms` ].filter(Boolean).join(" | ")); const serverInfo = []; status.software && serverInfo.push(status.software); if (status.edition) { const editionMap = { MCPE: "基岩版", MCEE: "教育版" }; serverInfo.push(editionMap[status.edition] || status.edition); } status.gamemode && serverInfo.push(status.gamemode); status.eula_blocked &&