initial upload

This commit is contained in:
bietiaop 2024-07-08 13:13:22 +08:00
commit 1c65f10e24
58 changed files with 2533 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

17
.eslintrc.cjs Normal file
View file

@ -0,0 +1,17 @@
module.exports = {
env: {
es2021: true,
node: true,
},
extends: [
'standard',
'eslint:recommended',
'standard',
'plugin:prettier/recommended',
],
rules: {
eqeqeq: ['off'],
'prefer-const': ['off'],
'arrow-body-style': 'off',
},
};

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
*.css.map
config/**/*.*
!config/.gitkeep

5
CHANGELOG.md Normal file
View file

@ -0,0 +1,5 @@
# 0.0.1
* 初始化项目
* 支持 UID 绑定
* 支持 note 查看

29
README.md Normal file
View file

@ -0,0 +1,29 @@
# ZZZ-Plugin
<p align="center">
<a href="https://github.com/ZZZure/ZZZ-Plugin"><img src="https://s2.loli.net/2024/04/19/hOEDmsoUFy6nH5d.jpg" width="480" height="270" alt="ZZZ-Plugin"></a>
</p>
<h1 align = "center">ZZZ- Plugin</h1>
<h4 align = "center">🚧Yunzai绝区零Bot插件🚧</h4>
# 安装
进入Yunzai根目录运行以下命令进行安装
```bash
git clone --depth=1 https://github.com/ZZZure/ZZZ-Plugin.git ./plugins/ZZZ-Plugin
```
安装后重启Yunzai即可使用。
# 功能
待更新
# 贡献
请先 `fork` 本仓库修改并测试完成后提交PR。
**请注意:**
* 你的 CSS 必须是 `scss` 编写

26
apps/bind.js Normal file
View file

@ -0,0 +1,26 @@
import { rulePrefix } from '../lib/common.js';
import { ZZZPlugin } from '../lib/plugin.js';
export class bind extends ZZZPlugin {
constructor() {
super({
name: '[ZZZ-Plugin]Bind',
dsc: 'zzzbind',
event: 'message',
priority: 100,
rule: [
{
reg: `^${rulePrefix}绑定(uid|UID)?(\\s)?[1-9][0-9]{7,9}$`,
fnc: 'bindUid',
},
],
});
}
async bindUid() {
const uid = parseInt(this.e.msg.replace(/[^0-9]/gi, ''));
const user = this.e.user_id;
await redis.set(`ZZZ:UID:${user}`, uid);
this.reply(`绑定成功,当前绑定[zzz]uid:${uid}`, false);
return false;
}
}

53
apps/note.js Normal file
View file

@ -0,0 +1,53 @@
import { ZZZPlugin } from '../lib/plugin.js';
import MysZZZApi from '../lib/mysapi.js';
import { getCk } from '../lib/common.js';
import _ from 'lodash';
import render from '../lib/render.js';
import { ZZZNoteResp } from '../model/note.js';
export class test extends ZZZPlugin {
constructor() {
super({
name: '[ZZZ-Plugin]Note',
dsc: 'zzznote',
event: 'message',
priority: 100,
rule: [
{
reg: `^#zzznote$`,
fnc: 'note',
},
],
});
}
async note(e) {
const { api, deviceFp } = await this.getAPI();
let userData = await api.getData('zzzUser');
if (!userData?.data || _.isEmpty(userData.data.list)) {
await e.reply('[zzznote]玩家信息获取失败');
return false;
}
userData = userData?.data?.list[0];
let noteData = await api.getData('zzzNote', { deviceFp });
noteData = await api.checkCode(e, noteData, 'zzzNote', {});
if (!noteData || noteData.retcode !== 0) {
await e.reply('[zzznote]每日数据获取失败');
return false;
}
noteData = noteData.data;
noteData = new ZZZNoteResp(noteData);
let avatar = this.e.bot.avatar;
// 头像
if (this.e.member?.getAvatarUrl) {
avatar = await this.e.member.getAvatarUrl();
} else if (this.e.friend?.getAvatarUrl) {
avatar = await this.e.friend.getAvatarUrl();
}
const finalData = {
avatar,
player: userData,
note: noteData,
};
await render(e, 'note/index.html', finalData);
}
}

0
config/.gitkeep Normal file
View file

2
defSet/config.yaml Normal file
View file

@ -0,0 +1,2 @@
render:
scale: 100

44
index.js Normal file
View file

@ -0,0 +1,44 @@
import fs from 'fs';
import { appPath } from './lib/path.js';
logger.info('*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*');
logger.info('ZZZ-Plugin 加载中');
logger.info('仓库地址 https://github.com/ZZZure/ZZZ-plugin');
logger.info('Created By ZZZure Project (MIHOMO)');
logger.info('*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*');
if (!global.segment) global.segment = (await import('oicq')).segment;
if (!global.core) {
try {
global.core = (await import('oicq')).core;
} catch (err) {
// ignore empty error
}
}
const files = fs.readdirSync(appPath).filter(file => file.endsWith('.js'));
const ret = [];
files.forEach(file => {
ret.push(import(`./apps/${file}`));
});
const retPromise = await Promise.allSettled(ret);
const apps = {};
for (const i in files) {
const name = files[i].replace('.js', '');
if (retPromise[i].status != 'fulfilled') {
logger.error(`[ZZZ-Plugin] 载入模块${logger.red(name)}错误`);
logger.error(retPromise[i].reason);
continue;
}
apps[name] = retPromise[i].value[Object.keys(retPromise[i].value)[0]];
}
export { apps };

53
lib/authkey.js Normal file
View file

@ -0,0 +1,53 @@
// import User from "../../xiaoyao-cvs-plugin/model/user.js";
import fetch from 'node-fetch';
import MysZZZApi from './mysapi.js';
let User;
try {
User = (await import('../../xiaoyao-cvs-plugin/model/user.js')).default;
} catch (e) {
logger.warn('建议安装逍遥插件以获得更佳体验');
}
/**
* 此方法依赖逍遥插件
* @returns {Promise<void>}
*/
export async function getAuthKey(e, srUid, authAppid = 'csc') {
if (!User) {
throw new Error('未安装逍遥插件,无法自动刷新抽卡链接');
}
let user = new User(e);
// set genshin uid
await user.getCookie(e);
let ck = await user.getStoken(e.user_id);
ck = `stuid=${ck.stuid};stoken=${ck.stoken};mid=${ck.mid};`;
let api = new MysZZZApi(srUid, ck);
let type = 'zzzPayAuthKey';
switch (authAppid) {
case 'csc': {
type = 'zzzPayAuthKey';
break;
}
default:
}
const { url, headers, body } = api.getUrl(type);
let res = await fetch(url, {
method: 'POST',
headers,
body,
});
res = await res.json();
return res?.data?.authkey;
}
export async function getStoken(e) {
if (!User) {
throw new Error('未安装逍遥插件,无法自动刷新抽卡链接');
}
let user = new User(e);
// set uid
await user.getCookie(e);
let ck = await user.getStoken(e.user_id);
ck = `stuid=${ck.stuid};stoken=${ck.stoken};mid=${ck.mid};`;
return ck;
}

36
lib/common.js Normal file
View file

@ -0,0 +1,36 @@
import User from '../../genshin/model/user.js';
import { getStoken } from './authkey.js';
export const rulePrefix = '((#|\\*)?(zzz|ZZZ|绝区零)|\\*|)';
export async function getCk(e, s = false) {
e.isSr = true;
let stoken = '';
let user = new User(e);
if (s) {
stoken = await getStoken(e);
}
if (typeof user.getCk === 'function') {
let ck = user.getCk();
Object.keys(ck).forEach(k => {
if (ck[k].ck) {
ck[k].ck = `${stoken}${ck[k].ck}`;
}
});
return ck;
}
let mysUser = (await user.user()).getMysUser('zzz');
let ck;
if (mysUser) {
ck = {
default: {
ck: `${stoken}${mysUser.ck}`,
uid: mysUser.getUid('zzz'),
qq: '',
ltuid: mysUser.ltuid,
device_id: mysUser.device,
},
};
}
return ck;
}

