// ==UserScript== // @name FF14跨区小助手 // @namespace https://github.com/LIDaoJY // @version 0.6 // @description FF14 跨区传送辅助脚本 - 自动监控服务器状态并执行跨区传送 // @author LIDaoJY // @match https://ff14bjz.sdo.com/RegionKanTelepo* // @grant GM_addStyle // @grant GM_xmlhttpRequest // @grant GM_notification // @connect ff14bjz.sdo.com // @license MIT // ==/UserScript== (function () { 'use strict'; // =============== 可设置参数 =============== let NOTIFICATION_TIMEOUT = 20000; // 通知自动关闭时间(毫秒) let POLLING_INTERVAL_TIMEOUT = 30000; // 轮询间隔时间(毫秒)建议大于30秒 let RETRY_DELAY_TIMEOUT = 80000; // 重试间隔时间(毫秒)强烈建议大于60秒 // =============== 状态 =============== let FF14_GROUP_LIST = null; let FF14_STATUS_INFO = null; let selectedRoleInfo = null; let selectedTargetAreaInfo = null; let statusCheckIntervalId = null; let retryTimeoutId = null; let audioContext = null; // =============== UI 引用 =============== let logContainer = null; let fetchButton = null; let controlSection = null; let linkStartBtn = null; let characterSelectionWindow = null; let targetAreaSelectionWindow = null; let statusWindow = null; // =============== 注册全局样式 =============== GM_addStyle(` /* 主窗口 */ #ff14-helper-window { position: fixed; top: 120px; left: 40px; width: 350px; height: 400px; background: rgba(30, 30, 30, 0.9); color: #fff; z-index: 999999; padding: 10px; border-radius: 6px; font-size: 14px; box-shadow: 0 0 6px rgba(0,0,0,0.5); display: flex; flex-direction: column; } #ff14-helper-header { cursor: move; font-weight: bold; margin-bottom: 6px; display: flex; justify-content: space-between; align-items: center; } #ff14-helper-btns { display: flex; gap: 6px; } .ff14-helper-btn { background: #444; border-radius: 3px; padding: 2px 6px; cursor: pointer; } .ff14-helper-btn:hover { background: #666; } /* 控制区与日志 */ #ff14-control-section { flex: 0 0 auto; padding-bottom: 10px; border-bottom: 1px solid #555; } #ff14-log-section { flex: 1 1 auto; overflow-y: auto; padding-top: 10px; } .ff14-log-entry { font-size: 12px; line-height: 1.4; margin-bottom: 2px; color: #ccc; } /* 通用按钮样式 */ .ff14-action-btn { display: inline-block; margin: 5px 5px 0 0; padding: 6px 10px; background: linear-gradient(to bottom, #2196F3, #1976D2); border: 1px solid #0d47a1; border-radius: 4px; color: white; font-size: 13px; cursor: pointer; box-shadow: 0 2px 4px rgba(0,0,0,0.2); transition: all 0.2s ease; } .ff14-action-btn:hover { background: linear-gradient(to bottom, #1976D2, #1565C0); box-shadow: 0 4px 8px rgba(0,0,0,0.3); transform: translateY(-1px); } .ff14-action-btn:active { transform: translateY(0); box-shadow: 0 1px 2px rgba(0,0,0,0.2); } /* 获取按钮 (绿色) */ #ff14-fetch-btn { margin-top: 5px; padding: 8px 12px; background: linear-gradient(to bottom, #4CAF50, #45a049); border: 1px solid #2e7d32; border-radius: 4px; color: white; font-size: 14px; cursor: pointer; box-shadow: 0 2px 4px rgba(0,0,0,0.2); transition: all 0.2s ease; } #ff14-fetch-btn:hover { background: linear-gradient(to bottom, #45a049, #3d8b40); box-shadow: 0 4px 8px rgba(0,0,0,0.3); transform: translateY(-1px); } #ff14-fetch-btn:active { transform: translateY(0); box-shadow: 0 1px 2px rgba(0,0,0,0.2); } /* 通知复选框 */ #ff14-notify-only-checkbox { margin-right: 4px; } /* 角色/大区窗口 */ .ff14-selection-window { position: fixed; top: 150px; left: 50%; transform: translateX(-50%); background: rgba(30, 30, 30, 0.95); color: #fff; z-index: 1000000; padding: 10px; border-radius: 6px; font-size: 14px; box-shadow: 0 0 10px rgba(0,0,0,0.7); display: flex; flex-direction: column; } .ff14-window-header { cursor: move; font-weight: bold; margin-bottom: 10px; display: flex; justify-content: space-between; align-items: center; } .ff14-window-btns { display: flex; gap: 6px; } .ff14-window-btn { background: #444; border-radius: 3px; padding: 2px 6px; cursor: pointer; } .ff14-window-btn:hover { background: #666; } .ff14-window-content { flex: 1; display: flex; flex-direction: column; overflow: hidden; } .ff14-list-container { flex: 1; overflow-y: auto; border: 1px solid #555; padding: 5px; background-color: #222; font-size: 12px; } .ff14-list-container ul { list-style: none; padding: 0; margin: 0; } .ff14-list-container li { padding: 4px 6px; cursor: pointer; border-radius: 2px; } .ff14-list-container li:hover { background-color: #333; } .ff14-list-container li.selected { background-color: #007acc; } .ff14-action-row { display: flex; justify-content: center; gap: 10px; margin-top: 10px; } /* 状态窗口 */ #ff14-status-window { position: fixed; top: 120px; left: 50%; transform: translateX(-50%); width: 500px; height: 400px; background: rgba(30, 30, 30, 0.95); color: #fff; z-index: 999998; padding: 10px; border-radius: 6px; font-size: 14px; box-shadow: 0 0 10px rgba(0,0,0,0.7); display: flex; flex-direction: column; } #ff14-status-content { display: flex; flex: 1; overflow: hidden; } .status-column { flex: 1; overflow-y: auto; padding: 5px; border: 1px solid #555; background-color: #222; font-size: 12px; } .status-column:first-child { margin-right: 5px; } .status-item { padding: 3px; border-radius: 2px; } .status-area { cursor: pointer; } .status-area:hover { background-color: #333; } .status-area.selected { background-color: #007acc; } .status-open { color: #4CAF50; } .status-busy { color: #FFC107; } .status-blocked, .status-full { color: #f44336; } /* 选择器样式 */ select { background-color: #333; color: #fff; border: 1px solid #555; border-radius: 3px; padding: 4px; font-size: 12px; } select option { background-color: #333; color: #fff; } label { color: #ccc; font-size: 12px; } `); // =============== 工具函数 =============== function addLog(message) { const timestamp = new Date().toLocaleTimeString(); const entry = document.createElement('div'); entry.className = 'ff14-log-entry'; entry.textContent = `[${timestamp}] ${message}`; if (logContainer) { logContainer.appendChild(entry); logContainer.scrollTop = logContainer.scrollHeight; } console.log(`[${timestamp}] ${message}`); } function safeJsonParse(str, fallback = null) { try { return JSON.parse(str); } catch (e) { return fallback; } } function apiGet(url, onSuccess, onError = console.error) { GM_xmlhttpRequest({ method: 'GET', url, headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, onload: (res) => { try { const data = JSON.parse(res.responseText); if (data.return_code === 0 && data.return_message === 'ok' && data.data?.resultCode === 0 && data.data?.resultMsg === '成功') { onSuccess(data.data); } else { const msg = data.data?.resultMsg || data.return_message || '未知错误'; addLog(`❌API 错误: ${msg}`); } } catch (e) { addLog(`❌响应解析失败: ${e.message}`); onError(e); } }, onerror: (err) => { const msg = err.statusText || err.error || '网络错误'; addLog(`❌请求失败: ${msg}`); onError(err); }, ontimeout: () => addLog('❌请求超时') }); } function playBeep() { // 尝试使用 Web Audio API 播放提示音 try { if (!audioContext) { audioContext = new (window.AudioContext || window.webkitAudioContext)(); } if (audioContext.state === 'suspended') { audioContext.resume(); } const now = audioContext.currentTime; // 创建两个振荡器 - 一个主音和一个高音和声 const oscillator1 = audioContext.createOscillator(); const oscillator2 = audioContext.createOscillator(); // 主音 - 上升的积极音调 oscillator1.type = 'sine'; oscillator1.frequency.setValueAtTime(800, now); oscillator1.frequency.exponentialRampToValueAtTime(1200, now + 0.2); // 和声 - 更高的音调, 增加积极感 oscillator2.type = 'sine'; oscillator2.frequency.setValueAtTime(1200, now); oscillator2.frequency.exponentialRampToValueAtTime(1600, now + 0.2); // 创建音量包络 - 稍微长一点, 更柔和 const gainNode1 = audioContext.createGain(); const gainNode2 = audioContext.createGain(); gainNode1.gain.setValueAtTime(0, now); gainNode1.gain.linearRampToValueAtTime(0.2, now + 0.05); gainNode1.gain.exponentialRampToValueAtTime(0.01, now + 0.3); gainNode2.gain.setValueAtTime(0, now); gainNode2.gain.linearRampToValueAtTime(0.15, now + 0.05); gainNode2.gain.exponentialRampToValueAtTime(0.01, now + 0.25); // 连接节点 oscillator1.connect(gainNode1); oscillator2.connect(gainNode2); gainNode1.connect(audioContext.destination); gainNode2.connect(audioContext.destination); // 播放 oscillator1.start(); oscillator2.start(); oscillator1.stop(now + 0.3); oscillator2.stop(now + 0.25); } catch (e) { console.warn("无法播放 Web Audio 提示音:", e); } } function enableDrag(element, handle) { let isDown = false, offsetX = 0, offsetY = 0; handle.addEventListener('mousedown', (e) => { isDown = true; const rect = element.getBoundingClientRect(); offsetX = e.clientX - rect.left; offsetY = e.clientY - rect.top; }); document.addEventListener('mouseup', () => isDown = false); document.addEventListener('mousemove', (e) => { if (isDown) { element.style.left = (e.clientX - offsetX) + 'px'; element.style.top = (e.clientY - offsetY) + 'px'; element.style.transform = 'none'; } }); } // =============== 主窗口 =============== function initMainUI() { const win = document.createElement('div'); win.id = 'ff14-helper-window'; win.innerHTML = `
⚔ FF14跨区小助手
×
`; document.body.appendChild(win); logContainer = document.getElementById('ff14-log-section'); fetchButton = document.getElementById('ff14-fetch-btn'); controlSection = document.getElementById('ff14-control-section'); // 拖拽 enableDrag(win, win.querySelector('#ff14-helper-header')); // 按钮事件 document.getElementById('ff14-min-btn').addEventListener('click', () => { const ctrl = controlSection, log = logContainer; ctrl.style.display = ctrl.style.display === 'none' ? 'block' : 'none'; log.style.display = ctrl.style.display === 'none' ? 'none' : 'block'; }); document.getElementById('ff14-close-btn').addEventListener('click', () => win.style.display = 'none'); fetchButton.addEventListener('click', fetchAndProcessServerList); addLog('✔️脚本已启动, 请在网页登录后获取服务器列表...'); } function hideFetchButton() { if (fetchButton) { fetchButton.style.display = 'none'; addLog('✔️服务器列表获取成功,'); } } function showActionButtons() { controlSection.insertAdjacentHTML('beforeend', `
`); document.getElementById('ff14-select-character-btn').addEventListener('click', openCharacterSelectionWindow); document.getElementById('ff14-select-target-btn').addEventListener('click', openTargetAreaSelectionWindow); linkStartBtn = document.getElementById('ff14-link-start-btn'); linkStartBtn.addEventListener('click', toggleStatusCheck); updateLinkStartButtonState(); addLog('✔️展示操作选单, 注意请在选择角色和目标区域后才能执行LINK START'); } function updateLinkStartButtonState() { if (selectedRoleInfo && selectedTargetAreaInfo && selectedRoleInfo.selectedAreaId !== selectedTargetAreaInfo.areaId && linkStartBtn) { linkStartBtn.disabled = false; linkStartBtn.style.opacity = "1"; linkStartBtn.style.cursor = "pointer"; addLog('✔️角色和目标大区已选择, LINK START按钮已启用'); addLog('⚠️请在确认好信息后再执行 LINK START'); } else if (linkStartBtn) { linkStartBtn.disabled = true; linkStartBtn.style.opacity = "0.5"; linkStartBtn.style.cursor = "not-allowed"; } } // =============== 服务器列表获取 =============== function fetchAndProcessServerList() { addLog('🟩开始请求服务器列表...'); apiGet( 'https://ff14bjz.sdo.com/api/orderserivce/queryGroupListTravelSource?appId=100001900', (data) => { const list = safeJsonParse(data.groupList); if (!list) { addLog('❌groupList 解析失败'); return; } FF14_GROUP_LIST = list; hideFetchButton(); showActionButtons(); addLog('🟩服务器列表加载成功'); } ); } // =============== 角色选择窗口 =============== function openCharacterSelectionWindow() { addLog('🟦打开角色选择窗口...'); if (characterSelectionWindow && document.body.contains(characterSelectionWindow)) { characterSelectionWindow.style.display = 'block'; return; } characterSelectionWindow = createWindow('ff14-character-selection-window', '⚔ 查询角色'); characterSelectionWindow.innerHTML += `
`; document.body.appendChild(characterSelectionWindow); populateAreaSelect(); bindCharacterWindowEvents(); // ✅ 此时 characterSelectionWindow 已插入 body enableDrag(characterSelectionWindow, characterSelectionWindow.querySelector('.ff14-window-header')); } function bindCharacterWindowEvents() { // ✅ 使用 characterSelectionWindow.querySelector 而非 document.getElementById const win = characterSelectionWindow; // 修复: 使用正确的关闭按钮ID win.querySelector('#ff14-character-selection-window-close-btn').addEventListener('click', () => win.style.display = 'none'); win.querySelector('#ff14-cancel-role-btn').addEventListener('click', () => win.style.display = 'none'); win.querySelector('#ff14-query-roles-btn').addEventListener('click', queryRoleList); win.querySelector('#ff14-confirm-role-btn').addEventListener('click', () => { if (selectedRoleInfo) { addLog(`🟦角色选择成功: ${selectedRoleInfo.roleName} (ID: ${selectedRoleInfo.roleId})`); win.style.display = 'none'; updateLinkStartButtonState(); } else { addLog('🟥请先选择一个角色'); } }); } function populateAreaSelect() { const areaSelect = characterSelectionWindow.querySelector('#area-select'); const groupSelect = characterSelectionWindow.querySelector('#group-select'); areaSelect.innerHTML = ''; groupSelect.innerHTML = ''; if (!FF14_GROUP_LIST || !Array.isArray(FF14_GROUP_LIST)) { addLog('❌错误: FF14_GROUP_LIST 未定义或格式不正确'); return; } FF14_GROUP_LIST.forEach(area => { const opt = document.createElement('option'); opt.value = area.areaId; opt.textContent = `${area.areaName} (ID: ${area.areaId})`; areaSelect.appendChild(opt); }); areaSelect.addEventListener('change', () => populateGroupSelect(areaSelect.value)); } function populateGroupSelect(areaId) { const groupSelect = characterSelectionWindow.querySelector('#group-select'); groupSelect.innerHTML = ''; if (!areaId || !FF14_GROUP_LIST) return; const area = FF14_GROUP_LIST.find(a => a.areaId == areaId); if (area?.groups) { area.groups.forEach(g => { const opt = document.createElement('option'); opt.value = g.groupId; opt.textContent = `${g.groupName} (ID: ${g.groupId})`; groupSelect.appendChild(opt); }); } } function queryRoleList() { const areaId = characterSelectionWindow.querySelector('#area-select').value; const groupId = characterSelectionWindow.querySelector('#group-select').value; if (!areaId || !groupId) { addLog('🟥请先选择区域和服务器'); return; } addLog(`🟦查询角色: 区域 ${areaId}, 服务器 ${groupId}`); apiGet( `https://ff14bjz.sdo.com/api/gmallgateway/queryRoleList4Migration?appId=100001900&areaId=${areaId}&groupId=${groupId}`, (data) => { const roles = safeJsonParse(data.roleList); displayRoleList(roles || []); } ); } function displayRoleList(roles) { const listDiv = characterSelectionWindow.querySelector('#ff14-role-list'); const actionsDiv = characterSelectionWindow.querySelector('#ff14-role-selection-actions'); listDiv.innerHTML = ''; actionsDiv.style.display = 'none'; selectedRoleInfo = null; if (!Array.isArray(roles) || roles.length === 0) { listDiv.textContent = '该服务器下没有可用角色'; return; } const ul = document.createElement('ul'); roles.forEach(role => { const li = document.createElement('li'); li.textContent = role.roleName; li.dataset.roleId = role.roleId; li.dataset.roleName = role.roleName; li.addEventListener('click', function () { characterSelectionWindow.querySelectorAll('#ff14-role-list li.selected').forEach(el => el.classList.remove('selected')); this.classList.add('selected'); selectedRoleInfo = { roleId: this.dataset.roleId, roleName: this.dataset.roleName, selectedAreaId: characterSelectionWindow.querySelector('#area-select').value, selectedGroupId: characterSelectionWindow.querySelector('#group-select').value }; actionsDiv.style.display = 'flex'; }); ul.appendChild(li); }); listDiv.appendChild(ul); } // =============== 目标大区窗口 =============== function openTargetAreaSelectionWindow() { addLog('🟦打开目标大区选择窗口...'); if (targetAreaSelectionWindow && document.body.contains(targetAreaSelectionWindow)) { targetAreaSelectionWindow.style.display = 'block'; return; } targetAreaSelectionWindow = createWindow('ff14-target-area-selection-window', '⚔ 选择目标大区'); targetAreaSelectionWindow.innerHTML += `
`; document.body.appendChild(targetAreaSelectionWindow); populateAreaList(); bindTargetAreaWindowEvents(); // ✅ 此时 targetAreaSelectionWindow 已插入 body enableDrag(targetAreaSelectionWindow, targetAreaSelectionWindow.querySelector('.ff14-window-header')); } function bindTargetAreaWindowEvents() { // ✅ 使用 targetAreaSelectionWindow.querySelector const win = targetAreaSelectionWindow; // 修复: 使用正确的关闭按钮ID win.querySelector('#ff14-target-area-selection-window-close-btn').addEventListener('click', () => win.style.display = 'none'); win.querySelector('#ff14-cancel-target-area-btn').addEventListener('click', () => win.style.display = 'none'); win.querySelector('#ff14-confirm-target-area-btn').addEventListener('click', () => { if (selectedTargetAreaInfo) { addLog(`🟦目标大区选择成功: ${selectedTargetAreaInfo.areaName} (ID: ${selectedTargetAreaInfo.areaId})`); win.style.display = 'none'; updateLinkStartButtonState(); } else { addLog('🟥请先选择一个大区'); } }); } function populateAreaList() { const listDiv = targetAreaSelectionWindow.querySelector('#ff14-area-list'); const actionsDiv = targetAreaSelectionWindow.querySelector('#ff14-target-area-selection-actions'); listDiv.innerHTML = ''; actionsDiv.style.display = 'none'; selectedTargetAreaInfo = null; if (!FF14_GROUP_LIST || !Array.isArray(FF14_GROUP_LIST)) { listDiv.textContent = '无法加载大区列表'; return; } const ul = document.createElement('ul'); FF14_GROUP_LIST.forEach(area => { const li = document.createElement('li'); li.textContent = `${area.areaName} (ID: ${area.areaId})`; li.dataset.areaId = area.areaId; li.dataset.areaName = area.areaName; li.addEventListener('click', function () { targetAreaSelectionWindow.querySelectorAll('#ff14-area-list li.selected').forEach(el => el.classList.remove('selected')); this.classList.add('selected'); selectedTargetAreaInfo = { areaId: this.dataset.areaId, areaName: this.dataset.areaName }; actionsDiv.style.display = 'flex'; }); ul.appendChild(li); }); listDiv.appendChild(ul); } // =============== 通用窗口 =============== function createWindow(id, title) { const win = document.createElement('div'); win.id = id; win.className = 'ff14-selection-window'; win.innerHTML = `
${title}
×
`; return win; } // =============== LINK START 服务器监控 =============== function getStateText(state) { switch (state) { case 0: return '开放'; case 1: return '繁忙'; case 2: return '阻塞'; default: return '未知'; } } function toggleStatusCheck() { // 重试模式 if (linkStartBtn.textContent === '取消重试') { cancelRetry(); return; } // LINK START <-> STOP 切换 if (statusCheckIntervalId) { // 停止状态检查 if (statusCheckIntervalId) { clearInterval(statusCheckIntervalId); statusCheckIntervalId = null; } linkStartBtn.textContent = 'LINK START'; linkStartBtn.style.background = 'linear-gradient(to bottom, #2196F3, #1976D2)'; if (statusWindow && document.body.contains(statusWindow)) { document.body.removeChild(statusWindow); statusWindow = null; } addLog('⏹️停止检查服务器状态'); } else { // 开始状态检查 startStatusCheck(); } } function startStatusCheck() { linkStartBtn.textContent = 'STOP'; linkStartBtn.style.background = 'linear-gradient(to bottom, #f44336, #d32f2f)'; openStatusWindow(); queryStatus(); statusCheckIntervalId = setInterval(queryStatus, POLLING_INTERVAL_TIMEOUT); addLog('▶️开始检查服务器状态...'); } // 自动重试模式 function enterRetryMode() { // 停止当前的状态检查 if (statusCheckIntervalId) { clearInterval(statusCheckIntervalId); statusCheckIntervalId = null; } // 设置按钮为黄色"取消重试" linkStartBtn.textContent = '取消重试'; linkStartBtn.style.background = 'linear-gradient(to bottom, #FFC107, #FF9800)'; // 黄色 addLog('🟡跨区申请失败, 80秒后自动重试... 点击"取消重试"可停止流程'); // 设置80秒后自动重试 retryTimeoutId = setTimeout(() => { addLog('🔄自动重试开始...'); clearTimeout(retryTimeoutId); retryTimeoutId = null; // 重新开始状态检查 startStatusCheck(); }, RETRY_DELAY_TIMEOUT); } // 取消自动重试 function cancelRetry() { if (retryTimeoutId) { clearTimeout(retryTimeoutId); retryTimeoutId = null; } // 恢复按钮为绿色LINK START linkStartBtn.textContent = 'LINK START'; linkStartBtn.style.background = 'linear-gradient(to bottom, #2196F3, #1976D2)'; addLog('⏹️用户取消了重试, 流程已停止'); // 关闭状态窗口 if (statusWindow && document.body.contains(statusWindow)) { document.body.removeChild(statusWindow); statusWindow = null; } } function checkTargetAreaAvailability() { if (!selectedTargetAreaInfo || !FF14_STATUS_INFO) return; const targetArea = FF14_STATUS_INFO.find(area => area.areaId == selectedTargetAreaInfo.areaId); if (!targetArea) return; // 状态0或1表示可用 if (targetArea.state === 0 || targetArea.state === 1) { const notifyOnly = document.getElementById('ff14-notify-only-checkbox')?.checked || false; // 停止监控 执行STOP if (statusCheckIntervalId) { clearInterval(statusCheckIntervalId); statusCheckIntervalId = null; linkStartBtn.textContent = 'LINK START'; linkStartBtn.style.background = 'linear-gradient(to bottom, #2196F3, #1976D2)'; if (statusWindow && document.body.contains(statusWindow)) { document.body.removeChild(statusWindow); statusWindow = null; } addLog('⏹️停止检查服务器状态'); } // 显示页面通知 showAvailabilityNotification(targetArea); // 使用GM_notification通知 showGMNotification(targetArea); // 响铃 playBeep(); addLog(`✅目标 ${targetArea.areaName} 状态: ${getStateText(targetArea.state)} 可用! 🎉`); // 如果不是仅通知模式, 执行自动跨区 if (!notifyOnly) { const availableGroups = targetArea.groups && targetArea.groups.filter(group => group.queueTime === 0 // 0表示服务器开放 ); if (!availableGroups || availableGroups.length === 0) { addLog(`🚫脚本出错, 目标大区 ${targetArea.areaName} 没有可用的服务器组, 出现此错误请暂停使用等待脚本更新`); return; } addLog('🚀开始执行自动跨区传送...'); performAutoMigration(targetArea, availableGroups); } } else { addLog(`⛔目标 ${targetArea.areaName} 状态: ${getStateText(targetArea.state)} 等待30秒`); } } function showAvailabilityNotification(targetArea) { // 页面内通知 (保持原有代码) const notification = document.createElement('div'); notification.innerHTML = `

