UNPKG

koishi-plugin-ddrace

Version:

DDRaceNetwork 玩家和地图数据查询,支持文本和图片两种展示方式

1,268 lines (1,262 loc) 55.2 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 __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/index.ts var src_exports = {}; __export(src_exports, { Config: () => Config, apply: () => apply, inject: () => inject, name: () => name, usage: () => usage }); module.exports = __toCommonJS(src_exports); var import_koishi = require("koishi"); var import_https = __toESM(require("https")); // src/text.ts var formatter = { /** * 时间转换为字符串 * @param seconds 秒数(可包含小数) */ time(seconds) { if (typeof seconds !== "number") return "未知时间"; const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); const ms = Math.round((seconds - Math.floor(seconds)) * 1e3); return `${mins}:${secs.toString().padStart(2, "0")}.${ms.toString().padStart(3, "0")}`; }, /** * 将Unix时间戳转换为日期字符串 * @param timestamp Unix时间戳(秒) * @param format 格式类型 */ date(timestamp, format = "full") { if (!timestamp) return "未知时间"; try { const date = new Date(timestamp * 1e3); switch (format) { case "short": return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, "0")}-${date.getDate().toString().padStart(2, "0")}`; case "year": return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`; case "full": default: return date.toLocaleString("zh-CN"); } } catch { return "日期格式错误"; } }, /** * 生成进度百分比 * @param current 当前值 * @param total 总值 */ percentage(current, total) { if (!total) return `${current} 项`; const percent = (current / total * 100).toFixed(1); return `${current}/${total} (${percent}%)`; }, /** * 地图类型映射 * @param type 英文地图类型 * @returns 中文地图类型 */ mapType(type) { const typeMapping = { "Novice": "简单", "Moderate": "中阶", "Brutal": "高阶", "Insane": "疯狂", "Dummy": "分身", "DDmaX.Easy": "古典.Easy", "DDmaX.Next": "古典.Next", "DDmaX.Pro": "古典.Pro", "DDmaX.Nut": "古典.Nut", "Oldschool": "传统", "Solo": "单人", "Race": "竞速", "Fun": "娱乐" }; return typeMapping[type] || type; } }; function formatPlayerSummary(playerData, config) { const playerId = playerData.player; let summary = `🏆 ${playerId} 的 DDRace 个人资料 `; const displayConfig = { showRankInfo: config?.showRankInfo !== false, showActivityInfo: config?.showActivityInfo !== false, showGameInfo: config?.showGameInfo !== false, showMapTypeStats: config?.showMapTypeStats !== false, recentFinishesCount: config?.recentFinishesCount ?? 5, favoritePartnersCount: config?.favoritePartnersCount ?? 5, showActivityStats: config?.showActivityStats !== false, mapDetailsCount: config?.mapDetailsCount ?? 6 }; if (displayConfig.showRankInfo) { summary += ` 📊 排名与积分 `; if (playerData.points && typeof playerData.points === "object") { const total = playerData.points.total || 0; const rank = playerData.points.rank || "未排名"; const points = playerData.points.points || 0; summary += `• 总积分: ${points}/${total} (全球第 ${rank} 名) `; } if (playerData.rank?.rank) { summary += `• 个人排名: 第 ${playerData.rank.rank} 名 (${playerData.rank.points || 0} 积分) `; } if (playerData.team_rank?.rank) { summary += `• 团队排名: 第 ${playerData.team_rank.rank} 名 (${playerData.team_rank.points || 0} 积分) `; } } if (displayConfig.showActivityInfo) { const hasRecentActivity = playerData.points_last_year || playerData.points_last_month || playerData.points_last_week; if (hasRecentActivity) { summary += ` 📅 近期活跃度 `; if (playerData.points_last_year?.points) { summary += `• 过去一年: ${playerData.points_last_year.points} 积分 (第 ${playerData.points_last_year.rank || "?"} 名) `; } if (playerData.points_last_month?.points) { summary += `• 过去一月: ${playerData.points_last_month.points} 积分 (第 ${playerData.points_last_month.rank || "?"} 名) `; } if (playerData.points_last_week?.rank) { summary += `• 过去一周: ${playerData.points_last_week.points || 0} 积分 (第 ${playerData.points_last_week.rank} 名) `; } else { summary += `• 过去一周: 暂无排名 `; } } } if (displayConfig.showGameInfo) { summary += ` 🎮 游戏信息 `; if (playerData.favorite_server) { const server = typeof playerData.favorite_server === "object" ? playerData.favorite_server.server || JSON.stringify(playerData.favorite_server) : playerData.favorite_server; summary += `• 常用服务器: ${server} `; } if (playerData.hours_played_past_365_days !== void 0) { summary += `• 年度游戏时长: ${playerData.hours_played_past_365_days} 小时 `; } if (playerData.first_finish) { const formattedDate = formatter.date(playerData.first_finish.timestamp, "year"); const map = playerData.first_finish.map; const timeStr = formatter.time(playerData.first_finish.time); summary += `• 首次完成: ${formattedDate} ${map} (${timeStr}) `; } } if (displayConfig.showMapTypeStats && playerData.types && typeof playerData.types === "object") { summary += ` 🗺️ 地图完成统计 `; Object.entries(playerData.types).forEach(([typeName, typeInfo]) => { if (!typeInfo?.maps) return; const mapEntries = Object.entries(typeInfo.maps); const completedMaps = mapEntries.filter( ([_, mapData]) => mapData.finishes && mapData.finishes > 0 ); const completedMapCount = completedMaps.length; const totalMapCount = mapEntries.length; let typePoints = 0; let earnedPoints = 0; let typeRank = "未排名"; if (typeInfo.points) { if (typeof typeInfo.points === "object") { earnedPoints = typeInfo.points.points || 0; typePoints = typeInfo.points.total || 0; if (typeInfo.points.rank) { typeRank = `第 ${typeInfo.points.rank} 名`; } } else { earnedPoints = typeInfo.points; typePoints = typeInfo.points; } } const displayTypeName = formatter.mapType(typeName); summary += `• ${displayTypeName}: ${earnedPoints}/${typePoints} 积分 (${typeRank}), 已完成 ${completedMapCount}/${totalMapCount} 张地图 `; if (totalMapCount > 0 && displayConfig.mapDetailsCount !== 0) { const limit = displayConfig.mapDetailsCount === -1 ? totalMapCount : Math.min(displayConfig.mapDetailsCount, totalMapCount); if (completedMaps.length > 0) { const shownCompletedMaps = completedMaps.slice(0, limit); summary += ` 已完成地图: `; shownCompletedMaps.forEach(([mapName, mapData]) => { const finishesText = mapData.finishes > 1 ? `完成${mapData.finishes}次` : "已完成"; const rankText = mapData.rank ? `排名#${mapData.rank}` : ""; const timeText = mapData.time ? `(${formatter.time(mapData.time)})` : ""; const pointsText = mapData.points ? `[${mapData.points}分]` : ""; summary += ` - ${mapName} ${pointsText} ${finishesText} ${rankText} ${timeText} `; }); const hasMore = completedMaps.length > limit && displayConfig.mapDetailsCount !== -1; if (hasMore) { summary += ` ... 以及其他 ${completedMaps.length - limit} 张已完成地图 `; } } const uncompletedMaps = mapEntries.filter( ([_, mapData]) => !mapData.finishes || mapData.finishes === 0 ); if (uncompletedMaps.length > 0) { const shownUncompletedMaps = uncompletedMaps.slice(0, limit); summary += ` 未完成地图: `; shownUncompletedMaps.forEach(([mapName, mapData]) => { const pointsText = mapData.points ? `[${mapData.points}分]` : ""; const totalFinishes = mapData.total_finishes ? `共${mapData.total_finishes}人完成` : ""; summary += ` - ${mapName} ${pointsText} ${totalFinishes} `; }); const hasMore = uncompletedMaps.length > limit && displayConfig.mapDetailsCount !== -1; if (hasMore) { summary += ` ... 以及其他 ${uncompletedMaps.length - limit} 张未完成地图 `; } } } }); } if (displayConfig.recentFinishesCount !== 0 && playerData.last_finishes?.length > 0) { summary += ` 🏁 最近通关记录 (${playerData.last_finishes.length}项) `; const limit = displayConfig.recentFinishesCount === -1 ? playerData.last_finishes.length : Math.min(displayConfig.recentFinishesCount, playerData.last_finishes.length); const records = playerData.last_finishes.slice(0, limit); records.forEach((finish) => { if (finish.timestamp && finish.map) { const formattedDate = formatter.date(finish.timestamp, "short"); const timeStr = formatter.time(finish.time); const displayType = formatter.mapType(finish.type || ""); const countryTag = finish.country ? `${finish.country} ` : ""; summary += `• ${finish.map} (${countryTag}${displayType}) - ${timeStr} [${formattedDate}] `; } }); } if (displayConfig.favoritePartnersCount !== 0 && playerData.favorite_partners?.length > 0) { summary += ` 👥 常用队友 (共${playerData.favorite_partners.length}位) `; const limit = displayConfig.favoritePartnersCount === -1 ? playerData.favorite_partners.length : Math.min(displayConfig.favoritePartnersCount, playerData.favorite_partners.length); playerData.favorite_partners.slice(0, limit).forEach((partner) => { if (partner.name && partner.finishes) { summary += `• ${partner.name}: 合作完成 ${partner.finishes} 次 `; } }); } if (displayConfig.showActivityStats && playerData.activity?.length > 0) { let totalHours = 0; let maxHours = 0; let activeDays = 0; let activeMonths = /* @__PURE__ */ new Set(); playerData.activity.forEach((day) => { if (day?.hours_played) { totalHours += day.hours_played; maxHours = Math.max(maxHours, day.hours_played); if (day.hours_played > 0) { activeDays++; if (day.date) { const month = day.date.substring(0, 7); activeMonths.add(month); } } } }); const avgHours = activeDays > 0 ? (totalHours / activeDays).toFixed(1) : "0"; summary += ` 📊 活跃度统计 `; summary += `• 活跃天数: ${activeDays} 天 `; summary += `• 活跃月数: ${activeMonths.size} 个月 `; summary += `• 单日最长游戏: ${maxHours} 小时 `; summary += `• 平均每日游戏: ${avgHours} 小时 `; } return summary; } __name(formatPlayerSummary, "formatPlayerSummary"); function formatMapInfo(mapData, config) { const displayConfig = { showMapBasicInfo: config?.showMapBasicInfo !== false, showMapStats: config?.showMapStats !== false, globalRanksCount: config?.globalRanksCount ?? 5, chinaRanksCount: config?.chinaRanksCount ?? 5, teamRanksCount: config?.teamRanksCount ?? 3, multiFinishersCount: config?.multiFinishersCount ?? 3, showMapFeatures: config?.showMapFeatures !== false, showMapLinks: config?.showMapLinks !== false }; const stars = "★".repeat(mapData.difficulty || 0) + "☆".repeat(Math.max(0, 5 - (mapData.difficulty || 0))); let result = `🗺️ 地图「${mapData.name}」详细信息 `; if (displayConfig.showMapBasicInfo) { const displayType = formatter.mapType(mapData.type || "未知"); result += `类型: ${displayType} (${stars}) `; result += `作者: ${mapData.mapper || "未知"} `; result += `难度: ${mapData.difficulty || 0}/5 • 积分值: ${mapData.points || 0} `; if (mapData.release) { result += `发布日期: ${formatter.date(mapData.release, "short")} `; } } if (displayConfig.showMapStats) { result += ` 📊 完成统计 `; result += `总完成次数: ${mapData.finishes || 0} `; result += `完成玩家数: ${mapData.finishers || 0} `; if (mapData.median_time) { result += `平均完成时间: ${formatter.time(mapData.median_time)} `; } if (mapData.first_finish) { result += `首次完成日期: ${formatter.date(mapData.first_finish, "short")} `; } if (mapData.last_finish) { result += `最近完成日期: ${formatter.date(mapData.last_finish, "short")} `; } if (mapData.biggest_team) { result += `最大团队规模: ${mapData.biggest_team} 人 `; } } if (displayConfig.globalRanksCount !== 0 && mapData.ranks && mapData.ranks.length > 0) { const limit = displayConfig.globalRanksCount === -1 ? mapData.ranks.length : Math.min(displayConfig.globalRanksCount, mapData.ranks.length); result += ` 🏆 全球排名 `; const topRanks = mapData.ranks.slice(0, limit); topRanks.forEach((rank, idx) => { if (rank.player && rank.time) { const countryTag = rank.country ? `[${rank.country}] ` : ""; const timeStr = formatter.time(rank.time); const dateStr = rank.timestamp ? ` [${formatter.date(rank.timestamp, "short")}]` : ""; result += `${idx + 1}. ${countryTag}${rank.player} - ${timeStr}${dateStr} `; } }); const remainingPlayers = mapData.ranks.length - limit; if (remainingPlayers > 0 && displayConfig.globalRanksCount !== -1) { result += `... 以及其他 ${remainingPlayers} 名玩家 `; } } if (displayConfig.chinaRanksCount !== 0) { const chinaPlayers = mapData.ranks?.filter((r) => r.country === "CHN") || []; if (chinaPlayers.length > 0) { const limit = displayConfig.chinaRanksCount === -1 ? chinaPlayers.length : Math.min(displayConfig.chinaRanksCount, chinaPlayers.length); result += ` 🇨🇳 国服排名 `; chinaPlayers.slice(0, limit).forEach((rank, idx) => { const timeStr = formatter.time(rank.time); const globalRank = mapData.ranks.findIndex((r) => r.player === rank.player) + 1; result += `${idx + 1}. ${rank.player} - ${timeStr} (全球第 ${globalRank} 名) `; }); const remaining = chinaPlayers.length - limit; if (remaining > 0 && displayConfig.chinaRanksCount !== -1) { result += `... 以及其他 ${remaining} 名国服玩家 `; } } } if (displayConfig.teamRanksCount !== 0 && mapData.team_ranks && mapData.team_ranks.length > 0) { const limit = displayConfig.teamRanksCount === -1 ? mapData.team_ranks.length : Math.min(displayConfig.teamRanksCount, mapData.team_ranks.length); result += ` 👥 团队排名 `; const topTeams = mapData.team_ranks.slice(0, limit); topTeams.forEach((teamRank, idx) => { if (teamRank.players && teamRank.time) { const countryTag = teamRank.country ? `[${teamRank.country}] ` : ""; const timeStr = formatter.time(teamRank.time); const playersStr = teamRank.players.join(", "); result += `${idx + 1}. ${countryTag}${playersStr} - ${timeStr} `; } }); const remaining = mapData.team_ranks.length - limit; if (remaining > 0 && displayConfig.teamRanksCount !== -1) { result += `... 以及其他 ${remaining} 支团队 `; } } if (displayConfig.multiFinishersCount !== 0 && mapData.max_finishes && mapData.max_finishes.length > 0) { const limit = displayConfig.multiFinishersCount === -1 ? mapData.max_finishes.length : Math.min(displayConfig.multiFinishersCount, mapData.max_finishes.length); result += ` 🔄 多次完成玩家 `; const topFinishers = mapData.max_finishes.slice(0, limit); topFinishers.forEach((finisher) => { if (finisher.player && finisher.num) { result += `• ${finisher.player}: 完成 ${finisher.num} 次 `; } }); const remaining = mapData.max_finishes.length - limit; if (remaining > 0 && displayConfig.multiFinishersCount !== -1) { result += `... 以及其他多次完成玩家 `; } } if (displayConfig.showMapFeatures && mapData.tiles && mapData.tiles.length > 0) { result += ` 🧩 地图特性 `; result += `尺寸: ${mapData.width || "?"} × ${mapData.height || "?"} `; result += `特殊方块: ${mapData.tiles.join(", ")} `; } if (displayConfig.showMapLinks) { result += ` 🔗 相关链接 `; if (mapData.website) { result += `• 详细信息: ${mapData.website} `; } if (mapData.web_preview) { result += `• 地图预览: ${mapData.web_preview} `; } if (mapData.thumbnail) { result += `• 地图缩略图: ${mapData.thumbnail} `; } } return result; } __name(formatMapInfo, "formatMapInfo"); // src/image.ts async function htmlToImage(html, ctx) { let page = null; try { page = await ctx.puppeteer.page(); await page.setContent(` <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> body { font-family: 'Arial', 'PingFang SC', 'Microsoft YaHei', sans-serif; background: transparent; color: #333; padding: 0; margin: 0; display: flex; justify-content: center; } .container { width: 960px; background-color: rgba(255, 255, 255); border-radius: 10px; padding: 12px; overflow: hidden; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); } .header { text-align: center; padding-bottom: 10px; border-bottom: 1px solid #eaeaea; margin-bottom: 12px; } .header h1 { margin: 0; color: #4a76a8; font-size: 22px; font-weight: 600; } .section { margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #eaeaea; } .section:last-child { border-bottom: none; margin-bottom: 0; } .section-title { font-weight: 600; color: #4a76a8; margin-bottom: 8px; font-size: 17px; } .stat-item { margin-bottom: 6px; line-height: 1.4; } .map-list { font-size: 13px; color: #666; margin-left: 15px; margin-top: 2px; } .small { font-size: 13px; } .highlight { font-weight: 600; color: #3b5998; } .recent-finishes { display: grid; grid-template-columns: repeat(auto-fill, minmax(370px, 1fr)); gap: 8px; } .finish-card, .partner-card, .finisher-card, .rank-item { background: rgba(249, 249, 249, 0.7); border-radius: 6px; padding: 8px; border-left: 3px solid #4a76a8; } .finish-time, .rank-time { color: #e63946; font-weight: 600; font-size: 13px; } .finish-date, .rank-date { color: #666; font-size: 12px; } .partners-grid { display: flex; flex-wrap: wrap; gap: 8px; } .partner-card { display: flex; align-items: center; flex: 1 0 calc(20% - 8px); max-width: calc(20% - 8px); box-sizing: border-box; } .partner-count, .finisher-count { margin-left: auto; background: #4a76a8; color: white; border-radius: 10px; padding: 2px 8px; font-size: 12px; font-weight: 600; } .stats-container { display: flex; gap: 15px; } .stats-column { flex: 1; } .stats-column .section { height: 100%; } .rank-list-vertical { list-style-type: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 8px; } .rank-item { display: flex; align-items: flex-start; padding: 6px 10px; } .rank-position { display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 50%; margin-right: 10px; font-weight: bold; font-size: 14px; color: #777; } .position-1 { background: linear-gradient(135deg, #ffd700, #e6b800); box-shadow: 0 2px 4px rgba(230, 184, 0, 0.3); color: #fff; } .position-2 { background: linear-gradient(135deg, #c0c0c0, #a0a0a0); box-shadow: 0 2px 4px rgba(160, 160, 160, 0.3); color: #fff; } .position-3 { background: linear-gradient(135deg, #cd7f32, #a06020); box-shadow: 0 2px 4px rgba(160, 96, 32, 0.3); color: #fff; } .rank-player { flex: 1; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding-right: 5px; } .rank-time-container { display: flex; flex-direction: column; align-items: flex-end; min-width: 65px; } .finisher-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 8px; } .finisher-card { display: flex; align-items: center; } .url-link { display: inline-block; background: rgba(249, 249, 249, 0.7); padding: 6px 10px; border-radius: 6px; margin-bottom: 6px; border-left: 3px solid #4a76a8; word-break: break-all; color: #4a76a8; font-weight: 500; } .thumbnail { max-width: 100%; margin-top: 10px; border-radius: 6px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); } .map-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 8px; } .map-card { background: rgba(249, 249, 249, 0.7); border-radius: 6px; padding: 8px; border-left: 3px solid #4a76a8; } .map-card.incomplete { border-left: 3px solid #e63946; } .map-name { font-weight: 600; color: #4a76a8; } .map-points { font-size: 12px; color: #666; } .map-details { font-size: 12px; color: #666; } </style> </head> <body> <div class="container"> ${html} </div> </body> </html> `, { waitUntil: "networkidle0", timeout: 3e3 }); const rect = await page.evaluate(() => { const container = document.querySelector(".container"); const rect2 = container.getBoundingClientRect(); return { width: rect2.width, height: rect2.height }; }); await page.setViewport({ width: Math.ceil(rect.width), height: Math.ceil(rect.height), deviceScaleFactor: 1.75 }); return await page.screenshot({ type: "png", fullPage: false, clip: { x: 0, y: 0, width: Math.ceil(rect.width), height: Math.ceil(rect.height) }, omitBackground: true }); } catch (error) { ctx.logger.error("图片渲染出错:", error); } finally { if (page) await page.close().catch(() => { }); } } __name(htmlToImage, "htmlToImage"); function playerDataToHtml(playerData, config) { const playerId = playerData.player; let htmlContent = ` <div class="header"> <h1>🏆 ${playerId} 的 DDRace 个人资料</h1> </div> `; const displayConfig = { showRankInfo: config?.showRankInfo !== false, showActivityInfo: config?.showActivityInfo !== false, showGameInfo: config?.showGameInfo !== false, showMapTypeStats: config?.showMapTypeStats !== false, recentFinishesCount: config?.recentFinishesCount ?? 5, favoritePartnersCount: config?.favoritePartnersCount ?? 5, showActivityStats: config?.showActivityStats !== false, mapDetailsCount: config?.mapDetailsCount ?? 6 }; if (displayConfig.showRankInfo || displayConfig.showGameInfo) { htmlContent += `<div class="section"><div class="section-title">📊 排名与游戏信息</div>`; if (displayConfig.showRankInfo) { if (playerData.points && typeof playerData.points === "object") { const total = playerData.points.total || 0; const rank = playerData.points.rank || "未排名"; const points = playerData.points.points || 0; htmlContent += `<div class="stat-item">• 总积分: <span class="highlight">${points}/${total}</span> (全球第 ${rank} 名)</div>`; } if (playerData.rank?.rank) { htmlContent += `<div class="stat-item">• 个人排名: 第 <span class="highlight">${playerData.rank.rank}</span> 名 (${playerData.rank.points || 0} 积分)</div>`; } if (playerData.team_rank?.rank) { htmlContent += `<div class="stat-item">• 团队排名: 第 <span class="highlight">${playerData.team_rank.rank}</span> 名 (${playerData.team_rank.points || 0} 积分)</div>`; } } if (displayConfig.showGameInfo) { if (playerData.favorite_server) { const server = typeof playerData.favorite_server === "object" ? playerData.favorite_server.server || JSON.stringify(playerData.favorite_server) : playerData.favorite_server; htmlContent += `<div class="stat-item">• 常用服务器: <span class="highlight">${server}</span></div>`; } if (playerData.hours_played_past_365_days !== void 0) { htmlContent += `<div class="stat-item">• 年度游戏时长: <span class="highlight">${playerData.hours_played_past_365_days}</span> 小时</div>`; } if (playerData.first_finish) { const formattedDate = formatter.date(playerData.first_finish.timestamp, "year"); const map = playerData.first_finish.map; const timeString = formatter.time(playerData.first_finish.time); htmlContent += `<div class="stat-item">• 首次完成: ${formattedDate} - <span class="highlight">${map}</span> (${timeString})</div>`; } } htmlContent += `</div>`; } const hasActivityInfo = displayConfig.showActivityInfo && (playerData.points_last_year || playerData.points_last_month || playerData.points_last_week); const hasActivityStats = displayConfig.showActivityStats && playerData.activity?.length > 0; if (hasActivityInfo || hasActivityStats) { htmlContent += `<div class="section"><div class="stats-container">`; if (hasActivityStats) { htmlContent += `<div class="stats-column">`; htmlContent += `<div class="section-title">📊 活跃度统计</div>`; let totalHours = 0; let maxHours = 0; let activeDays = 0; let activeMonths = /* @__PURE__ */ new Set(); playerData.activity.forEach((day) => { if (day?.hours_played) { totalHours += day.hours_played; maxHours = Math.max(maxHours, day.hours_played); if (day.hours_played > 0) { activeDays++; if (day.date) { const month = day.date.substring(0, 7); activeMonths.add(month); } } } }); const avgHours = activeDays > 0 ? (totalHours / activeDays).toFixed(1) : "0"; htmlContent += ` <div class="stat-item">• 活跃天数: <span class="highlight">${activeDays}</span> 天</div> <div class="stat-item">• 活跃月数: <span class="highlight">${activeMonths.size}</span> 个月</div> <div class="stat-item">• 单日最长游戏: <span class="highlight">${maxHours}</span> 小时</div> <div class="stat-item">• 平均每日游戏: <span class="highlight">${avgHours}</span> 小时</div> `; htmlContent += `</div>`; } if (hasActivityInfo) { htmlContent += `<div class="stats-column">`; htmlContent += `<div class="section-title">📅 近期活跃度</div>`; if (playerData.points_last_year?.points) { htmlContent += `<div class="stat-item">• 过去一年: <span class="highlight">${playerData.points_last_year.points}</span> 积分 (第 ${playerData.points_last_year.rank || "?"} 名)</div>`; } if (playerData.points_last_month?.points) { htmlContent += `<div class="stat-item">• 过去一月: <span class="highlight">${playerData.points_last_month.points}</span> 积分 (第 ${playerData.points_last_month.rank || "?"} 名)</div>`; } if (playerData.points_last_week?.rank) { htmlContent += `<div class="stat-item">• 过去一周: <span class="highlight">${playerData.points_last_week.points || 0}</span> 积分 (第 ${playerData.points_last_week.rank} 名)</div>`; } else { htmlContent += `<div class="stat-item">• 过去一周: 暂无排名</div>`; } htmlContent += `</div>`; } htmlContent += `</div></div>`; } if (displayConfig.showMapTypeStats && playerData.types && typeof playerData.types === "object") { htmlContent += `<div class="section"><div class="section-title">🗺️ 地图完成统计</div>`; const typesEntries = Object.entries(playerData.types); typesEntries.forEach(([typeName, typeInfo]) => { if (!typeInfo?.maps) return; const mapEntries = Object.entries(typeInfo.maps); const completedMaps = mapEntries.filter( ([_, mapData]) => mapData.finishes && mapData.finishes > 0 ); const completedMapCount = completedMaps.length; const totalMapCount = mapEntries.length; let earnedPoints = 0; let totalPoints = 0; let typeRank = "未排名"; if (typeInfo.points) { if (typeof typeInfo.points === "object") { earnedPoints = typeInfo.points.points || 0; totalPoints = typeInfo.points.total || 0; if (typeInfo.points.rank) { typeRank = `第 ${typeInfo.points.rank} 名`; } } else { earnedPoints = typeInfo.points; totalPoints = typeInfo.points; } } const displayTypeName = formatter.mapType(typeName); htmlContent += `<div class="stat-item">• ${displayTypeName}: <span class="highlight">${earnedPoints}/${totalPoints}</span> 积分 (${typeRank}), 已完成 <span class="highlight">${completedMapCount}/${totalMapCount}</span> 张地图</div>`; if (totalMapCount > 0 && displayConfig.mapDetailsCount !== 0) { const limit = displayConfig.mapDetailsCount === -1 ? totalMapCount : Math.min(displayConfig.mapDetailsCount, totalMapCount); if (completedMaps.length > 0) { const shownCompletedMaps = completedMaps.slice(0, limit); htmlContent += `<div class="map-list"><strong>已完成地图:</strong>`; htmlContent += `<div class="map-grid">`; shownCompletedMaps.forEach(([mapName, mapData]) => { const finishesText = mapData.finishes > 1 ? `完成${mapData.finishes}次` : "已完成"; const rankText = mapData.rank ? `排名#${mapData.rank}` : ""; const timeText = mapData.time ? formatter.time(mapData.time) : ""; htmlContent += ` <div class="map-card"> <div class="map-name">${mapName} <span class="map-points">[${mapData.points}分]</span></div> <div class="map-details">${finishesText} ${rankText} ${timeText}</div> </div> `; }); htmlContent += `</div>`; const hasMoreCompleted = completedMaps.length > limit && displayConfig.mapDetailsCount !== -1; if (hasMoreCompleted) { htmlContent += `<div class="small">... 以及其他 ${completedMaps.length - limit} 张已完成地图</div>`; } htmlContent += `</div>`; } const uncompletedMaps = mapEntries.filter( ([_, mapData]) => !mapData.finishes || mapData.finishes === 0 ); if (uncompletedMaps.length > 0) { const shownUncompletedMaps = uncompletedMaps.slice(0, limit); htmlContent += `<div class="map-list"><strong>未完成地图:</strong>`; htmlContent += `<div class="map-grid">`; shownUncompletedMaps.forEach(([mapName, mapData]) => { const totalFinishes = mapData.total_finishes ? `${mapData.total_finishes}人完成` : ""; htmlContent += ` <div class="map-card incomplete"> <div class="map-name">${mapName} <span class="map-points">[${mapData.points}分]</span></div> <div class="map-details">${totalFinishes}</div> </div> `; }); htmlContent += `</div>`; const hasMoreUncompleted = uncompletedMaps.length > limit && displayConfig.mapDetailsCount !== -1; if (hasMoreUncompleted) { htmlContent += `<div class="small">... 以及其他 ${uncompletedMaps.length - limit} 张未完成地图</div>`; } htmlContent += `</div>`; } } }); htmlContent += `</div>`; } if (displayConfig.recentFinishesCount !== 0 && playerData.last_finishes?.length > 0) { const limit = displayConfig.recentFinishesCount === -1 ? playerData.last_finishes.length : Math.min(displayConfig.recentFinishesCount, playerData.last_finishes.length); htmlContent += ` <div class="section"> <div class="section-title">🏁 最近通关记录 (${playerData.last_finishes.length}项)</div> <div class="recent-finishes"> `; playerData.last_finishes.slice(0, limit).forEach((finish) => { if (finish.timestamp && finish.map) { const formattedDate = formatter.date(finish.timestamp, "short"); const timeString = formatter.time(finish.time); const countryFlag = finish.country ? `${finish.country} ` : ""; htmlContent += ` <div class="finish-card"> <div>${finish.map} (${formatter.mapType(finish.type)}) <span class="finish-time">${timeString}</span></div> <div class="finish-date">${formattedDate} - ${countryFlag}服务器</div> </div> `; } }); htmlContent += `</div></div>`; } if (displayConfig.favoritePartnersCount !== 0 && playerData.favorite_partners?.length > 0) { const limit = displayConfig.favoritePartnersCount === -1 ? playerData.favorite_partners.length : Math.min(displayConfig.favoritePartnersCount, playerData.favorite_partners.length); htmlContent += ` <div class="section"> <div class="section-title">👥 常用队友 (共${playerData.favorite_partners.length}位)</div> <div class="partners-grid"> `; playerData.favorite_partners.slice(0, limit).forEach((partner) => { if (partner.name && partner.finishes) { htmlContent += ` <div class="partner-card"> ${partner.name} <span class="partner-count">${partner.finishes}次</span> </div> `; } }); htmlContent += `</div></div>`; } return htmlContent; } __name(playerDataToHtml, "playerDataToHtml"); function mapInfoToHtml(mapData, config) { const displayConfig = { showMapBasicInfo: config?.showMapBasicInfo !== false, showMapStats: config?.showMapStats !== false, globalRanksCount: config?.globalRanksCount ?? 5, chinaRanksCount: config?.chinaRanksCount ?? 5, teamRanksCount: config?.teamRanksCount ?? 3, multiFinishersCount: config?.multiFinishersCount ?? 5, showMapFeatures: config?.showMapFeatures !== false, showMapLinks: config?.showMapLinks !== false }; const stars = "★".repeat(mapData.difficulty || 0) + "☆".repeat(Math.max(0, 5 - (mapData.difficulty || 0))); let htmlContent = ` <div class="header"> <h1>🗺️ 地图「${mapData.name}」详细信息</h1> </div> `; if (displayConfig.showMapBasicInfo) { htmlContent += `<div class="section"> <div class="stat-item">类型: <span class="highlight">${mapData.type || "未知"}</span> (${stars})</div> <div class="stat-item">作者: <span class="highlight">${mapData.mapper || "未知"}</span></div> <div class="stat-item">难度: ${mapData.difficulty || 0}/5 • 积分值: ${mapData.points || 0}</div> `; if (mapData.release) { htmlContent += `<div class="stat-item">发布日期: ${formatter.date(mapData.release, "short")}</div>`; } htmlContent += `</div>`; } if (displayConfig.showMapFeatures && mapData.tiles && mapData.tiles.length > 0 || displayConfig.showMapLinks) { htmlContent += `<div class="section"> <div class="section-title">🧩 地图特性及相关链接</div>`; if (displayConfig.showMapFeatures && mapData.tiles && mapData.tiles.length > 0) { htmlContent += ` <div class="stat-item">尺寸: ${mapData.width || "?"} × ${mapData.height || "?"}</div> <div class="stat-item">特殊方块: ${mapData.tiles.join(", ")}</div> `; } if (displayConfig.showMapLinks) { if (mapData.website) { htmlContent += `<div class="url-link">详细信息: ${mapData.website}</div>`; } if (mapData.web_preview) { htmlContent += `<div class="url-link">地图预览: ${mapData.web_preview}</div>`; } if (mapData.thumbnail) { htmlContent += ` <div class="stat-item">地图缩略图:</div> <div><img src="${mapData.thumbnail}" alt="地图缩略图" class="thumbnail"></div> `; } } htmlContent += `</div>`; } if (displayConfig.showMapStats) { htmlContent += `<div class="section"> <div class="section-title">📊 完成统计</div> <div class="stats-container"> <div class="stats-column"> <div class="stat-item">总完成次数: <span class="highlight">${mapData.finishes || 0}</span></div> <div class="stat-item">完成玩家数: <span class="highlight">${mapData.finishers || 0}</span></div> ${mapData.median_time ? `<div class="stat-item">平均时间: <span class="highlight">${formatter.time(mapData.median_time)}</span></div>` : ""} </div> <div class="stats-column"> ${mapData.first_finish ? `<div class="stat-item">首次完成: ${formatter.date(mapData.first_finish, "short")}</div>` : ""} ${mapData.last_finish ? `<div class="stat-item">最近完成: ${formatter.date(mapData.last_finish, "short")}</div>` : ""} ${mapData.biggest_team ? `<div class="stat-item">最大团队: <span class="highlight">${mapData.biggest_team}</span> 人</div>` : ""} </div> </div> </div>`; } const hasGlobalRanks = displayConfig.globalRanksCount !== 0 && mapData.ranks && mapData.ranks.length > 0; const hasChinaRanks = displayConfig.chinaRanksCount !== 0 && mapData.ranks?.filter((r) => r.country === "CHN")?.length > 0; const hasTeamRanks = displayConfig.teamRanksCount !== 0 && mapData.team_ranks && mapData.team_ranks.length > 0; if (hasGlobalRanks || hasChinaRanks || hasTeamRanks) { htmlContent += `<div class="section"> <div class="section-title">🏆 排行榜</div> <div class="stats-container">`; const generateRanking = /* @__PURE__ */ __name((title, ranks, limit, getPlayerName = (r) => r.player) => { htmlContent += `<div class="stats-column"> <div class="section-title">${title}</div> <ul class="rank-list-vertical">`; ranks.slice(0, limit).forEach((rank, idx) => { const playerName = getPlayerName(rank); if (playerName && rank.time) { const countryTag = rank.country ? `[${rank.country}] ` : ""; const timeStr = formatter.time(rank.time); const dateStr = rank.timestamp ? formatter.date(rank.timestamp, "short") : ""; const positionClass = idx < 3 ? `position-${idx + 1}` : ""; htmlContent += ` <li class="rank-item"> <div class="rank-position ${positionClass}">${idx + 1}</div> <div class="rank-player">${countryTag}${playerName}</div> <div class="rank-time-container"> <span class="rank-time">${timeStr}</span> ${dateStr ? `<span class="rank-date">${dateStr}</span>` : ""} </div> </li>`; } }); htmlContent += `</ul></div>`; }, "generateRanking"); if (hasGlobalRanks) { const limit = displayConfig.globalRanksCount === -1 ? mapData.ranks.length : Math.min(displayConfig.globalRanksCount, mapData.ranks.length); generateRanking("全球排名", mapData.ranks, limit); } if (hasChinaRanks) { const chinaPlayers = mapData.ranks.filter((r) => r.country === "CHN") || []; const limit = displayConfig.chinaRanksCount === -1 ? chinaPlayers.length : Math.min(displayConfig.chinaRanksCount, chinaPlayers.length); generateRanking("🇨🇳 国服排名", chinaPlayers, limit); } if (hasTeamRanks) { const limit = displayConfig.teamRanksCount === -1 ? mapData.team_ranks.length : Math.min(displayConfig.teamRanksCount, mapData.team_ranks.length); generateRanking( "👥 团队排名", mapData.team_ranks, limit, (teamRank) => teamRank.players ? teamRank.players.join(", ") : "" ); } htmlContent += `</div></div>`; } if (displayConfig.multiFinishersCount !== 0 && mapData.max_finishes && mapData.max_finishes.length > 0) { const limit = displayConfig.multiFinishersCount === -1 ? mapData.max_finishes.length : Math.min(displayConfig.multiFinishersCount, mapData.max_finishes.length); htmlContent += `<div class="section"> <div class="section-title">🔄 多次完成玩家</div> <div class="finisher-grid"> `; mapData.max_finishes.slice(0, limit).forEach((finisher) => { if (finisher.player && finisher.num) { htmlContent += ` <div class="finisher-card"> <span>${finisher.player}</span> <span class="finisher-count">${finisher.num}次</span> </div>`; } }); htmlContent += `</div></div>`; } return htmlContent; } __name(mapInfoToHtml, "mapInfoToHtml"); // src/index.ts var name = "ddrace"; var inject = { optional: ["puppeteer"] }; var usage = ` <div style="border-radius: 10px; border: 1px solid #ddd; padding: 16px; margin-bottom: 20px; box-shadow: 0 2px 5px rgba(0,0,0,0.1);"> <h2 style="margin-top: 0; color: #4a6ee0;">📌 插件说明</h2> <p>📖 <strong>使用文档</strong>:请点击左上角的 <strong>插件主页</strong> 查看插件使用文档</p> <p>🔍 <strong>更多插件</strong>:可访问 <a href="https://github.com/YisRime" style="color:#4a6ee0;text-decoration:none;">苡淞的 GitHub</a> 查看本人的所有插件</p> </div> <div style="border-radius: 10px; border: 1px solid #ddd; padding: 16px; margin-bottom: 20px; box-shadow: 0 2px 5px rgba(0,0,0,0.1);"> <h2 style="margin-top: 0; color: #e0574a;">❤️ 支持与反馈</h2> <p>🌟 喜欢这个插件?请在 <a href="https://github.com/YisRime" style="color:#e0574a;text-decoration:none;">GitHub</a> 上给我一个 Star!</p> <p>🐛 遇到问题?请通过 <strong>Issues</strong> 提交反馈,或加入 QQ 群 <a href="https://qm.qq.com/q/PdLMx9Jowq" style="color:#e0574a;text-decoration:none;"><strong>855571375</strong></a> 进行交流</p> </div> `; var Config = import_koishi.Schema.intersect([ // 消息发送配置 import_koishi.Schema.object({ messageSendType: import_koishi.Schema.union([ import_koishi.Schema.const("image").description("图片"), import_koishi.Schema.const("forward").description("合并转发"), import_koishi.Schema.const("text").description("文本") ]).description("消息发送形式").default("forward") }).description("消息发送配置"), // 玩家信息显示配置 import_koishi.Schema.object({ showRankInfo: import_koishi.Schema.boolean().description("显示排名与积分").default(true), showGameInfo: import_koishi.Schema.boolean().description("显示游戏信息").default(true), showActivityStats: import_koishi.Schema.boolean().description("显示活跃度统计").default(true), showActivityInfo: import_koishi.Schema.boolean().description("显示近期活跃度").default(true), showMapTypeStats: import_koishi.Schema.boolean().description("显示地图完成统计").default(true), mapDetailsCount: import_koishi.Schema.number().description("显示地图名称的数量").default(10), recentFinishesCount: import_koishi.Schema.number().description("显示最近通关记录的数量").default(-1).max(10), favoritePartnersCount: import_koishi.Schema.number().description("显示常用队友的数量").default(-1).max(10) }).description("玩家信息显示配置"), // 地图信息显示配置 import_koishi.Schema.object({ showMapBasicInfo: import_koishi.Schema.boolean().description("显示地图信息").default(true), showMapFeatures: import_koishi.Schema.boolean().description("显示地图特性").default(true), showMapLinks: import_koishi.Schema.boolean().description("显示相关链接").default(true), showMapStats: import_koishi.Schema.boolean().description("显示完成统计").default(true), globalRanksCount: import_koishi.Schema.number().description("显示全球排名的数量").default(-1).max(20), chinaRanksCount: import_koishi.Schema.number().description("显示国服排名的数量").default(-1).max(20), teamRanksCount: import_koishi.Schema.number().description("显示团队排名的数量").default(-1).max(20), multiFinishersCount: import_koishi.Schema.number().description("显示多次完成玩家的数量").default(-1).max(20) }).description("地图信息显示配置") ]); async function apply(ctx, config) { const canRenderImages = ctx.puppeteer != null; function httpGet(url) { return new Promise((resolve, reject) => { import_https.default.get(url, (res) => { const statusCode = res.statusCode || 500; if (statusCode >= 400) { return reject(new Error(`请求失败,状态码: ${statusCode}`)); } let data = ""; res.on("data", (chunk) => { data += chunk; }); res.on("end", () => { try { const parsedData = JSON.parse(data); resolve(parsedData); } catch (e) { reject(new Error("解析数据失败,请稍后重试")); } }); }).on("error", (err) => { reject(new Error(`网络请求失败: ${err.message}`)); }); }); } __name(httpGet, "httpGet"); function splitTextContent(text) { if (text.length < 100) return [text]; const blocks = text.split(/\n\n(?=[\p{Emoji}\p{Emoji_Presentation}])/u); const segments = []; let currentSegment = ""; for (const block of blocks) { if (currentSegment.length + block.length > 800 && currentSegment.length > 0) { segments.push(currentSegment.trim()); currentSegment = block; } else { currentSegment += (currentSegment ? "\n\n" : "") + block; } } if (currentSegment.trim()) { segments.push(currentSegment.trim()); } return segments.length ? segments : [text]; } __name(splitTextContent, "splitTextContent"); async function sendForwardMsg(session, textContent, title) { if (!session?.onebot) return textContent; try { const messages = []; if (title) { messages.push({ type: "node", data: { name: "DDRace 查询", uin: session.selfId, content: title } }); } const segments = splitTextContent(textContent); for (const segment of segments) { messages.push({ type: "node", data: { name: "DDRace 查询", uin: session.selfId, content: segment } }); } const result = await session.onebot._request("send_forward_msg", { message_type: session.isDirect ? "private" : "group", user_id: session.isDirect ? session.userId : void 0, group_id: session.isDirect ? void 0 : session.guildId, messages }); return result.message_id; } catch (error) { ctx.logger.error("发送合并转发消息失败:", error); return textContent; } } __name(sendForwardMsg, "sendForwardMsg"); async function handleQuery(data, formatText, formatHtml, session) { switch (config.messageSendType) { case "image": if (canRenderImages && formatHtml) { try { const htmlContent = formatHtml(data, config); const imageBuffer = await htmlToImage(htmlContent, ctx); return import_koishi.h.image(imageBuffer, "image/png"); } catch (error) { ctx.logger.error("图片生成失败,回退到合并转发:", error); } } case "forward": if (session) { const textContent = formatText(data, config); if (textContent.length > 100) { try { const title = `${data.player || data.name || "查询"} 的信息`; const result = await sendForwardMsg(session, textContent, title); return result; } catch (erro