您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
绝区零 Seelie 网站数据同步脚本
// ==UserScript== // @name ZZZ Seelie 数据同步 // @namespace github.com/owwkmidream // @version 1.2.2 // @author owwkmidream // @description 绝区零 Seelie 网站数据同步脚本 // @license MIT // @icon https://zzz.seelie.me/img/logo.svg // @homepageURL https://github.com/owwkmidream/zzz-seelie-sync // @supportURL https://github.com/owwkmidream/zzz-seelie-sync/issues // @match https://zzz.seelie.me/* // @match https://do-not-exist.mihoyo.com/ // @require https://fastly.jsdelivr.net/npm/@trim21/[email protected] // @connect act-api-takumi.mihoyo.com // @connect api-takumi-record.mihoyo.com // @connect public-data-api.mihoyo.com // @connect api-takumi.mihoyo.com // @grant GM.cookie // @grant GM.xmlHttpRequest // @run-at document-end // ==/UserScript== (function (GM_fetch) { 'use strict'; class Logger { prefix; timestamp; showLocation; colors; fileColorMap = /* @__PURE__ */ new Map(); constructor(options = {}) { this.prefix = options.prefix || "[zzz-seelie-sync]"; this.timestamp = options.timestamp ?? true; this.showLocation = options.showLocation ?? true; this.colors = { log: "#333333", info: "#2196F3", warn: "#FF9800", error: "#F44336", debug: "#9C27B0", ...options.colors }; } /** * 生成随机颜色 */ generateRandomColor() { const colors = [ "#E91E63", "#9C27B0", "#673AB7", "#3F51B5", "#2196F3", "#03A9F4", "#00BCD4", "#009688", "#4CAF50", "#8BC34A", "#CDDC39", "#FFC107", "#FF9800", "#FF5722", "#795548", "#607D8B", "#E53935", "#D81B60", "#8E24AA", "#5E35B1" ]; return colors[Math.floor(Math.random() * colors.length)]; } /** * 获取文件颜色(为每个文件分配固定的随机颜色) */ getFileColor(fileName) { if (!this.fileColorMap.has(fileName)) { this.fileColorMap.set(fileName, this.generateRandomColor()); } return this.fileColorMap.get(fileName); } /** * 获取调用位置信息 */ getLocationInfo() { try { const stack = new Error().stack; if (!stack) return null; const lines = stack.split("\n"); for (let i = 3; i < Math.min(lines.length, 8); i++) { const targetLine = lines[i]; if (!targetLine) continue; if (targetLine.includes("Logger.") || targetLine.includes("formatMessage") || targetLine.includes("getLocationInfo")) { continue; } const patterns = [ /at.*?\((.+):(\d+):(\d+)\)/, // Chrome with function name /at\s+(.+):(\d+):(\d+)/, // Chrome without function name /@(.+):(\d+):(\d+)/, // Firefox/Safari /(.+):(\d+):(\d+)$/ // Fallback pattern ]; for (const pattern of patterns) { const match = targetLine.match(pattern); if (match) { const fullPath = match[1]; const lineNumber = parseInt(match[2], 10); const columnNumber = parseInt(match[3], 10); if (!fullPath || fullPath.includes("chrome-extension://") || fullPath.includes("moz-extension://")) { continue; } const fileName = fullPath.split("/").pop() || fullPath.split("\\").pop() || fullPath; if (fileName && !isNaN(lineNumber) && !isNaN(columnNumber)) { return { fileName, lineNumber, columnNumber }; } } } } return null; } catch { return null; } } formatMessage(level, color, ...args) { const timestamp = this.timestamp ? `[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}]` : ""; const location = this.showLocation ? this.getLocationInfo() : null; let prefix = `${timestamp} ${this.prefix} [${level.toUpperCase()}]`; let locationStr = ""; let locationColor = ""; if (location) { locationStr = ` [${location.fileName}:${location.lineNumber}]`; locationColor = this.getFileColor(location.fileName); } if (typeof window !== "undefined") { if (location) { return [ `%c${prefix}%c${locationStr}`, `color: ${color}; font-weight: bold;`, `color: ${locationColor}; font-weight: bold; font-style: italic;`, ...args ]; } else { return [ `%c${prefix}`, `color: ${color}; font-weight: bold;`, ...args ]; } } return [prefix + locationStr, ...args]; } /** * 普通日志输出 */ log(...args) { console.log(...this.formatMessage("log", this.colors.log, ...args)); } /** * 信息日志输出 */ info(...args) { console.info(...this.formatMessage("info", this.colors.info, ...args)); } /** * 警告日志输出 */ warn(...args) { console.warn(...this.formatMessage("warn", this.colors.warn, ...args)); } /** * 错误日志输出 */ error(...args) { console.error(...this.formatMessage("error", this.colors.error, ...args)); } /** * 调试日志输出 (仅在开发环境下输出) */ debug(...args) { } /** * 表格输出 */ table(data, columns) { if (this.timestamp || this.prefix) { this.info("Table data:"); } console.table(data, columns); } /** * 分组开始 */ group(label) { const formattedLabel = label ? this.formatMessage("group", this.colors.info, label)[2] : void 0; console.group(formattedLabel); } /** * 折叠分组开始 */ groupCollapsed(label) { const formattedLabel = label ? this.formatMessage("group", this.colors.info, label)[2] : void 0; console.groupCollapsed(formattedLabel); } /** * 分组结束 */ groupEnd() { console.groupEnd(); } /** * 计时开始 */ time(label) { console.time(label); } /** * 计时结束 */ timeEnd(label) { console.timeEnd(label); } /** * 清空控制台 */ clear() { console.clear(); } /** * 创建子 Logger 实例 */ createChild(childPrefix, options) { const childLogger = new Logger({ prefix: `${this.prefix}:${childPrefix}`, timestamp: this.timestamp, showLocation: this.showLocation, colors: this.colors, ...options }); childLogger.fileColorMap = this.fileColorMap; return childLogger; } } const logger = new Logger({ prefix: "[Seelie]", timestamp: true, showLocation: true, colors: { log: "#4CAF50", info: "#2196F3", warn: "#FF9800", error: "#F44336", debug: "#9C27B0" } }); logger.log.bind(logger); logger.info.bind(logger); logger.warn.bind(logger); logger.error.bind(logger); let pendingHooks = []; let routerObserver = null; let isObserving = false; function findVueRouter() { const appElement = document.querySelector("#app"); if (!appElement?.__vue_app__) { logger.debug("🔍 未找到 Vue App 实例,可能还在加载中..."); return null; } logger.debug("🔍 查找 Vue Router 实例..."); const router = appElement.__vue_app__.config?.globalProperties?.$router; if (router) { if (typeof router.afterEach === "function" && typeof router.beforeEach === "function" && typeof router.push === "function") { logger.info("✓ 从 __vue_app__.config.globalProperties.$router 找到 Router 实例"); logger.debug("Router 实例:", router); return router; } } const context = appElement.__vue_app__._context; if (context?.provides) { logger.debug("🔍 尝试从 provides 查找 Router..."); const provides = context.provides; const symbols = Object.getOwnPropertySymbols(provides); for (const symbol of symbols) { const value = provides[symbol]; if (value && typeof value === "object") { const potentialRouter = value; if (typeof potentialRouter.afterEach === "function" && typeof potentialRouter.beforeEach === "function" && typeof potentialRouter.push === "function") { logger.info("✓ 从 provides 找到 Router 实例:", symbol.toString()); logger.debug("Router 实例:", value); return potentialRouter; } } } } logger.debug("🔍 未找到 Vue Router 实例,可能还在初始化中..."); return null; } function stopRouterObserver() { if (routerObserver) { routerObserver.disconnect(); routerObserver = null; } isObserving = false; } function startRouterObserver() { const timeout = 3e3; if (isObserving || routerObserver) { return; } logger.debug("👀 启动 Vue Router 观察器..."); isObserving = true; routerObserver = new MutationObserver(() => { const router = findVueRouter(); if (router) { logger.info("✓ Vue Router 已加载,处理待注册的 Hook..."); stopRouterObserver(); processPendingHooks(router); } }); routerObserver.observe(document.querySelector("#app"), { childList: false, subtree: false, attributes: true }); setTimeout(() => { if (isObserving) { logger.warn("⚠️ Vue Router 观察器超时,停止观察"); stopRouterObserver(); processPendingHooks(null); } }, timeout); } function processPendingHooks(router) { logger.debug(`🔄 处理 ${pendingHooks.length} 个待注册的 Hook...`); const hooks = [...pendingHooks]; pendingHooks = []; hooks.forEach(({ callback, options, unwatchRef }) => { if (router) { const { unwatch } = registerRouterHook(router, callback, options); unwatchRef.current = unwatch; } else { logger.warn("⚠️ Vue Router 未找到,Hook 注册失败"); unwatchRef.current = () => { }; } }); } function registerRouterHook(router, callback, options) { const { delay = 100, immediate = false } = options; if (immediate) { setTimeout(() => { const currentRoute = router.currentRoute?.value || router.currentRoute; callback(currentRoute, null); }, delay); } const unwatch = router.afterEach((to, from) => { logger.debug("🔄 路由变化检测到:", from?.path, "->", to?.path); setTimeout(() => { callback(to, from); }, delay); }); return { router, unwatch, getCurrentRoute: () => { const currentRoute = router.currentRoute?.value || router.currentRoute; return currentRoute; } }; } function useRouterWatcher(callback, options = {}) { logger.debug("🚦 设置路由监听 Hook..."); const router = findVueRouter(); if (router) { logger.debug("✓ Vue Router 已存在,直接注册 Hook"); const result = registerRouterHook(router, callback, options); return result; } logger.debug("⏳ Vue Router 未找到,设置延迟注册..."); const unwatchRef = { current: null }; pendingHooks.push({ callback, options, unwatchRef }); startRouterObserver(); return { router: null, unwatch: () => { if (unwatchRef.current) { unwatchRef.current(); } }, getCurrentRoute: () => { const currentRouter = findVueRouter(); if (currentRouter) { const currentRoute = currentRouter.currentRoute?.value || currentRouter.currentRoute; return currentRoute; } return void 0; } }; } class ComponentInjector { component = null; config; factory; isCreating = false; createPromise = null; constructor(config, factory) { this.config = config; this.factory = factory; } /** * 检查组件是否已存在 */ checkExistence() { const targetContainer = document.querySelector(this.config.targetSelector); if (!targetContainer) return false; const componentElement = targetContainer.querySelector(this.config.componentSelector); return componentElement !== null; } /** * 检查创建条件 */ checkCondition() { const targetExists = document.querySelector(this.config.targetSelector) !== null; if (!targetExists) return false; if (this.config.condition && !this.config.condition()) { return false; } if (this.config.routePattern) { const currentPath = window.location.pathname; if (typeof this.config.routePattern === "string") { return currentPath.includes(this.config.routePattern); } else { return this.config.routePattern.test(currentPath); } } return true; } /** * 尝试创建组件 */ async tryCreate() { if (this.isCreating && this.createPromise) { logger.debug(`⏳ [${this.config.id}] 组件正在创建中,等待完成`); await this.createPromise; return; } if (!this.checkCondition()) { logger.debug(`🚫 [${this.config.id}] 条件检查失败,跳过创建`); return; } if (this.checkExistence()) { logger.debug(`✅ [${this.config.id}] 组件已存在,跳过创建`); return; } this.createPromise = this.createComponent(); await this.createPromise; } /** * 创建组件 */ async createComponent() { if (this.isCreating) { logger.debug(`⚠️ [${this.config.id}] 组件已在创建中,跳过重复创建`); return; } this.isCreating = true; try { if (this.checkExistence()) { logger.debug(`✅ [${this.config.id}] 组件已存在,取消创建`); return; } this.destroyComponent(); this.component = await this.factory(); await this.component.init(); logger.debug(`✅ [${this.config.id}] 组件创建成功`); } catch (error) { logger.error(`❌ [${this.config.id}] 创建组件失败:`, error); this.component = null; } finally { this.isCreating = false; this.createPromise = null; } } /** * 检查并重新创建组件 */ async checkAndRecreate() { if (this.isCreating) { logger.debug(`⏳ [${this.config.id}] 组件正在创建中,跳过检查`); return; } const shouldExist = this.checkCondition(); const doesExist = this.checkExistence(); if (shouldExist && !doesExist) { logger.debug(`🔧 [${this.config.id}] 组件缺失,重新创建组件`); await this.tryCreate(); } else if (!shouldExist && doesExist) { logger.debug(`🗑️ [${this.config.id}] 条件不满足,销毁组件`); this.destroyComponent(); } } /** * 销毁组件 */ destroyComponent() { if (this.isCreating && this.createPromise) { logger.debug(`⏳ [${this.config.id}] 等待创建完成后销毁`); this.createPromise.then(() => { if (this.component) { this.component.destroy(); this.component = null; logger.debug(`🗑️ [${this.config.id}] 组件已销毁(延迟)`); } }); return; } if (this.component) { this.component.destroy(); this.component = null; logger.debug(`🗑️ [${this.config.id}] 组件已销毁`); } } /** * 刷新组件 */ async refreshComponent() { if (this.component && this.component.refresh) { await this.component.refresh(); logger.debug(`🔄 [${this.config.id}] 组件已刷新`); } } /** * 处理路由变化 */ async handleRouteChange(_to, _from) { await this.checkAndRecreate(); } /** * 处理 DOM 变化 */ async handleDOMChange(_mutations) { await this.checkAndRecreate(); } /** * 清理资源 */ cleanup() { this.isCreating = false; this.createPromise = null; this.destroyComponent(); } /** * 获取组件实例 */ getComponent() { return this.component; } /** * 检查组件是否存在 */ hasComponent() { return this.component !== null && this.checkExistence(); } /** * 检查是否正在创建中 */ isCreatingComponent() { return this.isCreating; } /** * 获取配置 */ getConfig() { return this.config; } } class DOMInjectorManager { injectors = /* @__PURE__ */ new Map(); domObserver = null; routerUnwatch = null; isInitialized = false; options; constructor(options = {}) { this.options = { observerConfig: { childList: true, subtree: true }, enableGlobalRouterWatch: true, routerDelay: 100, ...options }; } /** * 注册组件注入器 */ register(config, factory) { if (this.injectors.has(config.id)) { logger.warn(`⚠️ 注入器 [${config.id}] 已存在,将被覆盖`); this.unregister(config.id); } const injector = new ComponentInjector(config, factory); this.injectors.set(config.id, injector); logger.debug(`📝 注册组件注入器: [${config.id}]`); if (this.isInitialized) { injector.tryCreate(); } return injector; } /** * 注销组件注入器 */ unregister(id) { const injector = this.injectors.get(id); if (injector) { injector.cleanup(); this.injectors.delete(id); logger.debug(`🗑️ 注销组件注入器: [${id}]`); return true; } return false; } /** * 获取注入器 */ getInjector(id) { return this.injectors.get(id) || null; } /** * 初始化管理器 */ init() { if (this.isInitialized) { logger.warn("⚠️ DOM 注入管理器已经初始化"); return; } logger.debug("🎯 初始化 DOM 注入管理器"); if (this.options.enableGlobalRouterWatch) { this.setupGlobalRouterWatcher(); } this.setupDOMObserver(); this.createAllComponents(); this.isInitialized = true; } /** * 设置全局路由监听 */ setupGlobalRouterWatcher() { const { unwatch } = useRouterWatcher( async (to, from) => { logger.debug("🔄 全局路由变化检测到:", from?.path, "->", to?.path); await this.handleGlobalRouteChange(to, from); }, { delay: this.options.routerDelay, immediate: false } ); this.routerUnwatch = unwatch; logger.debug("✅ 全局路由监听设置完成"); } /** * 设置 DOM 观察器 */ setupDOMObserver() { let debounceTimer = null; let isProcessing = false; let pendingMutations = []; let lastDebugTime = 0; const debugLogInterval = 3e3; this.domObserver = new MutationObserver(async (mutations) => { pendingMutations.push(...mutations); if (debounceTimer) { clearTimeout(debounceTimer); } debounceTimer = setTimeout(async () => { if (isProcessing) { logger.debug("🔍 DOM 变化处理中,跳过本次处理"); return; } isProcessing = true; const currentMutations = [...pendingMutations]; pendingMutations = []; try { const now = Date.now(); if (now - lastDebugTime >= debugLogInterval) { lastDebugTime = now; logger.debug(`🔍 检测到 ${currentMutations.length} 个 DOM 变化,通知所有组件`); } await this.handleGlobalDOMChange(currentMutations); } finally { isProcessing = false; debounceTimer = null; } }, 100); }); this.domObserver.observe(document.body, this.options.observerConfig); logger.debug("✅ DOM 观察器设置完成"); } /** * 处理全局路由变化 */ async handleGlobalRouteChange(to, from) { const promises = Array.from(this.injectors.values()).map( (injector) => injector.handleRouteChange(to, from) ); await Promise.allSettled(promises); } /** * 处理全局 DOM 变化 */ async handleGlobalDOMChange(mutations) { const promises = Array.from(this.injectors.values()).map( (injector) => injector.handleDOMChange(mutations) ); await Promise.allSettled(promises); } /** * 创建所有组件 */ async createAllComponents() { const promises = Array.from(this.injectors.values()).map((injector) => injector.tryCreate()); await Promise.allSettled(promises); } /** * 刷新所有组件 */ async refreshAllComponents() { const promises = Array.from(this.injectors.values()).map((injector) => injector.refreshComponent()); await Promise.allSettled(promises); } /** * 刷新指定组件 */ async refreshComponent(id) { const injector = this.injectors.get(id); if (injector) { await injector.refreshComponent(); } } /** * 销毁管理器 */ destroy() { logger.debug("🗑️ 销毁 DOM 注入管理器"); for (const injector of this.injectors.values()) { injector.cleanup(); } this.injectors.clear(); if (this.routerUnwatch) { this.routerUnwatch(); this.routerUnwatch = null; } if (this.domObserver) { this.domObserver.disconnect(); this.domObserver = null; } this.isInitialized = false; } /** * 获取所有注入器 ID */ getInjectorIds() { return Array.from(this.injectors.keys()); } /** * 获取注入器数量 */ getInjectorCount() { return this.injectors.size; } /** * 检查是否已初始化 */ isInit() { return this.isInitialized; } } const domInjector = new DOMInjectorManager({ enableGlobalRouterWatch: true, routerDelay: 200, observerConfig: { childList: true, subtree: true } }); var _GM = /* @__PURE__ */ (() => typeof GM != "undefined" ? GM : void 0)(); const DEVICE_INFO_KEY = "zzz_device_info"; const NAP_CULTIVATE_TOOL_URL = "https://act-api-takumi.mihoyo.com/event/nap_cultivate_tool"; const GAME_RECORD_URL = "https://api-takumi-record.mihoyo.com/event/game_record_zzz/api/zzz"; const DEVICE_FP_URL = "https://public-data-api.mihoyo.com/device-fp/api/getFp"; const GAME_ROLE_URL = "https://api-takumi.mihoyo.com/binding/api/getUserGameRolesByCookie?game_biz=nap_cn"; const NAP_TOEKN_URL = "https://api-takumi.mihoyo.com/common/badge/v1/login/account"; let NapTokenInitialized = false; let userInfoCache = null; let deviceInfoCache = { deviceId: generateUUID(), deviceFp: "0000000000000", timestamp: Date.now() }; let deviceInfoPromise = null; const appVer = "2.85.1"; const defaultHeaders = { "Accept": "application/json", "User-Agent": `Mozilla/5.0 (Linux; Android 13; Pixel 5 Build/TQ3A.230901.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/118.0.0.0 Mobile Safari/537.36 miHoYoBBS/${appVer}` }; async function getZZZHeaderWithDevice() { const deviceInfo = await getDeviceInfo(); return { ...defaultHeaders, "Referer": "https://act.mihoyo.com/", "x-rpc-app_version": appVer, "x-rpc-client_type": "5", "x-rpc-device_fp": deviceInfo.deviceFp, "x-rpc-device_id": deviceInfo.deviceId }; } async function initializeNapToken() { if (NapTokenInitialized) { return; } logger.debug("🔄 初始化 nap_token cookie..."); try { const rolesResponse = await GM_fetch(GAME_ROLE_URL, { method: "GET", headers: defaultHeaders }); if (!rolesResponse.ok) { throw new Error(`获取用户角色失败: HTTP ${rolesResponse.status}`); } const rolesData = await rolesResponse.json(); if (rolesData.retcode !== 0) { throw new Error(`获取用户角色失败: ${rolesData.message}`); } if (!rolesData.data?.list || rolesData.data.list.length === 0) { throw new Error("未找到绝区零游戏角色"); } const roleInfo = rolesData.data.list[0]; logger.debug(`🎮 找到角色: ${roleInfo.nickname} (UID: ${roleInfo.game_uid}, 等级: ${roleInfo.level})`); const tokenResponse = await GM_fetch(NAP_TOEKN_URL, { method: "POST", headers: { "Content-Type": "application/json", ...defaultHeaders }, body: JSON.stringify({ region: roleInfo.region, uid: roleInfo.game_uid, game_biz: roleInfo.game_biz }) }); if (!tokenResponse.ok) { throw new Error(`设置 nap_token 失败: HTTP ${tokenResponse.status}`); } const tokenData = await tokenResponse.json(); if (tokenData.retcode !== 0) { throw new Error(`设置 nap_token 失败: ${tokenData.message}`); } userInfoCache = { uid: roleInfo.game_uid, nickname: roleInfo.nickname, level: roleInfo.level, region: roleInfo.region, accountId: roleInfo.game_uid // 使用 game_uid 作为 accountId }; logger.debug("✅ nap_token cookie 初始化完成"); logger.info(`👤 用户信息: ${userInfoCache.nickname} (UID: ${userInfoCache.uid}, 等级: ${userInfoCache.level})`); NapTokenInitialized = true; } catch (error) { logger.error("❌ 初始化 nap_token 失败:", error); throw error; } } async function ensureUserInfo() { if (!userInfoCache) { await initializeNapToken(); } } async function request(endpoint, baseUrl, options = {}) { const { method = "GET", params = {}, body, headers = {} } = options; if (baseUrl === NAP_CULTIVATE_TOOL_URL) { await initializeNapToken(); } let url = `${baseUrl}${endpoint}`; if (Object.keys(params).length > 0) { const searchParams = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { searchParams.append(key, String(value)); }); url += `?${searchParams.toString()}`; } const deviceFpErrorCodes = [1034, 5003, 10035, 10041, 10053]; const executeRequest = async (isRetry = false) => { const zzzHeaders = await getZZZHeaderWithDevice(); const finalHeaders = { ...zzzHeaders, ...headers }; if (finalHeaders["x-rpc-device_fp"] === "0000000000000") { throw new Error("❌ 设备指纹有误,请检查"); } logger.debug(`🌐 请求 ${method} ${url}${isRetry ? " (重试)" : ""}`); try { const payload = [url, { method, headers: finalHeaders, body: body ? JSON.stringify(body) : void 0 }]; const response = await GM_fetch(...payload); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); if (data.retcode !== 0) { if (deviceFpErrorCodes.includes(data.retcode) && !isRetry) { logger.warn(`⚠️ 检测到设备指纹错误码 ${data.retcode}: ${data.message},正在刷新设备指纹...`); try { await getDeviceFingerprint(); logger.debug("✅ 设备指纹刷新完成,准备重试请求"); return await executeRequest(true); } catch (fpError) { logger.error("❌ 设备指纹刷新失败:", fpError); throw new Error(`设备指纹刷新失败,原始错误: API Error ${data.retcode}: ${data.message}`); } } logger.error("❌ 请求失败\n请求:", payload, "\n响应:", response, data); throw new Error(`API Error ${data.retcode}: ${data.message}`); } logger.debug(`✅ 请求成功: ${payload[0]}, ${data.retcode}: ${data.message}`); return data; } catch (error) { if (error instanceof Error && error.message.includes("API Error")) { throw error; } logger.error(`❌ 请求失败:`, error); throw error; } }; return await executeRequest(); } async function getDeviceFingerprint() { const mysCookies = await _GM.cookie.list({ url: "https://do-not-exist.mihoyo.com/" }); if (mysCookies.length !== 0) { for (const ck of mysCookies) { if (ck.name === "_MHYUUID") { logger.debug("🔐 从米游社获取到UUID", ck.value); deviceInfoCache.deviceId = ck.value; } } } if (!deviceInfoCache) { throw new Error("设备信息缓存未初始化"); } const productName = generateProductName(); const requestBody = { device_id: generateSeedId(), seed_id: generateUUID(), seed_time: Date.now().toString(), platform: "2", device_fp: deviceInfoCache.deviceFp, app_name: "bbs_cn", ext_fields: `{"proxyStatus":0,"isRoot":0,"romCapacity":"512","deviceName":"Pixel5","productName":"${productName}","romRemain":"512","hostname":"db1ba5f7c000000","screenSize":"1080x2400","isTablet":0,"aaid":"","model":"Pixel5","brand":"google","hardware":"windows_x86_64","deviceType":"redfin","devId":"REL","serialNumber":"unknown","sdCapacity":125943,"buildTime":"1704316741000","buildUser":"cloudtest","simState":0,"ramRemain":"124603","appUpdateTimeDiff":1716369357492,"deviceInfo":"google\\/${productName}\\/redfin:13\\/TQ3A.230901.001\\/2311.40000.5.0:user\\/release-keys","vaid":"","buildType":"user","sdkVersion":"33","ui_mode":"UI_MODE_TYPE_NORMAL","isMockLocation":0,"cpuType":"arm64-v8a","isAirMode":0,"ringMode":2,"chargeStatus":3,"manufacturer":"Google","emulatorStatus":0,"appMemory":"512","osVersion":"13","vendor":"unknown","accelerometer":"","sdRemain":123276,"buildTags":"release-keys","packageName":"com.mihoyo.hyperion","networkType":"WiFi","oaid":"","debugStatus":1,"ramCapacity":"125943","magnetometer":"","display":"TQ3A.230901.001","appInstallTimeDiff":1706444666737,"packageVersion":"2.20.2","gyroscope":"","batteryStatus":85,"hasKeyboard":10,"board":"windows"}`, bbs_device_id: deviceInfoCache.deviceId }; logger.debug(`🔐 获取设备指纹,设备ID: ${deviceInfoCache.deviceId}`); try { const response = await GM_fetch(`${DEVICE_FP_URL}`, { method: "POST", headers: { ...defaultHeaders, "Content-Type": "application/json" }, body: JSON.stringify(requestBody) }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); if (data.retcode !== 0 || data.data.code !== 200) { throw new Error(`设备指纹获取失败 ${data.retcode}: ${data.message}`); } deviceInfoCache.deviceFp = data.data.device_fp; deviceInfoCache.timestamp = Date.now(); localStorage.setItem(DEVICE_INFO_KEY, JSON.stringify(deviceInfoCache)); logger.debug(`✅ 设备指纹获取成功并更新缓存: ${data.data.device_fp}`); } catch (error) { logger.error(`❌ 设备指纹获取失败:`, error); throw error; } } function generateProductName() { const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; let result = ""; for (let i = 0; i < 6; i++) { result += chars[Math.floor(Math.random() * chars.length)]; } return result; } function generateUUID() { if (typeof crypto !== "undefined" && crypto.randomUUID) { return crypto.randomUUID(); } return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) { const r = Math.random() * 16 | 0; const v = c === "x" ? r : r & 3 | 8; return v.toString(16); }); } function generateSeedId() { return generateHexString(16); } function generateHexString(length) { const bytes = new Uint8Array(Math.ceil(length / 2)); if (typeof crypto !== "undefined" && crypto.getRandomValues) { crypto.getRandomValues(bytes); } else { for (let i = 0; i < bytes.length; i++) { bytes[i] = Math.floor(Math.random() * 256); } } const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join(""); return hex.substring(0, length); } async function getDeviceInfo(refresh) { if (deviceInfoPromise) { return deviceInfoPromise; } deviceInfoPromise = (async () => { const stored = localStorage.getItem(DEVICE_INFO_KEY); if (stored) { try { const storedDeviceInfo = JSON.parse(stored); logger.debug("📱 从localStorage获取设备信息:", storedDeviceInfo); deviceInfoCache = storedDeviceInfo; } catch (error) { logger.warn("⚠️ 解析设备信息失败,将重新生成:", error); } } let needRefresh = false; if (refresh === true) { needRefresh = true; logger.debug("📱 强制刷新设备指纹"); } else if (refresh === false) { needRefresh = false; logger.debug("📱 跳过设备指纹刷新"); } else { const now = Date.now(); const threeDaysInMs = 3 * 24 * 60 * 60 * 1e3; if (deviceInfoCache.deviceFp === "0000000000000") { needRefresh = true; logger.debug("📱 设备指纹为初始值,需要获取真实指纹"); } else if (now - deviceInfoCache.timestamp > threeDaysInMs) { needRefresh = true; logger.debug("📱 设备信息超过3天,需要刷新"); } else { logger.debug("📱 设备信息仍在有效期内"); } } if (needRefresh) { try { await getDeviceFingerprint(); logger.debug("✅ 设备指纹刷新完成"); } catch (error) { logger.error("❌ 设备指纹刷新失败:", error); throw error; } } return deviceInfoCache; })(); const result = await deviceInfoPromise; deviceInfoPromise = null; return result; } function getUserInfo() { return userInfoCache; } async function initializeUserInfo() { await ensureUserInfo(); return userInfoCache; } async function refreshDeviceInfo() { logger.debug("🔄 开始刷新设备信息..."); const newDeviceInfo = await getDeviceInfo(true); logger.debug("✅ 设备信息刷新完成:", newDeviceInfo); } async function resolveUserInfo(uid, region) { await ensureUserInfo(); const userInfoCache2 = getUserInfo(); if (userInfoCache2) { return { uid: userInfoCache2.uid, region: region || userInfoCache2.region }; } throw new Error("❌ 未提供 UID 且无法从缓存获取用户信息,请确保已登录米游社"); } async function processBatches(items, batchSize, processor) { if (items.length <= batchSize) { return processor(items); } const batches = []; for (let i = 0; i < items.length; i += batchSize) { batches.push(items.slice(i, i + batchSize)); } const batchPromises = batches.map((batch) => processor(batch)); const batchResults = await Promise.all(batchPromises); return batchResults.flat(); } async function getAvatarBasicList(uid, region) { const userInfo = await resolveUserInfo(uid, region); const response = await request("/user/avatar_basic_list", NAP_CULTIVATE_TOOL_URL, { method: "GET", params: { uid: userInfo.uid, region: userInfo.region } }); return response.data.list.filter((avatar) => avatar.unlocked === true); } async function batchGetAvatarDetail(avatarList, uid, region) { const userInfo = await resolveUserInfo(uid, region); const processedAvatarList = typeof avatarList[0] === "number" ? avatarList.map((id) => ({ avatar_id: id, is_teaser: false, teaser_need_weapon: false, teaser_sp_skill: false })) : avatarList; return processBatches( processedAvatarList, 10, async (batch) => { const response = await request("/user/batch_avatar_detail_v2", NAP_CULTIVATE_TOOL_URL, { method: "POST", params: { uid: userInfo.uid, region: userInfo.region }, body: { avatar_list: batch } }); return response.data.list; } ); } async function getGameNote(roleId, server) { const userInfo = await resolveUserInfo(roleId, server); const response = await request("/note", GAME_RECORD_URL, { method: "GET", params: { server: userInfo.region, role_id: userInfo.uid } }); return response.data; } class SeelieDataUpdater { static SEELIE_BASE_URL = "https://zzz.seelie.me"; static UNIQUE_ZZZ_KEYS = ["denny", "w_engine", "drive_disc"]; static STATS_FILE_PATTERNS = [ { name: "charactersStats", pattern: /stats-characters-[a-f0-9]+\.js/ }, { name: "weaponsStats", pattern: /stats-weapons-[a-f0-9]+\.js/ }, { name: "weaponsStatsCommon", pattern: /stats-weapons-common-[a-f0-9]+\.js/ } ]; /** * 获取网络内容 */ static async fetchContent(url) { try { const response = await GM_fetch(url); if (!response.ok) { throw new Error(`请求失败,状态码: ${response.status} - ${response.statusText}`); } return await response.text(); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`获取 ${url} 时网络错误: ${errorMessage}`); } } /** * 从 JS 内容中还原绝区零数据 */ static restoreZzzData(jsContent) { logger.debug("▶️ 开始从 JS 内容中还原绝区零数据..."); const exportMatch = jsContent.match(/\bexport\s*\{([\s\S]*?)\}/); if (!exportMatch) { throw new Error("在JS文件中未找到 export 语句。"); } const exportedVars = exportMatch[1].split(",").map((s) => s.trim().split(/\s+as\s+/)[0]).filter(Boolean); let executionCode = jsContent.replace(/\bexport\s*\{[\s\S]*?};/, ""); executionCode += ` // Appended by script return { ${exportedVars.map((v) => `${v}: ${v}`).join(", ")} };`; try { const scriptRunner = new Function(executionCode); const allDataBlocks = scriptRunner(); logger.debug(`🔍 正在 ${Object.keys(allDataBlocks).length} 个数据块中搜索绝区零数据...`); for (const blockName in allDataBlocks) { const block = allDataBlocks[blockName]; if (!block || typeof block !== "object") continue; const sources = [block.default, block]; for (const source of sources) { if (source && typeof source === "object" && this.UNIQUE_ZZZ_KEYS.some((key) => key in source)) { logger.debug(`🎯 命中!在变量 '${blockName}' 中找到关键词。`); return source; } } } throw new Error("未能在任何数据块中找到绝区零的锚点关键词。"); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`还原数据时发生错误: ${errorMessage}`); } } /** * 解析统计数据 JS 文件 */ static parseStatsFile(jsContent) { try { const exportMatch = jsContent.match(/\bexport\s*\{([\s\S]*?)\}/); if (!exportMatch) { throw new Error("在统计文件中未找到 export 语句"); } const exportItems = exportMatch[1].split(",").map((s) => s.trim()); const exportMappings = {}; let defaultExportVar = null; exportItems.forEach((item) => { const parts = item.split(/\s+as\s+/); if (parts.length === 2) { const [varName, exportName] = parts; if (exportName.trim() === "default") { defaultExportVar = varName.trim(); } exportMappings[exportName.trim()] = varName.trim(); } else { const varName = item.trim(); exportMappings[varName] = varName; } }); let executionCode = jsContent.replace(/\bexport\s*\{[\s\S]*?};/, ""); if (defaultExportVar) { executionCode += ` // Appended by script return ${defaultExportVar};`; } else { const allVars = Object.values(exportMappings); executionCode += ` // Appended by script return { ${allVars.map((v) => `${v}: ${v}`).join(", ")} };`; } const scriptRunner = new Function(executionCode); return scriptRunner(); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`解析统计文件时发生错误: ${errorMessage}`); } } /** * 处理统计数据文件(并行版本) */ static async processStatsFiles(indexScriptContent) { logger.debug("▶️ 开始并行处理统计数据文件..."); const statsPromises = this.STATS_FILE_PATTERNS.map(async ({ name, pattern }) => { const match = indexScriptContent.match(pattern); if (!match) { logger.warn(`⚠️ 未找到 ${name} 文件,跳过...`); return { name, data: null }; } const fileName = match[0]; const statsFileUrl = `${this.SEELIE_BASE_URL}/assets/${fileName}`; logger.debug(`📥 下载 ${name} -> ${statsFileUrl}`); try { const statsFileContent = await this.fetchContent(statsFileUrl); const parsedData = this.parseStatsFile(statsFileContent); logger.debug(`✅ ${name} 处理完成`); return { name, data: parsedData }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error(`❌ 处理 ${name} 时出错: ${errorMessage}`); return { name, data: null }; } }); const results = await Promise.all(statsPromises); const statsData = {}; results.forEach(({ name, data }) => { if (data !== null) { statsData[name] = data; } }); logger.debug(`✅ 统计数据并行处理完成,共处理 ${Object.keys(statsData).length} 个文件`); return statsData; } /** * 更新 Seelie 数据(优化并行版本) */ static async updateSeelieData() { try { logger.debug("🚀 开始更新 Seelie 数据..."); logger.debug("第一步:获取 Seelie.me 主页..."); const mainPageHtml = await this.fetchContent(this.SEELIE_BASE_URL); const indexScriptMatch = mainPageHtml.match(/\/assets\/index-([a-f0-9]+)\.js/); if (!indexScriptMatch) { throw new Error("在主页HTML中未找到 index-....js 脚本。"); } const indexScriptUrl = `${this.SEELIE_BASE_URL}${indexScriptMatch[0]}`; logger.debug(`第二步:发现主脚本 -> ${indexScriptUrl}`); const indexScriptContent = await this.fetchContent(indexScriptUrl); const stringsFileMatch = indexScriptContent.match(/strings-zh-([a-f0-9]+)\.js/); if (!stringsFileMatch) { throw new Error("在主脚本中未找到 strings-zh-....js 语言包。"); } const stringsFileUrl = `${this.SEELIE_BASE_URL}/assets/locale/${stringsFileMatch[0]}`; logger.debug(`第三步:发现中文语言包 -> ${stringsFileUrl}`); logger.debug("🔄 开始并行处理语言包和统计数据..."); const [stringsFileContent, statsData] = await Promise.all([ this.fetchContent(stringsFileUrl), this.processStatsFiles(indexScriptContent) ]); logger.debug("✅ 语言包和统计数据并行处理完成"); const languageData = this.restoreZzzData(stringsFileContent); logger.debug("🎉 Seelie 数据更新完成!"); return { languageData, statsData }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error(`❌ Seelie 数据更新失败: ${errorMessage}`); throw error; } } /** * 缓存数据到 localStorage */ static cacheData(languageData, statsData) { try { localStorage.setItem("seelie_language_data", JSON.stringify(languageData)); localStorage.setItem("seelie_stats_data", JSON.stringify(statsData)); localStorage.setItem("seelie_data_timestamp", Date.now().toString()); logger.debug("✅ 数据已缓存到 localStorage"); } catch (error) { logger.error("❌ 缓存数据失败:", error); } } /** * 从缓存获取数据 */ static getCachedData() { try { const languageDataStr = localStorage.getItem("seelie_language_data"); const statsDataStr = localStorage.getItem("seelie_stats_data"); const timestampStr = localStorage.getItem("seelie_data_timestamp"); if (!languageDataStr || !statsDataStr || !timestampStr) { return null; } return { languageData: JSON.parse(languageDataStr), statsData: JSON.parse(statsDataStr), timestamp: parseInt(timestampStr) }; } catch (error) { logger.error("❌ 获取缓存数据失败:", error); return null; } } /** * 获取最新数据(优先网络请求,失败时使用缓存) */ static async getLatestData() { try { logger.debug("🔄 请求最新 Seelie 数据..."); const { languageData, statsData } = await this.updateSeelieData(); this.cacheData(languageData, statsData); return { languageData, statsData }; } catch (error) { logger.warn("⚠️ 网络请求失败,尝试使用缓存数据:", error); const cachedData = this.getCachedData(); if (cachedData) { logger.debug("✅ 使用缓存的 Seelie 数据"); return { languageData: cachedData.languageData, statsData: cachedData.statsData }; } throw new Error("网络请求失败且无可用缓存数据"); } } } const ASCENSIONS = [1, 10, 20, 30, 40, 50, 60]; const SKILLS = { 0: "basic", // 普通攻击 1: "special", // 特殊技 2: "dodge", // 闪避 3: "chain", // 连携技 5: "core", // 核心被动 6: "assist" // 支援技 }; const RESIN_INTERVAL = 360; let runtimeDataCache = {}; async function lazyLoadSeelieData() { if (runtimeDataCache.loaded) { return; } if (runtimeDataCache.loading) { await runtimeDataCache.loading; return; } runtimeDataCache.loading = (async () => { try { logger.debug("🔄 懒加载 Seelie 数据..."); const { languageData, statsData } = await SeelieDataUpdater.getLatestData(); runtimeDataCache.languageData = languageData; runtimeDataCache.statsData = statsData; runtimeDataCache.loaded = true; logger.info("✅ Seelie 数据加载完成"); } catch (error) { logger.error("❌ Seelie 数据加载失败:", error); throw error; } finally { runtimeDataCache.loading = void 0; } })(); await runtimeDataCache.loading; } async function getLanguageData() { await lazyLoadSeelieData(); return runtimeDataCache.languageData; } async function getStatsData() { await lazyLoadSeelieData(); return runtimeDataCache.statsData; } async function getCharacterStats() { try { const statsData = await getStatsData(); if (statsData?.charactersStats && Array.isArray(statsData.charactersStats)) { logger.debug("✅ 使用动态角色统计数据"); return statsData.charactersStats; } } catch (error) { logger.warn("⚠️ 获取角色统计数据失败:", error); } throw new Error("无法获取角色统计数据"); } async function getWeaponStats() { try { const statsData = await getStatsData(); if (statsData?.weaponsStats && typeof statsData.weaponsStats === "object") { logger.debug("✅ 使用动态武器统计数据"); return statsData.weaponsStats; } } catch (error) { logger.warn("⚠️ 获取武器统计数据失败:", error); } throw new Error("无法获取武器统计数据"); } async function getWeaponStatsCommon() { try { const statsData = await getStatsData(); if (statsData?.weaponsStatsCommon && typeof statsData.weaponsStatsCommon === "object") { logger.debug("✅ 使用动态武器通用统计数据"); return statsData.weaponsStatsCommon; } } catch (error) { logger.warn("⚠️ 获取武器通用统计数据失败:", error); } throw new Error("无法获取武器通用统计数据"); } class SeelieCore { appElement = null; rootComponent = null; constructor() { this.init(); } /** * 初始化,获取 #app 元素和根组件 */ init() { this.appElement = document.querySelector("#app"); if (!this.appElement) { logger.warn("⚠️ SeelieCore: 未找到 #app 元素"); return; } if (this.appElement._vnode?.component) { this.completeInit(); return; } this.waitForVNodeComponent(); } /** * 等待 _vnode.component 出现 */ waitForVNodeComponent() { const timeoutValue = 3e3; if (!this.appElement) return; logger.debug("🔍 SeelieCore: 等待 _vnode.component 出现...", this.appElement?._vnode?.component); const observer = new MutationObserver(() => { logger.debug("🔍 SeelieCore: 等待 _vnode.component 出现...", this.appElement?._vnode?.component); if (this.appElement?._vnode?.component) { clean(); this.completeInit(); } }); observer.observe(this.appElement, { attributes: true, childList: false, subtree: false }); const timeoutTimer = setTimeout(() => { if (!this.rootComponent) { clean(); logger.warn(`⚠️ SeelieCore: 等待 _vnode.component 超时 ${timeoutValue / 1e3}秒`); } }, timeoutValue); const clean = () => { observer.disconnect(); clearTimeout(timeoutTimer); }; } /** * 完成初始化 */ completeInit() { if (!this.appElement?._vnode?.component) { logger.warn("⚠️ SeelieCore: 完成初始化时 _vnode.component 不存在"); return; } this.rootComponent = this.appElement._vnode.component; lazyLoadSeelieData(); logger.debug("✅ SeelieCore: 已尝试初始化 stats 数据"); logger.log("✅ SeelieCore 初始化成功"); } /** * 确保组件已初始化 */ ensureInitialized() { if (!this.rootComponent) { this.init(); } return !!this.rootComponent; } /** * 获取根组件的 proxy 对象 */ getProxy() { if (!this.ensureInitialized()) { return null; } return this.rootComponent?.proxy; } /** * 获取 accountResin 属性值 */ getAccountResin() { const proxy = this.getProxy(); if (!proxy) { logger.warn("⚠️ 无法获取组件 proxy 对象"); return null; } const accountResin = proxy.accountResin; logger.debug("📖 获取 accountResin:", accountResin); return accountResin; } /** * 设置 accountResin 属性值 */ setAccountResin(value) { const proxy = this.getProxy(); if (!proxy) { logger.warn("⚠️ 无法获取组件 proxy 对象"); return false; } try { const oldValue = proxy.accountResin; const convertedValue = this.convertToAccountResinFormat(value); proxy.accountResin = convertedValue; logger.debug("✏️ 设置 accountResin:", { oldValue, inputValue: value, convertedValue }); return true; } catch (error) { logger.error("❌ 设置 accountResin 失败:", error); return false; } } /** * 将输入参数转换为 accountResin 格式 */ convertToAccountResinFormat(input) { if (!input || !input.progress) { throw new Error("输入参数格式错误,缺少 progress 字段"); } const { progress, restore } = input; const currentAmount = progress.current; const maxAmount = progress.max; const restoreSeconds = restore; const now = /* @__PURE__ */ new Date(); const theoreticalRestoreTime = (maxAmount - currentAmount) * RESIN_INTERVAL; const updateTime = new Date(now.getTime() + (restoreSeconds - theoreticalRestoreTime) * 1e3); return { amount: currentAmount, time: updateTime.toString() }; } /** * 设置 Toast 消息 */ setToast(message, type = "") { const proxy = this.getProxy(); if (!proxy) { logger.warn("⚠️ 无法获取组件 proxy 对象"); return false; } try { proxy.toast = message; proxy.toastType = type; logger.debug("🍞 设置 Toast:", { message, type }); return true; } catch (error) { logger.error("❌ 设置 Toast 失败:", error); return false; } } /** * 调用组件的 addGoal 方法 */ addGoal(goal) { const proxy = this.getProxy(); if (!proxy) { logger.warn("⚠️ 无法获取组件 proxy 对象"); return false; } if (typeof proxy.addGoal !== "function") { logger.warn("⚠️ addGoal 方法不存在"); return false; } try { proxy.addGoal(goal); return true; } catch (error) { logger.error("❌ 调用 addGoal 失败:", error); return false; } } /** * 调用组件的 removeGoal 方法 */ removeGoal(goal) { const proxy = this.getProxy(); if (!proxy) { logger.warn("⚠️ 无法获取组件 proxy 对象"); return false; } if (typeof proxy.removeGoal !== "function") { logger.warn("⚠️ removeGoal 方法不存在"); return false; } try { proxy.removeGoal(goal); return true; } catch (error) { logger.error("❌ 调用 removeGoal 失败:", error); return false; } } /** * 调用组件的 setInventory 方法 */ setInventory(type, item, tier, value) { const proxy = this.getProxy(); if (!proxy) { logger.warn("⚠️ 无法获取组件 proxy 对象"); return false; } if (typeof proxy.setInventory !== "function") { logger.warn("⚠️ setInventory 方法不存在"); return false; } try { proxy.setInventory(type, item, tier, value); return true; } catch (error) { logger.error("❌ 调用 setInventory 失败:", error); return false; } } /** * 获取组件的 characters 数据 */ getCharacters() { const proxy = this.getProxy(); return proxy?.characters || {}; } /** * 获取组件的 weapons 数据 */ getWeapons() { const proxy = this.getProxy(); return proxy?.weapons || {}; } /** * 获取组件的 goals 数据 */ getGoals() { const proxy = this.getProxy(); return proxy?.goals || []; } /** * 获取组件的 items 数据 */ getItems() { const proxy = this.getProxy(); return proxy?.items || {}; } /** * 获取完整的组件上下文信息(调试用) */ getContextInfo() { const proxy = this.getProxy(); if (!proxy) { return null; } return { keys: Object.keys(proxy), accountResin: proxy.accountResin, hasAccountResin: "accountResin" in proxy, contextType: typeof proxy }; } /** * 重新初始化(当页面路由变化时调用) */ refresh() { logger.debug("🔄 SeelieCore 重新初始化..."); this.appElement = null; this.rootComponent = null; this.init(); } } async function calculateCharacterAsc(character) { try { const characterStats = await getCharacterStats(); const stats = characterStats.find((s) => s.id === character.id); if (!stats) { logger.warn(`⚠️ 未找到角色 ${character.name_mi18n} 的统计数据`); return ASCENSIONS.findIndex((level) => level >= character.level); } const hpProperty = character.properties.find((p) => p.property_id === 1); if (!hpProperty) { logger.warn(`⚠️ 角色 ${character.name_mi18n} 缺少生命值属性`); return ASCENSIONS.findIndex((level) => level >= character.level); } const actualHP = parseInt(hpProperty.base || hpProperty.final); const baseHP = stats.base; const growthHP = (character.level - 1) * stats.growth / 1e4; const coreSkill = character.skills.find((s) => s.skill_type === 5); const coreHP = coreSkill && stats.core ? stats.core[coreSkill.level - 2] || 0 : 0; const calculatedBaseHP = baseHP + growthHP + coreHP; for (let i = 0; i < stats.ascHP.length; i++) { const ascHP = stats.ascHP[i]; if (Math.floor(calculatedBaseHP + ascHP) === actualHP) { return i; } } logger.debug(`HP error: ${character.name_mi18n}, base: ${baseHP}, growth: ${growthHP}, core: ${coreHP}, fixed: ${calculatedBaseHP}, target: ${actualHP}`); return ASCENSIONS.findIndex((level) => level >= character.level); } catch (error) { logger.error("❌ 计算角色突破等级失败:", error); return ASCENSIONS.findIndex((level) => level >= character.level); } } async function calculateWeaponAsc(weapon) { try { const weaponStatsCommon = await getWeaponStatsCommon(); const weaponStats = await getWeaponStats(); const levelRate = weaponStatsCommon.rate[weapon.level] || 0; const atkProperty = weapon.main_properties.find((p) => p.property_id === 12101); if (!atkProperty) { logger.warn(`⚠️ 武器 ${weapon.name} 缺少攻击力属性`); return ASCENSIONS.findIndex((level) => level >= weapon.level); } const actualATK = parseInt(atkProperty.base); const baseATK = weaponStats[weapon.id] || 48; const growthATK = baseATK * levelRate / 1e4; const calculatedBaseATK = baseATK + growthATK; for (let i = 0; i < weaponStatsCommon.ascRate.length; i++) { const ascRate = weaponStatsCommon.ascRate[i]; const ascATK = baseATK * ascRate / 1e4; if (Math.floor(calculatedBaseATK + ascATK) === actualATK) { return i; } } logger.debug(`ATK error: ${weapon.name}, base: ${baseATK}, growth: ${growthATK}, fixed: ${calculatedBaseATK}, target: ${actualATK}`); return ASCENSIONS.findIndex((level) => level >= weapon.level); } catch (error) { logger.error("❌ 计算武器突破等级失败:", error); return ASCENSIONS.findIndex((level) => level >= weapon.level); } } function calculateSkillLevel(skillLevel, skillType, characterRank) { let currentLevel = skillLevel; if (skillType === "core") { currentLevel--; } else if (characterRank >= 5) { currentLevel -= 4; } else if (characterRank >= 3) { currentLevel -= 2; } return Math.max(1, currentLevel); } class CharacterManager extends SeelieCore { /** * 设置角色基础数据 */ async setCharacter(data) { try { const character = data.avatar || data; const characterKey = this.findCharacterKey(character.id); if (!characterKey) { throw new Error("Character not found."); } const existingGoal = this.findExistingGoal(characterKey, "character"); const currentAsc = await calculateCharacterAsc(character); const existingGoalData = existingGoal; let targetLevel = existingGoalData?.goal?.level; if (!targetLevel || targetLevel < character.level) { targetLevel = character.level; } let targetAsc = existingGoalData?.goal?.asc; if (!targetAsc || targetAsc < currentAsc) { targetAsc = currentAsc; } const goal = { type: "character", character: characterKey, cons: character.rank, current: { level: character.level, asc: currentAsc }, goal: { level: targetLevel || character.level, asc: targetAsc || currentAsc } }; if (this.addGoal(goal)) { logger.debug("✓ 角色数据设置成功:", { character: characterKey, level: character.level, rank: character.rank, currentAsc, targetLevel, targetAsc }); return true; } return false; } catch (error) { logger.error("❌ 设置角色数据失败:", error); return false; } } /** * 设置角色天赋数据 */ setTalents(data) { try { const character = data.avatar || data; const characterKey = this.findCharacterKey(character.id); if (!characterKey) { throw new Error("Character not found."); } const existingGoal = this.findExistingGoal(characterKey, "talent"); const talents = {}; character.skills.forEach((skill) => { const skillType = SKILLS[skill.skill_type]; if (!skillType) return; const currentLevel = calculateSkillLevel(skill.level, skillType, character.rank); const existingSkillGoal = existingGoal; let targetLevel = existingSkillGoal?.[skillType]?.goal; if (!targetLevel || targetLevel < currentLevel) { targetLevel = currentLevel; } talents[skillType] = { current: currentLevel, goal: targetLevel || currentLevel }; }); const goal = { type: "talent", character: characterKey, ...talents }; if (this.addGoal(goal)) { logger.debug("✓ 角色天赋数据设置成功:", { character: characterKey, talents }); return true; } return false; } catch (error) { logger.error("❌ 设置角色天赋数据失败:", error); return false; } } /** * 设置武器数据 */ async setWeapon(data) { try { const character = data.avatar || data; const weapon = data.weapon; const characterKey = this.findCharacterKey(character.id); if (!characterKey) { throw new Error("Character not found."); } const existingGoal = this.findExistingGoal(characterKey, "weapon"); if (!weapon) { if (existingGoal && this.removeGoal(existingGoal)) { logger.debug("✓ 移除武器目标成功"); } return true; } const weaponKey = this.findWeaponKey(weapon.id); if (!weaponKey) { throw new Error("Weapon not found."); } const currentAsc = await calculateWeaponAsc(weapon); const current = { level: weapon.level, asc: currentAsc }; let goal = { level: current.level, asc: current.asc }; const weapons = this.getWeapons(); const existingGoalData = existingGoal; const existingWeapon = existingGoalData?.weapon ? weapons[existingGoalData.weapon] : null; const newWeapon = weapons[weaponKey]; if (existingWeapon?.id === newWeapon?.id && existingGoalData?.goal) { goal.level = Math.max(existingGoalData.goal.level || current.level, current.level); goal.asc = Math.max(existingGoalData.goal.asc || current.asc, current.asc); if (newWeapon.craftable) { current.craft = weapon.star; goal.craft = Math.max(existingGoalData.goal.craft || weapon.star, weapon.star); } } else { if (newWeapon.craftable) { current.craft = weapon.star; goal.craft = weapon.star; } } const weaponGoal = { type: "weapon", character: characterKey, weapon: weaponKey, current, goal }; if (this.addGoal(weaponGoal)) { logger.debug("✓ 武器数据设置成功:", { character: characterKey, weapon: weaponKey, current, goal }); return true; } return false; } catch (error) { logger.error("❌ 设置武器数据失败:", error); return false; } } /** * 同步单个角色的完整数据 */ async syncCharacter(data) { const result = { success: 0, failed: 0, errors: [] }; const character = data.avatar || data; const characterName = character.name_mi18n || `角色ID:${character.id}`; logger.debug(`🔄 开始同步角色: ${characterName}`); const operations = [ { name: "角色数据", fn: () => this.setCharacter(data) }, { name: "天赋数据", fn: () => this.setTalents(data) }, { name: "武器数据", fn: () => this.setWeapon(data) } ]; const operationPromises = operations.map(async ({ name, fn }) => { try { const success = await fn(); if (success) { logger.debug(`✓ ${characterName} - ${name}同步成功`); return { success: true, error: null }; } else { const errorMsg = `${characterName} - ${name}同步失败`; return { success: false, error: errorMsg }; } } catch (error) { const errorMsg = `${characterName} - ${name}同步错误: ${error}`; logger.error(`❌ ${errorMsg}`); return { success: false, error: errorMsg }; } }); const results = await Promise.all(operationPromises); results.forEach(({ success, error }) => { if (success) { result.success++; } else { result.failed++; if (error) { result.errors.push(error); } } }); logger.debug(`✅ ${characterName} 同步完成 - 成功: ${result.success}, 失败: ${result.failed}`); return result; } /** * 同步多个角色的完整数据 */ async syncAllCharacters(dataList) { const overallResult = { total: dataList.length, success: 0, failed: 0, errors: [], details: [] }; logger.debug(`🚀 开始批量同步 ${dataList.length} 个角色`); const syncPromises = dataList.map(async (data, index) => { const character = data.avatar || data; const characterName = character.name_mi18n || `角色ID:${character.id}`; logger.debug(`📝 [${index + 1}/${dataList.length}] 同步角色: ${characterName}`); try { const result = await this.syncCharacter(data); return { character: characterName, result, success: result.failed === 0 }; } catch (error) { const errorMsg = `${characterName} - 批量同步失败: ${error}`; logger.error(`❌ ${errorMsg}`); return { character: characterName, result: { success: 0, failed: 1, errors: [errorMsg] }, success: false }; } }); const results = await Promise.all(syncPromises); results.forEach(({ character, result, success }) => { overallResult.details.push({ character, result }); if (success) { overallResult.success++; } else { overallResult.failed++; overallResult.errors.push(...result.errors); } }); this.logBatchResult(overallResult); return overallResult; } /** * 查找角色键名 */ findCharacterKey(characterId) { const characters = this.getCharacters(); return Object.keys(characters).find((key) => characters[key].id === characterId) || null; } /** * 查找武器键名 */ findWeaponKey(weaponId) { const weapons = this.getWeapons(); return Object.keys(weapons).find((key) => weapons[key].id === weaponId) || null; } /** * 查找现有目标 */ findExistingGoal(characterKey, type) { const goals = this.getGoals(); return goals.find((goal) => { const g = goal; return g.character === characterKey && g.type === type; }); } /** * 记录批量同步结果 */ logBatchResult(result) { logger.debug(`🎯 批量同步完成:`); logger.debug(` 总计: ${result.total} 个角色`); logger.debug(` 成功: ${result.success} 个角色`); logger.debug(` 失败: ${result.failed} 个角色`); if (result.errors.length > 0) { logger.debug(` 错误详情:`); result.errors.forEach((error) => logger.debug(` - ${error}`)); } } /** * 显示批量同步 Toast */ // private showBatchToast(result: BatchSyncResult): void { // if (result.success > 0) { // this.setToast( // `成功同步 ${result.success}/${result.total} 个角色`, // result.failed === 0 ? 'success' : 'warning' // ) // } // if (result.failed > 0) { // this.setToast( // `${result.failed} 个角色同步失败,请查看控制台`, // 'error' // ) // } // } // 辅助函数 // 缓存变量 _minimumSetCoverCache = null; _minimumSetWeaponsCache = null; /** * 使用贪心算法找到最小集合覆盖的角色ID列表 * 目标是用最少的角色覆盖所有属性组合(属性、风格、模拟材料、周本) */ findMinimumSetCoverIds() { if (this._minimumSetCoverCache !== null) { logger.debug("📦 使用缓存的最小集合覆盖结果"); return this._minimumSetCoverCache; } const charactersData = this.getCharacters(); const charactersArray = Object.values(charactersData); const universeOfAttributes = /* @__PURE__ */ new Set(); for (const char of charactersArray) { universeOfAttributes.add(char.attribute); universeOfAttributes.add(char.style); universeOfAttributes.add(char.boss); universeOfAttributes.add(char.boss_weekly); } const attributesToCover = new Set(universeOfAttributes); const resultIds = []; const usedCharacterIds = /* @__PURE__ */ new Set(); while (attributesToCover.size > 0) { let bestCharacter = null; let maxCoveredCount = 0; for (const char of charactersArray) { if (usedCharacterIds.has(char.id)) { continue; } if (new Date(char.release) > /* @__PURE__ */ new Date()) { continue; } const characterAttributes = /* @__PURE__ */ new Set([ char.attribute, char.style, char.boss, char.boss_weekly ]); let currentCoverCount = 0; for (const attr of characterAttributes) { if (attributesToCover.has(attr)) { currentCoverCount++; } } if (currentCoverCount > maxCoveredCount) { maxCoveredCount = currentCoverCount; bestCharacter = char; } } if (bestCharacter === null) { logger.warn("⚠️ 无法覆盖所有属性,可能缺少某些属性的组合"); break; } resultIds.push({ id: bestCharacter.id, style: bestCharacter.style }); usedCharacterIds.add(bestCharacter.id); const bestCharacterAttributes = /* @__PURE__ */ new Set([ bestCharacter.attribute, bestCharacter.style, bestCharacter.boss, bestCharacter.boss_weekly ]); for (const attr of bestCharacterAttributes) { attributesToCover.delete(attr); } logger.debug(`✅ 选择角色 ${bestCharacter.id},覆盖 ${maxCoveredCount} 个属性`); } logger.debug(`🎯 最小集合覆盖完成,共选择 ${resultIds.length} 个角色: ${resultIds.join(", ")}`); this._minimumSetCoverCache = resultIds; return resultIds; } /** * 返回每个职业对应一个武器 */ findMinimumSetWeapons() { if (this._minimumSetWeaponsCache !== null) { logger.debug("📦 使用缓存的最小武器集合结果"); return this._minimumSetWeaponsCache; } const weaponsData = this.getWeapons(); const weaponsArray = Object.values(weaponsData); const result = {}; for (const weapon of weaponsArray) { if (weapon.tier === 5 && !result[weapon.style] && /* @__PURE__ */ new Date() >= new Date(weapon.release)) { result[weapon.style] = weapon.id; } } this._minimumSetWeaponsCache = result; return result; } } class SeelieDataManager extends CharacterManager { // 继承所有功能,无需额外实现 } const seelieDataManager = new SeelieDataManager(); const setResinData = (data) => { return seelieDataManager.setAccountResin(data); }; const setToast = (message, type = "success") => { return seelieDataManager.setToast(message, type); }; const syncCharacter = async (data) => { return await seelieDataManager.syncCharacter(data); }; const syncAllCharacters$1 = async (dataList) => { return await seelieDataManager.syncAllCharacters(dataList); }; const setInventory = (type, item, tier, value) => { return seelieDataManager.setInventory(type, item, tier, value); }; const findMinimumSetCoverIds = () => { return seelieDataManager.findMinimumSetCoverIds(); }; const findMinimumSetWeapons = () => { return seelieDataManager.findMinimumSetWeapons(); }; const getItems = () => { return seelieDataManager.getItems(); }; var SkillType = /* @__PURE__ */ ((SkillType2) => { SkillType2[SkillType2["NormalAttack"] = 0] = "NormalAttack"; SkillType2[SkillType2["SpecialSkill"] = 1] = "SpecialSkill"; SkillType2[SkillType2["Dodge"] = 2] = "Dodge"; SkillType2[SkillType2["Chain"] = 3] = "Chain"; SkillType2[SkillType2["CorePassive"] = 5] = "CorePassive"; SkillType2[SkillType2["SupportSkill"] = 6] = "SupportSkill"; return SkillType2; })(SkillType || {}); async function getAvatarItemCalc(avatar_id, weapon_id, uid, region) { const userInfo = await resolveUserInfo(uid, region); const body = { avatar_id: Number(avatar_id), avatar_level: ASCENSIONS[ASCENSIONS.length - 1], // 最大等级 avatar_current_level: 1, avatar_current_promotes: 1, skills: Object.values(SkillType).filter((value) => typeof value !== "string").map((skillType) => ({ skill_type: skillType, level: skillType === SkillType.CorePassive ? 7 : 12, init_level: 1 // 初始 })), weapon_info: { weapon_id: Number(weapon_id), weapon_level: ASCENSIONS[ASCENSIONS.length - 1], weapon_promotes: 0, weapon_init_level: 0 } }; const response = await request("/user/avatar_calc", NAP_CULTIVATE_TOOL_URL, { method: "POST", params: { uid: userInfo.uid, region: userInfo.region }, body }); return response.data; } async function batchGetAvatarItemCalc(calcAvatars, uid, region) { const promises = calcAvatars.map( (item) => getAvatarItemCalc(item.avatar_id, item.weapon_id, uid, region) ); return await Promise.all(promises); } class SyncService { /** * 同步电量(树脂)数据 */ async syncResinData() { try { logger.debug("🔋 开始同步电量数据..."); const gameNote = await getGameNote(); if (!gameNote) { logger.error("❌ 获取游戏便笺失败"); setToast("获取游戏便笺失败", "error"); return false; } const resinData = gameNote.energy; const success = setResinData(resinData); if (success) { logger.debug("✅ 电量数据同步成功"); setToast(`电量同步成功: ${resinData.progress.current}/${resinData.progress.max}`, "success"); } else { logger.error("❌ 电量数据设置失败"); setToast("电量数据设置失败", "error"); } return success; } catch (error) { logger.error("❌ 电量数据同步失败:", error); setToast("电量数据同步失败", "error"); return false; } } /** * 同步单个角色数据 */ async syncSingleCharacter(avatarId) { try { logger.debug(`👤 开始同步角色数据: ${avatarId}`); const avatarDetails = await batchGetAvatarDetail([avatarId], void 0); if (!avatarDetails || avatarDetails.length === 0) { const message = "获取角色详细信息失败"; logger.error(`❌ ${message}`); setToast(message, "error"); return { success: 0, failed: 1, errors: [message] }; } const avatarDetail = avatarDetails[0]; const result = await syncCharacter(avatarDetail); if (result.success > 0) { logger.debug(`✅ 角色 ${avatarDetail.avatar.name_mi18n} 同步成功`); setToast(`角色 ${avatarDetail.avatar.name_mi18n} 同步成功`, "success"); } else { logger.error(`❌ 角色 ${avatarDetail.avatar.name_mi18n} 同步失败`); setToast(`角色 ${avatarDetail.avatar.name_mi18n} 同步失败`, "error"); } return result; } catch (error) { const message = `角色 ${avatarId} 同步失败`; logger.error(`❌ ${message}:`, error); setToast(message, "error"); return { success: 0, failed: 1, errors: [String(error)] }; } } /** * 同步所有角色数据 */ async syncAllCharacters() { try { logger.debug("👥 开始同步所有角色数据..."); const avatarList = await getAvatarBasicList(); if (!avatarList || avatarList.length === 0) { const message = "获取角色列表失败或角色列表为空"; logger.error(`❌ ${message}`); setToast(message, "error"); return { success: 0, failed: 1, errors: [message], total: 0, details: [] }; } logger.debug(`📋 找到 ${avatarList.length} 个角色`); setToast(`开始同步 ${avatarList.length} 个角色...`, ""); const avatarIds = avatarList.map((avatar) => avatar.avatar.id); const avatarDetails = await batchGetAvatarDetail(avatarIds, void 0); if (!avatarDetails || avatarDetails.length === 0) { const message = "获取角色详细信息失败"; logger.error(`❌ ${message}`); setToast(message, "error"); return { success: 0, failed: 1, errors: [message], total: 0, details: [] }; } const batchResult = await syncAllCharacters$1(avatarDetails); if (batchResult.success > 0) { logger.debug(`✅ 所有角色同步完成: 成功 ${batchResult.success},失败 ${batchResult.failed}`); setToast(`角色同步完成: 成功 ${batchResult.success},失败 ${batchResult.failed}`, "success"); } else { logger.error(`❌ 角色批量同步失败`); setToast("角色批量同步失败", "error"); } return batchResult; } catch (error) { const message = "所有角色同步失败"; logger.error(`❌ ${message}:`, error); setToast(message, "error"); return { success: 0, failed: 1, errors: [String(error)], total: 0, details: [] }; } } /** * 同步养成材料数据 */ async syncItemsData() { try { logger.debug("🔋 开始始同步养成材料数据..."); const minSetChar = findMinimumSetCoverIds(); const minSetWeapon = findMinimumSetWeapons(); const calcParams = minSetChar.map((item) => ({ avatar_id: item.id, weapon_id: minSetWeapon[item.style] })); const itemsData = await batchGetAvatarItemCalc(calcParams); if (!itemsData) { const message = "获取养成材料数据失败"; logger.error(`❌ ${message}`); setToast(message, "error"); return false; } const allItemsInfo = this.collectAllItemsInfo(itemsData); const itemsInventory = this.buildItemsInventory(itemsData, allItemsInfo); const seelieItems = getItems(); seelieItems["denny"] = { type: "denny" }; const i18nData = await getLanguageData(); if (!i18nData) { const message = "获取语言数据失败"; logger.error(`❌ ${message}`); setToast(message, "error"); return false; } const cnName2SeelieItemName = this.buildCnToSeelieNameMapping(i18nData); const { successNum, failNum } = this.syncItemsToSeelie( itemsInventory, cnName2SeelieItemName, seelieItems ); const success = successNum > 0; const total = successNum + failNum; if (success) { logger.debug(`✅ 养成材料同步成功: ${successNum}/${total}`); const toastType = failNum === 0 ? "success" : "warning"; setToast(`养成材料同步成功: ${successNum}/${total}`, toastType); } else { logger.error("❌ 养成材料同步失败"); setToast("养成材料同步失败", "error"); } return success; } catch (error) { const message = "养成材料同步失败"; logger.error(`❌ ${message}:`, error); setToast(message, "error"); return false; } } /** * 收集所有物品信息(从所有消耗类型中获取完整的物品信息) */ collectAllItemsInfo(itemsData) { const allItemsInfo = {}; for (const data of itemsData) { const allConsumes = [ ...data.avatar_consume, ...data.weapon_consume, ...data.skill_consume, ...data.need_get ]; for (const item of allConsumes) { const id = item.id.toString(); if (!(id in allItemsInfo)) { allItemsInfo[id] = { id: item.id, name: item.name }; } } } return allItemsInfo; } /** * 构建物品库存数据(名称到数量的映射) */ buildItemsInventory(itemsData, allItemsInfo) { const inventory = {}; const userOwnItems = {}; for (const data of itemsData) { Object.assign(userOwnItems, data.user_owns_materials); } for (const [id, itemInfo] of Object.entries(allItemsInfo)) { const count = userOwnItems[id] || 0; inventory[itemInfo.name] = count; } return inventory; } /** * 构建中文名称到 Seelie 物品名称的映射 */ buildCnToSeelieNameMapping(i18nData) { const mapping = {}; for (const [key, value] of Object.entries(i18nData)) { if (typeof value === "string") { mapping[value] = key; } else if (Array.isArray(value)) { value.forEach((v, index) => { mapping[v] = `${key}+${index}`; }); } } return mapping; } /** * 同步物品到 Seelie */ syncItemsToSeelie(itemsInventory, cnName2SeelieItemName, seelieItems) { let successNum = 0; let failNum = 0; for (const [cnName, count] of Object.entries(itemsInventory)) { const seelieName = cnName2SeelieItemName[cnName]; if (!seelieName) { failNum++; continue; } try { const seelieNameParts = seelieName.split("+"); if (seelieNameParts.length > 1) { const realName = seelieNameParts[0]; const tier = Number(seelieNameParts[1]); const type = seelieItems[realName].type; if (type && setInventory(type, realName, tier, count)) { successNum++; } else { failNum++; } } else { const type = seelieItems[seelieName]?.type; if (type && setInventory(type, seelieName, 0, count)) { successNum++; } else { failNum++; } } } catch { failNum++; } } return { successNum, failNum }; } /** * 执行完整同步(电量 + 所有角色 + 养成材料) */ async syncAll() { logger.debug("🚀 开始执行完整同步..."); setToast("开始执行完整同步...", ""); const [resinSync, characterSync, itemsSync] = await Promise.all([ this.syncResinData(), this.syncAllCharacters(), this.syncItemsData() ]); const totalSuccess = resinSync && characterSync.success > 0 && itemsSync; const message = totalSuccess ? "完整同步成功" : "完整同步部分失败"; logger.debug(`${totalSuccess ? "✅" : "⚠️"} ${message}`); setToast(message, totalSuccess ? "success" : "error"); return { resinSync, characterSync, itemsSync }; } } const syncService = new SyncService(); const syncResinData = () => { return syncService.syncResinData(); }; const syncAllCharacters = () => { return syncService.syncAllCharacters(); }; const syncItemsData = () => { return syncService.syncItemsData(); }; const syncAll = () => { return syncService.syncAll(); }; const MYS_URL = "https://www.miyoushe.com/zzz/"; class SeeliePanel { container = null; userInfo = null; isLoading = false; isExpanded = false; // 控制二级界面展开状态 // 组件相关的选择器常量 static TARGET_SELECTOR = "div.flex.flex-col.items-center.justify-center.w-full.mt-3"; static PANEL_SELECTOR = '[data-seelie-panel="true"]'; constructor() { } /** * 初始化面板 - 由外部调用 */ async init() { try { await this.createPanel(); } catch (error) { logger.error("初始化 Seelie 面板失败:", error); throw error; } } /** * 创建面板 */ async createPanel() { const targetContainer = document.querySelector(SeeliePanel.TARGET_SELECTOR); if (!targetContainer) { throw new Error("目标容器未找到"); } const existingPanel = targetContainer.querySelector(SeeliePanel.PANEL_SELECTOR); if (existingPanel) { existingPanel.remove(); logger.debug("清理了目标容器中的旧面板"); } if (this.container && targetContainer.contains(this.container)) { logger.debug("面板已存在,跳过创建"); return; } await this.loadUserInfo(); this.container = this.createPanelElement(); targetContainer.insertBefore(this.container, targetContainer.firstChild); logger.info("✅ Seelie 面板创建成功"); } /** * 加载用户信息 */ async loadUserInfo() { try { this.userInfo = await initializeUserInfo(); logger.debug("用户信息加载成功:", this.userInfo); } catch (error) { logger.error("加载用户信息失败:", error); this.userInfo = null; const errorMessage = String(error); if (errorMessage.includes("获取用户角色失败") || errorMessage.includes("HTTP 401") || errorMessage.includes("HTTP 403")) { this.userInfo = { error: "login_required", message: "请先登录米游社账号" }; } else if (errorMessage.includes("未找到绝区零游戏角色")) { this.userInfo = { error: "no_character", message: "未找到绝区零游戏角色" }; } else if (errorMessage.includes("网络") || errorMessage.includes("timeout") || errorMessage.includes("fetch")) { this.userInfo = { error: "network_error", message: "网络连接失败,请重试" }; } else { this.userInfo = { error: "unknown", message: "用户信息加载失败" }; } } } /** * 创建面板元素 */ createPanelElement() { const panel = document.createElement("div"); panel.className = "w-full mb-3 p-3 bg-gray-800 rounded-lg border border-gray-200/20"; panel.setAttribute("data-seelie-panel", "true"); const userInfoSection = this.createUserInfoSection(); const syncSection = this.createSyncSection(); panel.appendChild(userInfoSection); panel.appendChild(syncSection); return panel; } /** * 创建用户信息区域 */ createUserInfoSection() { const section = document.createElement("div"); section.className = "flex flex-col items-center justify-center mb-3"; const infoText = document.createElement("div"); infoText.className = "flex flex-col items-center text-center"; if (this.userInfo && !("error" in this.userInfo)) { const nickname = document.createElement("div"); nickname.className = "text-sm font-medium text-white"; nickname.textContent = this.userInfo.nickname; const uid = document.createElement("div"); uid.className = "text-xs text-gray-400"; uid.textContent = `UID: ${this.userInfo.uid}`; infoText.appendChild(nickname); infoText.appendChild(uid); } else if (this.userInfo && "error" in this.userInfo) { const errorInfo = this.userInfo; const errorContainer = document.createElement("div"); errorContainer.className = "flex flex-col items-center"; const errorIcon = document.createElement("div"); errorIcon.className = "text-red-400 mb-2"; errorIcon.innerHTML = ` <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"></path> </svg> `; const errorMessage = document.createElement("div"); errorMessage.className = "text-sm text-red-400 mb-2"; errorMessage.textContent = errorInfo.message; errorContainer.appendChild(errorIcon); errorContainer.appendChild(errorMessage); if (errorInfo.error === "login_required") { const loginHint = document.createElement("div"); loginHint.className = "text-xs text-gray-400 mb-2 text-center"; loginHint.textContent = "请在新标签页中登录米游社后刷新页面"; const loginButton = document.createElement("button"); loginButton.className = "px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white text-xs rounded transition-all duration-200"; loginButton.textContent = "前往米游社登录"; loginButton.addEventListener("click", () => { window.open(MYS_URL, "_blank"); }); errorContainer.appendChild(loginHint); errorContainer.appendChild(loginButton); } else if (errorInfo.error === "no_character") { const characterHint = document.createElement("div"); characterHint.className = "text-xs text-gray-400 mb-2 text-center"; characterHint.textContent = "请先在米游社绑定绝区零游戏角色"; const bindButton = document.createElement("button"); bindButton.className = "px-3 py-1 bg-purple-600 hover:bg-purple-700 text-white text-xs rounded transition-all duration-200"; bindButton.textContent = "前往绑定角色"; bindButton.addEventListener("click", () => { window.open(MYS_URL, "_blank"); }); errorContainer.appendChild(characterHint); errorContainer.appendChild(bindButton); } else if (errorInfo.error === "network_error") { const retryButton = document.createElement("button"); retryButton.className = "px-3 py-1 bg-green-600 hover:bg-green-700 text-white text-xs rounded transition-all duration-200"; retryButton.textContent = "重试"; retryButton.addEventListener("click", () => this.refreshUserInfo()); errorContainer.appendChild(retryButton); } else { const retryButton = document.createElement("button"); retryButton.className = "px-3 py-1 bg-gray-600 hover:bg-gray-700 text-white text-xs rounded transition-all duration-200"; retryButton.textContent = "重试"; retryButton.addEventListener("click", () => this.refreshUserInfo()); errorContainer.appendChild(retryButton); } infoText.appendChild(errorContainer); } else { const errorText = document.createElement("div"); errorText.className = "text-sm text-red-400"; errorText.textContent = "用户信息加载失败"; infoText.appendChild(errorText); } section.appendChild(infoText); return section; } /** * 创建同步按钮区域 */ createSyncSection() { const section = document.createElement("div"); section.className = "flex flex-col items-center"; const isUserInfoValid = this.userInfo && !("error" in this.userInfo); const disabledClass = isUserInfoValid ? "" : " opacity-50 cursor-not-allowed"; const disabledBgClass = isUserInfoValid ? "bg-gray-700 hover:bg-gray-600" : "bg-gray-800"; const mainSyncButton = document.createElement("button"); mainSyncButton.className = `flex items-center justify-center px-6 py-2 ${disabledBgClass} text-white font-medium rounded-lg transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed mb-2${disabledClass}`; mainSyncButton.disabled = !isUserInfoValid; mainSyncButton.innerHTML = ` <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path> </svg> <span class="sync-text">${isUserInfoValid ? "同步全部" : "请先登录"}</span> `; const expandButton = document.createElement("button"); expandButton.className = `flex items-center justify-center px-4 py-1 ${isUserInfoValid ? "bg-gray-600 hover:bg-gray-500" : "bg-gray-700"} text-white text-sm rounded transition-all duration-200${disabledClass}`; expandButton.disabled = !isUserInfoValid; expandButton.innerHTML = ` <span class="mr-1 text-xs">更多选项</span> <svg class="w-3 h-3 expand-icon transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path> </svg> `; if (isUserInfoValid) { mainSyncButton.addEventListener("click", () => this.handleSyncAll(mainSyncButton)); expandButton.addEventListener("click", () => this.toggleExpanded(expandButton)); } const detailsContainer = document.createElement("div"); detailsContainer.className = "w-full mt-2 overflow-hidden transition-all duration-300"; detailsContainer.style.maxHeight = "0"; detailsContainer.style.opacity = "0"; const detailsContent = this.createDetailedSyncOptions(); detailsContainer.appendChild(detailsContent); section.appendChild(mainSyncButton); section.appendChild(expandButton); section.appendChild(detailsContainer); return section; } /** * 创建详细同步选项 */ createDetailedSyncOptions() { const container = document.createElement("div"); container.className = "grid grid-cols-2 gap-2"; const isUserInfoValid = this.userInfo && !("error" in this.userInfo); const syncOptions = [ { text: "同步电量", icon: `<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path> </svg>`, handler: (event) => this.handleSyncResin(event) }, { text: "同步角色", icon: `<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path> </svg>`, handler: (event) => this.handleSyncCharacters(event) }, { text: "同步材料", icon: `<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"></path> </svg>`, handler: (event) => this.handleSyncItems(event) }, { text: "重置设备", icon: `<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15M12 3v9m0 0l-3-3m3 3l3-3"></path> </svg>`, handler: (event) => this.handleResetDeviceInfo(event) } ]; syncOptions.forEach((option) => { const button = document.createElement("button"); const buttonClass = isUserInfoValid ? "bg-gray-600 hover:bg-gray-500" : "bg-gray-700 opacity-50 cursor-not-allowed"; button.className = `flex items-center justify-center px-3 py-2 ${buttonClass} text-white text-sm font-medium rounded transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed`; button.disabled = !isUserInfoValid; button.innerHTML = `${option.icon}<span class="sync-text">${option.text}</span>`; if (isUserInfoValid) { button.addEventListener("click", option.handler); } container.appendChild(button); }); return container; } /** * 切换展开状态 */ toggleExpanded(expandButton) { if (this.isLoading) return; this.isExpanded = !this.isExpanded; const detailsContainer = this.container?.querySelector(".overflow-hidden"); const expandIcon = expandButton.querySelector(".expand-icon"); if (this.isExpanded) { detailsContainer.style.maxHeight = "200px"; detailsContainer.style.opacity = "1"; expandIcon.style.transform = "rotate(180deg)"; } else { detailsContainer.style.maxHeight = "0"; detailsContainer.style.opacity = "0"; expandIcon.style.transform = "rotate(0deg)"; } } /** * 处理同步全部按钮点击 */ async handleSyncAll(button) { if (this.isLoading) return; if (!button) { button = this.container?.querySelector(".sync-text")?.closest("button"); if (!button) return; } await this.performSyncOperation(button, "同步中...", async () => { logger.debug("开始同步全部数据..."); await this.performSync(); logger.debug("✅ 同步完成"); }); } /** * 处理同步电量 */ async handleSyncResin(event) { const button = event?.target?.closest("button"); if (!button) return; await this.performSyncOperation(button, "同步中...", async () => { logger.debug("开始同步电量数据..."); const success = await syncResinData(); if (!success) { throw new Error("电量同步失败"); } logger.debug("✅ 电量同步完成"); }); } /** * 处理同步角色 */ async handleSyncCharacters(event) { const button = event?.target?.closest("button"); if (!button) return; await this.performSyncOperation(button, "同步中...", async () => { logger.debug("开始同步角色数据..."); const result = await syncAllCharacters(); if (result.success === 0) { throw new Error("角色同步失败"); } logger.debug("✅ 角色同步完成"); }); } /** * 处理同步材料 */ async handleSyncItems(event) { const button = event?.target?.closest("button"); if (!button) return; await this.performSyncOperation(button, "同步中...", async () => { logger.debug("开始同步材料数据..."); const success = await syncItemsData(); if (!success) { throw new Error("材料同步失败"); } logger.debug("✅ 材料同步完成"); }); } /** * 处理重置设备信息 */ async handleResetDeviceInfo(event) { const button = event?.target?.closest("button"); if (!button) return; await this.performSyncOperation(button, "重置中...", async () => { logger.debug("开始重置设备信息..."); try { await refreshDeviceInfo(); logger.debug("✅ 设备信息重置完成"); setToast("设备信息已重置", "success"); } catch (error) { logger.error("设备信息重置失败:", error); setToast("设备信息重置失败", "error"); } }); } /** * 通用同步操作处理器 */ async performSyncOperation(button, loadingText, syncOperation) { if (this.isLoading) return; this.isLoading = true; const syncText = button.querySelector(".sync-text"); const originalText = syncText.textContent; try { this.setAllButtonsDisabled(true); syncText.textContent = loadingText; const icon = button.querySelector("svg"); if (icon) { icon.classList.add("animate-spin"); } await syncOperation(); this.showSyncResult(button, syncText, originalText, icon, "success"); } catch (error) { logger.error("同步失败:", error); const icon = button.querySelector("svg"); this.showSyncResult(button, syncText, originalText, icon, "error"); } } /** * 执行同步操作 */ async performSync() { try { logger.debug("开始执行完整同步..."); const result = await syncAll(); const { resinSync, characterSync, itemsSync } = result; const totalSuccess = resinSync && characterSync.success > 0 && itemsSync; if (!totalSuccess) { const errorMessages = []; if (!resinSync) errorMessages.push("电量同步失败"); if (characterSync.success === 0) { const charErrors = characterSync.errors || ["角色同步失败"]; errorMessages.push(...charErrors); } if (!itemsSync) errorMessages.push("养成材料同步失败"); const errorMessage = errorMessages.length > 0 ? errorMessages.join(", ") : "同步过程中出现错误"; throw new Error(errorMessage); } logger.info(`✅ 同步完成 - 电量: ${resinSync ? "成功" : "失败"}, 角色: ${characterSync.success}/${characterSync.total}, 养成材料: ${itemsSync ? "成功" : "失败"}`); } catch (error) { logger.error("同步操作失败:", error); throw error; } } /** * 设置所有按钮的禁用状态 */ setAllButtonsDisabled(disabled) { if (!this.container) return; const buttons = this.container.querySelectorAll("button"); buttons.forEach((button) => { button.disabled = disabled; }); } /** * 显示同步结果 */ showSyncResult(button, syncText, originalText, icon, type) { const isSuccess = type === "success"; syncText.textContent = isSuccess ? "同步完成" : "同步失败"; const originalBgClass = button.className.match(/bg-gray-\d+/)?.[0] || "bg-gray-700"; const originalHoverClass = button.className.match(/hover:bg-gray-\d+/)?.[0] || "hover:bg-gray-600"; const newColorClass = isSuccess ? "bg-green-600" : "bg-red-600"; const newHoverClass = isSuccess ? "hover:bg-green-700" : "hover:bg-red-700"; button.className = button.className.replace(originalBgClass, newColorClass).replace(originalHoverClass, newHoverClass); setTimeout(() => { syncText.textContent = originalText || "同步全部"; button.className = button.className.replace(newColorClass, originalBgClass).replace(newHoverClass, originalHoverClass); if (icon) { icon.classList.remove("animate-spin"); } this.setAllButtonsDisabled(false); this.isLoading = false; }, 2e3); } /** * 销毁面板 */ destroy() { if (this.container && this.container.parentNode) { this.container.parentNode.removeChild(this.container); this.container = null; } const allPanels = document.querySelectorAll(SeeliePanel.PANEL_SELECTOR); allPanels.forEach((panel) => { if (panel.parentNode) { panel.parentNode.removeChild(panel); } }); logger.debug("Seelie 面板已销毁"); } /** * 刷新组件(实现接口要求) */ async refresh() { await this.refreshUserInfo(); } /** * 刷新用户信息 */ async refreshUserInfo() { try { await this.loadUserInfo(); if (this.container) { const parent = this.container.parentNode; if (parent) { this.destroy(); await this.createPanel(); } } } catch (error) { logger.error("刷新用户信息失败:", error); } } } function registerSeeliePanel() { const config = { id: "seelie-panel", targetSelector: SeeliePanel.TARGET_SELECTOR, componentSelector: SeeliePanel.PANEL_SELECTOR, condition: () => { return true; } }; domInjector.register(config, () => new SeeliePanel()); logger.debug("📝 Seelie 面板组件注册完成"); } const componentRegisters = { seeliePanel: registerSeeliePanel }; function registerAllComponents() { logger.debug("🎯 开始注册所有组件"); Object.values(componentRegisters).forEach((register) => register()); logger.debug("✅ 所有组件注册完成"); } function initApp() { logger.log("🎯 zzz-seelie-sync 脚本已加载"); initDOMInjector(); } function initDOMInjector() { try { if (domInjector.isInit()) { logger.debug("DOM 注入管理器已初始化,跳过"); return; } registerAllComponents(); domInjector.init(); logger.debug("✅ DOM 注入管理器初始化完成"); } catch (error) { logger.error("❌ 初始化 DOM 注入管理器失败:", error); } } initApp(); })(GM_fetch);