koishi-plugin-ddrace
Version:
DDRaceNetwork 玩家和地图数据查询,支持文本和图片两种展示方式
1,268 lines (1,262 loc) • 55.2 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 __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