246
lib/mysapi.js Normal file
View file

@ -0,0 +1,246 @@
import MysApi from '../../genshin/model/mys/mysApi.js';
import md5 from 'md5';
import _ from 'lodash';
import crypto from 'crypto';
import ZZZApiTool from './mysapi/tool.js';
// const DEVICE_ID = randomString(32).toUpperCase()
const DEVICE_NAME = randomString(_.random(1, 10));
export default class MysZZZApi extends MysApi {
constructor(uid, cookie, option = {}) {
super(uid, cookie, option, true);
this.uid = uid;
this.server = this.getServer();
// this.isSr = true
// this.server = 'hkrpg_cn'
this.apiTool = new ZZZApiTool(uid, this.server);
if (typeof this.cookie != 'string' && this.cookie) {
let ck =
this.cookie[Object.keys(this.cookie).filter(k => this.cookie[k].ck)[0]];
this._device = ck?.device_id || ck?.device;
this.cookie = ck?.ck;
}
if (!this._device) {
this._device = crypto.randomUUID();
}
}
getServer() {
switch (String(this.uid).slice(0, -8)) {
case '1':
case '2':
return 'prod_gf_cn'; // 官服
case '5':
return 'prod_qd_cn'; // B服
case '6':
return 'prod_official_usa'; // 美服
case '7':
return 'prod_official_euro'; // 欧服
case '8':
case '18':
return 'prod_official_asia'; // 亚服
case '9':
return 'prod_official_cht'; // 港澳台服
}
return 'prod_gf_cn';
}
getUrl(type, data = {}) {
data.deviceId = this._device;
let urlMap = this.apiTool.getUrlMap(data);
if (!urlMap[type]) return false;
let {
url,
query = '',
body = '',
noDs = false,
dsSalt = '',
} = urlMap[type];
if (query) url += `?${query}`;
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 } };
}
headers.cookie = this.cookie;
if (this._device) {
headers['x-rpc-device_id'] = this._device;
}
switch (dsSalt) {
case 'web': {
headers.DS = this.getDS2();
break;
}
default:
}
if (type === 'zzzPayAuthKey') {
let extra = {
'x-rpc-app_version': '2.40.1',
'User-Agent': 'okhttp/4.8.0',
'x-rpc-client_type': '5',
Referer: 'https://app.mihoyo.com',
Origin: 'https://webstatic.mihoyo.com',
// Cookie: this.cookies,
// DS: this.getDS2(),
'x-rpc-sys_version': '12',
'x-rpc-channel': 'mihoyo',
'x-rpc-device_id': this._device,
'x-rpc-device_name': DEVICE_NAME,
'x-rpc-device_model': 'Mi 10',
Host: 'api-takumi.mihoyo.com',
};
headers = Object.assign(headers, extra);
} else {
headers.DS = this.getDs(query, body);
}
if (noDs) {
delete headers.DS;
if (this._device) {
body = JSON.parse(body);
body.device_id = this._device;
body = JSON.stringify(body);
}
}
return { url, headers, body };
}
getDs(q = '', b = '') {
let n = '';
if (['prod_gf_cn', 'prod_qd_cn'].includes(this.server)) {
n = 'xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs';
} else if (/official/.test(this.server)) {
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}`);
return `${t},${r},${DS}`;
}
getDS2() {
let t = Math.round(new Date().getTime() / 1000);
let r = randomString(6);
let sign = md5(`salt=jEpJb9rRARU2rXDA9qYbZ3selxkuct9a&t=${t}&r=${r}`);
return `${t},${r},${sign}`;
}
getHeaders(query = '', body = '') {
const cn = {
app_version: '2.44.1',
User_Agent:
'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) miHoYoBBS/2.44.1',
client_type: '5',
Origin: 'https://webstatic.mihoyo.com',
X_Requested_With: 'com.mihoyo.hyperion',
Referer: 'https://webstatic.mihoyo.com/',
};
const os = {
app_version: '2.55.0',
User_Agent:
'Mozilla/5.0 (Linux; Android 11; J9110 Build/55.2.A.4.332; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/124.0.6367.179 Mobile Safari/537.36 miHoYoBBSOversea/2.55.0',
client_type: '2',
Origin: 'https://act.hoyolab.com',
X_Requested_With: 'com.mihoyo.hoyolab',
Referer: 'https://act.hoyolab.com/',
};
let client;
if (/official/.test(this.server)) {
client = os;
} else {
client = cn;
}
return {
'x-rpc-app_version': client.app_version,
'x-rpc-client_type': client.client_type,
// 'x-rpc-page': '3.1.3_#/rpg',
'User-Agent': client.User_Agent,
Referer: client.Referer,
DS: this.getDs(query, body),
Origin: client.Origin,
};
}
/**
* 校验状态码
* @param e 消息e
* @param res 请求返回
* @param type 请求类型 srNote
* @param data 查询请求的数据
* @returns {Promise<*|boolean>}
*/
async checkCode(e, res, type, data = {}) {
if (!res || !e) {
this.e.reply('米游社接口请求失败,暂时无法查询');
return false;
}
this.e = e;
this.e.isSr = true;
res.retcode = Number(res.retcode);
switch (res.retcode) {
case 0:
break;
case 10035:
case 1034: {
let handler = this.e.runtime?.handler || {};
// 如果有注册的mys.req.err调用
if (handler.has('mys.req.err')) {
logger.mark(
`[米游社zzz查询失败][uid:${this.uid}][qq:${this.userId}] 遇到验证码,尝试调用 Handler mys.req.err`
);
res =
(await handler.call('mys.req.err', this.e, {
mysApi: this,
type,
res,
data,
mysInfo: this,
})) || res;
}
if (!res || res?.retcode === 1034 || res?.retcode === 10035) {
logger.mark(
`[米游社zzz查询失败][uid:${this.uid}][qq:${this.userId}] 遇到验证码`
);
this.e.reply('米游社zzz查询遇到验证码请稍后再试');
}
break;
}
default:
if (/(登录|login)/i.test(res.message)) {
logger.mark(`[ck失效][uid:${this.uid}]`);
this.e.reply(`UID:${this.uid}米游社cookie已失效`);
} else {
this.e.reply(
`米游社接口报错,暂时无法查询:${res.message || 'error'}`
);
}
break;
}
if (res.retcode !== 0) {
logger.mark(
`[米游社zzz接口报错]${JSON.stringify(res)}uid${this.uid}`
);
}
return res;
}
}
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;
}

21
lib/mysapi/api.js Normal file
View file

@ -0,0 +1,21 @@
export const OLD_URL = 'https://api-takumi.mihoyo.com',
NEW_URL = 'https://api-takumi-record.mihoyo.com';
export const ZZZ_API = `${NEW_URL}/event/game_record_zzz/api/zzz`,
ZZZ_INDEX_API = `${ZZZ_API}/index`,
ZZZ_NOTE_API = `${ZZZ_API}/note`,
ZZZ_BUDDY_INFO_API = `${ZZZ_API}/buddy/info`,
ZZZ_AVATAR_BASIC_API = `${ZZZ_API}/avatar/basic`,
ZZZ_AVATAR_INFO_API = `${ZZZ_API}/avatar/info`,
ZZZ_CHALLENGE_API = `${ZZZ_API}/challenge`,
ZZZ_BIND_API = `${OLD_URL}/binding/api`,
ZZZ_GAME_INFO_API = `${ZZZ_BIND_API}/getUserGameRolesByCookie?game_biz=nap_cn`;
// Resource
export const ZZZ_RES = 'https://act-webstatic.mihoyo.com/game_record/zzz',
ZZZ_SQUARE_AVATAR = `${ZZZ_RES}/role_square_avatar`,
ZZZ_SQUARE_BANGBOO = `${ZZZ_RES}/bangboo_rectangle_avatar`;
export const PUBLIC_API = 'https://public-operation-nap.mihoyo.com',
PUBILC_GACHA_LOG_API = `${PUBLIC_API}/common/gacha_record/api`,
ZZZ_GET_GACHA_LOG_API = `${PUBILC_GACHA_LOG_API}/getGachaLog`;

81
lib/mysapi/tool.js Normal file
View file

@ -0,0 +1,81 @@
import { generateSeed } from '../mysapi.js';
import crypto from 'crypto';
/**
* derived from miao-yunzai
*/
export default class ZZZApiTool {
/**
*
* @param {uid} uid
* @param {server} server
*/
constructor(uid, server) {
this.uid = uid;
this.isSr = true;
this.server = server;
this.game = 'zzz';
this.uuid = crypto.randomUUID();
}
getUrlMap = (data = {}) => {
let host, hostRecord, hostPublicData;
if (['prod_gf_cn', 'prod_qd_cn'].includes(this.server)) {
host = 'https://api-takumi.mihoyo.com/';
hostRecord = 'https://api-takumi-record.mihoyo.com/';
hostPublicData = 'https://public-data-api.mihoyo.com/';
} else if (/official/.test(this.server)) {
host = 'https://sg-public-api.hoyolab.com/';
hostRecord = 'https://bbs-api-os.hoyolab.com/';
hostPublicData = 'https://sg-public-data-api.hoyoverse.com/';
}
let urlMap = {
zzz: {
...(['prod_gf_cn', 'prod_qd_cn'].includes(this.server)
? {
zzzUser: {
url: `${host}binding/api/getUserGameRolesByCookie`,
query: `game_biz=nap_cn&region=${this.server}&game_uid=${this.uid}`,
},
getFp: {
url: `${hostPublicData}device-fp/api/getFp`,
body: {
seed_id: `${generateSeed(16)}`,
device_id: data.deviceId,
platform: '1',
seed_time: new Date().getTime() + '',
ext_fields: `{"ramCapacity":"3746","hasVpn":"0","proxyStatus":"0","screenBrightness":"0.550","packageName":"com.miHoYo.mhybbs","romRemain":"100513","deviceName":"iPhone","isJailBreak":"0","magnetometer":"-160.495300x-206.488358x58.534348","buildTime":"1706406805675","ramRemain":"97","accelerometer":"-0.419876x-0.748367x-0.508057","cpuCores":"6","cpuType":"CPU_TYPE_ARM64","packageVersion":"2.20.1","gyroscope":"0.133974x-0.051780x-0.062961","batteryStatus":"45","appUpdateTimeDiff":"1707130080397","appMemory":"57","screenSize":"414×896","vendor":"--","model":"iPhone12,5","IDFV":"${data.deviceId.toUpperCase()}","romCapacity":"488153","isPushEnabled":"1","appInstallTimeDiff":"1696756955347","osVersion":"17.2.1","chargeStatus":"1","isSimInserted":"1","networkType":"WIFI"}`,
app_name: 'account_cn',
device_fp: '38d7f0fa36179',
},
noDs: true,
},
}
: {
zzzUser: {
url: `${host}binding/api/getUserGameRolesByCookie`,
query: `game_biz=nap_global&region=${this.server}&game_uid=${this.uid}`,
},
getFp: {
url: `${hostPublicData}device-fp/api/getFp`,
body: {
seed_id: `${this.uuid}`,
device_id: '35315696b7071100',
hoyolab_device_id: `${this.uuid}`,
platform: '2',
seed_time: new Date().getTime() + '',
ext_fields: `{"proxyStatus":1,"isRoot":1,"romCapacity":"512","deviceName":"Xperia 1","productName":"J9110","romRemain":"483","hostname":"BuildHost","screenSize":"1096x2434","isTablet":0,"model":"J9110","brand":"Sony","hardware":"qcom","deviceType":"J9110","devId":"REL","serialNumber":"unknown","sdCapacity":107433,"buildTime":"1633631032000","buildUser":"BuildUser","simState":1,"ramRemain":"98076","appUpdateTimeDiff":1716545162858,"deviceInfo":"Sony\/J9110\/J9110:11\/55.2.A.4.332\/055002A004033203408384484:user\/release-keys","buildType":"user","sdkVersion":"30","ui_mode":"UI_MODE_TYPE_NORMAL","isMockLocation":0,"cpuType":"arm64-v8a","isAirMode":0,"ringMode":2,"app_set_id":"${this.uuid}","chargeStatus":1,"manufacturer":"Sony","emulatorStatus":0,"appMemory":"512","adid":"${this.uuid}","osVersion":"11","vendor":"unknown","accelerometer":"-0.9233304x7.574181x6.472585","sdRemain":97931,"buildTags":"release-keys","packageName":"com.mihoyo.hoyolab","networkType":"WiFi","debugStatus":1,"ramCapacity":"107433","magnetometer":"-9.075001x-27.300001x-3.3000002","display":"55.2.A.4.332","appInstallTimeDiff":1716489549794,"packageVersion":"","gyroscope":"0.027029991x-0.04459185x0.032222193","batteryStatus":45,"hasKeyboard":0,"board":"msmnile"}`,
app_name: 'bbs_oversea',
device_fp: '38d7f2352506c',
},
noDs: true,
},
}),
zzzNote: {
url: `${hostRecord}event/game_record_zzz/api/zzz/note`,
query: `role_id=${this.uid}&server=${this.server}`,
},
},
};
return urlMap[this.game];
};
}

29
lib/path.js Normal file
View file

@ -0,0 +1,29 @@
import path from 'path';
import { fileURLToPath } from 'url';
// 获取当前模块的 URL
const metaUrl = import.meta.url;
// 将 URL 转换为文件路径
const metaPath = fileURLToPath(new URL(metaUrl));
// 插件路径
export const pluginPath = path.join(metaPath, '../../');
// 插件名
export const pluginName = path.basename(pluginPath);
// apps 路径
export const appPath = path.join(pluginPath, 'apps');
// resources
export const resourcesPath = path.join(pluginPath, 'resources');
// config 路径
export const configPath = path.join(pluginPath, 'config');
// 默认配置路径
export const defPath = path.join(pluginPath, 'defSet');
// data 路径
export const dataPath = path.join(pluginPath, 'data');

65
lib/plugin.js Normal file
View file

@ -0,0 +1,65 @@
import MysZZZApi from './mysapi.js';
import { getCk } from './common.js';
import _ from 'lodash';
export class ZZZPlugin extends plugin {
async miYoSummerGetUid() {
const key = `ZZZ:UID:${this.e.user_id}`;
const ck = await getCk(this.e);
if (!ck) return false;
let api = new MysZZZApi('', ck);
let userData = await api.getData('zzzUser');
if (!userData?.data || _.isEmpty(userData.data.list)) return false;
userData = userData.data.list[0];
let { game_uid: gameUid } = userData;
await redis.set(key, gameUid);
await redis.setEx(
`ZZZ:userData:${gameUid}`,
60 * 60,
JSON.stringify(userData)
);
return userData;
}
async getAPI() {
let user = this.e.user_id;
let ats = this.e.message.filter(m => m.type === 'at');
if (ats.length > 0 && !e.atBot) {
user = ats[0].qq;
this.e.user_id = user;
this.User = new User(this.e);
}
let uid = this.e.msg.match(/\d+/)?.[0];
await this.miYoSummerGetUid();
uid =
uid || (await redis.get(`ZZZ:UID:${user}`)) || this.e.user?.getUid('zzz');
if (!uid) {
await this.reply('尚未绑定uid,请发送#zzz绑定uid进行绑定');
return false;
}
const ck = await getCk(this.e);
if (!ck || Object.keys(ck).filter(k => ck[k].ck).length === 0) {
await this.reply('尚未绑定cookie请先使用逍遥插件绑定cookie');
return false;
}
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, {
headers: sdk.headers,
method: 'POST',
body: sdk.body,
});
let fpRes = await res.json();
deviceFp = fpRes?.data?.device_fp;
if (deviceFp) {
await redis.set(`ZZZ:DEVICE_FP:${uid}`, deviceFp, {
EX: 86400 * 7,
});
}
}
return { api, uid, deviceFp };
}
}

56
lib/render.js Normal file
View file

@ -0,0 +1,56 @@
import path from 'path';
import { pluginName, resourcesPath } from './path.js';
import setting from './settings.js';
import version from './version.js';
/**
* 渲染函数
* @param {any} e 消息事件
* @param {string} renderPath 渲染路径
* @param {object} renderData 渲染数据
* @param {object} cfg 配置
* @returns
*/
function render(e, renderPath, renderData = {}, cfg = {}) {
// 判断是否存在e.runtime
if (!e.runtime) {
console.log('未找到e.runtime请升级至最新版Yunzai');
}
// 获取渲染精度配置
const scaleCfg = setting.getConfig('config')?.render?.scale || 100;
// 计算配置缩放比例
const scaleCfgValue = Math.min(2, Math.max(0.5, scaleCfg / 100));
// 将函数参数中的缩放比例与配置缩放比例相乘
const scale = (cfg?.scale || 1) * scaleCfgValue;
// 将缩放比例转换为style属性
const pct = `style='transform:scale(${scale})'`;
// 获取布局路径
const layoutPathFull = path.join(resourcesPath, 'common/layout/');
// 调用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 {
...data,
_res_path: resPath,
_layout_path: layoutPath,
defaultLayout: path.join(layoutPathFull, 'index.html'),
sys: {
scale: pct,
resourcesPath: resPath,
currentPath: renderPathFull,
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;

195
lib/settings.js Normal file
View file

@ -0,0 +1,195 @@
import YAML from 'yaml';
import chokidar from 'chokidar';
import fs from 'fs';
import path from 'path';
import { configPath, dataPath, defPath, pluginName } from './path.js';
class Setting {
constructor() {
/** 默认设置 */
this.defPath = defPath;
this.defSet = {};
/** 用户设置 */
this.configPath = configPath;
this.config = {};
this.dataPath = dataPath;
this.data = {};
/** 监听文件 */
this.watcher = { config: {}, defSet: {} };
this.initCfg();
}
/** 初始化配置 */
initCfg() {
const files = fs
.readdirSync(this.defPath)
.filter(file => file.endsWith('.yaml'));
for (let file of files) {
if (!fs.existsSync(path.join(this.configPath, file))) {
fs.copyFileSync(
path.join(this.defPath, file),
path.join(this.configPath, file)
);
}
this.watch(
path.join(this.configPath, file),
file.replace('.yaml', ''),
'config'
);
}
}
// 配置对象化 用于锅巴插件界面填充
merge() {
let sets = {};
let appsConfig = fs
.readdirSync(this.defPath)
.filter(file => file.endsWith('.yaml'));
for (let appConfig of appsConfig) {
// 依次将每个文本填入键
let filename = appConfig.replace(/.yaml/g, '').trim();
sets[filename] = this.getConfig(filename);
}
return sets;
}
// 配置对象分析 用于锅巴插件界面设置
analysis(config) {
for (let key of Object.keys(config)) {
this.setConfig(key, config[key]);
}
}
// 获取对应模块数据文件
getData(filepath, filename) {
filename = `${filename}.yaml`;
filepath = path.join(this.dataPath, filepath);
try {
if (!fs.existsSync(path.join(filepath, filename))) {
return false;
}
return YAML.parse(fs.readFileSync(path.join(filepath, filename), 'utf8'));
} catch (error) {
logger.error(`[${pluginName}] [${filename}] 读取失败 ${error}`);
return false;
}
}
// 写入对应模块数据文件
setData(filepath, filename, data) {
filename = `${filename}.yaml`;
filepath = path.join(this.dataPath, filepath);
try {
if (!fs.existsSync(filepath)) {
// 递归创建目录
fs.mkdirSync(filepath, { recursive: true });
}
fs.writeFileSync(
path.join(filepath, filename),
YAML.stringify(data),
'utf8'
);
} catch (error) {
logger.error(`[${pluginName}] [${filename}] 写入失败 ${error}`);
return false;
}
}
// 获取对应模块默认配置
getdefSet(app) {
return this.getYaml(app, 'defSet');
}
// 获取对应模块用户配置
getConfig(app) {
return { ...this.getdefSet(app), ...this.getYaml(app, 'config') };
// return this.mergeConfigObjectArray({...this.getdefSet(app)},{...this.getYaml(app, 'config')});
}
//合并两个对象 相同的数组对象 主要用于yml的列表属性合并 并去重 先备份一下方法
mergeConfigObjectArray(obj1, obj2) {
for (const key in obj2) {
if (Array.isArray(obj2[key]) && Array.isArray(obj1[key])) {
//合并两个对象中相同 数组属性
const uniqueElements = new Set([...obj1[key], ...obj2[key]]);
obj1[key] = [...uniqueElements];
} else {
//否则以obj2中的为准
obj1[key] = obj2[key];
}
}
return obj1;
}
// 设置对应模块用户配置
setConfig(app, Object) {
return this.setYaml(app, 'config', { ...this.getdefSet(app), ...Object });
}
// 将对象写入YAML文件
setYaml(app, type, Object) {
let file = this.getFilePath(app, type);
try {
fs.writeFileSync(file, YAML.stringify(Object), 'utf8');
} catch (error) {
logger.error(`[${app}] 写入失败 ${error}`);
return false;
}
}
// 读取YAML文件 返回对象
getYaml(app, type) {
let file = this.getFilePath(app, type);
if (this[type][app]) return this[type][app];
try {
this[type][app] = YAML.parse(fs.readFileSync(file, 'utf8'));
} catch (error) {
logger.error(`[${app}] 格式错误 ${error}`);
return false;
}
this.watch(file, app, type);
return this[type][app];
}
// 获取YAML文件目录
getFilePath(app, type) {
const appFilename = `${app}.yaml`;
if (type === 'defSet') return path.join(this.defPath, appFilename);
else {
try {
if (!fs.existsSync(path.join(this.configPath, appFilename))) {
fs.copyFileSync(
path.join(this.defPath, appFilename),
path.join(this.configPath, appFilename)
);
}
} catch (error) {
logger.error(`[${pluginName}]缺失默认文件[${app}]${error}`);
}
return path.join(this.configPath, `${app}.yaml`);
}
}
// 监听配置文件
watch(file, app, type = 'defSet') {
if (this.watcher[type][app]) return;
const watcher = chokidar.watch(file);
watcher.on('change', path => {
delete this[type][app];
logger.mark(`[${pluginName}][修改配置文件][${type}][${app}]`);
if (this[`change_${app}`]) {
this[`change_${app}`]();
}
});
this.watcher[type][app] = watcher;
}
}
export default new Setting();

101
lib/version.js Normal file
View file

@ -0,0 +1,101 @@
import fs from 'fs';
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;
let packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
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>');
return line;
};
try {
if (fs.existsSync(_logPath)) {
logs = fs.readFileSync(_logPath, 'utf8') || '';
logs = logs.split('\n');
let temp = {};
let lastLine = {};
lodash.forEach(logs, line => {
if (versionCount <= -1) {
return false;
}
let versionRet = /^#\s*([0-9a-zA-Z\\.~\s]+?)\s*$/.exec(line);
if (versionRet && versionRet[1]) {
let v = versionRet[1].trim();
if (!currentVersion) {
currentVersion = v;
} else {
changelogs.push(temp);
if (/0\s*$/.test(v) && versionCount > 0) {
versionCount = 0;
} else {
versionCount--;
}
}
temp = {
version: v,
logs: [],
};
} else {
if (!line.trim()) {
return;
}
if (/^\*/.test(line)) {
lastLine = {
title: getLine(line),
logs: [],
};
temp.logs.push(lastLine);
} else if (/^\s{2,}\*/.test(line)) {
lastLine.logs.push(getLine(line));
}
}
});
}
} catch (e) {
// void error
}
const yunzaiVersion = packageJson.version;
const isV3 = yunzaiVersion[0] === '3';
let isMiao = false;
let name = 'Yunzai-Bot';
if (packageJson.name === 'miao-yunzai') {
isMiao = true;
name = 'Miao-Yunzai';
} else if (packageJson.name === 'trss-yunzai') {
isMiao = true;
name = 'TRSS-Yunzai';
}
const version = {
isV3,
isMiao,
name,
get version() {
return currentVersion;
},
get yunzai() {
return yunzaiVersion;
},
get changelogs() {
return changelogs;
},
};
export default version;

218
model/avatar.js Normal file
View file

@ -0,0 +1,218 @@
/**
* @class
*/
export class Avatar {
/**
* @param {number} id
* @param {number} level
* @param {string} name_mi18n
* @param {string} full_name_mi18n
* @param {number} element_type
* @param {string} camp_name_mi18n
* @param {number} avatar_profession
* @param {string} rarity
* @param {string} group_icon_path
* @param {string} hollow_icon_path
* @param {number} rank
* @param {boolean} is_chosen
*/
constructor(
id,
level,
name_mi18n,
full_name_mi18n,
element_type,
camp_name_mi18n,
avatar_profession,
rarity,
group_icon_path,
hollow_icon_path,
rank,
is_chosen
) {
this.id = id;
this.level = level;
this.name_mi18n = name_mi18n;
this.full_name_mi18n = full_name_mi18n;
this.element_type = element_type;
this.camp_name_mi18n = camp_name_mi18n;
this.avatar_profession = avatar_profession;
this.rarity = rarity;
this.group_icon_path = group_icon_path;
this.hollow_icon_path = hollow_icon_path;
this.rank = rank;
this.is_chosen = is_chosen;
}
}
/**
* @class
*/
export class AvatarIconPaths {
/**
* @param {string} group_icon_path
* @param {string} hollow_icon_path
*/
constructor(group_icon_path, hollow_icon_path) {
this.group_icon_path = group_icon_path;
this.hollow_icon_path = hollow_icon_path;
}
}
/**
* @class
*/
export class ZZZAvatarBasic {
/**
* @param {number} id
* @param {number} level
* @param {string} name_mi18n
* @param {string} full_name_mi18n
* @param {number} element_type
* @param {string} camp_name_mi18n
* @param {number} avatar_profession
* @param {string} rarity
* @param {AvatarIconPaths} icon_paths
* @param {number} rank
* @param {boolean} is_chosen
*/
constructor(
id,
level,
name_mi18n,
full_name_mi18n,
element_type,
camp_name_mi18n,
avatar_profession,
rarity,
icon_paths,
rank,
is_chosen
) {
this.id = id;
this.level = level;
this.name_mi18n = name_mi18n;
this.full_name_mi18n = full_name_mi18n;
this.element_type = element_type;
this.camp_name_mi18n = camp_name_mi18n;
this.avatar_profession = avatar_profession;
this.rarity = rarity;
this.icon_paths = icon_paths;
this.rank = rank;
this.is_chosen = is_chosen;
}
}
/**
* @class
*/
export class Rank {
/**
* @param {number} id
* @param {string} name
* @param {string} desc
* @param {number} pos
* @param {boolean} is_unlocked
*/
constructor(id, name, desc, pos, is_unlocked) {
this.id = id;
this.name = name;
this.desc = desc;
this.pos = pos;
this.is_unlocked = is_unlocked;
}
}
/**
* @class
*/
export class ZZZAvatarInfo {
/**
* @param {number} id
* @param {number} level
* @param {string} name_mi18n
* @param {string} full_name_mi18n
* @param {number} element_type
* @param {string} camp_name_mi18n
* @param {number} avatar_profession
* @param {string} rarity
* @param {string} group_icon_path
* @param {string} hollow_icon_path
* @param {Equip[]} equip
* @param {Weapon} weapon
* @param {Property[]} properties
* @param {Skill[]} skills
* @param {number} rank
* @param {Rank[]} ranks
*/
constructor(
id,
level,
name_mi18n,
full_name_mi18n,
element_type,
camp_name_mi18n,
avatar_profession,
rarity,
group_icon_path,
hollow_icon_path,
equip,
weapon,
properties,
skills,
rank,
ranks
) {
this.id = id;
this.level = level;
this.name_mi18n = name_mi18n;
this.full_name_mi18n = full_name_mi18n;
this.element_type = element_type;
this.camp_name_mi18n = camp_name_mi18n;
this.avatar_profession = avatar_profession;
this.rarity = rarity;
this.group_icon_path = group_icon_path;
this.hollow_icon_path = hollow_icon_path;
this.equip = equip;
this.weapon = weapon;
this.properties = properties;
this.skills = skills;
this.rank = rank;
this.ranks = ranks;
}
}
/**
* @class
*/
export class ZZZUser {
/**
* @param {string} game_biz
* @param {string} region
* @param {string} game_uid
* @param {string} nickname
* @param {number} level
* @param {boolean} is_chosen
* @param {string} region_name
* @param {boolean} is_official
*/
constructor(
game_biz,
region,
game_uid,
nickname,
level,
is_chosen,
region_name,
is_official
) {
this.game_biz = game_biz;
this.region = region;
this.game_uid = game_uid;
this.nickname = nickname;
this.level = level;
this.is_chosen = is_chosen;
this.region_name = region_name;
this.is_official = is_official;
}
}

27
model/bangboo.js Normal file
View file

@ -0,0 +1,27 @@
/**
* @class
*/
export class BangbooWiki {
/**
* @param {string} item_id
* @param {string} wiki_url
*/
constructor(item_id, wiki_url) {
this.item_id = item_id;
this.wiki_url = wiki_url;
}
}
/**
* @class
*/
export class ZZZBangbooResp {
/**
* @param {Item[]} items
* @param {BangbooWiki} bangboo_wiki
*/
constructor(items, bangboo_wiki) {
this.items = items;
this.bangboo_wiki = bangboo_wiki;
}
}

133
model/equip.js Normal file
View file

@ -0,0 +1,133 @@
/**
* @class
*/
export class EquipProperty {
/**
* @param {string} property_name
* @param {number} property_id
* @param {string} base
*/
constructor(property_name, property_id, base) {
this.property_name = property_name;
this.property_id = property_id;
this.base = base;
}
}
/**
* @class
*/
export class EquipMainProperty {
/**
* @param {string} property_name
* @param {number} property_id
* @param {string} base
*/
constructor(property_name, property_id, base) {
this.property_name = property_name;
this.property_id = property_id;
this.base = base;
}
}
/**
* @class
*/
export class EquipSuit {
/**
* @param {number} suit_id
* @param {string} name
* @param {number} own
* @param {string} desc1
* @param {string} desc2
*/
constructor(suit_id, name, own, desc1, desc2) {
this.suit_id = suit_id;
this.name = name;
this.own = own;
this.desc1 = desc1;
this.desc2 = desc2;
}
}
/**
* @class
*/
export class Equip {
/**
* @param {number} id
* @param {number} level
* @param {string} name
* @param {string} icon
* @param {string} rarity
* @param {EquipProperty[]} properties
* @param {EquipMainProperty[]} main_properties
* @param {EquipSuit} equip_suit
* @param {number} equipment_type
*/
constructor(
id,
level,
name,
icon,
rarity,
properties,
main_properties,
equip_suit,
equipment_type
) {
this.id = id;
this.level = level;
this.name = name;
this.icon = icon;
this.rarity = rarity;
this.properties = properties;
this.main_properties = main_properties;
this.equip_suit = equip_suit;
this.equipment_type = equipment_type;
}
}
/**
* @class
*/
export class Weapon {
/**
* @param {number} id
* @param {number} level
* @param {string} name
* @param {number} star
* @param {string} icon
* @param {string} rarity
* @param {EquipProperty[]} properties
* @param {EquipMainProperty[]} main_properties
* @param {string} talent_title
* @param {string} talent_content
* @param {number} profession
*/
constructor(
id,
level,
name,
star,
icon,
rarity,
properties,
main_properties,
talent_title,
talent_content,
profession
) {
this.id = id;
this.level = level;
this.name = name;
this.star = star;
this.icon = icon;
this.rarity = rarity;
this.properties = properties;
this.main_properties = main_properties;
this.talent_title = talent_title;
this.talent_content = talent_content;
this.profession = profession;
}
}

63
model/gacha.js Normal file
View file

@ -0,0 +1,63 @@
/**
* @class
*/
export class SingleGachaLog {
/**
* @param {string} uid
* @param {string} gacha_id
* @param {string} gacha_type
* @param {string} item_id
* @param {string} count
* @param {string} time
* @param {string} name
* @param {string} lang
* @param {string} item_type
* @param {string} rank_type
* @param {string} id
*/
constructor(
uid,
gacha_id,
gacha_type,
item_id,
count,
time,
name,
lang,
item_type,
rank_type,
id
) {
this.uid = uid;
this.gacha_id = gacha_id;
this.gacha_type = gacha_type;
this.item_id = item_id;
this.count = count;
this.time = time;
this.name = name;
this.lang = lang;
this.item_type = item_type;
this.rank_type = rank_type;
this.id = id;
}
}
/**
* @class
*/
export class ZZZGachaLogResp {
/**
* @param {string} page
* @param {string} size
* @param {SingleGachaLog[]} list
* @param {string} region
* @param {number} region_time_zone
*/
constructor(page, size, list, region, region_time_zone) {
this.page = page;
this.size = size;
this.list = list;
this.region = region;
this.region_time_zone = region_time_zone;
}
}

83
model/index.js Normal file
View file

@ -0,0 +1,83 @@
/**
* @class
*/
export class Buddy {
/**
* @param {number} id
* @param {string} name
* @param {string} rarity
* @param {number} level
* @param {number} star
*/
constructor(id, name, rarity, level, star) {
this.id = id;
this.name = name;
this.rarity = rarity;
this.level = level;
this.star = star;
}
}
/**
* @class
*/
export class Stats {
/**
* @param {number} active_days
* @param {number} avatar_num
* @param {string} world_level_name
* @param {number} cur_period_zone_layer_count
* @param {number} buddy_num
*/
constructor(
active_days,
avatar_num,
world_level_name,
cur_period_zone_layer_count,
buddy_num
) {
this.active_days = active_days;
this.avatar_num = avatar_num;
this.world_level_name = world_level_name;
this.cur_period_zone_layer_count = cur_period_zone_layer_count;
this.buddy_num = buddy_num;
}
}
/**
* @class
*/
export class ZZZIndexResp {
/**
* @param {Stats} stats
* @param {Avatar[]} avatar_list
* @param {string} cur_head_icon_url
* @param {Buddy[]} buddy_list
*/
constructor(stats, avatar_list, cur_head_icon_url, buddy_list) {
this.stats = stats;
this.avatar_list = avatar_list;
this.cur_head_icon_url = cur_head_icon_url;
this.buddy_list = buddy_list;
}
}
/**
* @class
*/
export class Item {
/**
* @param {number} id
* @param {string} name
* @param {string} rarity
* @param {number} level
* @param {number} star
*/
constructor(id, name, rarity, level, star) {
this.id = id;
this.name = name;
this.rarity = rarity;
this.level = level;
this.star = star;
}
}

103
model/note.js Normal file
View file

@ -0,0 +1,103 @@
import { converSecondsToHM } from '../utils/time.js';
/**
* @class
*/
export class Vitality {
/**
* @param {number} max
* @param {number} current
*/
constructor(max, current) {
this.max = max;
this.current = current;
}
get finish() {
return this.max === this.current;
}
}
/**
* @class
*/
export class VhsSale {
/**
* @param {string} sale_state
*/
constructor(sale_state) {
this.sale_state = sale_state;
}
get state() {
if (this.sale_state.includes('Doing')) {
return true;
}
return false;
}
get state_label() {
if (this.sale_state.includes('Doing')) {
return '正在营业';
}
return '尚未营业';
}
}
/**
* @class
*/
export class EnergyProgress {
/**
* @param {number} max
* @param {number} current
*/
constructor(max, current) {
this.max = max;
this.current = current;
}
}
/**
* @class
*/
export class Energy {
/**
* @param {EnergyProgress} progress
* @param {number} restore
*/
constructor(progress, restore) {
this.progress = progress;
this.restore = restore;
const leftHM = converSecondsToHM(restore);
this.progress.rest = `${leftHM[0]}小时${leftHM[1]}分钟`;
this.percent = parseInt((progress.current / progress.max) * 100);
}
}
/**
* @class
*/
export class ZZZNoteResp {
/**
* @param {{ energy: Energy, vitality:Vitality, vhs_sale: VhsSale, card_sign: string }} data
*/
constructor(data) {
const { energy, vitality, vhs_sale, card_sign } = data;
this.energy = new Energy(energy.progress, energy.restore);
this.vitality = new Vitality(vitality.max, vitality.current);
this.vhs_sale = new VhsSale(vhs_sale.sale_state);
this.card_sign = card_sign;
}
get sign() {
if (this.card_sign?.includes('Done')) {
return true;
}
return false;
}
get sign_label() {
if (this.card_sign?.includes('Done')) {
return '已抽奖';
}
return '未抽奖';
}
}

19
model/property.js Normal file
View file

@ -0,0 +1,19 @@
/**
* @class
*/
class Property {
/**
* @param {string} property_name
* @param {number} property_id
* @param {string} base
* @param {string} add
* @param {string} final
*/
constructor(property_name, property_id, base, add, final) {
this.property_name = property_name;
this.property_id = property_id;
this.base = base;
this.add = add;
this.final = final;
}
}

29
model/skill.js Normal file
View file

@ -0,0 +1,29 @@
/**
* @class
*/
export class SkillItem {
/**
* @param {string} title
* @param {string} text
*/
constructor(title, text) {
this.title = title;
this.text = text;
}
}
/**
* @class
*/
export class Skill {
/**
* @param {number} level
* @param {number} skill_type
* @param {SkillItem[]} items
*/
constructor(level, skill_type, items) {
this.level = level;
this.skill_type = skill_type;
this.items = items;
}
}

10
package.json Normal file
View file

@ -0,0 +1,10 @@
{
"name": "zzz-plugin",
"type": "module",
"version": "0.0.1",
"description": "",
"main": "index.js",
"keywords": [],
"author": "ZZZure",
"license": "ISC"
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

View file

@ -0,0 +1,30 @@
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
body {
font-family: "zzz", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
transform-origin: 0 0;
font-size: 24px;
width: 840px;
}
.container {
background: url("../images/bg.jpg") no-repeat center center;
background-size: cover;
color: white;
}
.copyright {
text-align: center;
font-size: 0.7em;
color: #797979;
padding-bottom: 2em;
}
.copyright .highlight {
color: #fff;
}
/*# sourceMappingURL=index.css.map */

View file

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ZZZ-Plugin</title>
<link rel="stylesheet" type="text/css" href="{{@sys.resourcesPath}}/common/style/index.css">
<link rel="stylesheet" type="text/css" href="{{@sys.resourcesPath}}/common/layout/index.css" />
{{block 'css'}}
{{/block}}
</head>
<body {{@sys.scale}}>
<div class="container" id="container">
{{block 'main'}}{{/block}}
<div class="copyright">{{@sys.createdby}}</div>
</div>
</body>
</html>

View file

@ -0,0 +1,31 @@
@charset "UTF-8";
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
body {
font-family: 'zzz', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI',
Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
transform-origin: 0 0;
font-size: 24px;
width: 840px;
}
.container {
background: url('../images/bg.jpg') no-repeat center center;
background-size: cover;
color: white;
}
.copyright {
text-align: center;
font-size: 0.7em;
color: #797979;
padding-bottom: 2em;
.highlight {
color: #fff;
}
}

View file

@ -0,0 +1,21 @@
@font-face {
font-family: "zzz";
src: url("../fonts/inpinhongmengti.ttf");
}
@font-face {
font-family: "Source Han Serif";
src: url("../fonts/SourceHanSerifCN-Heavy.otf");
}
.zzz-font {
font-family: "zzz", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
}
.no-zzz-font {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
}
.zzz-serif {
font-family: "Source Han Serif", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
}
/*# sourceMappingURL=index.css.map */

View file

@ -0,0 +1,41 @@
@charset "UTF-8";
@font-face {
font-family: 'zzz';
// src: url('../fonts/RoGSanSrfStd-Bd.otf');
src: url('../fonts/inpinhongmengti.ttf');
// font-weight: 400;
}
// @font-face {
// font-family: 'zzz';
// src: url('../fonts/REEJI-CloudChaoCuHeiGBT.ttf');
// font-weight: 900;
// }
@font-face {
font-family: 'Source Han Serif';
src: url('../fonts/SourceHanSerifCN-Heavy.otf');
}
.zzz-font {
font-family: 'zzz', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI',
Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
.no-zzz-font {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
// .zzz-title {
// font-family: 'zzz', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI',
// Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
// font-weight: 900;
// }
.zzz-serif {
font-family: 'Source Han Serif', system-ui, -apple-system, BlinkMacSystemFont,
'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue',
sans-serif;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 546 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

253
resources/note/index.css Normal file
View file

@ -0,0 +1,253 @@
.container {
padding-top: 3em;
}
.card {
border-image: url("./images/BgFrame.png");
border-image-slice: 325 0 162.5 0 fill;
border-image-width: 10em 0em 5em 0em;
border-image-repeat: round stretch;
min-height: 30em;
overflow: hidden;
padding: 5em 2.2em 2.2em 2.2em;
}
.card .user-info {
display: flex;
align-items: center;
margin-bottom: 1em;
margin-top: 1.7em;
gap: 1em;
padding: 0 1em;
}
.card .user-info .avatar {
width: 4em;
height: 4em;
border-radius: 50%;
border-image: url("./images/SuitBg.png");
border-image-width: 0;
border-image-outset: 0.4em;
border-image-repeat: round;
border-image-slice: 1 fill;
overflow: hidden;
}
.card .user-info .avatar img {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
.card .user-info .info-bar {
width: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
flex: 1;
gap: 0.2em;
}
.card .user-info .info-bar .info {
width: 100%;
display: flex;
overflow: hidden;
gap: 0.5em;
padding-left: 0.5em;
}
.card .user-info .info-bar .info .nickname {
font-size: 1.2em;
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card .user-info .info-bar .info .label {
-webkit-clip-path: polygon(0.2em 0%, calc(100% - 0.2em) 0%, 100% 0.2em, 100% calc(100% - 0.2em), calc(100% - 0.2em) 100%, 0.2em 100%, 0% calc(100% - 0.2em), 0% 0.2em);
clip-path: polygon(0.2em 0%, calc(100% - 0.2em) 0%, 100% 0.2em, 100% calc(100% - 0.2em), calc(100% - 0.2em) 100%, 0.2em 100%, 0% calc(100% - 0.2em), 0% 0.2em);
padding: 0 0.4em;
font-size: 0.9em;
display: flex;
justify-content: center;
align-items: center;
color: rgb(43, 38, 40);
margin: 0.1em 0;
}
.card .user-info .info-bar .info .label.level {
background-color: rgb(243, 203, 69);
}
.card .user-info .info-bar .info .label.server {
background-color: rgb(81, 177, 253);
}
.card .user-info .info-bar .uid {
border-image: url("./images/UIDBg.png");
border-image-slice: 0 100 0 100 fill;
border-image-width: 0em 1.6em 0em 1.6em;
border-image-outset: 0;
border-image-repeat: stretch stretch;
padding: 0.3em 1.6em;
font-size: 0.9em;
color: rgba(255, 255, 255, 0.6);
}
.card > .title {
width: 100%;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
display: flex;
align-items: stretch;
padding: 0 0.4em;
position: relative;
margin: 0.5em 0;
}
.card > .title .parallelograms {
display: flex;
flex-wrap: nowrap;
align-items: flex-end;
position: inherit;
padding-bottom: 0.4em;
z-index: 1;
}
.card > .title .parallelograms span {
width: 1.3em;
height: 80%;
background-color: rgb(246, 202, 69);
margin-right: -0.2em;
-webkit-clip-path: polygon(0.7em 0%, 100% 0%, calc(100% - 0.7em) 100%, 0% 100%);
clip-path: polygon(0.7em 0%, 100% 0%, calc(100% - 0.7em) 100%, 0% 100%);
}
.card > .title .bg-content {
position: absolute;
bottom: -0.1em;
left: 50%;
transform: translate(-50%, 0) skewX(-8deg);
color: rgba(255, 255, 255, 0.1);
font-size: 2.7em;
}
.card > .title .content {
flex-grow: 1;
flex-shrink: 1;
font-size: 1.5em;
text-align: center;
position: inherit;
z-index: 1;
}
.card .battery {
display: flex;
align-items: center;
gap: 1.5em;
padding: 0.5em;
border: solid 0.2em rgb(174, 174, 174);
border-radius: 0.7em;
color: rgb(167, 167, 167);
background: url("./images/batteryBg.png") no-repeat center center;
background-size: cover;
margin: 1.5em 0;
}
.card .battery .icon {
width: 7em;
height: 7em;
border-radius: 50%;
border-image: url("./images/PetSelectBG.png");
border-image-width: 0;
border-image-outset: 0em;
border-image-repeat: round;
border-image-slice: 1 fill;
overflow: hidden;
padding: 1.4em;
flex-grow: 0;
flex-shrink: 0;
}
.card .battery .icon img {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
.card .battery .info {
flex-grow: 1;
flex-shrink: 1;
margin-right: 1em;
}
.card .battery .info .bvalue {
display: flex;
align-items: flex-end;
}
.card .battery .info .bvalue .title {
font-size: 1.2em;
}
.card .battery .info .bvalue .value .big {
font-size: 1.5em;
color: rgb(253, 209, 73);
margin: 0 0.7em;
}
.card .battery .info .bleft {
display: flex;
gap: 0.2em;
font-size: 0.8em;
}
.card .battery .info .bleft .value {
color: white;
}
.card .battery .info .texture {
width: 100%;
border-image: url("./images/ActivityGeneralBtnBg02.png");
border-image-slice: 30 40 30 24 fill;
border-image-width: 1em 1.5em 1em 0.9em;
border-image-repeat: stretch stretch;
padding: 1.1em 1.23em 1.1em 0.6em;
margin-top: 0.7em;
}
.card .battery .info .texture .progress {
height: 0.5em;
border-radius: 0.3em;
background-color: rgb(244, 198, 68);
}
.card .active-list {
font-size: 1.2em;
padding: 0 1em;
margin-top: -0.5em;
}
.card .active-list .active {
display: flex;
border-bottom: solid 0.07em rgba(255, 255, 255, 0.3);
align-items: center;
color: rgb(216, 216, 216);
gap: 0.5em;
padding-right: 1em;
padding-top: 1em;
padding-bottom: 0.2em;
}
.card .active-list .active .status {
width: 1.8em;
height: 1.8em;
border-radius: 50%;
background-color: rgb(34, 39, 37);
flex-grow: 0;
flex-shrink: 0;
background: url("./images/no.png") no-repeat center center;
background-size: contain;
}
.card .active-list .active .status.finish {
background: url("./images/yes.png") no-repeat center center;
background-size: contain;
}
.card .active-list .active .title {
flex-grow: 1;
padding: 0;
font-size: 1.1em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card .active-list .active .value {
display: flex;
align-items: flex-end;
font-size: 1.2em;
color: rgb(253, 209, 73);
flex-grow: 0;
flex-shrink: 0;
}
.card .active-list .active .value .sub {
font-size: 0.8em;
color: rgb(255, 255, 255);
margin-left: 0.2em;
}
/*# sourceMappingURL=index.css.map */

98
resources/note/index.html Normal file
View file

@ -0,0 +1,98 @@
{{extend defaultLayout}}
{{block 'css'}}
<link rel="stylesheet" href="{{@sys.currentPath}}/index.css">
{{/block}}
{{block 'main'}}
<div class="card">
<div class="user-info">
<div class="avatar">
<img src="{{avatar}}" alt="Avatar">
</div>
<div class="info-bar">
<div class="info">
<div class="nickname">{{player.nickname}}</div>
<div class="label level">Lv{{player.level}}</div>
<div class="label server">{{player.region_name}}</div>
</div>
<div class="uid">UID {{player.game_uid}}</div>
</div>
</div>
<div class="title">
<div class="bg-content">
STAMINA
</div>
<div class="parallelograms">
<span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span>
</div>
<div class="content">
电池电量
</div>
<div class="parallelograms">
<span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span>
</div>
</div>
<div class="battery">
<div class="icon">
<img src="{{@sys.currentPath}}/images/IconStamina.png" alt="">
</div>
<div class="info">
<div class="bvalue">
<div class="title">电量</div>
<div class="value"><span class="big">{{note.energy.progress.current}}</span>/{{note.energy.progress.max}}</div>
</div>
<div class="bleft">
<div class="title">剩余</div>
<div class="value">{{note.energy.progress.rest}}</div>
</div>
<div class="texture">
<div class="bar">
<div class="progress" style="width: {{note.energy.progress.percent}}%;"></div>
</div>
</div>
</div>
</div>
<div class="title">
<div class="bg-content">
ACTIVE
</div>
<div class="parallelograms">
<span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span>
</div>
<div class="content">
每日情况
</div>
<div class="parallelograms">
<span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span>
</div>
</div>
<div class="active-list">
<div class="active">
<div class="status {{note.vitality.finish && 'finish'}}"></div>
<div class="title">今日活跃度</div>
<div class="value">
{{note.vitality.current}}
<span class="sub">/{{note.vitality.max}}</span>
</div>
</div>
<div class="active">
<div class="status {{note.vhs_sale.state && 'finish'}}"></div>
<div class="title">录像店经营</div>
<div class="value">
{{note.vhs_sale.state_label}}
</div>
</div>
<div class="active">
<div class="status {{note.sign && 'finish'}}"></div>
<div class="title">刮刮卡</div>
<div class="value">
{{note.sign_label}}
</div>
</div>
</div>
</div>
{{/block}}

284
resources/note/index.scss Normal file
View file

@ -0,0 +1,284 @@
.container {
padding-top: 3em;
}
.card {
border-image: url('./images/BgFrame.png');
border-image-slice: 325 0 162.5 0 fill;
border-image-width: 10em 0em 5em 0em;
border-image-repeat: round stretch;
min-height: 30em;
overflow: hidden;
padding: 5em 2.2em 2.2em 2.2em;
.user-info {
display: flex;
align-items: center;
margin-bottom: 1em;
margin-top: 1.7em;
gap: 1em;
padding: 0 1em;
.avatar {
width: 4em;
height: 4em;
border-radius: 50%;
border-image: url('./images/SuitBg.png');
border-image-width: 0;
border-image-outset: 0.4em;
border-image-repeat: round;
border-image-slice: 1 fill;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
}
.info-bar {
width: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
flex: 1;
gap: 0.2em;
.info {
width: 100%;
display: flex;
overflow: hidden;
gap: 0.5em;
padding-left: 0.5em;
.nickname {
font-size: 1.2em;
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.label {
$label-width: 0.2em;
-webkit-clip-path: polygon(
$label-width 0%,
calc(100% - $label-width) 0%,
100% $label-width,
100% calc(100% - $label-width),
calc(100% - $label-width) 100%,
$label-width 100%,
0% calc(100% - $label-width),
0% $label-width
);
clip-path: polygon(
$label-width 0%,
calc(100% - $label-width) 0%,
100% $label-width,
100% calc(100% - $label-width),
calc(100% - $label-width) 100%,
$label-width 100%,
0% calc(100% - $label-width),
0% $label-width
);
padding: 0 0.4em;
font-size: 0.9em;
display: flex;
justify-content: center;
align-items: center;
color: rgb(43, 38, 40);
margin: 0.1em 0;
&.level {
background-color: rgb(243, 203, 69);
}
&.server {
background-color: rgb(81, 177, 253);
}
}
}
.uid {
border-image: url('./images/UIDBg.png');
border-image-slice: 0 100 0 100 fill;
border-image-width: 0em 1.6em 0em 1.6em;
border-image-outset: 0;
border-image-repeat: stretch stretch;
padding: 0.3em 1.6em;
font-size: 0.9em;
color: rgba(255, 255, 255, 0.6);
}
}
}
> .title {
width: 100%;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
display: flex;
align-items: stretch;
padding: 0 0.4em;
position: relative;
margin: 0.5em 0;
.parallelograms {
$pwidth: 0.7em;
display: flex;
flex-wrap: nowrap;
align-items: flex-end;
position: inherit;
padding-bottom: 0.4em;
z-index: 1;
span {
width: 1.3em;
height: 80%;
background-color: rgb(246, 202, 69);
margin-right: -0.2em;
-webkit-clip-path: polygon(
$pwidth 0%,
100% 0%,
calc(100% - $pwidth) 100%,
0% 100%
);
clip-path: polygon(
$pwidth 0%,
100% 0%,
calc(100% - $pwidth) 100%,
0% 100%
);
}
}
.bg-content {
position: absolute;
bottom: -0.1em;
left: 50%;
transform: translate(-50%, 0) skewX(-8deg);
color: rgba(255, 255, 255, 0.1);
font-size: 2.7em;
}
.content {
flex-grow: 1;
flex-shrink: 1;
font-size: 1.5em;
text-align: center;
position: inherit;
z-index: 1;
}
}
.battery {
display: flex;
align-items: center;
gap: 1.5em;
padding: 0.5em;
border: solid 0.2em rgb(174, 174, 174);
border-radius: 0.7em;
color: rgb(167, 167, 167);
background: url('./images/batteryBg.png') no-repeat center center;
background-size: cover;
margin: 1.5em 0;
.icon {
width: 7em;
height: 7em;
border-radius: 50%;
border-image: url('./images/PetSelectBG.png');
border-image-width: 0;
border-image-outset: 0em;
border-image-repeat: round;
border-image-slice: 1 fill;
overflow: hidden;
padding: 1.4em;
flex-grow: 0;
flex-shrink: 0;
img {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
}
.info {
flex-grow: 1;
flex-shrink: 1;
margin-right: 1em;
.bvalue {
display: flex;
align-items: flex-end;
.title {
font-size: 1.2em;
}
.value {
.big {
font-size: 1.5em;
color: rgb(253, 209, 73);
margin: 0 0.7em;
}
}
}
.bleft {
display: flex;
gap: 0.2em;
font-size: 0.8em;
.value {
color: white;
}
}
.texture {
width: 100%;
border-image: url('./images/ActivityGeneralBtnBg02.png');
border-image-slice: 30 40 30 24 fill;
border-image-width: 1em 1.5em 1em 0.9em;
border-image-repeat: stretch stretch;
padding: 1.1em 1.23em 1.1em 0.6em;
margin-top: 0.7em;
.progress {
height: 0.5em;
border-radius: 0.3em;
background-color: rgb(244, 198, 68);
}
}
}
}
.active-list {
font-size: 1.2em;
padding: 0 1em;
margin-top: -0.5em;
.active {
display: flex;
border-bottom: solid 0.07em rgba(255, 255, 255, 0.3);
align-items: center;
color: rgb(216, 216, 216);
gap: 0.5em;
padding-right: 1em;
padding-top: 1em;
padding-bottom: 0.2em;
.status {
width: 1.8em;
height: 1.8em;
border-radius: 50%;
background-color: rgb(34, 39, 37);
flex-grow: 0;
flex-shrink: 0;
background: url('./images/no.png') no-repeat center center;
background-size: contain;
&.finish {
background: url('./images/yes.png') no-repeat center center;
background-size: contain;
}
}
.title {
flex-grow: 1;
padding: 0;
font-size: 1.1em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.value {
display: flex;
align-items: flex-end;
font-size: 1.2em;
color: rgb(253, 209, 73);
flex-grow: 0;
flex-shrink: 0;
.sub {
font-size: 0.8em;
color: rgb(255, 255, 255);
margin-left: 0.2em;
}
}
}
}
}

6
utils/time.js Normal file
View file

@ -0,0 +1,6 @@
export const converSecondsToHM = seconds => {
const d = new Date(seconds * 1000);
const hh = d.getUTCHours();
const mm = d.getUTCMinutes();
return [hh, mm];
};