Android短信監控技術實現:合法合規的遠程采集方案

一年經驗的全棧程序員,目前頭發健在,但不知道能撐多久。

該項目已成功部署并穩定運行于企業生產環境,如需個性化定制方案,歡迎聯系作者進行深度合作。

文章目錄

前言

一、頁面設計

1.頁面顯示

?2.代碼實現

?二、具體代碼實現

1.添加網絡權限和短信權限

2.實現短信監聽(BroadcastReceiver)

3.AndroidManifest.xml?中注冊廣播接收器

4. 封裝網絡請求(HttpURLConnection)?

?三、MainActivity主程序編寫

1. 權限管理模塊

2. 短信接收與處理模塊?

?3. 數據存儲與展示模塊

4. 用戶配置管理模塊

5. 定時清理模塊(可選)?

?總結

🙌?求點贊、收藏、關注!?


前言

由于公司業務需求需要監控大批手機號的驗證碼所以有了這個項目,在 Android 應用開發中,短信監控通常用于合規場景,如企業設備管理、金融風控(驗證碼自動填充)或家長監護。不同于直接讀取短信數據庫(ContentProvider),使用?BroadcastReceiver?監聽短信廣播(android.provider.Telephony.SMS_RECEIVED)是一種更輕量、實時性更強的方案。

本文將介紹如何通過?BroadcastReceiver?捕獲短信,并使用?原生?HttpURLConnection(而非第三方庫)將數據安全上傳至服務器,涵蓋以下關鍵點:

  1. 短信監聽機制:注冊廣播接收器,過濾有效短信(如特定發送方或驗證碼)。

  2. 網絡請求實現:手動封裝?HttpURLConnection,支持?POST/GET?請求。

  3. 安全與合規性

    • 動態申請?RECEIVE_SMS?權限,確保用戶知情同意。

    • 避免存儲敏感信息,僅傳輸必要數據。

注意:未經用戶授權的短信監控屬于違法行為,本文僅限技術探討,請確保應用符合 Google Play 政策及《個人信息保護法》。


一、頁面設計

由于沒有做統一的日志管理但是也需要查看短信是否有監聽到使用頁面需要顯示監聽的手機號和內容。

1.頁面顯示

?2.代碼實現

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity"><!-- 應用名稱標題 --><TextViewandroid:id="@+id/appNameTextView"android:layout_width="wrap_content"android:layout_height="wrap_content"android:padding="16dp"android:text="短信接收器"android:textSize="20sp"android:textStyle="bold"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintRight_toRightOf="parent"app:layout_constraintTop_toTopOf="parent" /><!-- 卡1標題 --><Buttonandroid:id="@+id/baoc"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginEnd="16dp"android:onClick="save"android:text="保存"app:layout_constraintBottom_toBottomOf="@+id/appNameTextView"app:layout_constraintRight_toRightOf="parent"app:layout_constraintTop_toTopOf="@+id/appNameTextView" /><TextViewandroid:id="@+id/card1TitleTextView"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginStart="16dp"android:layout_marginTop="16dp"android:text="卡1:"android:textSize="16sp"android:textStyle="bold"app:layout_constraintBottom_toBottomOf="@+id/editTextPhone1"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintTop_toBottomOf="@+id/appNameTextView" /><!-- 卡1輸入框 --><EditTextandroid:id="@+id/editTextPhone1"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_marginStart="8dp"android:layout_marginEnd="8dp"android:hint="輸入卡1號碼"android:textSize="16sp"app:layout_constraintLeft_toRightOf="@+id/card1TitleTextView"app:layout_constraintRight_toLeftOf="@+id/switch1"app:layout_constraintTop_toTopOf="@+id/card1TitleTextView" /><!-- 卡1開關 --><Switchandroid:id="@+id/switch1"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginEnd="16dp"app:layout_constraintBottom_toBottomOf="@+id/editTextPhone1"app:layout_constraintRight_toRightOf="parent"app:layout_constraintTop_toTopOf="@+id/editTextPhone1" /><!-- 卡2標題 --><TextViewandroid:id="@+id/card2TitleTextView"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginStart="16dp"android:layout_marginTop="16dp"android:text="卡2:"android:textSize="16sp"android:textStyle="bold"app:layout_constraintBottom_toBottomOf="@+id/editTextPhone2"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintTop_toBottomOf="@+id/editTextPhone1" /><!-- 卡2輸入框 --><EditTextandroid:id="@+id/editTextPhone2"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_marginStart="8dp"android:layout_marginEnd="8dp"android:hint="輸入卡2號碼"android:textSize="16sp"app:layout_constraintLeft_toRightOf="@+id/card2TitleTextView"app:layout_constraintRight_toLeftOf="@+id/switch2"app:layout_constraintTop_toTopOf="@+id/card2TitleTextView" /><!-- 卡2開關 --><Switchandroid:id="@+id/switch2"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginEnd="16dp"app:layout_constraintBottom_toBottomOf="@+id/editTextPhone2"app:layout_constraintRight_toRightOf="parent"app:layout_constraintTop_toTopOf="@+id/editTextPhone2" /><!-- 短信內容顯示框 --><TextViewandroid:id="@+id/smsTextView"android:layout_width="0dp"android:layout_height="300dp"android:layout_marginTop="8dp"android:background="#000000"android:padding="8dp"android:text="監控短信中"android:textColor="#FFFFFF"android:scrollbars="vertical"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintRight_toRightOf="parent"app:layout_constraintTop_toBottomOf="@+id/editTextPhone2" /></androidx.constraintlayout.widget.ConstraintLayout>

