ZZZ-Plugin/apps/panel.js
2025-04-03 00:01:31 +08:00

548 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { ZZZPlugin } from '../lib/plugin.js';
import {
getPanelList,
refreshPanel as refreshPanelFunction,
getPanelOrigin,
updatePanelData,
formatPanelData,
getPanelListOrigin,
} from '../lib/avatar.js';
import settings from '../lib/settings.js';
import _ from 'lodash';
import { rulePrefix } from '../lib/common.js';
import { getZzzEnkaData } from '../lib/ekapi/query.js'
import { _enka_data_to_mys_data } from '../lib/ekapi/enka_to_mys.js'
export class Panel extends ZZZPlugin {
constructor() {
super({
name: '[ZZZ-Plugin]Panel',
dsc: 'zzzpanel',
event: 'message',
priority: _.get(settings.getConfig('priority'), 'panel', 70),
rule: [
{
reg: `${rulePrefix}(.*)面板(刷新|更新|列表)?$`,
fnc: 'handleRule',
},
{
reg: `${rulePrefix}练度(统计)?$`,
fnc: 'proficiency',
},
{
reg: `${rulePrefix}原图$`,
fnc: 'getCharOriImage',
},
],
handler: [
{ key: 'zzz.tool.panel', fn: 'getCharPanelTool' },
{ key: 'zzz.tool.panelList', fn: 'getCharPanelListTool' },
],
});
}
async handleRule() {
if (!this.e.msg) return;
const reg = new RegExp(`${rulePrefix}(.*)面板(刷新|更新|列表)?$`);
const pre = this.e.msg.match(reg)[4]?.trim();
const suf = this.e.msg.match(reg)[5]?.trim();
if (['刷新', '更新'].includes(pre) || ['刷新', '更新'].includes(suf))
return await this.refreshPanel();
if (!pre || suf === '列表') return await this.getCharPanelList();
const queryPanelReg = new RegExp(`${rulePrefix}(.*)面板$`);
if (queryPanelReg.test(this.e.msg)) return await this.getCharPanel();
return false;
}
// async refreshPanel() {
// const uid = await this.getUID();
// // ... 省略获取 lastQueryTime 和冷卻时间判断的代码 ...
// this.result = null; // 先清空结果
//
// const useEnka = _.get(settings.getConfig('config'), 'useEnka', false);
// logger.mark(`[panel.js] useEnka 设置值: ${useEnka}`); // 1. 确认是否启用 Enka 逻辑
//
// if (useEnka) { // 检查这个 if 是否进入
// logger.mark('[panel.js] 进入 useEnka 逻辑块');
// let enkaData = null; // 初始化 enkaData
// try {
// logger.mark(`[panel.js] 准备调用 getZzzEnkaData for UID: ${uid}`);
// enkaData = await getZzzEnkaData(uid);
// logger.mark('[panel.js] getZzzEnkaData 调用完成');
//
// // 2. 详细检查 enkaData 的状态
// if (enkaData === null || enkaData === undefined) {
// logger.error('[panel.js] getZzzEnkaData 返回了 null 或 undefined');
// await this.reply('获取Enka数据失败 (返回null/undefined),请稍后再试');
// return false;
// } else if (enkaData === -1) {
// logger.warn('[panel.js] getZzzEnkaData 返回了 -1 (表示获取失败)');
// await this.reply('获取Enka数据失败 (返回-1),请稍后再试');
// return false;
// } else if (!enkaData.PlayerInfo || !enkaData.PlayerInfo.ShowcaseDetail || !enkaData.PlayerInfo.ShowcaseDetail.AvatarList) {
// logger.error('[panel.js] 获取到的 enkaData 结构不完整:', enkaData);
// await this.reply('获取到的Enka数据结构不完整无法处理');
// return false;
// } else {
// logger.mark('[panel.js] 成功获取到有效的 enkaData 结构');
// console.log('[panel.js] enkaData.PlayerInfo.ShowcaseDetail.AvatarList:', enkaData.PlayerInfo.ShowcaseDetail.AvatarList); // 打印角色列表确认
// }
//
// // 3. 检查 _enka_data_to_mys_data 函数是否已正确导入
// if (typeof _enka_data_to_mys_data !== 'function') {
// logger.error('[panel.js] _enka_data_to_mys_data 不是一个函数! 请检查导入语句和文件是否存在。');
// await this.reply('内部错误:数据转换函数加载失败');
// return false;
// }
//
// logger.mark('[panel.js] _enka_data_to_mys_data 函数已找到,类型:', typeof _enka_data_to_mys_data);
// logger.mark('[panel.js] 即将调用 _enka_data_to_mys_data...');
//
// // 4. 使用 try...catch 包裹调用,捕获可能的内部错误
// try {
// this.result = await _enka_data_to_mys_data(enkaData);
// logger.mark('[panel.js] _enka_data_to_mys_data 调用完成。');
// // 在这里可以检查 this.result 是否是你期望的格式
// console.log('[panel.js] 转换后的 result (部分示例):', this.result ? JSON.stringify(this.result[0], null, 2).substring(0, 500) + '...' : 'null or empty');
// } catch (conversionError) {
// logger.error('[panel.js] 调用 _enka_data_to_mys_data 时发生严重错误:', conversionError);
// // 打印详细错误堆栈
// console.error(conversionError);
// await this.reply(`处理Enka数据时出错: ${conversionError.message}`);
// return false; // 出错则不再继续
// }
//
// } catch (fetchError) {
// // 这个 catch 捕获 getZzzEnkaData 自身的 await 可能抛出的错误
// logger.error('[panel.js] 调用 getZzzEnkaData 时发生错误:', fetchError);
// console.error(fetchError); // 打印错误堆栈
// await this.reply(`获取Enka数据时发生网络或API错误: ${fetchError.message}`);
// return false;
// }
//
// } else { // 如果 useEnka 是 false
// logger.mark('[panel.js] 未启用 useEnka跳过 Enka 面板逻辑');
// // 这里可以继续执行原有的 mysapi 逻辑(如果需要的话)
// // logger.mark('mysapi执行');
// // ... (原有的 mysapi 刷新逻辑) ...
// // 注意:如果 useEnka 为 falsethis.result 可能需要从 mysapi 获取
// }
//
// // ----- 后续处理 this.result 的代码 -----
// // (确保无论走 Enka 逻辑还是 mysapi 逻辑this.result 都有合适的值)
//
// // 例如,原有的 newChar 计算和渲染逻辑:
// if (this.result && Array.isArray(this.result)) { // 检查 this.result 是否有效
// const newChar = this.result.filter(item => item.isNew); // 假设 MYS 数据结构中有 isNew
// const finalData = {
// newChar: newChar.length,
// list: this.result, // 使用转换后的或 mysapi 的结果
// };
// await this.render('panel/refresh.html', finalData);
// } else if (!useEnka) {
// // 如果没用 Enka 且没有执行 mysapi 逻辑(或 mysapi 逻辑没产生 result可能需要提示
// logger.warn('[panel.js] 没有可用的面板数据用于渲染 (useEnka=false, result无效)');
// // 可能需要添加回复告诉用户没有数据?
// } else if (useEnka && (!this.result || this.result.length === 0)) {
// logger.warn('[panel.js] Enka 数据转换后结果为空或无效');
// // 根据需要决定是否回复用户
// }
//
// }
// async refreshPanel() {
// const uid = await this.getUID();
// const lastQueryTime = await redis.get(`ZZZ:PANEL:${uid}:LASTTIME`);
// const panelSettings = settings.getConfig('panel');
// const coldTime = _.get(panelSettings, 'interval', 300);
// this.result = null; // Initialize instance result
//
// // --- Enka Path ---
// if(_.get(settings.getConfig('config'), 'useEnka', false)){ // Default is false if setting missing
// logger.mark('enka面板执行')
// const enkaData = await getZzzEnkaData(uid); // Fetch Enka data
//
// // --- Issue 1: Check for failure *before* processing ---
// if (enkaData === -1) { // Check for specific failure code
// await this.reply('获取enka数据失败请稍后再试');
// return false; // Exit early
// }
// // Add more checks for null/undefined/incomplete data if needed here
//
// console.log('[panel.js] 获取到 enkaData准备转换...');
// console.log('Enka AvatarList:', enkaData?.PlayerInfo?.ShowcaseDetail?.AvatarList);
// console.log('[panel.js] 即将调用 _enka_data_to_mys_data...');
//
// // --- This is where previous errors occurred due to name_convert.js load failure ---
// // Assuming name_convert.js path fix is applied, this should now work
// this.result = await _enka_data_to_mys_data(enkaData); // Assign to instance variable
// logger.mark('[panel.js] _enka_data_to_mys_data 调用完成.'); // Added log for confirmation
//
// // --- MYS API Path ---
// } else {
// logger.mark('mysapi执行')
// if (lastQueryTime && Date.now() - lastQueryTime < 1000 * coldTime) {
// await this.reply(`${coldTime}秒内只能刷新一次,请稍后再试`);
// return false;
// }
// const { api } = await this.getAPI();
// await redis.set(`ZZZ:PANEL:${uid}:LASTTIME`, Date.now());
// await this.reply('正在刷新面板列表,请稍候...');
// await this.getPlayerInfo(); // Assumed necessary for MYS panel
//
// // --- Issue 2: Assign result correctly ---
// let mysResult = null; // Use a temporary variable for MYS result
// try {
// mysResult = await refreshPanelFunction(api); // Call the imported MYS refresh
// console.dir('MYS API res:', mysResult); // Log MYS result
// if (!mysResult) {
// await this.reply('面板列表刷新失败 (MYS API),请稍后再试');
// return false;
// }
// // Assign the successful MYS result to the instance variable
// this.result = mysResult; // <<<< IMPORTANT ASSIGNMENT
// } catch (e) {
// this.reply(e.message);
// // Consider logging the full error for debugging
// logger.error("MYS API refresh failed:", e);
// // Depending on desired behavior, you might return false or re-throw
// return false; // Exit on MYS API error
// }
// } // End if/else
//
// // --- Post-processing: Use this.result consistently ---
// // Ensure this.result is an array before proceeding
// if (!this.result || !Array.isArray(this.result)) {
// logger.error('[panel.js] 最终结果无效或不是数组:', this.result);
// // Maybe reply to the user that no data could be processed
// await this.reply('未能获取或处理有效的面板数据。');
// return false;
// }
//
// // --- Issue 3: Use this.result for calculations ---
// // Filter based on the unified this.result
// const newChar = this.result.filter(item => item && item.isNew); // Added check for item existence
//
// // Prepare final data using the unified this.result
// const finalData = {
// newChar: newChar.length,
// list: this.result, // Use the instance variable
// };
//
// // Render the result
// await this.render('panel/refresh.html', finalData);
// }
async refreshPanel() {
const uid = await this.getUID();
// --- 获取玩家信息 (带容错处理) ---
// ... (这部分代码保持不变,使用带有占位符逻辑的版本) ...
logger.mark('[panel.js] 准备调用 getPlayerInfo...');
let playerInfo = null;
try {
playerInfo = await this.getPlayerInfo();
if (!playerInfo) playerInfo = this.e.player;
if (!playerInfo) {
logger.warn(`[panel.js] getPlayerInfo 未返回有效信息使用默认占位。UID: ${uid}`);
playerInfo = { uid: uid, nickname: `用户${uid}`, level: '??', region_name: '未知服务器' };
}
logger.mark('[panel.js] getPlayerInfo 尝试完成.');
} catch (playerInfoError) {
logger.error('[panel.js] 调用 getPlayerInfo 时出错 (使用占位):', playerInfoError.message);
playerInfo = { uid: uid, nickname: `用户${uid}`, level: '??', region_name: '错误', error: playerInfoError.message };
}
// --- End 获取玩家信息 ---
this.result = null; // 初始化结果
// ----- 选择 Enka 或 MYS API -----
const useEnka = _.get(settings.getConfig('config'), 'useEnka', true); // 读取配置Enka 优先
logger.mark(`[panel.js] useEnka 设置值: ${useEnka}`);
// ----- End -----
// --- 数据获取和处理逻辑 ---
if (useEnka) {
logger.mark('[panel.js] 进入 Enka 逻辑块');
try {
const enkaData = await getZzzEnkaData(uid);
logger.mark('[panel.js] getZzzEnkaData 调用完成.'); // <-- 日志:调用后
// ----- !!! 在这里添加打印原始 Enka 数据的日志 !!! -----
console.log('===== ZZZ Enka Raw Data Start =====');
// 使用 JSON.stringify 完整打印null, 2 用于格式化输出
console.log(JSON.stringify(enkaData, null, 2));
console.log('===== ZZZ Enka Raw Data End =====');
// ----- !!! 日志添加结束 !!! -----
if (!enkaData || enkaData === -1 || !enkaData.PlayerInfo) { throw new Error('获取或验证 Enka 数据失败'); }
logger.mark('[panel.js] 即将调用 _enka_data_to_mys_data...');
this.result = await _enka_data_to_mys_data(enkaData); // <<< Enka 结果赋给 this.result
logger.mark('[panel.js] _enka_data_to_mys_data 调用完成.');
// ----- !!! 在这里添加打印转换后数据的日志 !!! -----
console.log('===== Enka Converted Data (First Avatar) Start =====');
if (this.result && Array.isArray(this.result) && this.result.length > 0) {
console.log(JSON.stringify(this.result[0], null, 2)); // 打印第一个转换后的角色
} else {
console.log('Converted result is empty or invalid.');
}
console.log('===== Enka Converted Data (First Avatar) End =====');
// ----- !!! 日志添加结束 !!! -----
logger.mark('[panel.js] _enka_data_to_mys_data 调用完成.');
} catch (enkaError) {
logger.error('[panel.js] 处理 Enka 逻辑时出错:', enkaError);
await this.reply(`处理Enka数据时出错: ${enkaError.message}`);
return false;
}
} else {
logger.mark('[panel.js] 进入 mysapi 逻辑块');
try {
const { api } = await this.getAPI(); // MYS 需要 api 对象
// MYS 逻辑需要冷却判断
const lastQueryTime = await redis.get(`ZZZ:PANEL:${uid}:LASTTIME`);
const panelSettings = settings.getConfig('panel');
const coldTime = _.get(panelSettings, 'interval', 300);
if (lastQueryTime && Date.now() - lastQueryTime < 1000 * coldTime) {
await this.reply(`${coldTime}秒内只能刷新一次,请稍后再试`);
return false;
}
await redis.set(`ZZZ:PANEL:${uid}:LASTTIME`, Date.now());
await this.reply('正在刷新面板列表 (MYS API),请稍候...');
const mysResult = await refreshPanelFunction(api); // 调用 MYS 刷新函数
if (!mysResult) { throw new Error('MYS API 返回空结果'); }
this.result = mysResult; // <<< MYS 结果赋给 this.result
logger.mark('[panel.js] MYS API refreshPanelFunction 调用完成.');
} catch (mysError) {
logger.error('[panel.js] MYS API 刷新出错:', mysError);
this.reply(`MYS API 刷新出错: ${mysError.message}`);
return false;
}
}
// --- End 数据获取和处理逻辑 ---
// --- !!! 关键步骤:更新面板数据缓存 !!! ---
if (this.result && Array.isArray(this.result)) { // 确保有有效数据 (非 null, 是数组)
// 并且至少包含一个角色数据才存,避免存空数组?(可选)
if (this.result.length > 0) {
try {
logger.mark(`[panel.js] 准备调用 updatePanelData 更新缓存,包含 ${this.result.length} 个角色数据...`);
// 调用导入的 updatePanelData 函数
await updatePanelData(uid, this.result); // <<< 核心:写入缓存
logger.mark('[panel.js] updatePanelData 调用完成,缓存已更新。');
} catch (cacheError) {
logger.error('[panel.js] 调用 updatePanelData 更新缓存时出错:', cacheError);
// 记录错误,但可能继续
}
} else {
logger.warn('[panel.js] 获取到的角色列表为空数组,不执行缓存更新。');
// 如果是 Enka 路径且展示柜为空,这是正常情况
}
} else {
logger.warn('[panel.js] 没有有效的角色列表数据 (this.result),跳过缓存更新。');
// 如果之前的步骤没有 return false这里可能需要提示用户
if (!useEnka) { // MYS 失败的情况
await this.reply('未能获取或处理有效的面板列表数据。');
return false; // 如果 MYS 失败且结果无效,应该退出
}
// 如果是 Enka 路径且结果无效/非数组,也提示并退出
await this.reply('处理后的面板数据格式无效。');
return false;
}
// --- !!! End 更新缓存 !!! ---
// --- 后续处理:构建 finalData 用于渲染刷新摘要 ---
const currentResult = this.result || []; // 保证 currentResult 是数组
// newChar 的计算可能依赖于 MYS 的特定字段Enka 结果可能没有
// 保持之前的兼容逻辑,如果 Enka 结果没有 isNewnewCharCount 为 0
const newCharCount = (currentResult.length > 0 && currentResult[0]?.isNew !== undefined)
? currentResult.filter(item => item && item.isNew).length
: 0;
const finalData = {
newChar: newCharCount,
list: currentResult,
player: playerInfo,
uid: uid
};
logger.mark('[panel.js] 准备渲染 refresh.html 模板...');
// 渲染刷新摘要页面
try {
await this.render('panel/refresh.html', finalData);
} catch (renderError) {
logger.error('[panel.js] 渲染 refresh.html 模板失败:', renderError);
await this.reply(`生成刷新结果图片时出错: ${renderError.message}`);
}
}
async getCharPanelListTool(uid, origin = false) {
if (!uid) {
return false;
}
if (origin) {
const result = getPanelListOrigin(uid);
return result;
}
const result = getPanelList(uid);
return result;
}
async getCharPanel() {
const uid = await this.getUID();
const reg = new RegExp(`${rulePrefix}(.+)面板$`);
const match = this.e.msg.match(reg);
if (!match) return false;
const name = match[4];
const data = getPanelOrigin(uid, name);
if (!data) {
await this.reply(`未找到角色${name}的面板信息,请先刷新面板`);
return;
}
let handler = this.e.runtime.handler || {};
if (handler.has('zzz.tool.panel')) {
await handler.call('zzz.tool.panel', this.e, {
uid,
data: data,
needSave: false,
});
}
return false;
}
async getCharPanelTool(e, _data = {}) {
if (e) this.e = e;
if (e?.reply) this.reply = e.reply;
const {
uid = undefined,
data = undefined,
needSave = true,
reply = true,
needImg = true
} = _data;
if (!uid) {
await this.reply('UID为空');
return false;
}
if (!data) {
await this.reply('数据为空');
return false;
}
if (needSave) {
updatePanelData(uid, [data]);
}
const timer = setTimeout(() => {
const msg = '查询成功,正在下载图片资源,请稍候。'
if (this?.reply && needImg) {
this.reply(msg);
} else {
logger.mark(msg)
}
}, 5000);
const parsedData = formatPanelData(data);
await parsedData.get_detail_assets();
clearTimeout(timer);
const finalData = {
uid,
charData: parsedData,
};
const image = needImg ? await this.render('panel/card.html', finalData, {
retType: 'base64',
}) : needImg;
if (reply) {
const res = await this.reply(image);
if (res?.message_id && parsedData.role_icon)
await redis.set(
`ZZZ:PANEL:IMAGE:${res.message_id}`,
parsedData.role_icon,
{
EX: 3600 * 3,
}
);
return {
message: res,
image,
};
}
return image;
}
async proficiency() {
const uid = await this.getUID();
const result = getPanelList(uid);
if (!result) {
await this.reply('未找到面板数据,请先%刷新面板');
return false;
}
await this.getPlayerInfo();
result.sort((a, b) => {
return b.proficiency_score - a.proficiency_score;
});
const WeaponCount = result.filter(item => item?.weapon).length,
SWeaponCount = result.filter(
item => item?.weapon && item.weapon.rarity === 'S'
).length;
const general = {
total: result.length,
SCount: result.filter(item => item.rarity === 'S').length,
SWeaponRate: (SWeaponCount / WeaponCount) * 100,
SSSCount: result.reduce((acc, item) => {
if (item.equip) {
acc += item.equip.filter(
equip => equip.comment === 'SSS' || equip.comment === 'ACE'
).length;
}
return acc;
}, 0),
highRank: result.filter(item => item.rank > 4).length,
};
const timer = setTimeout(() => {
if (this?.reply) {
this.reply('查询成功,正在下载图片资源,请稍候。');
}
}, 5000);
for (const item of result) {
await item.get_small_basic_assets();
}
clearTimeout(timer);
const finalData = {
general,
list: result,
};
await this.render('proficiency/index.html', finalData);
}
async getCharOriImage() {
let source;
if (this.e.getReply) {
source = await this.e.getReply();
} else if (this.e.source) {
if (this.e.group?.getChatHistory) {
// 支持at图片添加以及支持后发送
source = (
await this.e.group.getChatHistory(this.e.source?.seq, 1)
).pop();
} else if (this.e.friend?.getChatHistory) {
source = (
await this.e.friend.getChatHistory(this.e.source?.time + 1, 1)
).pop();
}
}
const id = source?.message_id;
if (!id) {
await this.reply('未找到消息源,请引用要查看的图片');
return false;
}
const image = await redis.get(`ZZZ:PANEL:IMAGE:${id}`);
if (!image) {
await this.reply('未找到原图');
return false;
}
await this.reply(segment.image(image));
return false;
}
}