koishi-plugin-mc-tools
Version:
我的世界(Minecraft/MC)工具。支持查询MCWiki/MCMod/CurseForge/Modrinth、服务器信息、最新版本和玩家皮肤;推送MC更新通知,运行命令等
1,265 lines (1,261 loc) • 101 kB
JavaScript
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 &&