為什么這里需要設置卡號顯示起除是因為安卓開發系統的匹配度問題有些手機系統是不能自動識別手機號的所以顯示這個卡號是看看有沒有自動識別,如果沒有需要手動輸入并且保存,以為后面是根據具體卡槽id識別是哪個手機號接收到的驗證碼。


?二、具體代碼實現

1.添加網絡權限和短信權限

在AndroidManifest.xml

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />

2.實現短信監聽(BroadcastReceiver)

新建一個SMSReceiver.class服務監聽短信

public class SMSReceiver extends BroadcastReceiver {@Overridepublic void onReceive(Context context, Intent intent) {// 1. 從廣播意圖中提取短信數據Bundle bundle = intent.getExtras();if (bundle != null) {// 2. 獲取短信PDU數組和SIM卡訂閱IDObject[] pdus = (Object[]) bundle.get("pdus");int subscriptionId = bundle.getInt("subscription", -1);if (pdus != null) {for (Object pdu : pdus) {// 3. 解析短信內容(發送方、正文)SmsMessage smsMessage = SmsMessage.createFromPdu((byte[]) pdu);String messageBody = smsMessage.getMessageBody();String sender = smsMessage.getDisplayOriginatingAddress();// 4. 獲取SIM卡槽位信息(雙卡場景)String slotInfo = getSlotInfo(context, subscriptionId);// 5. 發送自定義廣播傳遞短信數據Intent smsIntent = new Intent("smsReceived");smsIntent.putExtra("message", messageBody);smsIntent.putExtra("sender", sender);smsIntent.putExtra("slotInfo", slotInfo);context.sendBroadcast(smsIntent);}}}}/*** 獲取SIM卡槽位信息(兼容雙卡)* @param subscriptionId SIM卡訂閱ID* @return 如 "Slot 1" 或 "Unknown Slot"*/private String getSlotInfo(Context context, int subscriptionId) {// 實現邏輯:通過SubscriptionManager查詢對應SIM卡槽// ...}/*** 獲取接收方手機號(需權限)* @note 因權限問題已注釋,實際使用需動態申請READ_PHONE_STATE權限*/@RequiresApi(api = Build.VERSION_CODES.N)private String getReceiverPhoneNumber(Context context, int subscriptionId) {// 實現邏輯:通過TelephonyManager獲取本機號碼// ...}
}

3.AndroidManifest.xml?中注冊廣播接收器

