commit 1c65f10e24b44b440213460ceb1f7622161b653c
Author: bietiaop <1527109126@qq.com>
Date: Mon Jul 8 13:13:22 2024 +0800
initial upload
diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000..0e98056
Binary files /dev/null and b/.DS_Store differ
diff --git a/.eslintrc.cjs b/.eslintrc.cjs
new file mode 100644
index 0000000..7ba2b10
--- /dev/null
+++ b/.eslintrc.cjs
@@ -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',
+ },
+};
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..66cfa01
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+*.css.map
+config/**/*.*
+!config/.gitkeep
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..4d3ac9c
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# 0.0.1
+
+* 初始化项目
+* 支持 UID 绑定
+* 支持 note 查看
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..fe1f07a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,29 @@
+# ZZZ-Plugin
+
+
+
+
+ZZZ- Plugin
+🚧Yunzai绝区零Bot插件🚧
+
+# 安装
+
+进入Yunzai根目录运行以下命令进行安装:
+
+```bash
+git clone --depth=1 https://github.com/ZZZure/ZZZ-Plugin.git ./plugins/ZZZ-Plugin
+```
+
+安装后重启Yunzai即可使用。
+
+# 功能
+
+待更新
+
+# 贡献
+
+请先 `fork` 本仓库,修改并测试完成后提交PR。
+
+**请注意:**
+
+* 你的 CSS 必须是 `scss` 编写
diff --git a/apps/bind.js b/apps/bind.js
new file mode 100644
index 0000000..75eebed
--- /dev/null
+++ b/apps/bind.js
@@ -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;
+ }
+}
diff --git a/apps/note.js b/apps/note.js
new file mode 100644
index 0000000..5b84fb6
--- /dev/null
+++ b/apps/note.js
@@ -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);
+ }
+}
diff --git a/config/.gitkeep b/config/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/defSet/config.yaml b/defSet/config.yaml
new file mode 100644
index 0000000..9cbe634
--- /dev/null
+++ b/defSet/config.yaml
@@ -0,0 +1,2 @@
+render:
+ scale: 100
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..61ac541
--- /dev/null
+++ b/index.js
@@ -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 };
diff --git a/lib/authkey.js b/lib/authkey.js
new file mode 100644
index 0000000..277f745
--- /dev/null
+++ b/lib/authkey.js
@@ -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}
+ */
+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;
+}
diff --git a/lib/common.js b/lib/common.js
new file mode 100644
index 0000000..8613eae
--- /dev/null
+++ b/lib/common.js
@@ -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;
+}
diff --git a/lib/mysapi.js b/lib/mysapi.js
new file mode 100644
index 0000000..4895650
--- /dev/null
+++ b/lib/mysapi.js
@@ -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;
+}
diff --git a/lib/mysapi/api.js b/lib/mysapi/api.js
new file mode 100644
index 0000000..840b748
--- /dev/null
+++ b/lib/mysapi/api.js
@@ -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`;
diff --git a/lib/mysapi/tool.js b/lib/mysapi/tool.js
new file mode 100644
index 0000000..fbd57da
--- /dev/null
+++ b/lib/mysapi/tool.js
@@ -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®ion=${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®ion=${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];
+ };
+}
diff --git a/lib/path.js b/lib/path.js
new file mode 100644
index 0000000..53eff54
--- /dev/null
+++ b/lib/path.js
@@ -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');
diff --git a/lib/plugin.js b/lib/plugin.js
new file mode 100644
index 0000000..66699e9
--- /dev/null
+++ b/lib/plugin.js
@@ -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 };
+ }
+}
diff --git a/lib/render.js b/lib/render.js
new file mode 100644
index 0000000..354eed8
--- /dev/null
+++ b/lib/render.js
@@ -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}${version.yunzai} & ${pluginName}${version.version}`,
+ createdby: `Created By ${pluginName} & Powered By ZZZure`,
+ },
+ quality: 100,
+ };
+ },
+ });
+}
+
+export default render;
diff --git a/lib/settings.js b/lib/settings.js
new file mode 100644
index 0000000..e45143d
--- /dev/null
+++ b/lib/settings.js
@@ -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();
diff --git a/lib/version.js b/lib/version.js
new file mode 100644
index 0000000..dd53ed0
--- /dev/null
+++ b/lib/version.js
@@ -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, '$1');
+ line = line.replace(/`\s*/g, '');
+ line = line.replace(/\s*\*\*([^\*]+\*\*)/g, '$1');
+ line = line.replace(/\*\*\s*/g, '');
+ line = line.replace(/ⁿᵉʷ/g, '');
+ 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;
diff --git a/model/avatar.js b/model/avatar.js
new file mode 100644
index 0000000..1c937fc
--- /dev/null
+++ b/model/avatar.js
@@ -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;
+ }
+}
diff --git a/model/bangboo.js b/model/bangboo.js
new file mode 100644
index 0000000..579ece9
--- /dev/null
+++ b/model/bangboo.js
@@ -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;
+ }
+}
diff --git a/model/equip.js b/model/equip.js
new file mode 100644
index 0000000..8597344
--- /dev/null
+++ b/model/equip.js
@@ -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;
+ }
+}
diff --git a/model/gacha.js b/model/gacha.js
new file mode 100644
index 0000000..5523112
--- /dev/null
+++ b/model/gacha.js
@@ -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;
+ }
+}
diff --git a/model/index.js b/model/index.js
new file mode 100644
index 0000000..9558202
--- /dev/null
+++ b/model/index.js
@@ -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;
+ }
+}
diff --git a/model/note.js b/model/note.js
new file mode 100644
index 0000000..8e27ebc
--- /dev/null
+++ b/model/note.js
@@ -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 '未抽奖';
+ }
+}
diff --git a/model/property.js b/model/property.js
new file mode 100644
index 0000000..4bf41bb
--- /dev/null
+++ b/model/property.js
@@ -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;
+ }
+}
diff --git a/model/skill.js b/model/skill.js
new file mode 100644
index 0000000..53fff4a
--- /dev/null
+++ b/model/skill.js
@@ -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;
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..1f6b2ca
--- /dev/null
+++ b/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "zzz-plugin",
+ "type": "module",
+ "version": "0.0.1",
+ "description": "",
+ "main": "index.js",
+ "keywords": [],
+ "author": "ZZZure",
+ "license": "ISC"
+}
diff --git a/resources/common/fonts/ExQteFeverNum.ttf b/resources/common/fonts/ExQteFeverNum.ttf
new file mode 100644
index 0000000..0f15585
Binary files /dev/null and b/resources/common/fonts/ExQteFeverNum.ttf differ
diff --git a/resources/common/fonts/FC_Sound.ttf b/resources/common/fonts/FC_Sound.ttf
new file mode 100644
index 0000000..8f9e14e
Binary files /dev/null and b/resources/common/fonts/FC_Sound.ttf differ
diff --git a/resources/common/fonts/Mont_FF-Heavy.otf b/resources/common/fonts/Mont_FF-Heavy.otf
new file mode 100644
index 0000000..c776874
Binary files /dev/null and b/resources/common/fonts/Mont_FF-Heavy.otf differ
diff --git a/resources/common/fonts/REEJI-CloudChaoCuHeiGBT.ttf b/resources/common/fonts/REEJI-CloudChaoCuHeiGBT.ttf
new file mode 100644
index 0000000..0050bcf
Binary files /dev/null and b/resources/common/fonts/REEJI-CloudChaoCuHeiGBT.ttf differ
diff --git a/resources/common/fonts/RoGSanSrfStd-Bd.otf b/resources/common/fonts/RoGSanSrfStd-Bd.otf
new file mode 100644
index 0000000..e8ed429
Binary files /dev/null and b/resources/common/fonts/RoGSanSrfStd-Bd.otf differ
diff --git a/resources/common/fonts/SCDream9.otf b/resources/common/fonts/SCDream9.otf
new file mode 100644
index 0000000..44f0e8b
Binary files /dev/null and b/resources/common/fonts/SCDream9.otf differ
diff --git a/resources/common/fonts/SourceHanSerifCN-Heavy.otf b/resources/common/fonts/SourceHanSerifCN-Heavy.otf
new file mode 100644
index 0000000..014238c
Binary files /dev/null and b/resources/common/fonts/SourceHanSerifCN-Heavy.otf differ
diff --git a/resources/common/fonts/TiltWarp-Regular.ttf b/resources/common/fonts/TiltWarp-Regular.ttf
new file mode 100644
index 0000000..8ed2730
Binary files /dev/null and b/resources/common/fonts/TiltWarp-Regular.ttf differ
diff --git a/resources/common/fonts/impact.ttf b/resources/common/fonts/impact.ttf
new file mode 100644
index 0000000..7b7956f
Binary files /dev/null and b/resources/common/fonts/impact.ttf differ
diff --git a/resources/common/fonts/inpinhongmengti.ttf b/resources/common/fonts/inpinhongmengti.ttf
new file mode 100644
index 0000000..5115d0d
Binary files /dev/null and b/resources/common/fonts/inpinhongmengti.ttf differ
diff --git a/resources/common/images/bg.jpg b/resources/common/images/bg.jpg
new file mode 100644
index 0000000..d1129f8
Binary files /dev/null and b/resources/common/images/bg.jpg differ
diff --git a/resources/common/layout/index.css b/resources/common/layout/index.css
new file mode 100644
index 0000000..2dcefb0
--- /dev/null
+++ b/resources/common/layout/index.css
@@ -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 */
diff --git a/resources/common/layout/index.html b/resources/common/layout/index.html
new file mode 100644
index 0000000..eb3016a
--- /dev/null
+++ b/resources/common/layout/index.html
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+ ZZZ-Plugin
+
+
+ {{block 'css'}}
+ {{/block}}
+
+
+
+
+ {{block 'main'}}{{/block}}
+
{{@sys.createdby}}
+
+
+
+
diff --git a/resources/common/layout/index.scss b/resources/common/layout/index.scss
new file mode 100644
index 0000000..7d0830f
--- /dev/null
+++ b/resources/common/layout/index.scss
@@ -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;
+ }
+}
diff --git a/resources/common/style/index.css b/resources/common/style/index.css
new file mode 100644
index 0000000..28eb92e
--- /dev/null
+++ b/resources/common/style/index.css
@@ -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 */
diff --git a/resources/common/style/index.scss b/resources/common/style/index.scss
new file mode 100644
index 0000000..33d3479
--- /dev/null
+++ b/resources/common/style/index.scss
@@ -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;
+}
diff --git a/resources/note/images/ActivityGeneralBtnBg02.png b/resources/note/images/ActivityGeneralBtnBg02.png
new file mode 100644
index 0000000..0771e74
Binary files /dev/null and b/resources/note/images/ActivityGeneralBtnBg02.png differ
diff --git a/resources/note/images/BgFrame.png b/resources/note/images/BgFrame.png
new file mode 100644
index 0000000..0bdeb49
Binary files /dev/null and b/resources/note/images/BgFrame.png differ
diff --git a/resources/note/images/IconStamina.png b/resources/note/images/IconStamina.png
new file mode 100644
index 0000000..fff8259
Binary files /dev/null and b/resources/note/images/IconStamina.png differ
diff --git a/resources/note/images/PetSelectBG.png b/resources/note/images/PetSelectBG.png
new file mode 100644
index 0000000..e0d665a
Binary files /dev/null and b/resources/note/images/PetSelectBG.png differ
diff --git a/resources/note/images/SuitBg.png b/resources/note/images/SuitBg.png
new file mode 100644
index 0000000..d178d26
Binary files /dev/null and b/resources/note/images/SuitBg.png differ
diff --git a/resources/note/images/UIDBg.png b/resources/note/images/UIDBg.png
new file mode 100644
index 0000000..658b404
Binary files /dev/null and b/resources/note/images/UIDBg.png differ
diff --git a/resources/note/images/batteryBg.png b/resources/note/images/batteryBg.png
new file mode 100644
index 0000000..0021657
Binary files /dev/null and b/resources/note/images/batteryBg.png differ
diff --git a/resources/note/images/no.png b/resources/note/images/no.png
new file mode 100644
index 0000000..9fc659d
Binary files /dev/null and b/resources/note/images/no.png differ
diff --git a/resources/note/images/yes.png b/resources/note/images/yes.png
new file mode 100644
index 0000000..ed43900
Binary files /dev/null and b/resources/note/images/yes.png differ
diff --git a/resources/note/index.css b/resources/note/index.css
new file mode 100644
index 0000000..a8adda6
--- /dev/null
+++ b/resources/note/index.css
@@ -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 */
diff --git a/resources/note/index.html b/resources/note/index.html
new file mode 100644
index 0000000..0828758
--- /dev/null
+++ b/resources/note/index.html
@@ -0,0 +1,98 @@
+{{extend defaultLayout}}
+
+{{block 'css'}}
+
+{{/block}}
+
+{{block 'main'}}
+
+
+
+

+
+
+
+
{{player.nickname}}
+
Lv{{player.level}}
+
{{player.region_name}}
+
+
UID {{player.game_uid}}
+
+
+
+
+ STAMINA
+
+
+
+
+
+ 电池电量
+
+
+
+
+
+
+
+
+

+
+
+
+
电量
+
{{note.energy.progress.current}}/{{note.energy.progress.max}}
+
+
+
剩余
+
{{note.energy.progress.rest}}
+
+
+
+
+
+
+
+ ACTIVE
+
+
+
+
+
+ 每日情况
+
+
+
+
+
+
+
+
+
+
今日活跃度
+
+ {{note.vitality.current}}
+ /{{note.vitality.max}}
+
+
+
+
+
录像店经营
+
+ {{note.vhs_sale.state_label}}
+
+
+
+
+
刮刮卡
+
+ {{note.sign_label}}
+
+
+
+
+{{/block}}
diff --git a/resources/note/index.scss b/resources/note/index.scss
new file mode 100644
index 0000000..36f9150
--- /dev/null
+++ b/resources/note/index.scss
@@ -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;
+ }
+ }
+ }
+ }
+}
diff --git a/utils/time.js b/utils/time.js
new file mode 100644
index 0000000..6e5201e
--- /dev/null
+++ b/utils/time.js
@@ -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];
+};