您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
An optimized and robust helper for Torn City pickpocketing, merging Cyclist Ring and Pickpocketing Colors.
// ==UserScript== // @name Torn Pickpocketing Helper (Enhanced & Optimized) // @namespace torn.pickpocketing.helper.rebuilt.v15.2 // Updated namespace // @version 15.6 // Updated version // @description An optimized and robust helper for Torn City pickpocketing, merging Cyclist Ring and Pickpocketing Colors. // @author eaksquad, Microbes, Korbrm // @match https://www.torn.com/loader.php?sid=crimes* // @icon https://www.google.com/s2/favicons?sz=64&domain=torn.com // @grant none // @license MIT // ==/UserScript== (function() { 'use strict'; // Use the version number from the metadata for internal logging and consistency. const SCRIPT_VERSION = "15.6"; // Updated internal version to match metadata console.log(`[PPHelper v${SCRIPT_VERSION}] Script loading.`); // --- Configuration --- // User-configurable settings for the script's features. const SETTINGS = { cyclistAlerts: { enabled: true, soundUrl: 'https://audio.jukehost.co.uk/gxd2HB9RibSHhr13OiW6ROCaaRbD8103', // Custom sound for cyclist appearance highlightColor: '#00ff00', // Bright green for cyclist highlight highlightOpacity: '0.3' // Transparency level for the highlight background }, difficultyColors: { enabled: true, showCategoryText: true // Whether to append the difficulty category name (e.g., "(Safe)") to crime titles }, // NEW: Setting to control hiding the Police Officer target. hidePolice: { enabled: true // Default to enabled, can be toggled by the button. } }; // --- Data Definitions --- // Maps target names to difficulty categories. These are used for applying specific colors. const markGroups = { "Safe": ["Drunk man", "Drunk woman", "Homeless person", "Junkie", "Elderly man", "Elderly woman"], "Moderately Unsafe": ["Classy lady", "Laborer", "Postal worker", "Young man", "Young woman", "Student"], "Unsafe": ["Rich kid", "Sex worker", "Thug"], "Risky": ["Jogger", "Businessman", "Businesswoman", "Gang member", "Mobster"], "Dangerous": ["Cyclist"], // Specifically targets Cyclists for alerts. "Very Dangerous": ["Police officer"] // Typically the highest risk category. }; // Standard color mapping for each difficulty category. const categoryColorMap = { "Safe": "#37b24d", // Green "Moderately Unsafe": "#74b816", // Light Green/Yellow "Unsafe": "#f59f00", // Orange "Risky": "#f76707", // Dark Orange "Dangerous": "#f03e3e", // Red "Very Dangerous": "#7048e8" // Purple (often for special or very high risk) }; // Color mapping for borders, dynamically determined by the player's skill tiers. const skillTiers = { tier1: { "Safe": "#37b24d", "Moderately Unsafe": "#f76707", "Unsafe": "#f03e3e", "Risky": "#f03e3e", "Dangerous": "#f03e3e", "Very Dangerous": "#7048e8" }, tier2: { "Safe": "#37b24d", "Moderately Unsafe": "#37b24d", "Unsafe": "#f76707", "Risky": "#f03e3e", "Dangerous": "#f03e3e", "Very Dangerous": "#7048e8" }, tier3: { "Safe": "#37b24d", "Moderately Unsafe": "#37b24d", "Unsafe": "#37b24d", "Risky": "#f76707", "Dangerous": "#f03e3e", "Very Dangerous": "#7048e8" }, tier4: { "Safe": "#37b24d", "Moderately Unsafe": "#37b24d", "Unsafe": "#37b24d", "Risky": "#37b24d", "Dangerous": "#f76707", "Very Dangerous": "#7048e8" }, tier5: { "Safe": "#37b24d", "Moderately Unsafe": "#37b24d", "Unsafe": "#37b24d", "Risky": "#37b24d", "Dangerous": "#37b24d", "Very Dangerous": "#7048e8" } }; // --- State Management --- // Flags to track the enabled status of script features. let isCyclistAlertsEnabled = SETTINGS.cyclistAlerts.enabled; let isDifficultyColorsEnabled = SETTINGS.difficultyColors.enabled; // NEW: State variable for hiding police officers. let isHidePoliceEnabled = SETTINGS.hidePolice.enabled; // State variables for managing the cyclist sound alert's cooldown and visibility. let wasCyclistVisibleLastRun = false; let lastCyclistCheckTime = 0; // --- Enhanced Utility Functions --- /** * Helper function to debounce rapid calls to a function. * Ensures a function is only executed after a specified period of inactivity. * @param {Function} func - The function to debounce. * @param {number} wait - The number of milliseconds to delay execution. * @returns {Function} The debounced function. */ function debounce(func, wait) { let timeout; // Stores the timeout ID return function executedFunction(...args) { // `this` context and arguments are preserved for the debounced function const context = this; const later = () => { clearTimeout(timeout); // Clear the timeout so it doesn't run again func.apply(context, args); // Execute the original function }; clearTimeout(timeout); // Clear any previously scheduled execution timeout = setTimeout(later, wait); // Schedule the execution }; } /** * Plays the configured alert sound for cyclists. * Includes a fallback mechanism to generate a simple beep using the Web Audio API if the audio file fails to play. */ function playAlertSound() { try { const audio = new Audio(SETTINGS.cyclistAlerts.soundUrl); audio.volume = 0.7; // Set volume to 70% for clarity. audio.play().catch(error => { console.error(`[PPHelper v${SCRIPT_VERSION}] Audio playback failed:`, error); // Fallback to a simple beep if audio playback fails. try { const audioContext = new (window.AudioContext || window.webkitAudioContext)(); const oscillator = audioContext.createOscillator(); const gainNode = audioContext.createGain(); oscillator.connect(gainNode); gainNode.connect(audioContext.destination); oscillator.type = 'sine'; // Use a sine wave for a pure tone. oscillator.frequency.setValueAtTime(800, audioContext.currentTime); // Set frequency to 800 Hz. gainNode.gain.setValueAtTime(0.3, audioContext.currentTime); // Set gain (volume) to 30%. oscillator.start(); oscillator.stop(audioContext.currentTime + 0.5); // Stop the oscillator after 0.5 seconds. } catch (beepError) { console.error(`[PPHelper v${SCRIPT_VERSION}] Fallback beep generation also failed:`, beepError); } }); } catch (error) { console.error(`[PPHelper v${SCRIPT_VERSION}] Error creating Audio object:`, error); } } /** * Waits for a specific DOM element to appear in the document. * This is crucial for elements that are loaded asynchronously or after initial page rendering. * @param {string} selector - The CSS selector for the element to wait for. * @returns {Promise<Element>} A promise that resolves with the found element. */ function waitForElementToExist(selector) { return new Promise(resolve => { // Check if the element already exists. const existingElement = document.querySelector(selector); if (existingElement) { return resolve(existingElement); } // If not, set up a MutationObserver to watch for changes. const observer = new MutationObserver((mutations, obs) => { mutations.forEach(mutation => { // If nodes were added, check them. if (mutation.type === 'childList' && mutation.addedNodes && mutation.addedNodes.length > 0) { mutation.addedNodes.forEach(node => { // Ensure it's an element node and contains or is the target selector. if (node.nodeType === 1) { if (node.querySelector(selector)) { resolve(node.querySelector(selector)); // Resolve with the found element. obs.disconnect(); // Stop observing once found. } } }); } // Also check if the target node itself was modified and now matches. if (mutation.target.nodeType === 1 && mutation.target.matches(selector)) { resolve(mutation.target); obs.disconnect(); } }); }); // Start observing the entire document body for any changes. observer.observe(document.body, { childList: true, subtree: true }); }); } /** * Intercepts network requests to specific URLs. * This is used to detect when the game fetches crime data, signaling a good time to update styles. * @param {string} urlPart - A substring to match in the response URL (e.g., "torn.com"). * @param {string} queryPart - A substring to match in the response URL's path/query (e.g., "/page.php?sid=crimesData"). * @param {Function} callback - A function to be called with the parsed JSON response data. */ function interceptFetch(urlPart, queryPart, callback) { const originalFetch = window.fetch; // Store the original fetch function. // Override window.fetch with our custom function. window.fetch = function(...args) { // Call the original fetch and process its response. return originalFetch.apply(this, args).then(response => { // Check if the response URL matches our criteria. if (response.url.includes(urlPart) && response.url.includes(queryPart)) { // Clone the response to ensure the original response remains available. // Then, parse the cloned response as JSON. response.clone().json().then(json => { callback(json); // Pass the JSON data to the callback function. }).catch(error => { console.error(`[PPHelper v${SCRIPT_VERSION}] Intercepted fetch JSON parsing failed:`, error); callback(null); // Call callback with null if JSON parsing fails. }); } return response; // Return the original response object. }); }; } /** * Applies difficulty-based styling and cyclist highlighting to crime options, * and hides the Police Officer target if enabled. * This function is optimized to perform styling in a single pass over the crime elements, * improving efficiency and responsiveness. */ function applyAllStyling() { // Find the player's skill button to determine their current skill level. const skillButton = document.querySelector('button[aria-label^="Skill:"]'); if (!skillButton) { // If the skill button isn't found, we cannot determine skill tiers, so we exit early. return; } const skillText = skillButton.getAttribute('aria-label'); const currentSkill = parseFloat(skillText.replace('Skill: ', '')); // Extract skill value. // Determine the active color tier based on the player's current skill level. let activeTierColors; if (currentSkill < 10) { activeTierColors = skillTiers.tier1; } else if (currentSkill < 35) { activeTierColors = skillTiers.tier2; } else if (currentSkill < 65) { activeTierColors = skillTiers.tier3; } else if (currentSkill < 80) { activeTierColors = skillTiers.tier4; } else { activeTierColors = skillTiers.tier5; } // Find all primary containers for crime options. These are the elements we will style or hide. const allCrimeWrappers = document.querySelectorAll('div[class*="crimeOptionWrapper"]'); let isCyclistVisibleThisRun = false; // Flag to track if any cyclist is detected in the current update cycle. // --- Single Pass Styling & Hiding: Iterate through each crime option wrapper --- allCrimeWrappers.forEach(container => { // --- Reset all previous styling applied by this script --- // Resetting display to '' ensures elements are visible by default before the hiding logic is applied. container.style.backgroundColor = ''; container.style.borderLeft = ''; container.style.boxShadow = ''; container.style.border = ''; // Reset any custom script border. container.style.display = ''; // Reset display to ensure elements are visible by default. container.classList.remove('cyclist-highlight'); // Remove the specific cyclist highlight class. // Find the title element within the container, which typically holds the target's name. const titleElement = container.querySelector('div[class*="titleAndProps"] > div'); if (!titleElement) { // If a container lacks a title element, skip it. return; } // Extract the base target name, stripping any category suffix (e.g., "(Safe)") that might have been added by previous runs of this script. const originalTextFull = titleElement.textContent.trim(); const originalTextBase = originalTextFull.split(' (')[0]; // --- Police Officer Hiding Logic --- // If hiding police is enabled and the target is a "Police officer", hide the entire container. if (isHidePoliceEnabled && originalTextBase === "Police officer") { container.style.display = 'none'; // Skip all further styling and checks for this hidden element. return; } // --- Reset title element styles --- titleElement.style.color = ''; titleElement.style.fontWeight = ''; titleElement.style.textShadow = ''; // --- Difficulty Coloring Logic --- let category = null; // Variable to hold the determined difficulty category. if (isDifficultyColorsEnabled) { // Find the difficulty category for the current target name. category = Object.keys(markGroups).find(cat => markGroups[cat].includes(originalTextBase)); if (category) { // Apply text color based on the category's standard color map. titleElement.style.color = categoryColorMap[category]; // Apply the left border color based on the player's skill tier and the target's category. container.style.borderLeft = `3px solid ${activeTierColors[category]}`; // Optionally append the category name to the title text if configured and the screen width is sufficient. if (SETTINGS.difficultyColors.showCategoryText && window.innerWidth > 400) { titleElement.textContent = `${originalTextBase} (${category})`; } else { // Otherwise, ensure only the base target name is displayed. titleElement.textContent = originalTextBase; } } else { // If the target name is not found in any category, ensure it displays only its base name. titleElement.textContent = originalTextBase; } } else { // If difficulty colors are globally disabled, ensure only the base target name is displayed. titleElement.textContent = originalTextBase; } // --- Cyclist Highlighting Logic --- // Determine if the current target is a cyclist. This check prioritizes exact matches and falls back to broader string checks. const isCurrentTargetACyclist = ( originalTextBase.toLowerCase() === 'cyclist' || // Exact match for "Cyclist". originalTextBase.toLowerCase().includes('cyclist') || // Check if "cyclist" is part of the name. container.textContent.toLowerCase().includes('cyclist') // Broader fallback: check entire container text. ); if (isCurrentTargetACyclist) { isCyclistVisibleThisRun = true; // Mark that at least one cyclist has been found in this scan. if (isCyclistAlertsEnabled) { // Apply specific, prominent styles for cyclist highlights. container.style.backgroundColor = SETTINGS.cyclistAlerts.highlightColor + SETTINGS.cyclistAlerts.highlightOpacity; container.style.boxShadow = `0 0 15px ${SETTINGS.cyclistAlerts.highlightColor}`; container.style.border = `2px solid ${SETTINGS.cyclistAlerts.highlightColor}`; // Override border if it was set by difficulty coloring. container.classList.add('cyclist-highlight'); // Add the CSS class which includes the pulsating animation. // Enhance the title text for better visibility. titleElement.style.fontWeight = 'bold'; titleElement.style.textShadow = `0 0 5px ${SETTINGS.cyclistAlerts.highlightColor}`; } } }); // --- Sound Alert Logic --- // Trigger the sound alert if: // 1. Cyclist alerts are enabled. // 2. A cyclist was visible in the current update cycle (`isCyclistVisibleThisRun`). // 3. No cyclist was visible in the previous run (`!wasCyclistVisibleLastRun`), indicating a new appearance. // 4. The cooldown period has passed since the last alert was triggered. const currentTime = Date.now(); if (isCyclistAlertsEnabled && isCyclistVisibleThisRun && !wasCyclistVisibleLastRun) { if (currentTime - lastCyclistCheckTime > 2000) { // Enforce a 2-second cooldown to prevent rapid, annoying alerts. console.log(`[PPHelper v${SCRIPT_VERSION}] Cyclist has appeared! Playing sound.`); playAlertSound(); // Play the alert sound. lastCyclistCheckTime = currentTime; // Record the time of this alert. } } // Update the state variable for the next cycle to track visibility continuity. wasCyclistVisibleLastRun = isCyclistVisibleThisRun; } /** * Initializes the DOM observer and periodic checks to detect changes * and trigger styling updates efficiently. */ function initializeCrimeObserver() { // Method 1: Intercept fetch requests for `crimesData`. // This is often the most reliable trigger as the game explicitly fetches this data when crime lists update. interceptFetch("torn.com", "/page.php?sid=crimesData", () => { console.log(`[PPHelper v${SCRIPT_VERSION}] Intercepted crimesData, scheduling style update.`); // Use setTimeout to allow the DOM to potentially update after fetch success before styling. // The debounced call prevents multiple rapid updates if multiple fetches occur. debouncedApplyAllStyling(); }); // Method 2: Use a MutationObserver to watch for changes in the DOM. // This catches dynamic content additions that might not directly correspond to a fetch call. const observer = new MutationObserver((mutations) => { let shouldUpdate = false; // Iterate through all mutations detected. mutations.forEach((mutation) => { // We are primarily interested in nodes that were added to the DOM. if (mutation.type === 'childList' && mutation.addedNodes && mutation.addedNodes.length > 0) { mutation.addedNodes.forEach((node) => { // Check if the added node is an element and if it, or its descendants, contain elements relevant to the script. if (node.nodeType === 1) { // nodeType 1 indicates an Element node. // Heuristic check: if the node contains crime wrappers, relevant buttons, or the keyword 'cyclist', // it's a strong indication that the crime list has been updated. if (node.querySelector('div[class*="crimeOptionWrapper"]') || node.querySelector('button[aria-label="Pickpocket, 5 nerve"]') || node.textContent.toLowerCase().includes('cyclist')) { shouldUpdate = true; } } }); } }); if (shouldUpdate) { console.log(`[PPHelper v${SCRIPT_VERSION}] DOM mutation detected, scheduling style update.`); // Call the debounced version of applyAllStyling to prevent rapid, repeated executions. debouncedApplyAllStyling(); } }); // Start observing the entire document body for changes. // `childList: true` observes direct children, `subtree: true` observes all descendants. // This ensures comprehensive detection of dynamic content changes. observer.observe(document.body, { childList: true, subtree: true, // characterData: true // Generally not needed unless text content changes directly without node replacement. }); // Method 3: Periodic check as a fallback. // This serves as a safety net to ensure styling is applied even if observers somehow miss an update or if the page state doesn't trigger them. // It runs every 5 seconds. setInterval(() => { // Only perform the check if relevant elements are present on the page, to avoid unnecessary operations. if (document.querySelector('div[class*="crimeOptionWrapper"]')) { console.log(`[PPHelper v${SCRIPT_VERSION}] Periodic check triggered, applying styles.`); // Use the debounced function for the periodic check as well. debouncedApplyAllStyling(); } }, 5000); } /** * Sets up the user interface elements (buttons) for controlling the script's features. * This function has been refactored to use vanilla JavaScript, removing the jQuery dependency. * The "Test Sound" button is repurposed to toggle the "Hide Police" functionality. */ function setupInterface() { // Wait for the main container element of the pickpocketing section to ensure it has loaded. waitForElementToExist('.pickpocketing-root').then((pickpocketingRoot) => { // Prevent re-adding controls if they already exist (e.g., during rapid reloads or script updates). if (document.getElementById('pp-helper-controls')) return; // HTML structure for the control panel containing toggle buttons. // The 'Test Sound' button is repurposed for controlling the 'Hide Police' feature. const controlsContainerHTML = ` <div id="pp-helper-controls" style="margin-bottom: 10px; display: flex; gap: 10px; flex-wrap: nowrap; overflow-x: auto; padding-bottom: 5px;"> <button id="cyclist-toggle-btn" class="torn-btn" style="min-width: 120px;">Cyclist Alerts: ON</button> <button id="colors-toggle-btn" class="torn-btn" style="min-width: 120px;">Risk Colors: ON</button> <button id="police-hide-btn" class="torn-btn" style="background: #2196F3; color: white; min-width: 120px;">Hide Police: ON</button> </div> `; // Insert the HTML for the controls into the page using a native DOM method. pickpocketingRoot.insertAdjacentHTML('afterbegin', controlsContainerHTML); // Get references to the newly created buttons using native DOM methods. const cyclistBtn = document.getElementById('cyclist-toggle-btn'); const colorsBtn = document.getElementById('colors-toggle-btn'); // Renamed button for its new function const policeHideBtn = document.getElementById('police-hide-btn'); /** * Updates the text and visual styling of the toggle buttons to accurately reflect their current enabled state. */ function updateButtons() { // Update text content for cyclist alerts. cyclistBtn.textContent = `Cyclist Alerts: ${isCyclistAlertsEnabled ? 'ON' : 'OFF'}`; cyclistBtn.style.background = isCyclistAlertsEnabled ? '#4CAF50' : ''; // Green when ON. cyclistBtn.style.color = isCyclistAlertsEnabled ? 'white' : ''; // Update text content for difficulty colors. colorsBtn.textContent = `Difficulty Colors: ${isDifficultyColorsEnabled ? 'ON' : 'OFF'}`; colorsBtn.style.background = isDifficultyColorsEnabled ? '#4CAF50' : ''; // Green when ON. colorsBtn.style.color = isDifficultyColorsEnabled ? 'white' : ''; // Update text content and styling for the police hiding button. policeHideBtn.textContent = `Hide Police: ${isHidePoliceEnabled ? 'ON' : 'OFF'}`; policeHideBtn.style.background = isHidePoliceEnabled ? '#4CAF50' : ''; // Green when ON. policeHideBtn.style.color = isHidePoliceEnabled ? 'white' : ''; } /** * Attempts to trigger a page refresh or directly re-applies styles. * This is useful to ensure that UI changes (like toggling features) are immediately visible. */ function forceRefreshOrApplyStyles() { // Try to find and click the game's built-in refresh button for a natural refresh. // The selector for the refresh button might change with Torn updates. const refreshButton = document.querySelector('div[class*="refresh-icon_"]'); // Example selector. if (refreshButton) { refreshButton.click(); } else { // If the refresh button is not found, directly re-apply all styles. console.log(`[PPHelper v${SCRIPT_VERSION}] Refresh button not found, forcing style re-application.`); // Use the debounced function for safety against rapid calls. debouncedApplyAllStyling(); } } // Add event listeners to the buttons for user interaction. cyclistBtn.addEventListener('click', () => { isCyclistAlertsEnabled = !isCyclistAlertsEnabled; // Toggle the cyclist alerts setting. updateButtons(); // Update the button's appearance. forceRefreshOrApplyStyles(); // Apply the new setting to the crime list. }); colorsBtn.addEventListener('click', () => { isDifficultyColorsEnabled = !isDifficultyColorsEnabled; // Toggle the difficulty colors setting. updateButtons(); // Update the button's appearance. forceRefreshOrApplyStyles(); // Apply the new setting to the crime list. }); // NEW EVENT LISTENER: Repurposed button toggles Police hiding. policeHideBtn.addEventListener('click', () => { isHidePoliceEnabled = !isHidePoliceEnabled; // Toggle the hide police setting. updateButtons(); // Update the button's appearance. // Re-applying styles will now hide/show the police officer based on the new setting. forceRefreshOrApplyStyles(); }); // Initialize the buttons' appearance based on current settings. updateButtons(); // Apply styling to the crime list initially. A slight delay helps ensure the DOM is fully ready. setTimeout(debouncedApplyAllStyling, 500); }); } // --- Script Entry Point --- console.log(`[PPHelper v${SCRIPT_VERSION}] Initializing...`); // Define the debounced styling function here so it's available globally within the script's scope. const debouncedApplyAllStyling = debounce(applyAllStyling, 200); // 200ms debounce delay. // Ensure the script logic executes only after the DOM is fully loaded and parsed. if (document.readyState === 'loading') { // If the document is still loading, wait for the 'DOMContentLoaded' event. document.addEventListener('DOMContentLoaded', () => { setupInterface(); // Set up the user interface controls. initializeCrimeObserver(); // Initialize the DOM observers and periodic checks. }); } else { // If the document is already loaded (e.g., script injected after page load), proceed immediately. setupInterface(); initializeCrimeObserver(); } // Inject CSS for cyclist highlighting animation and smoother style transitions. const style = document.createElement('style'); style.textContent = ` /* Keyframes for the pulsating animation on cyclist highlights. */ @keyframes cyclistPulse { 0% { opacity: 1; } 50% { opacity: 0.7; } 100% { opacity: 1; } } /* Apply the animation to elements with the cyclist-highlight class. */ .cyclist-highlight { animation: cyclistPulse 2s infinite; } /* Add smooth transitions to crime option wrappers for visual feedback when styles change. */ div[class*="crimeOptionWrapper"] { transition: background-color 0.3s ease, border-left 0.3s ease, box-shadow 0.3s ease, display 0.1s ease-in-out; /* Added display transition for smoother hiding/showing */ } /* Basic styling for control panel buttons to ensure consistency. */ #pp-helper-controls button.torn-btn { cursor: pointer; /* Indicates the button is clickable. */ padding: 6px 12px; /* Standard padding for buttons. */ border-radius: 4px; /* Slightly rounded corners. */ border: 1px solid #555; /* Default border. */ background-color: #3a3a3a; /* Dark background, typical for Torn UI elements. */ color: #eee; /* Light text color. */ font-size: 0.9em; /* Readable font size. */ display: inline-flex; /* Helps align content within the button. */ align-items: center; /* Vertically center text. */ justify-content: center; /* Horizontally center text. */ flex-shrink: 0; /* Prevent buttons from shrinking, ensuring they stay in one row */ } #pp-helper-controls button.torn-btn:hover { background-color: #555; /* Slightly lighter background on hover. */ } `; document.head.appendChild(style); console.log(`[PPHelper v${SCRIPT_VERSION}] Script fully loaded and initialized.`); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址