 <receiver android:name=".SMSReceiver"android:exported="true"android:enabled="true"><intent-filter><action android:name="android.provider.Telephony.SMS_RECEIVED"/></intent-filter></receiver>

4. 封裝網絡請求(HttpURLConnection)?

public class MyRequest {public String post(String url1, String data) {try {URL url = new URL(url1);HttpURLConnection Connection = (HttpURLConnection) url.openConnection();//創建連接Connection.setRequestMethod("POST");Connection.setConnectTimeout(3000);Connection.setReadTimeout(3000);Connection.setDoInput(true);Connection.setDoOutput(true);Connection.setUseCaches(false);Connection.connect();DataOutputStream dos = new DataOutputStream(Connection.getOutputStream());String title = data;//這里是POST請求需要的參數字符串類型,例如"id=1&data=2"dos.write(title.getBytes());dos.flush();dos.close();//寫完記得關閉int responseCode = Connection.getResponseCode();if (responseCode == Connection.HTTP_OK) {//判斷請求是否成功InputStream inputStream = Connection.getInputStream();ByteArrayOutputStream arrayOutputStream = new ByteArrayOutputStream();byte[] bytes = new byte[1024];int length = 0;while ((length = inputStream.read(bytes)) != -1) {arrayOutputStream.write(bytes, 0, length);arrayOutputStream.flush();}//讀取響應體的內容String s = arrayOutputStream.toString();return s;//返回請求到的內容,字符串形式} else {return "-1";//如果請求失敗返回-1}} catch (Exception e) {return "-1";//出現異常也返回-1}}public String get(String url1) {try {URL url = new URL(url1);HttpURLConnection Connection = (HttpURLConnection) url.openConnection();Connection.setRequestMethod("GET");Connection.setConnectTimeout(3000);Connection.setReadTimeout(3000);int responseCode = Connection.getResponseCode();if (responseCode == Connection.HTTP_OK) {InputStream inputStream = Connection.getInputStream();ByteArrayOutputStream arrayOutputStream = new ByteArrayOutputStream();byte[] bytes = new byte[1024];int length = 0;while ((length = inputStream.read(bytes)) != -1) {arrayOutputStream.write(bytes, 0, length);arrayOutputStream.flush();//強制釋放緩沖區}String s = arrayOutputStream.toString();return s;} else {return "-1";}} catch (Exception e) {return "-1"+e;}}}


?三、MainActivity主程序編寫

1. 權限管理模塊

// 定義所需權限
private static final int PERMISSION_REQUEST_CODE = 1;
String[] permissions = {Manifest.permission.READ_SMS, Manifest.permission.RECEIVE_SMS,Manifest.permission.READ_PHONE_STATE,Manifest.permission.READ_PHONE_NUMBERS
};// 檢查并請求權限
if (未全部授權) {ActivityCompat.requestPermissions(this, permissions, PERMISSION_REQUEST_CODE);
} else {registerSmsReceiver(); // 注冊廣播接收器
}// 權限請求結果回調
@Override
public void onRequestPermissionsResult(...) {if (權限通過) {registerSmsReceiver();} else {showToast("短信監控功能不可用");}
}

2. 短信接收與處理模塊?

// 自定義廣播接收器
private BroadcastReceiver smsReceiver = new BroadcastReceiver() {@Overridepublic void onReceive(Context context, Intent intent) {// 解析短信數據String message = intent.getStringExtra("message");String sender = intent.getStringExtra("sender");String slotInfo = intent.getStringExtra("slotInfo");// 根據SIM卡槽匹配接收號碼String receiverNumber = slotInfo.contains("1") ? editTextPhone1.getText().toString() : editTextPhone2.getText().toString();// 過濾重復消息if (pdMessages(message)) {// 啟動網絡請求線程new Thread(() -> {String url = "服務器接口;String response = new MyRequest().get(url);handleResponse(response, receiverNumber, sender, message);}).start();}}
};// 消息去重檢查
private boolean pdMessages(String mess) {Set<String> savedMessages = sp.getStringSet("messages", new HashSet<>());if (savedMessages.contains(mess)) {return false; // 重復消息}savedMessages.add(mess);sp.edit().putStringSet("messages", savedMessages).apply();return true;
}

?3. 數據存儲與展示模塊

// 存儲消息記錄(包含狀態和時間戳)
private void storeMessageStatus(String receiver, String sender, String message, String status) {String key = "message_" + System.currentTimeMillis();String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());String record = String.format("接收號: %s\n發送方: %s\n時間: %s\n內容: %s\n狀態: %s\n",receiver, sender, time, message, status);sp.edit().putString(key + "_data", record).putLong(key + "_time", System.currentTimeMillis()).apply();
}// 顯示歷史消息
private void displayStoredMessages() {StringBuilder sb = new StringBuilder();for (Map.Entry<String, ?> entry : sp.getAll().entrySet()) {if (entry.getKey().endsWith("_data")) {sb.append(entry.getValue()).append("\n");}}smsTextView.setText(sb.length() > 0 ? sb.toString() : "無記錄");
}

4. 用戶配置管理模塊

// 保存用戶設置的手機號
public void save(View view) {sp.edit().putString("editTextPhone1", editTextPhone1.getText().toString()).putString("editTextPhone2", editTextPhone2.getText().toString()).apply();showToast("保存成功");
}// 初始化時加載配置
private void loadConfig() {editTextPhone1.setText(sp.getString("editTextPhone1", ""));editTextPhone2.setText(sp.getString("editTextPhone2", ""));
}

5. 定時清理模塊(可選)?

// 設置每天中午12點清理舊數據
private void setDailyCleanupAlarm() {AlarmManager alarmManager = (AlarmManager) getSystemService(ALARM_SERVICE);Intent intent = new Intent(this, CleanupReceiver.class);PendingIntent pendingIntent = PendingIntent.getBroadcast(...);Calendar calendar = Calendar.getInstance();calendar.set(Calendar.HOUR_OF_DAY, 12); // 12:00 PMif (已過當前時間) calendar.add(Calendar.DAY_OF_YEAR, 1);alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), pendingIntent);
}

?總結

  1. 實現了雙卡短信監控:通過BroadcastReceiver捕獲短信,并根據SIM卡槽自動匹配預設的手機號,支持雙卡場景下的短信分類處理。

  2. 數據安全與合規性:動態申請權限確保用戶知情權,使用SharedPreferences存儲消息記錄,避免敏感信息泄露,符合隱私保護要求。

  3. 網絡上傳與狀態反饋:通過HttpURLConnection將短信內容上傳至服務器,并實時顯示發送狀態(成功/失敗),數據持久化便于追溯。

  4. 可擴展性強:模塊化設計(權限管理、消息處理、數據存儲)便于后續擴展,如增加加密傳輸或對接其他API。

🙌?求點贊、收藏、關注!?

如果這篇文章對你有幫助,不妨:
👍?點個贊?→ 讓更多人看到這篇干貨!
??收藏一下?→ 方便以后隨時查閱!
🔔?加關注?→ 獲取更多?前端/后端/全棧技術深度解析

你的支持,是我持續創作的最大動力!?🚀

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/78360.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/78360.shtml
英文地址,請注明出處:http://en.pswp.cn/web/78360.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

前端跨域問題怎么在后端解決

目錄 簡單的解決方法&#xff1a; 添加配置類&#xff1a; 為什么會跨域 1. 什么是源 2. URL結構 3. 同源不同源舉&#x1f330; 同源例子 不同源例子 4. 瀏覽器為什么需要同源策略 5. 常規前端請求跨域 簡單的解決方法&#xff1a; 添加配置類&#xff1a; packag…

【中間件】brpc_基礎_execution_queue

execution_queue 源碼 1 簡介 execution_queue.h 是 Apache BRPC 中實現 高性能異步任務執行隊列 的核心組件&#xff0c;主要用于在用戶態線程&#xff08;bthread&#xff09;中實現任務的 異步提交、有序執行和高效調度。 該模塊通過解耦任務提交與執行過程&#xff0c;提…

java學習之數據結構:一、數組

主要是對數組所有的東西進行總結&#xff0c;整理 適合小白~ 目錄 1.什么是數組 1.1數組定義 1.2數組創建 1&#xff09;靜態創建 2&#xff09;動態創建 1.3數組遍歷 1&#xff09;for和while遍歷 2&#xff09;foreach遍歷 2.數組越界問題及解決 2.1數組越界問題 2…

[Survey]SAM2 for Image and Video Segmentation: A Comprehensive Survey

BaseInfo TitleSAM2 for Image and Video Segmentation: A Comprehensive SurveyAdresshttps://arxiv.org/abs/2503.12781Journal/Time2503Author四川大學&#xff0c;北京大學 1. Introduction 圖像分割專注于識別單個圖像中的目標、邊界或紋理&#xff0c;而視頻分割則將這…

用Maven定位和解決依賴沖突

用Maven定位和解決依賴沖突 一、依賴沖突的常見表現二、定位沖突依賴的4種方法2.1 使用Maven命令分析依賴樹2.2 使用IDE可視化工具2.3 使用Maven Enforcer插件2.4 運行時分析 三、解決依賴沖突的5種方案3.1 排除特定傳遞依賴3.2 統一指定版本&#xff08;推薦&#xff09;3.3 使…

穿越數據森林與網絡迷宮:樹與圖上動態規劃實戰指南

在 C 算法的浩瀚宇宙中&#xff0c;樹與圖就像是神秘的迷宮和茂密的森林&#xff0c;充滿了未知與挑戰。而動態規劃則是我們探索其中的神奇羅盤&#xff0c;幫助我們找到最優路徑。今天&#xff0c;就讓我們一起深入這片神秘領域&#xff0c;揭開樹與圖上動態規劃的神秘面紗&am…

UDP / TCP 協議

目錄 一、前言&#xff1a; 數據封裝與分用&#xff1a; 二、網絡協議分層模型&#xff1a; 三、UDP / TCP 協議 UDP 協議&#xff1a; 1、UDP 協議段格式&#xff1a; 2、UDP 的特點&#xff1a; TCP 協議&#xff1a; 1、TCP 協議段格式&#xff1a; 2、TCP 協議的十…

Python 實現的運籌優化系統數學建模詳解(動態規劃模型)

相關代碼鏈接&#xff1a;https://download.csdn.net/download/heikediguoshinib/90713747?spm1001.2014.3001.5503 一、引言 在計算機科學與數學建模的廣闊領域中&#xff0c;算法如同精密的齒輪&#xff0c;推動著問題的解決與系統的運行。當面對復雜的優化問題時&…

langfuse本地安裝

目錄 安裝命令項目準備用openai測試 安裝命令 本地&#xff08;docker compose&#xff09;&#xff1a;使用 Docker Compose 在你的機器上于 5 分鐘內運行 Langfuse。 # 獲取最新的 Langfuse 倉庫副本 git clone https://github.com/langfuse/langfuse.git cd langfuse# 運行 …

每天學一個 Linux 命令(35):dos2unix

每天學一個 Linux 命令(35):dos2unix 命令簡介 dos2unix 是一個用于將 Windows/DOS 格式的文本文件轉換為 Unix/Linux 格式的實用工具。它主要處理行尾符的轉換(將 CRLF 轉換為 LF),同時也能處理編碼問題和字符集轉換。這個命令在跨平臺文件共享、代碼遷移和系統管理場…

第6章 Python 基本數據類型詳解(int, float, bool, str)細節補充

文章目錄 Python 基本數據類型深入解析(int, float, bool, str)一、整型(int)的底層機制二、浮點型(float)的陷阱與解決方案三、布爾型(bool)的底層本質四、字符串(str)的不可變性與優化五、類型間的隱式轉換與陷阱六、性能優化與工具總結:關鍵細節與最佳實踐Python…

19. LangChain安全與倫理:如何避免模型“幻覺“與數據泄露?

引言&#xff1a;當AI成為企業"數字員工"時的責任邊界 2025年某金融機構因AI客服泄露用戶信用卡信息被罰款2300萬美元。本文將基于LangChain的安全架構與Deepseek-R1的合規實踐&#xff0c;揭示如何構建既強大又安全的AI系統。 一、AI安全風險矩陣 1.1 2025年最新威…

Java快速上手之實驗六

1. 編寫ItemEventDemo.java&#xff0c;當選中或取消選中單選鈕、復選鈕和列表框時顯示所選的結果。 2&#xff0e;編寫GUIExample.java&#xff0c;當選中或取消選中單選鈕、復選鈕時在標簽中顯示相應結果。 import javax.swing.*; import java.awt.*; import java.awt.event.…

QT6 源(72):閱讀與注釋單選框這個類型的按鈕 QRadioButton,及各種屬性驗證,

&#xff08;1&#xff09;按鈕間的互斥&#xff1a; &#xff08;2&#xff09;源碼來自于頭文件 qradiobutton . h &#xff1a; #ifndef QRADIOBUTTON_H #define QRADIOBUTTON_H#include <QtWidgets/qtwidgetsglobal.h> #include <QtWidgets/qabstractbutton.h>…

【算法滑動窗口】 將x減到0的最小操作數

將x減到0的最小操作數 個人總結的八步歸納AI的歸納**8步歸納法&#xff08;極簡直白版&#xff09;**1. 問題本質2. 問題特征3. 切入點4. 解決流程5. 每步目標與操作6. 注意事項7. 最終目標8. 整體總結 代碼對照&#xff08;逐行解析&#xff09;舉個栗子&#x1f330;**一句話…

RISC-V GPU架構研究進展:在深度學習推理場景的可行性驗證

一、新型算力架構的突圍戰 在英偉達CUDA生態主導的GPU市場中&#xff0c;RISC-V架構正以?開源基因?和?模塊化設計?開辟新賽道。當前主流GPU架構面臨兩大痛點&#xff1a; 指令集封閉性?&#xff1a;NVIDIA的SASS指令集與AMD的GCN/RDNA架構均采用私有指令編碼&#xff0c…

LVGL -滑動條

1 滑動條 LVGL 的滑動條(Slider)是一個非常有用的控件,允許用戶通過拖動滑塊或點擊滑條來選擇一個值。 1.1 基本定義 滑動條允許用戶在一個預定義的數值范圍內選擇一個特定的值。它通常由一個軌道(track)和一個滑塊(thumb)組成。用戶可以通過點擊或拖動滑塊來調整數值。…

ROS2學習筆記|Python實現訂閱消息并朗讀的詳細步驟

本教程將詳細介紹如何使用 ROS 2 實現一個節點訂閱另一個節點發布的消息&#xff0c;并將接收到的消息通過 espeakng 庫進行朗讀的完整流程。以下步驟假設你已經安裝好了 ROS 2 環境&#xff08;以 ROS 2 Humble 為例&#xff09;&#xff0c;并熟悉基本的 Linux 操作。 注意&…

WPF封裝常用的TCP、串口、Modbus、MQTT、Webapi、PLC通訊工具類

WPF封裝常用通訊工具類 下面我將為您封裝常用的TCP、串口、Modbus、MQTT、WebAPI和PLC通訊工具類,適用于WPF應用程序開發。 一、TCP通訊工具類 using System; using System.Net.Sockets; using System.Text; using System.Threading.Tasks;public class TcpClientHelper : …

npm pnpm yarn 設置國內鏡像

國內鏡像 常用的國內鏡像&#xff1a; 淘寶鏡像 https://registry.npmmirror.com 騰訊云鏡像?? https://mirrors.cloud.tencent.com/npm/ 華為云鏡像?? https://repo.huaweicloud.com/repository/npm/ CNPM&#xff08;阿里系&#xff09; ?? https://r.cnpmjs.org/ 清華…