🎉 目标大区可用!

${targetArea.areaName}

状态: ${getStateText(targetArea.state)}

`; document.body.appendChild(notification); // 关闭按钮事件 notification.querySelector('#ff14-notification-close').addEventListener('click', () => { document.body.removeChild(notification); }); setTimeout(() => { if (document.body.contains(notification)) { document.body.removeChild(notification); } }, NOTIFICATION_TIMEOUT); } function showGMNotification(targetArea) { // 使用油猴的GM_notification API GM_notification({ text: `大区: ${targetArea.areaName}\n状态: ${getStateText(targetArea.state)}`, title: "🎉 FF14 目标大区可用", image: "https://ff14bjz.sdo.com/favicon.ico", timeout: NOTIFICATION_TIMEOUT, onclick: function () { window.focus(); } }); } function queryStatus() { apiGet( 'https://ff14bjz.sdo.com/api/orderserivce/queryGroupListTravelTarget?appId=100001900&areaId=-1&groupId=-1', (data) => { const list = safeJsonParse(data.groupList); if (!list) return; FF14_STATUS_INFO = list.map(area => ({ areaId: area.areaId, areaName: area.areaName, state: area.state, groups: area.groups.map(g => ({ groupId: g.groupId, groupName: g.groupName, groupCode: g.groupCode, queueTime: g.queueTime })) })); updateStatusWindowContent(FF14_STATUS_INFO); checkTargetAreaAvailability(); }, (err) => { if (statusWindow) { const areaList = statusWindow.querySelector('#ff14-status-area-list'); const serverList = statusWindow.querySelector('#ff14-status-server-list'); if (areaList) areaList.innerHTML = '
查询失败
'; if (serverList) serverList.innerHTML = '
查询失败
'; } } ); } function openStatusWindow() { if (statusWindow && document.body.contains(statusWindow)) return; statusWindow = document.createElement('div'); statusWindow.id = 'ff14-status-window'; statusWindow.innerHTML = `
⚔ 服务器状态监视器 (在主面板STOP关闭此窗口)
加载中...
加载中...
`; document.body.appendChild(statusWindow); enableDrag(statusWindow, statusWindow.querySelector('#ff14-status-header')); statusWindow.querySelector('#ff14-status-area-list').addEventListener('click', (e) => { if (e.target.classList.contains('status-area')) { statusWindow.querySelectorAll('#ff14-status-area-list .status-area.selected').forEach(el => el.classList.remove('selected')); e.target.classList.add('selected'); showServersForArea(e.target.dataset.areaId); } }); } function updateStatusWindowContent(statusInfo) { if (!statusWindow) return; const areaList = statusWindow.querySelector('#ff14-status-area-list'); const serverList = statusWindow.querySelector('#ff14-status-server-list'); if (!areaList || !serverList) return; const selectedId = statusWindow.querySelector('#ff14-status-area-list .status-area.selected')?.dataset.areaId; areaList.innerHTML = ''; statusInfo.forEach(area => { const div = document.createElement('div'); div.className = 'status-item status-area'; div.dataset.areaId = area.areaId; div.textContent = area.areaName; if (area.state === 0) div.classList.add('status-open'); else if (area.state === 1) div.classList.add('status-busy'); else div.classList.add('status-blocked'); areaList.appendChild(div); }); if (selectedId) { const el = areaList.querySelector(`[data-area-id="${selectedId}"]`); if (el) { el.classList.add('selected'); showServersForArea(selectedId); } else { serverList.innerHTML = '
请先选择区域
'; } } else if (selectedTargetAreaInfo) { showServersForArea(selectedTargetAreaInfo.areaId); } else { serverList.innerHTML = '
请先选择目标大区
'; } } function showServersForArea(areaId) { const serverList = statusWindow.querySelector('#ff14-status-server-list'); if (!serverList || !FF14_STATUS_INFO) { serverList.innerHTML = '
状态未加载
'; return; } const area = FF14_STATUS_INFO.find(a => a.areaId == areaId); if (!area) { serverList.innerHTML = '
区域未找到
'; return; } serverList.innerHTML = ''; area.groups.forEach(g => { const div = document.createElement('div'); div.className = 'status-item status-server'; div.textContent = g.groupName; if (g.queueTime === 0) div.classList.add('status-open'); else if (g.queueTime === -999) div.classList.add('status-full'); else div.classList.add('status-busy'); serverList.appendChild(div); }); } // =============== LINK START 跨区申请 =============== function performAutoMigration(targetArea, availableGroups) { // 获取源区域和服务器信息 const sourceArea = FF14_GROUP_LIST.find(area => area.areaId == selectedRoleInfo.selectedAreaId); const sourceGroup = sourceArea?.groups?.find(group => group.groupId == selectedRoleInfo.selectedGroupId); if (!sourceArea || !sourceGroup) { addLog('🚫错误: 无法获取源服务器信息, 请暂停使用脚本'); return; } let targetGroup = null; let candidateGroups = []; // 寻找可用的服务器组 (优先选择无队列的服务器) const noQueueGroups = availableGroups.filter(group => group.queueTime === 0); if (noQueueGroups.length > 0) { candidateGroups = noQueueGroups; addLog(`✅发现 ${noQueueGroups.length} 个无队列服务器, 进行随机选择`); } else { // 如果没有无队列服务器, 在所有可用服务器中随机选择 candidateGroups = availableGroups; addLog(`⚠️无完全符合期望的服务器, 在 ${availableGroups.length} 个可用服务器中随机选择`); } // 随机选择服务器 if (candidateGroups.length > 0) { const randomIndex = Math.floor(Math.random() * candidateGroups.length); targetGroup = candidateGroups[randomIndex]; addLog(`🎲随机选择: ${targetGroup.groupName} (队列时间: ${targetGroup.queueTime})`); } if (!targetGroup) { addLog('🚫错误: 无法找到可用的目标服务器组, 请暂停使用脚本'); return; } // 构建角色列表参数 const roleList = [{ roleId: selectedRoleInfo.roleId, roleName: selectedRoleInfo.roleName, key: 1 }]; // 构建请求URL const params = new URLSearchParams({ appId: '100001900', // 应用ID, 固定值 areaId: sourceArea.areaId, // 源区域ID, 从选择的角色信息中获取 areaName: sourceArea.areaName, // 源区域名称, URL编码 groupId: sourceGroup.groupId, // 源服务器组ID, 从选择的角色信息中获取 groupCode: sourceGroup.groupCode, // 源服务器组代码 groupName: sourceGroup.groupName, // 源服务器组名称, URL编码 productId: '1', // 产品ID, 固定值 productNum: '1', // 猜测为产品数量, 固定值 migrationType: '4', // 猜测为迁移类型, 4表示跨区传送, 固定值 targetArea: targetArea.areaId, // 目标区域ID, 从选择的目标区域信息中获取 targetAreaName: targetArea.areaName, // 目标区域名称, URL编码 targetGroupId: targetGroup.groupId, // 目标服务器组ID, 从目标区域中选择的第一个可用服务器组 targetGroupCode: targetGroup.groupCode, // 目标服务器组代码 targetGroupName: targetGroup.groupName, // 目标服务器组名称, URL编码 roleList: JSON.stringify(roleList), // 角色列表, JSON格式, 包含角色ID, 名称和key=1 isMigrationTimes: '0' // 暂无猜测, 目前是固定值 }); // 检查关键参数是否存在 const criticalParams = [ 'areaId', 'areaName', 'groupId', 'groupCode', 'groupName', 'targetArea', 'targetAreaName', 'targetGroupId', 'targetGroupCode', 'targetGroupName' ]; let hasMissingParams = false; criticalParams.forEach(param => { const value = params.get(param); if (!value || value === 'undefined' || value === 'null') { addLog(`🚫错误: 参数 ${param} 为空或无效: ${value}`); hasMissingParams = true; } }); if (hasMissingParams) { addLog('🚫跨区请求参数检查失败, 请暂停使用脚本并检查日志'); return; } const url = `https://ff14bjz.sdo.com/api/orderserivce/travelOrder?${params.toString()}`; addLog(`🚀发送跨区请求: 从 ${sourceArea.areaName}-${sourceGroup.groupName} 到 ${selectedTargetAreaInfo.areaName}-${targetGroup.groupName}`); console.log(`实际请求的URL: ${url}`) // 发送跨区请求 GM_xmlhttpRequest({ method: 'GET', url: url, onload: function (response) { try { const data = JSON.parse(response.responseText); if (data.return_code === 0 && data.data?.resultCode === 0) { addLog(`✅跨区申请成功! 订单ID: ${data.data.orderId}`); // 确认跳转到订单页面 showSuccessConfirmation(data.data.orderId) } else { addLog(`🚫跨区申请失败: ${data.data?.resultMsg || data.return_message || '未知错误'}`); // 进入重试模式 enterRetryMode(); } } catch (e) { addLog(`❌响应解析失败: ${e.message}`); } }, onerror: function (error) { addLog(`❌跨区请求失败: ${error.statusText || error.error || '网络错误'}`); // 进入重试模式 enterRetryMode(); } }); } // 跨区申请成功提交确认对话框 function showSuccessConfirmation(orderId) { const confirmation = document.createElement('div'); confirmation.id = 'ff14-success-confirmation'; confirmation.innerHTML = `

✅ 跨区申请已提交

您的跨区传送申请已成功提交!

订单ID: ${orderId}

点击确认按钮跳转到订单页面查看处理状态

`; document.body.appendChild(confirmation); // 确认按钮跳转到订单页面 confirmation.querySelector('#ff14-confirm-redirect').addEventListener('click', () => { document.body.removeChild(confirmation); window.location.href = 'https://ff14bjz.sdo.com/orderList'; }); } // =============== 启动 =============== if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initMainUI); } else { initMainUI(); } })();