typo: 代码注释

This commit is contained in:
bietiaop 2024-07-15 14:25:42 +08:00
parent 51b3908afd
commit 4c90ca5354
12 changed files with 457 additions and 139 deletions

View file

@ -1,10 +1,12 @@
import { findLowestLatencyUrl } from '../utils/network.js';
// 保存上次找的节点
let lastFindFastestUrl = {
url: null,
time: 0,
};
// 节点列表
const URL_LIB = {
'[JPFRP]': 'http://jp-3.lcf.1l1.icu:17217',
'[HKFRP]': 'http://hk-1.lcf.1l1.icu:10200',
@ -15,35 +17,46 @@ const URL_LIB = {
'[Singapore]': 'https://sg.qxqx.cf',
};
// 文件类型路径
const TYPE_PATH = {
wiki: 'wiki',
resource: 'resource',
guide: 'guide',
};
// 资源路径
const RESOURCE_PATH = {
role: 'role',
role_circle: 'role_circle',
weapon: 'weapon',
};
// 图鉴路径
const GUIDE_PATH = {
flower: 'flower',
};
/**
* 获取最快节点
* @returns {Promise<string>}
*/
export const getFatestUrl = async () => {
if (
lastFindFastestUrl.url &&
Date.now() - lastFindFastestUrl.time < 1000 * 60 * 5
) {
// 如果上次找到的节点在 5 分钟内,直接返回
return lastFindFastestUrl.url;
}
// 获取最快节点
const urls = Object.values(URL_LIB);
const url = findLowestLatencyUrl(urls);
// 保存节点
lastFindFastestUrl = {
url,
time: Date.now(),
};
// 返回节点
return url;
};

View file

@ -14,15 +14,21 @@ try {
* @param {string} mysid 米游社ID
* @returns
*/
export function getStoken(e, mysid = '') {
let userId = e.user_id;
let user = new User(e);
let file = `${user.stokenPath}${userId}.yaml`;
export const getStoken = (e, mysid = '') => {
// 获取QQ号
const userId = e.user_id;
// 实例化用户
const user = new User(e);
// 获取 sk 文件路径
const filePath = `${user.stokenPath}${userId}.yaml`;
try {
let cks = fs.readFileSync(file, 'utf-8');
cks = YAML.parse(cks);
// 读取文件
const file = fs.readFileSync(filePath, 'utf-8');
// 解析文件
const cks = YAML.parse(file);
for (const ck in cks) {
if (cks[ck]['stuid'] === mysid) {
if (cks?.[ck]?.['stuid'] && cks[ck]['stuid'] === mysid) {
// 如果 ck 存在并且 stuid 与 mysid 相同则返回(这两者都是字符串,不需要类型转换,因此不需要使用弱比较,强比较速度更快,没有隐式转换一步骤,注意代码规范与代码风格)
return cks[ck];
}
}
@ -31,20 +37,26 @@ export function getStoken(e, mysid = '') {
logger.debug(`[zzz:error]getStoken`, error);
return null;
}
}
};
/**
* 此方法依赖逍遥插件
* @returns {Promise<void>}
* @param {*} e yunzai Event
* @param {*} _user
* @param {string} zzzUid
* @param {string} authAppid
* @returns {Promise<string>}
*/
export async function getAuthKey(e, _user, zzzUid, authAppid = 'csc') {
export const getAuthKey = async (e, _user, zzzUid, authAppid = 'csc') => {
if (!User) {
throw new Error('未安装逍遥插件,无法自动刷新抽卡链接');
}
// 获取 UID 数据
const uidData = _user.getUidData(zzzUid, 'zzz', e);
if (!uidData || uidData?.type != 'ck' || !uidData?.ltuid) {
throw new Error(`当前UID${zzzUid}未绑定cookie请切换UID后尝试`);
}
// 获取 sk
let ck = getStoken(e, uidData.ltuid);
if (!ck) {
throw new Error(
@ -56,7 +68,9 @@ export async function getAuthKey(e, _user, zzzUid, authAppid = 'csc') {
`当前UID${zzzUid}查询所使用的米游社ID${ck.stuid}与当前切换的米游社ID${uidData.ltuid}不匹配请切换UID后尝试`
);
}
// 拼接 sk
ck = `stuid=${ck.stuid};stoken=${ck.stoken};mid=${ck.mid};`;
// 实例化 API
const api = new MysZZZApi(zzzUid, ck);
let type = 'zzzAuthKey';
switch (authAppid) {
@ -66,14 +80,16 @@ export async function getAuthKey(e, _user, zzzUid, authAppid = 'csc') {
}
default:
}
logger.mark(type);
// 获取链接
const { url, headers, body } = api.getUrl(type);
logger.mark(url);
// 发送请求
let res = await fetch(url, {
method: 'POST',
headers,
body,
});
// 获取数据
res = await res.json();
// 返回 authkey
return res?.data?.authkey;
}
};

View file

@ -11,17 +11,20 @@ import { char } from './convert.js';
* @param {boolean} origin 是否返回原始数据
* @returns {Promise<ZZZAvatarBasic[] | null>}
*/
export async function getAvatarBasicList(e, api, deviceFp, origin = false) {
export const getAvatarBasicList = async (e, api, deviceFp, origin = false) => {
// 获取米游社角色列表
const avatarBaseListData = await api.getFinalData(e, 'zzzAvatarList', {
deviceFp,
});
if (!avatarBaseListData) return null;
// 是否返回原始数据
if (origin) return avatarBaseListData.avatar_list;
// 格式化数据
const result = avatarBaseListData.avatar_list.map(
item => new ZZZAvatarBasic(item)
);
return result;
}
};
/**
* 获取角色详细信息列表
@ -31,9 +34,11 @@ export async function getAvatarBasicList(e, api, deviceFp, origin = false) {
* @param {string} deviceFp
* @param {boolean} origin 是否返回原始数据
*/
export async function getAvatarInfoList(e, api, deviceFp, origin = false) {
export const getAvatarInfoList = async (e, api, deviceFp, origin = false) => {
// 获取角色基础信息列表
const avatarBaseList = await getAvatarBasicList(e, api, deviceFp, origin);
if (!avatarBaseList) return null;
// 获取角色详细信息
const avatarInfoList = await api.getFinalData(e, 'zzzAvatarInfo', {
deviceFp,
query: {
@ -41,12 +46,14 @@ export async function getAvatarInfoList(e, api, deviceFp, origin = false) {
},
});
if (!avatarInfoList) return null;
// 是否返回原始数据
if (origin) return avatarInfoList.avatar_list;
// 格式化数据
const result = avatarInfoList.avatar_list.map(
item => new ZZZAvatarInfo(item)
);
return result;
}
};
/**
* 刷新面板
@ -56,38 +63,47 @@ export async function getAvatarInfoList(e, api, deviceFp, origin = false) {
* @param {string} deviceFp
* @returns {Promise<ZZZAvatarInfo[] | null>}
*/
export async function refreshPanel(e, api, uid, deviceFp) {
export const refreshPanel = async (e, api, uid, deviceFp) => {
// 获取已保存数据
const originData = getPanelData(uid);
// 获取新数据
const newData = await getAvatarInfoList(e, api, deviceFp, true);
if (!newData) return null;
// 初始化最终数据
const finalData = [...newData];
// 如果有已保存的数据
if (originData) {
// 合并数据
for (const item of originData) {
if (!finalData.find(i => i.id === item.id)) {
// 将已保存的数据添加到最终数据中(放在后面)
finalData.push(item);
}
}
}
// 保存数据
savePanelData(uid, finalData);
// 格式化数据
finalData.forEach(item => {
item.isNew = newData.find(i => i.id === item.id);
});
const formattedData = finalData.map(item => new ZZZAvatarInfo(item));
for (const item of formattedData) {
// 下载图片资源
await item.get_basic_assets();
}
return formattedData;
}
};
/**
*获取面板数据
* @param {string} uid
* @returns {ZZZAvatarInfo[]}
*/
export function getPanelList(uid) {
export const getPanelList = uid => {
const data = getPanelData(uid);
return data.map(item => new ZZZAvatarInfo(item));
}
};
/**
* 获取某个角色的面板数据
@ -95,12 +111,15 @@ export function getPanelList(uid) {
* @param {string} name
* @returns {ZZZAvatarInfo | null}
*/
export function getPanel(uid, name) {
export const getPanel = (uid, name) => {
const _data = getPanelData(uid);
// 获取所有面板数据
const data = _data.map(item => new ZZZAvatarInfo(item));
// 通过名称(包括别名)获取角色 ID
const id = char.atlasToID(name);
if (!id) return null;
// 通过 ID 获取角色数据
const result = data.find(item => item.id === id);
if (!result) return null;
return result;
}
};

View file

@ -3,12 +3,18 @@ import { getStoken } from './authkey.js';
export const rulePrefix = '^((#|\\%)?(zzz|ZZZ|绝区零))';
export async function getCk(e, s = false) {
/**
* 获取米游社用户的 cookie
* @param {Object} e yunzai事件
* @param {boolean} s 是否获取 stoken
* @returns {Promise<Object>}
*/
export const getCk = async (e, s = false) => {
e.isSr = true;
let stoken = '';
let user = new User(e);
const user = new User(e);
if (s) {
stoken = await getStoken(e);
stoken = getStoken(e);
}
if (typeof user.getCk === 'function') {
let ck = user.getCk();
@ -19,7 +25,7 @@ export async function getCk(e, s = false) {
});
return ck;
}
let mysUser = (await user.user()).getMysUser('zzz');
const mysUser = (await user.user()).getMysUser('zzz');
let ck;
if (mysUser) {
ck = {
@ -33,4 +39,4 @@ export async function getCk(e, s = false) {
};
}
return ck;
}
};

View file

@ -25,19 +25,24 @@ const ZZZ_SUIT_PATH = path.join(imageResourcesPath, 'suit');
*/
const downloadFile = async (url, savePath) => {
const _download = async (url, savePath, retry = 0) => {
// 重试次数超过 5 次则返回 null
if (retry > 5) {
return null;
}
// 下载文件
try {
const download = await fetch(url);
const arrayBuffer = await download.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
// 保存文件
if (!fs.existsSync(path.dirname(savePath))) {
fs.mkdirSync(path.dirname(savePath), { recursive: true });
}
fs.writeFileSync(savePath, buffer);
// 返回保存路径
return savePath;
} catch (error) {
// 下载失败,重试
return await _download(url, savePath, retry + 1);
}
};
@ -97,7 +102,7 @@ export const getWeaponImage = async id => {
*/
export const getRoleImage = async id => {
const sprite = char.IDToCharSprite(id);
if (sprite === null) return null;
if (!sprite) return null;
const filename = `IconRole${sprite}.png`;
const rolePath = path.join(ZZZ_ROLE_PATH, filename);
if (fs.existsSync(rolePath)) return rolePath;
@ -114,7 +119,7 @@ export const getRoleImage = async id => {
*/
export const getRoleCircleImage = async id => {
const sprite = char.IDToCharSprite(id);
if (sprite === null) return null;
if (!sprite) return null;
const filename = `IconRoleCircle${sprite}.png`;
const roleCirclePath = path.join(ZZZ_ROLE_CIRCLE_PATH, filename);
if (fs.existsSync(roleCirclePath)) return roleCirclePath;

View file

@ -4,6 +4,7 @@ import { rank } from './convert.js';
import { getGachaLog, saveGachaLog } from './db.js';
import { ZZZ_GET_GACHA_LOG_API } from './mysapi/api.js';
// 池子代码
export const gacha_type_meta_data = {
音擎频段: ['3001'],
独家频段: ['2001'],
@ -11,13 +12,50 @@ export const gacha_type_meta_data = {
邦布频段: ['5001'],
};
// 欧非阈值
const FLOORS_MAP = {
邦布频段: [50, 70],
音擎频段: [50, 70],
独家频段: [60, 80],
常驻频段: [60, 80],
};
// 欧非标签
const HOMO_TAG = ['非到极致', '运气不好', '平稳保底', '小欧一把', '欧狗在此'];
// 欧非表情
const EMOJI = [
[4, 8, 13],
[1, 10, 5],
[16, 15, 2],
[12, 3, 9],
[6, 14, 7],
];
// 常驻名称
const NORMAL_LIST = [
'「11号」',
'猫又',
'莱卡恩',
'丽娜',
'格莉丝',
'珂蕾妲',
'拘缚者',
'燃狱齿轮',
'嵌合编译器',
'钢铁肉垫',
'硫磺石',
'啜泣摇篮',
];
/**
* @param {string} authKey
* @param {string} gachaType
* 获取抽卡链接
* @param {string} authKey 米游社认证密钥
* @param {string} gachaType 祈愿类型池子代码
* @param {string} initLogGachaBaseType
* @param {number} page
* @param {string} endId
* @returns {Promise<string>}
* @param {number} page 页数
* @param {string} endId 最后一个数据的 id
* @returns {Promise<string>} 抽卡链接
*/
export const getZZZGachaLink = async (
authKey,
@ -26,10 +64,11 @@ export const getZZZGachaLink = async (
page = 1,
endId = '0'
) => {
// 暂时直接写死服务器为国服
const serverId = 'prod_gf_cn';
const url = ZZZ_GET_GACHA_LOG_API;
const timestamp = Math.floor(Date.now() / 1000);
// 请求参数
const params = new URLSearchParams({
authkey_ver: '1',
sign_type: '2',
@ -50,26 +89,27 @@ export const getZZZGachaLink = async (
size: '20',
end_id: endId,
});
// 完整链接
return `${url}?${params}`;
};
/**
*
* @param {string} authKey
* @param {*} gachaType
* @param {*} initLogGachaBaseType
* @param {number} page
* @param {string} endId
* @returns {Promise<ZZZGachaLogResp>}
* 通过米游社认证密钥获取抽卡记录
* @param {string} authKey 米游社认证密钥
* @param {string} gachaType 祈愿类型池子代码
* @param {string} initLogGachaBaseType
* @param {number} page 页数
* @param {string} endId 最后一个数据的 id
* @returns {Promise<ZZZGachaLogResp>} 抽卡记录
*/
export async function getZZZGachaLogByAuthkey(
export const getZZZGachaLogByAuthkey = async (
authKey,
gachaType = '2001',
initLogGachaBaseType = '2',
page = 1,
endId = '0'
) {
) => {
// 获取抽卡链接
const link = await getZZZGachaLink(
authKey,
gachaType,
@ -77,25 +117,25 @@ export async function getZZZGachaLogByAuthkey(
page,
endId
);
// 发送请求
const response = await fetch(link, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
// 获取数据
const data = await response.json();
if (!data || !data?.data) return null;
return new ZZZGachaLogResp(data.data);
}
};
/**
*
* @param {string} authKey
* @param {string} uid
* 更新抽卡数据
* @param {string} authKey 米游社认证密钥
* @param {string} uid ZZZUID
* @returns {Promise<{
* data: {
* [x: string]: SingleGachaLog[];
@ -103,26 +143,39 @@ export async function getZZZGachaLogByAuthkey(
* count: {
* [x: string]: number;
* }
* }>}
* }>} 更新后的抽卡数据
*/
export async function updateGachaLog(authKey, uid) {
export const updateGachaLog = async (authKey, uid) => {
// 获取之前的抽卡数据
let previousLog = getGachaLog(uid);
if (!previousLog) {
// 如果没有数据,初始化为空对象
previousLog = {};
}
// 新的抽卡数据
let newCount = {};
// 遍历所有池子
for (const name in gacha_type_meta_data) {
if (!previousLog[name]) {
// 如果没有数据,初始化为空数组
previousLog[name] = [];
}
// 初始化新数据计数(当前池子)
newCount[name] = 0;
// 转换为 SingleGachaLog 对象
previousLog[name] = previousLog[name].map(i => new SingleGachaLog(i));
// 获取上一次保存的数据
const lastSaved = previousLog[name]?.[0];
// 初始化页数和最后一个数据的 id
let page = 1;
let endId = '0';
// 新数据
const newData = [];
// 遍历当前池子的所有类型
for (const type of gacha_type_meta_data[name]) {
// 循环获取数据
queryLabel: while (true) {
// 获取抽卡记录
const log = await getZZZGachaLogByAuthkey(
authKey,
type,
@ -133,58 +186,42 @@ export async function updateGachaLog(authKey, uid) {
if (!log || !log?.list || log?.list?.length === 0) {
break;
}
// 遍历数据 (从最新的开始)
for (const item of log.list) {
if (lastSaved && lastSaved.equals(item)) {
// 如果数据相同,说明已经获取完毕
break queryLabel;
}
// 添加到新数据中
newData.push(item);
// 当前池子新数据计数加一
newCount[name]++;
}
// 更新页数和最后一个数据的 id
endId = log.list[log.list.length - 1]?.id || endId;
page++;
// 防止请求过快
await sleep(400);
}
}
// 合并新数据和之前的数据
previousLog[name] = [...newData, ...previousLog[name]];
}
// 保存数据到文件
saveGachaLog(uid, previousLog);
// 返回数据
return {
data: previousLog,
count: newCount,
};
}
const HOMO_TAG = ['非到极致', '运气不好', '平稳保底', '小欧一把', '欧狗在此'];
const EMOJI = [
[4, 8, 13],
[1, 10, 5],
[16, 15, 2],
[12, 3, 9],
[6, 14, 7],
];
const NORMAL_LIST = [
'「11号」',
'猫又',
'莱卡恩',
'丽娜',
'格莉丝',
'珂蕾妲',
'拘缚者',
'燃狱齿轮',
'嵌合编译器',
'钢铁肉垫',
'硫磺石',
'啜泣摇篮',
];
const FLOORS_MAP = {
邦布频段: [50, 70],
音擎频段: [50, 70],
独家频段: [60, 80],
常驻频段: [60, 80],
};
function getLevelFromList(ast, lst) {
/**
* 欧非分析
* @param {number} ast
* @param {number[]} lst
*/
const getLevelFromList = (ast, lst) => {
if (ast === 0) {
return 2;
}
@ -197,40 +234,77 @@ function getLevelFromList(ast, lst) {
}
}
return level;
}
};
export async function anaylizeGachaLog(uid) {
/**
* 抽卡分析
* @param {string} uid ZZZUID
* @returns {Promise<{
* name: string;
* timeRange: string;
* list: SingleGachaLog[];
* lastFive: number | string;
* fiveStars: number;
* upCount: number;
* totalCount: number;
* avgFive: string;
* avgUp: string;
* level: number;
* tag: string;
* emoji: number;
* }[]>} 分析结果
*/
export const anaylizeGachaLog = async uid => {
// 读取已保存的数据
const savedData = getGachaLog(uid);
if (!savedData) {
return null;
}
// 初始化结果
const result = [];
// 遍历所有池子
for (const name in savedData) {
// 转换为 SingleGachaLog 对象
const data = savedData[name].map(item => new SingleGachaLog(item));
// 获取最早和最晚的数据
const earliest = data[data.length - 1];
const latest = data[0];
// 初始化五星列表
const list = [];
// 初始化最后五星位置
let lastFive = null;
// 初始化前一个索引
let preIndex = 0;
// 遍历数据
let i = 0;
for (const item of data) {
// 是否为UP
let isUp = true;
// 如果是五星
if (item.rank_type === '4') {
// 下载图片资源
await item.get_assets();
// 判断是否为常驻
if (NORMAL_LIST.includes(item.name)) {
isUp = false;
}
// 如果是第一个五星
if (lastFive === null) {
// 记录位置
lastFive = i;
}
// 如果不是第一个五星
if (list.length > 0) {
// 计算前一个五星到当前五星的次数(即前一个五星出卡所用的抽数)
list[list.length - 1]['totalCount'] = i - preIndex;
// 根据次数设置颜色
if (i - preIndex <= FLOORS_MAP[name][0]) {
list[list.length - 1]['color'] = 'rgb(63, 255, 0)';
} else if (i - preIndex >= FLOORS_MAP[name][1]) {
list[list.length - 1]['color'] = 'rgb(255, 20, 20)';
}
}
// 添加到列表中
list.push({
...item,
rank_type_label: rank.getRankChar(item.rank_type),
@ -240,8 +314,11 @@ export async function anaylizeGachaLog(uid) {
});
preIndex = i;
}
// 如果是最后一个数据
if (i === data.length - 1 && list.length > 0) {
// 计算前一个五星到当前五星的次数(即前一个五星出卡所用的抽数)
list[list.length - 1]['totalCount'] = i - preIndex + 1;
// 根据次数设置颜色
if (i - preIndex + 1 <= FLOORS_MAP[name][0]) {
list[list.length - 1]['color'] = 'rgb(63, 255, 0)';
} else if (i - preIndex + 1 >= FLOORS_MAP[name][1]) {
@ -250,23 +327,37 @@ export async function anaylizeGachaLog(uid) {
}
i++;
}
// 计算五星数量和UP数量
const upCount = list.filter(i => i.isUp).length;
// 计算总次数
const totalCount = data.length;
// 计算五星数量
const fiveStars = list.length;
// 初始化时间范围
let timeRange = '还没有抽卡';
// 初始化平均五星次数
let avgFive = '-';
// 初始化平均UP次数
let avgUp = '-';
// 初始化欧非等级
let level = 2;
// 如果有数据
if (data.length > 0) {
// 设置时间范围
timeRange = `${latest.time} ${earliest.time}`;
// 计算平均五星次数
if (fiveStars > 0)
avgFive = ((totalCount - lastFive) / fiveStars).toFixed(1);
// 计算平均UP次数
if (upCount > 0) avgUp = ((totalCount - lastFive) / upCount).toFixed(1);
}
// 如果没有最后五星
if (!lastFive && fiveStars === 0) {
// 设置最后五星为 '-'
if (totalCount > 0) lastFive = totalCount;
else lastFive = '-';
}
// 根据不同池子计算欧非等级
if (avgUp !== '-') {
if ('音擎频段' === name) {
level = getLevelFromList(avgUp, [62, 75, 88, 99, 111]);
@ -281,10 +372,13 @@ export async function anaylizeGachaLog(uid) {
level = getLevelFromList(avgFive, [53, 60, 68, 73, 75]);
}
}
// 设置欧非标签
const tag = HOMO_TAG[level];
// 设置欧非表情
const emojis = EMOJI[level];
// 随机选取一个
const emoji = emojis[Math.floor(Math.random() * emojis.length)];
// 写入数据
result.push({
name,
timeRange,
@ -301,4 +395,4 @@ export async function anaylizeGachaLog(uid) {
});
}
return result;
}
};

View file

@ -2,10 +2,12 @@ import md5 from 'md5';
import _ from 'lodash';
import crypto from 'crypto';
import ZZZApiTool from './mysapi/tool.js';
import { randomString } from '../utils/data.js';
import MysApi from '../../genshin/model/mys/mysApi.js';
// const DEVICE_ID = randomString(32).toUpperCase()
const DEVICE_NAME = randomString(_.random(1, 10));
// const DEVICE_NAME = randomString(_.random(1, 10));
const game_region = [
'prod_gf_cn',
'prod_gf_us',
@ -13,9 +15,14 @@ const game_region = [
'prod_gf_jp',
'prod_gf_sg',
];
/**
* 米游社ZZZAPI继承自MysApi
*/
export default class MysZZZApi extends MysApi {
constructor(uid, cookie, option = {}) {
super(uid, cookie, option, true);
// 初始化 uid、server、apiTool
this.uid = uid;
this.server = this.getServer(uid);
this.apiTool = new ZZZApiTool(uid, this.server);
@ -30,6 +37,10 @@ export default class MysZZZApi extends MysApi {
}
}
/**
* 获取服务器
* @returns {string}
*/
getServer() {
const _uid = this.uid?.toString();
if (_uid.length < 10) {
@ -47,10 +58,19 @@ export default class MysZZZApi extends MysApi {
}
}
/**
* 获取请求网址
* @param {string} type
* @param {object} data
* @returns {object|boolean}
*/
getUrl(type, data = {}) {
// 设置设备ID
data.deviceId = this._device;
let urlMap = this.apiTool.getUrlMap(data);
// 获取请求地址
const urlMap = this.apiTool.getUrlMap(data);
if (!urlMap[type]) return false;
// 获取请求参数即APITool中默认的请求参数此参数理应是不可获取的详细请参照 lib/mysapi/tool.js`
let {
url,
query = '',
@ -58,7 +78,9 @@ export default class MysZZZApi extends MysApi {
noDs = false,
dsSalt = '',
} = urlMap[type];
// 如果有query拼接到url上
if (query) url += `?${query}`;
// 如果传入了 query 参数,将 query 参数拼接到 url 上
if (data.query) {
let str = '';
for (let key in data.query) {
@ -77,19 +99,22 @@ export default class MysZZZApi extends MysApi {
url += `?${str}`;
}
}
// 写入 body
if (body) body = JSON.stringify(body);
// 获取请求头
let headers = this.getHeaders(query, body);
if (data.deviceFp) {
headers['x-rpc-device_fp'] = data.deviceFp;
// 兼容喵崽
this._device_fp = { data: { device_fp: data.deviceFp } };
}
// 写入 cookie
headers.cookie = this.cookie;
// 写入设备ID
if (this._device) {
headers['x-rpc-device_id'] = this._device;
}
// 写入DS
switch (dsSalt) {
case 'web': {
headers.DS = this.getDS2();
@ -106,6 +131,7 @@ export default class MysZZZApi extends MysApi {
} else {
headers.DS = this.getDs(query, body);
}
// 如果不需要 DS删除 DS
if (noDs) {
delete headers.DS;
if (this._device) {
@ -115,9 +141,16 @@ export default class MysZZZApi extends MysApi {
}
}
logger.debug(`[mysapi]请求url${url}`);
// 返回请求参数
return { url, headers, body };
}
/**
* 获取DS
* @param {string} q
* @param {string} b
* @returns {string}
*/
getDs(q = '', b = '') {
let n = '';
if (['prod_gf_cn'].includes(this.server)) {
@ -125,19 +158,29 @@ export default class MysZZZApi extends MysApi {
} else {
n = 'okr4obncj8bw5a65hbnn5oo6ixjc3l9w';
}
let t = Math.round(new Date().getTime() / 1000);
let r = Math.floor(Math.random() * 900000 + 100000);
let DS = md5(`salt=${n}&t=${t}&r=${r}&b=${b}&q=${q}`);
const t = Math.round(new Date().getTime() / 1000);
const r = Math.floor(Math.random() * 900000 + 100000);
const DS = md5(`salt=${n}&t=${t}&r=${r}&b=${b}&q=${q}`);
return `${t},${r},${DS}`;
}
/**
* 获取DS2
* @returns {string}
*/
getDS2() {
let t = Math.round(new Date().getTime() / 1000);
let r = randomString(6);
let sign = md5(`salt=BIPaooxbWZW02fGHZL1If26mYCljPgst&t=${t}&r=${r}`);
const t = Math.round(new Date().getTime() / 1000);
const r = randomString(6);
const sign = md5(`salt=BIPaooxbWZW02fGHZL1If26mYCljPgst&t=${t}&r=${r}`);
return `${t},${r},${sign}`;
}
/**
* 获取请求头
* @param {string} query
* @param {string} body
* @returns {object}
*/
getHeaders(query = '', body = '') {
const cn = {
app_version: '2.63.1',
@ -191,7 +234,7 @@ export default class MysZZZApi extends MysApi {
return false;
}
this.e = e;
this.e.isSr = true;
this.e.isZZZ = true;
res.retcode = Number(res.retcode);
switch (res.retcode) {
case 0:
@ -255,20 +298,3 @@ export default class MysZZZApi extends MysApi {
return _data.data;
}
}
export function randomString(length) {
let randomStr = '';
for (let i = 0; i < length; i++) {
randomStr += _.sample('abcdefghijklmnopqrstuvwxyz0123456789');
}
return randomStr;
}
export function generateSeed(length = 16) {
const characters = '0123456789abcdef';
let result = '';
for (let i = 0; i < length; i++) {
result += characters[Math.floor(Math.random() * characters.length)];
}
return result;
}

View file

@ -6,49 +6,62 @@ import settings from '../lib/settings.js';
export class ZZZPlugin extends plugin {
/**
*
* @returns {Promise<string>}
* 获取用户 UID如果需要同时获取API可以直接调用 getAPI
* @returns {Promise<string | boolean>}
*/
async getUID() {
// 默认为当前用户
let user = this.e;
// 获取配置
const query = settings.getConfig('config').query;
const allow = _.get(query, 'others', true);
// 如果 at 存在且允许查看其他用户
if (this.e.at && allow) {
// 将当前用户的 user_id 设置为 at 对象的 user_id
this.e.user_id = this.e.at;
// 将当前用户设置为 at 对象
user = this.e.at;
}
// 获取用户信息(米游社),因此这里会导致查询一次米游社的信息
this.User = await NoteUser.create(user);
// let uid = this.e.msg.match(/\d+/)?.[0];
// 获取用户 UID
const uid = this.User?.getUid('zzz');
// 如果 UID 不存在,说明没有绑定 cookie
if (!uid) {
await this.reply('uid为空米游社查询请先绑定cookie其他查询请携带uid');
return false;
}
// 返回 UID
return uid;
}
/**
*
* 获取米游社 API
* @returns {Promise<{api: MysZZZApi, uid: string, deviceFp: string}>}
*/
async getAPI() {
let uid = await this.getUID();
// 直接调用获取 UID
const uid = await this.getUID();
if (!uid) return false;
// 获取用户的 cookie
const ck = await getCk(this.e);
// 如果 cookie 不存在或者 cookie 为空,说明没有绑定 cookie
if (!ck || Object.keys(ck).filter(k => ck[k].ck).length === 0) {
await this.reply('尚未绑定cookie请先绑定cookie');
return false;
}
// 创建米游社 API 对象
const api = new MysZZZApi(uid, ck);
// 获取设备指纹
let deviceFp = await redis.get(`ZZZ:DEVICE_FP:${uid}`);
if (!deviceFp) {
let sdk = api.getUrl('getFp');
let res = await fetch(sdk.url, {
const sdk = api.getUrl('getFp');
const res = await fetch(sdk.url, {
headers: sdk.headers,
method: 'POST',
body: sdk.body,
});
let fpRes = await res.json();
const fpRes = await res.json();
deviceFp = fpRes?.data?.device_fp;
if (deviceFp) {
await redis.set(`ZZZ:DEVICE_FP:${uid}`, deviceFp, {
@ -56,19 +69,25 @@ export class ZZZPlugin extends plugin {
});
}
}
// 返回数据API、UID、设备指纹
return { api, uid, deviceFp };
}
/**
*
* 获取玩家信息当调用此方法时会获取用户的玩家信息并将其保存到`e.playerCard`方便渲染用户信息此部分请查阅`lib/render.js`中两个模块的作用
* @returns {Promise<boolean | object>}
*/
async getPlayerInfo() {
// 获取 米游社 API
const { api } = await this.getAPI();
if (!api) return false;
// 获取用户信息
let userData = await api.getFinalData(this.e, 'zzzUser');
if (!userData) return false;
// 取第一个用户信息
userData = userData?.list[0];
// 获取用户头像
let avatar = this.e.bot.avatar;
if (this.e?.user_id) {
avatar = `https://q1.qlogo.cn/g?b=qq&s=0&nk=${this.e.user_id}`;
@ -77,10 +96,12 @@ export class ZZZPlugin extends plugin {
} else if (this.e.friend?.getAvatarUrl) {
avatar = await this.e.friend.getAvatarUrl();
}
// 写入数据
this.e.playerCard = {
avatar: avatar,
player: userData,
};
// 返回数据
return userData;
}
}

View file

@ -11,7 +11,7 @@ import version from './version.js';
* @param {object} cfg 配置
* @returns
*/
function render(e, renderPath, renderData = {}, cfg = {}) {
const render = (e, renderPath, renderData = {}, cfg = {}) => {
// 判断是否存在e.runtime
if (!e.runtime) {
console.log('未找到e.runtime请升级至最新版Yunzai');
@ -30,31 +30,55 @@ function render(e, renderPath, renderData = {}, cfg = {}) {
// 调用e.runtime.render方法渲染
return e.runtime.render(pluginName, renderPath, renderData, {
// 合并传入的配置
...cfg,
beforeRender({ data }) {
// 资源路径
const resPath = data.pluResPath;
// 布局路径
const layoutPath = data.pluResPath + 'common/layout/';
// 当前的渲染路径
const renderPathFull = data.pluResPath + renderPath.split('/')[0] + '/';
// 合并数据
return {
// 玩家信息
player: e?.playerCard?.player,
// 玩家头像
avatar: e?.playerCard?.avatar,
// 传入的数据
...data,
// 资源路径
_res_path: resPath,
// 布局路径
_layout_path: layoutPath,
// 默认布局路径
defaultLayout: path.join(layoutPathFull, 'index.html'),
// 系统配置
sys: {
// 缩放比例
scale: pct,
// 资源路径
resourcesPath: resPath,
// 当前渲染的路径
currentPath: renderPathFull,
/**
* 下面两个模块的作用在于你可以在你的布局文件中使用这两个模块就可以显示用户信息和特殊标题使用方法如下
* 1. 展示玩家信息首先你要在查询的时候调用`this.getPlayerInfo()`这样玩家数据就会保存在`e.playerCard`然后你就可以在布局文件中使用`{{include sys.playerInfo}}`来展示玩家信息
* 2. 展示特殊标题你可以在布局文件中使用`<% include(sys.specialTitle, {en: 'PROPERTY' , cn: '属性' , count: 6 }) %>`来展示特殊标题其中`count`为可选参数默认为9
*/
// 玩家信息模块
playerInfo: path.join(layoutPathFull, 'playerinfo.html'),
// 特殊标题模块
specialTitle: path.join(layoutPathFull, 'specialtitle.html'),
// 版权信息
copyright: `Created By ${version.name}<span class="version">${version.yunzai}</span> & ${pluginName}<span class="version">${version.version}</span>`,
// 版权信息(简化版)
createdby: `Created By <span class="highlight">${pluginName}</span> & Powered By <span class="highlight">ZZZure</span>`,
},
quality: 100,
};
},
});
}
};
export default render;

View file

@ -59,12 +59,17 @@ class Setting {
// 配置对象分析 用于锅巴插件界面设置
analysis(config) {
for (let key of Object.keys(config)) {
for (const key of Object.keys(config)) {
this.setConfig(key, config[key]);
}
}
// 获取对应模块数据文件
/**
* 获取对应模块数据文件
* @param {string} filepath
* @param {string} filename
* @returns {object | false}
*/
getData(filepath, filename) {
filename = `${filename}.yaml`;
filepath = path.join(this.dataPath, filepath);
@ -79,7 +84,13 @@ class Setting {
}
}
// 写入对应模块数据文件
/**
* 写入对应模块数据文件
* @param {string} filepath
* @param {string} filename
* @param {object} data
* @returns {boolean}
*/
setData(filepath, filename, data) {
filename = `${filename}.yaml`;
filepath = path.join(this.dataPath, filepath);
@ -93,24 +104,38 @@ class Setting {
YAML.stringify(data),
'utf8'
);
return true;
} catch (error) {
logger.error(`[${pluginName}] [${filename}] 写入失败 ${error}`);
return false;
}
}
// 获取对应模块默认配置
/**
* 获取对应模块默认配置
* @param {'atlas'|'config'|'gacha'|'panel'} app
* @returns {object}
*/
getdefSet(app) {
return this.getYaml(app, 'defSet');
}
// 获取对应模块用户配置
/**
* 获取对应模块用户配置配置文件名
* @param {'atlas'|'config'|'gacha'|'panel'} app
* @returns {object}
*/
getConfig(app) {
return { ...this.getdefSet(app), ...this.getYaml(app, 'config') };
// return this.mergeConfigObjectArray({...this.getdefSet(app)},{...this.getYaml(app, 'config')});
}
//合并两个对象 相同的数组对象 主要用于yml的列表属性合并 并去重 先备份一下方法
/**
* 合并两个对象 相同的数组对象 主要用于yml的列表属性合并 并去重 先备份一下方法
* @param {object} obj1
* @param {object} obj2
* @returns {object}
*/
mergeConfigObjectArray(obj1, obj2) {
for (const key in obj2) {
if (Array.isArray(obj2[key]) && Array.isArray(obj1[key])) {
@ -126,9 +151,14 @@ class Setting {
return obj1;
}
// 设置对应模块用户配置
setConfig(app, Object) {
return this.setYaml(app, 'config', { ...this.getdefSet(app), ...Object });
/**
* 设置对应模块用户配置
* @param {'atlas'|'config'|'gacha'|'panel'} app
* @param {object} obj
* @returns
*/
setConfig(app, obj) {
return this.setYaml(app, 'config', { ...this.getdefSet(app), ...obj });
}
// 将对象写入YAML文件

View file

@ -3,42 +3,70 @@ import lodash from 'lodash';
import path from 'path';
import { pluginPath } from './path.js';
// 更新日志文件位置
const _logPath = path.join(pluginPath, 'CHANGELOG.md');
// 存放日志
let logs = {};
// 存放更新日志
let changelogs = [];
// 当前版本
let currentVersion;
// 版本数量
let versionCount = 4;
// 读取 package.json此处为读取Yunzai-Bot的package.json
let packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
/**
* Markdown 行转换为 HTML
* @param {*} line
* @returns
*/
const getLine = function (line) {
// 去除行首空格和换行符
line = line.replace(/(^\s*\*|\r)/g, '');
// 替换行内代码块
line = line.replace(/\s*`([^`]+`)/g, '<span class="cmd">$1');
line = line.replace(/`\s*/g, '</span>');
// 替换行内加粗
line = line.replace(/\s*\*\*([^\*]+\*\*)/g, '<span class="strong">$1');
line = line.replace(/\*\*\s*/g, '</span>');
// 替换行内表示更新内容
line = line.replace(/ⁿᵉʷ/g, '<span class="new"></span>');
// 返回转换后的行内容HTML
return line;
};
// 尝试读取更新日志文件
try {
if (fs.existsSync(_logPath)) {
// 如果文件存在,读取文件内容
logs = fs.readFileSync(_logPath, 'utf8') || '';
// 将文件内容按行分割
logs = logs.split('\n');
// 遍历每一行,提取版本号和更新内容
let temp = {};
let lastLine = {};
lodash.forEach(logs, line => {
// 如果版本数量小于0返回false
if (versionCount <= -1) {
return false;
}
let versionRet = /^#\s*([0-9a-zA-Z\\.~\s]+?)\s*$/.exec(line);
// 匹配版本号
const versionRet = /^#\s*([0-9a-zA-Z\\.~\s]+?)\s*$/.exec(line);
if (versionRet && versionRet[1]) {
let v = versionRet[1].trim();
// 如果匹配到版本号,提取版本号
const v = versionRet[1].trim();
if (!currentVersion) {
// 如果当前版本号不存在,将当前版本号设置为匹配到的版本号
currentVersion = v;
} else {
// 写入更新日志
changelogs.push(temp);
if (/0\s*$/.test(v) && versionCount > 0) {
versionCount = 0;
@ -46,15 +74,16 @@ try {
versionCount--;
}
}
temp = {
version: v,
logs: [],
};
} else {
// 如果行为空,不继续执行
if (!line.trim()) {
return;
}
// 如果行以 * 开头,表示更新内容
if (/^\*/.test(line)) {
lastLine = {
title: getLine(line),
@ -71,18 +100,27 @@ try {
// void error
}
// 读取Yunzai-Bot的版本号
const yunzaiVersion = packageJson.version;
// 判断是否为Yunzai-Bot v3
const isV3 = yunzaiVersion[0] === '3';
// 是否为喵(默认为否)
let isMiao = false;
// bot名称
let name = 'Yunzai-Bot';
if (packageJson.name === 'miao-yunzai') {
// 如果是喵,将 isMiao 设置为 truename 设置为 Miao-Yunzai
isMiao = true;
name = 'Miao-Yunzai';
} else if (packageJson.name === 'trss-yunzai') {
// 如果是 TRSS-Yunzai将 isMiao 设置为 truename 设置为 TRSS-Yunzai
isMiao = true;
name = 'TRSS-Yunzai';
}
// 导出版本信息
const version = {
isV3,
isMiao,