Chrome 瀏覽器插件從 Manifest V2 升級到 V3 版本所需要修改的點

一、Manifest V2 支持時間表

Chrome 瀏覽器官方已經(jīng)給出確定的時間來棄用 V2 版本的插件了。

最早從 2024 年 6 月Chrome 127 開始,我們將開始停用 Chrome 的不穩(wěn)定版本(開發(fā)者版、Canary 版和 Beta 版)中的 Manifest V2 擴展程序。受此變化影響的用戶會在瀏覽器中看到 Manifest V2 擴展程序自動停用,并且無法再從 Chrome 應(yīng)用商店安裝 Manifest V2 擴展程序。此外,Manifest V2 擴展程序在 Chrome 應(yīng)用商店中將不再擁有“精選”徽章(如果目前已有該徽章)。

如果企業(yè)如果使用 ExtensionManifestV2Availability 政策確保其組織中的 Manifest V2 擴展程序能持續(xù)正常運行,則其組織中還有一年的時間(即在 2025 年 6 月之前)遷移 Manifest V2 擴展程序。在此之前,已啟用此政策的瀏覽器不會受到棄用安排的影響。

如果擴展程序發(fā)布商目前仍在發(fā)布 Manifest V2 擴展程序,我們強烈建議您在 2024 年 6 月之前完成向 Manifest V3 的遷移。

官方時間線

1、2022 年 6 月:Chrome 應(yīng)用商店 - 不再有新的專用擴展程序

Chrome 應(yīng)用商店不再接受公開范圍設(shè)為“不公開”的新 Manifest V2 擴展程序。

2、2024 年 6 月:在穩(wěn)定發(fā)布前棄用 Chrome MV2

3、2024 年 6 月 + 1-X 個月:棄用 Chrome MV2 并穩(wěn)定發(fā)布

4、2025 年 6 月:Chrome MV2 棄用(企業(yè)版)

二、Manifest V3 遷移核對列表

1、Manifest.json 文件

1. 更新 manifest_version 版本號

manifest_version 字段的值從 2 更改為 3。

{
  "manifest_version": 3
}

2. 更新 permissionshost_permissions 字段

Manifest V3 中的主機權(quán)限是一個單獨的字段;

不需要在 permissionsoptional_permissions 中指定這些權(quán)限;

有單獨的字段 host_permissions 來表示。

2.1. V2 版本

{
  "permissions": [
    "tabs",
    "bookmarks",
    "https://www.blogger.com/",
  ],
  "optional_permissions": [
    "unlimitedStorage",
    "*://*/*",
  ]
}

2.2. V3 版本

{
  "permissions": [
    "tabs",
    "bookmarks"
  ],
  "optional_permissions": [
    "unlimitedStorage"
  ],
  "host_permissions": [
    "https://www.blogger.com/",
  ],
  "optional_host_permissions": [
    "*://*/*",
  ]
}

3. 更新 web_accessible_resources 字段

Manifest V3 會限制哪些網(wǎng)站和擴展程序可以訪問擴展程序中的資源,以此來限制數(shù)據(jù)公開范圍;

Manifest V2 中,默認(rèn)情況下,指定資源可供所有網(wǎng)站訪問;

在下面的 Manifest V3 示例中,這些資源僅可供匹配的網(wǎng)站使用,而只有某些圖片可供所有網(wǎng)站使用。

3.1. V2 版本

{
  "web_accessible_resources": [
    "images/*",
    "style/extension.css",
    "script/extension.js"
  ],
}

3.2. V3 版本

{
    "web_accessible_resources": [
    {
      "resources": [
        "images/*"
      ],
      "matches": [
        "*://*/*"
      ]
    },
    {
      "resources": [
        "style/extension.css",
        "script/extension.js"
      ],
      "matches": [
        "https://example.com/*"
      ]
    }
  ],
}

2、遷移到 Service Worker

使用 service worker 替換 backgroundevent pages,以確保后臺代碼遠(yuǎn)離主線程,這樣可以讓擴展程序僅在需要時運行,從而節(jié)省資源。

1. BackgroundService Worker 之間的區(qū)別

1.1. Service Workerbackground 之間的差異

  • 在主線程以外運行,這意味著不會干擾擴展程序內(nèi)容;
  • 具有特殊功能,例如攔截擴展程序來源上的提取事件,例如攔截工具欄彈出式窗口中的提取事件;
  • 可以通過客戶端界面與其他上下文進(jìn)行通信和交互。

1.2. 改動點

  • 由于它們無法訪問 DOMwindow 接口,因此需要將此類調(diào)用移至其他 API 或移至屏幕外文檔中;
  • 不應(yīng)注冊事件監(jiān)聽器來響應(yīng)返回的 promise 或在事件回調(diào)內(nèi)部;
  • 不向后兼容 XMLHttpRequest(),因此需要將接口的調(diào)用替換為 fetch();
  • 由于它們在不使用時終止,因此需要保留應(yīng)用狀態(tài),而不是依賴于全局變量;
  • 終止 Service Worker 還可以在計時器完成之前結(jié)束計時器。需要將其替換為 alarms。

2. 更新 manifest.json 中的 background 字段

Manifest V3 中,background 頁面被 Service Worker 所取代:

  • manifest.json 中的 "background.scripts" 替換為 "background.service_worker"
  • "service_worker" 字段接受字符串,而不是字符串?dāng)?shù)組;
  • manifest.json 中移除 "background.persistent"。

2.1. V2 版本

{
  "background": {
    "scripts": [
      "backgroundContextMenus.js",
      "backgroundOauth.js"
    ],
    "persistent": false
  },
}

2.2. V3 版本

{
  "background": {
    "service_worker": "service_worker.js",
    "type": "module"
  }
}

"service_worker" 字段接受單個字符串。只有使用 ES module (使用 import 關(guān)鍵字)時,才需要 "type" 字段。其值將始終為 "module"。

3. 將 DOMwindow 調(diào)用移至屏幕外文檔

某些擴展程序需要訪問 DOMwindow 對象,無需打開新的窗口或標(biāo)簽頁。Offscreen API 支持這類使用情形,因為這類 API 可以打開和關(guān)閉與擴展程序打包在一起的未顯示文檔,而不會干擾用戶體驗。除了消息傳遞之外,屏幕外文檔不會與其他擴展程序上下文共享 API,而是起到完整網(wǎng)頁的作用,供擴展程序進(jìn)行互動。

如需使用 Offscreen API,請通過 Service Worker 創(chuàng)建屏幕外文檔。

chrome.offscreen.createDocument({
  url: chrome.runtime.getURL('offscreen.html'),
  reasons: ['CLIPBOARD'],
  justification: 'testing the offscreen API',
});

4. 將 localStorage 轉(zhuǎn)換為其他類型

Web 平臺的 Storage 接口(可從 window.localStorage 訪問)無法在 Service Worker 中使用。

請使用 chrome.storage.local

5. 同步注冊監(jiān)聽器

異步注冊監(jiān)聽器(例如在 promisecallback 中)注冊并不一定能在 Manifest V3 中有效。

5.1. V2 版本

chrome.storage.local.get(["badgeText"], ({ badgeText }) => {
  chrome.browserAction.setBadgeText({ text: badgeText });
  chrome.browserAction.onClicked.addListener(handleActionClick);
});

Manifest V3 中,系統(tǒng)會在分派事件時重新初始化 Service Worker。這意味著當(dāng)事件觸發(fā)時,系統(tǒng)不會注冊監(jiān)聽器(因為它們是異步添加的),系統(tǒng)還會錯過事件。

5.2. V3 版本

改為將事件監(jiān)聽器注冊移至腳本的頂層。這樣可以確保 Chrome 能夠立即找到并調(diào)用操作的點擊處理程序,即使擴展程序尚未執(zhí)行其啟動邏輯也是如此。

chrome.action.onClicked.addListener(handleActionClick);

chrome.storage.local.get(["badgeText"], ({ badgeText }) => {
  chrome.action.setBadgeText({ text: badgeText });
});

6. 將 XMLHttpRequest() 替換為全局 fetch()

無法從 Service Worker、擴展程序或其他方法調(diào)用 XMLHttpRequest()。將后臺腳本對 XMLHttpRequest() 的調(diào)用替換為對fetch() 的調(diào)用。

const response = await fetch('https://www.example.com/greeting.json');
console.log(response.statusText);

7. 保存狀態(tài)

Service Worker 是臨時的,這意味著它們可能會在用戶的瀏覽器會話期間反復(fù)啟動、運行和終止。這也意味著,自之前的上下文銷毀后,數(shù)據(jù)并非立即在全局變量中可用。如需解決此問題,請使用存儲 API。

對于 Manifest V3 來說,要將全局變量替換為對 Storage API 的調(diào)用。

chrome.runtime.onMessage.addListener(({ type, name }) => {
  if (type === "set-name") {
    chrome.storage.local.set({ name });
  }
});

chrome.action.onClicked.addListener(async (tab) => {
  const { name } = await chrome.storage.local.get(["name"]);
  chrome.tabs.sendMessage(tab.id, { name });
});

8. 把計時器/定時器替換為 alarms

setTimeout()setInterval()Web 開發(fā)中比較常見。不過,在 Service Worker 中可能會失敗,因為每當(dāng) Service Worker 終止時,計時器就會取消。

8.1. V2 版本

// 3 minutes in milliseconds
const TIMEOUT = 3 * 60 * 1000;
setTimeout(() => {
  chrome.action.setIcon({
    path: getRandomIconPath(),
  });
}, TIMEOUT);

8.2. V3 版本

async function startAlarm(name, duration) {
  await chrome.alarms.create(name, { delayInMinutes: 3 });
}

chrome.alarms.onAlarm.addListener(() => {
  chrome.action.setIcon({
    path: getRandomIconPath(),
  });
});

3、API 調(diào)用

1. 將 tab.executeScript() 替換為 scripting.executeScript()

Manifest V3 中,executeScript()tabs API 移至 scripting API。這就需要在實際的代碼更改的基礎(chǔ)上更改 manifest.json 文件中的權(quán)限。

對于 executeScript() 方法:

  • "scripting" 權(quán)限;
  • host permissions 權(quán)限或 "activeTab" 權(quán)限。

scripting.executeScript() 方法與其使用 tabs.executeScript() 的方式類似。但還是有一些區(qū)別。

  • 舊方法只能接受一個文件,而新方法可以接受一組文件;
  • 還將傳遞 ScriptInjection 對象,而不是 InjectDetails

1.1. V2 版本

async function getCurrentTab() {/* ... */}
let tab = await getCurrentTab();

chrome.tabs.executeScript(
  tab.id,
  {
    file: 'content-script.js'
  }
);

代碼在 background 腳本中

1.2. V3 版本

async function getCurrentTab()
let tab = await getCurrentTab();

chrome.scripting.executeScript({
  target: {tabId: tab.id},
  files: ['content-script.js']
});

代碼在 service worker 中

2. 將 tab.insertCSS()tab.removeCSS() 替換為 scripting.insertCSS()scripting.removeCSS()

Manifest V3 中,insertCSS()removeCSS() 已從 tabs API 移至 scriptingAPI。不僅需要更改代碼,還需要對 manifest.json 文件中的權(quán)限進(jìn)行更改:

  • "scripting" 權(quán)限;
  • host permissions 權(quán)限或 "activeTab" 權(quán)限。

scripting API 上的函數(shù)與 tabs 上的函數(shù)類似。但還是有一些區(qū)別。

  • 調(diào)用這些方法時,需要傳遞 CSSInjection 對象,而不是 InjectDetails;
  • tabId 作為 CSSInjection.target 的成員(而不是方法參數(shù))進(jìn)行傳遞。

2.1. V2 版本

chrome.tabs.insertCSS(tabId, injectDetails, () => {
  // callback code
});

代碼在 background 腳本中

2.2. V3 版本

const insertPromise = await chrome.scripting.insertCSS({
  files: ["style.css"],
  target: { tabId: tab.id }
});
// Remaining code. 

代碼在 service worker

3. 將 Browser ActionsPage Actions 替換為 Actions

Manifest V2 中,Browser ActionsPage Actions 是兩個單獨的概念。雖然一開始他們扮演的角色不同,但隨著時間的推移,它們之間的差異越來越小。在 Manifest V3 中,這些概念已整合到 Action API 中。這需要更改 manifest.json 和擴展代碼。

Manifest V3 中的操作與瀏覽器操作最為相似;不過,action API 不像 pageAction 那樣提供 hide()show()。如果仍需要頁面操作,可以使用聲明性內(nèi)容模擬這些操作,也可以使用標(biāo)簽頁 ID 調(diào)用 enable()disable()。

3.1. 將 "browser_action""page_action" 替換為 "action"

manifest.json 中,將 "browser_action""page_action" 字段替換為 "action" 字段。如需了解 "action" 字段的相關(guān)信息,請參閱參考文檔。

3.1.1. V2 版本
{
  "page_action": { ... },
  "browser_action": {
    "default_popup": "popup.html"
   }

}
3.1.2. V3 版本
{
  "action": {
    "default_popup": "popup.html"
  }
}

3.2. 將 BrowserAction APIpageAction API 替換為 Action API

如果 Manifest V2 使用 browserActionpageAction API,現(xiàn)在應(yīng)使用 action API

3.2.1. V2 版本
chrome.browserAction.onClicked.addListener(tab => { ... });
chrome.pageAction.onClicked.addListener(tab => { ... });
3.2.2. V3 版本
chrome.action.onClicked.addListener(tab => { ... });

4. 將 callback 替換為 promise

Manifest V3 中,許多擴展程序 API 方法都會返回 promise

為了實現(xiàn)向后兼容性,許多方法在添加 promise 支持后會繼續(xù)支持回調(diào)。需要注意的是,不能在同一函數(shù)調(diào)用中同時使用這兩者。如果傳遞回調(diào),則函數(shù)不會返回 promise;如果希望返回 promise,則也不要傳遞回調(diào)。某些 API 功能(例如事件監(jiān)聽器)將繼續(xù)需要回調(diào)。

如需從回調(diào)轉(zhuǎn)換為 promise,請移除回調(diào)并處理返回的 promise

4.1. Callback

chrome.permissions.request(newPerms, (granted) => {
  if (granted) {
    console.log('granted');
  } else {
    console.log('not granted');
  }
});

4.2. Promise

const newPerms = { permissions: ['topSites'] };
chrome.permissions.request(newPerms)
.then((granted) => {
  if (granted) {
    console.log('granted');
  } else {
    console.log('not granted');
  }
});

5. 替換需要 Manifest V2 background 上下文的函數(shù)

其他擴展程序上下文只能使用消息傳遞與擴展程序 Service Worker 交互。因此,需要替換需要后臺上下文的調(diào)用,具體而言:

  • chrome.runtime.getBackgroundPage()
  • chrome.extension.getBackgroundPage()
  • chrome.extension.getExtensionTabs()

擴展程序腳本應(yīng)使用消息傳遞在 Service Worker 和擴展程序的其他部分之間進(jìn)行通信。目前,這需要使用 sendMessage(),并在擴展 Service Worker 中實現(xiàn) chrome.runtime.onMessage。從長遠(yuǎn)來看,應(yīng)計劃將這些調(diào)用替換為 postMessage()Service Worker消息事件處理程序。

6. 替換不受支持的 API

需要在 Manifest V3 中更改下列方法和屬性。

Manifest V2 方法或?qū)傩?/th> 替換為 Manifest V3 方法或?qū)傩?/th>
chrome.extension.connect() chrome.runtime.connect()
chrome.extension.connectNative() chrome.runtime.connectNative()
chrome.extension.getExtensionTabs() chrome.extension.getViews()
chrome.extension.getURL() chrome.runtime.getURL()
chrome.extension.lastError 如果方法返回 promise,請使用 promise.catch()
chrome.extension.onConnect chrome.runtime.onConnect
chrome.extension.onConnectExternal chrome.runtime.onConnectExternal
chrome.extension.onMessage chrome.runtime.onMessage
chrome.extension.onRequest chrome.runtime.onRequest
chrome.extension.onRequestExternal chrome.runtime.onMessageExternal
chrome.extension.sendMessage() chrome.runtime.sendMessage()
chrome.extension.sendNativeMessage() chrome.runtime.sendNativeMessage()
chrome.extension.sendRequest() chrome.runtime.sendMessage()
chrome.runtime.onSuspendbackground 腳本) 在擴展 Service Worker 中不受支持。請改用 beforeunload 文檔事件。
chrome.tabs.getAllInWindow() chrome.tabs.query()
chrome.tabs.getSelected() chrome.tabs.query()
chrome.tabs.onActiveChanged chrome.tabs.onActivated
chrome.tabs.onHighlightChanged chrome.tabs.onHighlighted
chrome.tabs.onSelectionChanged chrome.tabs.onActivated
chrome.tabs.sendRequest() chrome.runtime.sendMessage()
chrome.tabs.Tab.selected chrome.tabs.Tab.highlighted

4、替換屏蔽 Web 請求監(jiān)聽器

Manifest V3 更改了擴展程序處理網(wǎng)絡(luò)請求修改的方式。擴展程序會指定規(guī)則來描述在滿足一組給定條件時要執(zhí)行的操作,而不是攔截網(wǎng)絡(luò)請求并在運行時使用 chrome.webRequest 更改請求。

Web Request API 和聲明式網(wǎng)絡(luò)請求 API 有很大的區(qū)別。需要根據(jù)用例重新編寫代碼,而不是將一個函數(shù)調(diào)用替換為另一個函數(shù)調(diào)用。

1. 更新 permissions

manifest.json 中的 "permissions" 字段進(jìn)行以下更改。

  • 如果不再需要觀察網(wǎng)絡(luò)請求,請移除 "webRequest" 權(quán)限;
  • 將匹配模式從 "permissions" 移至 "host_permissions"。

需要根據(jù)使用場景添加其他權(quán)限。這些權(quán)限通過其支持的用例進(jìn)行描述。

2. 創(chuàng)建聲明性網(wǎng)絡(luò)請求規(guī)則

如需創(chuàng)建聲明性 net 請求規(guī)則,需要向 manifest.json 添加 "declarative_net_request" 對象。"declarative_net_request" 代碼塊包含指向規(guī)則文件的 "rule_resource" 對象數(shù)組。規(guī)則文件包含一組對象,用于指定操作以及調(diào)用這些操作的條件。

3. 常見使用場景

3.1. 屏蔽單個網(wǎng)址

Manifest V2 中的一個常見用例是在后臺腳本中使用 onBeforeRequest 事件來屏蔽網(wǎng)絡(luò)請求。

3.1.1. Background 腳本改為 V3 規(guī)則文件
3.1.1.1. 規(guī)則文件
  1. rule.json
[
  {
    "id": 1,
    "priority": 1,
    "action": { "type": "block" },
    "condition": {
      "urlFilter": "||example.com",
      "resourceTypes": ["main_frame"]
    }
  }
]
  1. 示例

[圖片上傳失敗...(image-67c199-1705057590208)]

  1. Manifest.json 文件引入
{
  "name": "URL Blocker",
  "version": "0.1",
  "manifest_version": 3,
  "description": "Uses the chrome.declarativeNetRequest API to block requests.",
  "background": {
    "service_worker": "service_worker.js"
  },
  "declarative_net_request": {
    "rule_resources": [
      {
        "id": "ruleset_1",
        "enabled": true,
        "path": "rules_1.json"
      }
    ]
  },
  "permissions": ["declarativeNetRequest", "declarativeNetRequestFeedback"]
}
3.1.1.2. V2 版本
chrome.webRequest.onBeforeRequest.addListener((e) => {
    return { cancel: true };
}, { urls: ["https://www.example.com/*"] }, ["blocking"]);
3.1.1.3. V3 版本

對于 Manifest V3,請使用 "block" 操作類型創(chuàng)建新的 declarativeNetRequest 規(guī)則。請注意示例規(guī)則中的 "condition" 對象。其 "urlFilter" 取代了傳遞給 webRequest 監(jiān)聽器的 urls 選項。"resourceTypes" 數(shù)組指定要屏蔽的資源的類別。

[
  {
    "id" : 1,
    "priority": 1,
    "action" : { "type" : "block" },
    "condition" : {
      "urlFilter" : "||example.com",
      "resourceTypes" : ["main_frame"]
    }
  }
]
3.1.2. 需要更新該擴展程序的權(quán)限。

manifest.json 中,將 "webRequestBlocking" 權(quán)限替換為 "declarativeNetRequest" 權(quán)限。請注意,由于屏蔽內(nèi)容不需要主機權(quán)限,因此該網(wǎng)址已從 "permissions" 字段中移除。

3.1.2.1. V2 版本
  "permissions": [
    "webRequestBlocking",
    "https://*.example.com/*"
  ]
3.1.2.2. V3 版本
"permissions": [
  "declarativeNetRequest",
]

3.2. 重定向多個網(wǎng)址

Manifest V2 中的另一個常見用例是使用 BeforeRequest 事件重定向網(wǎng)絡(luò)請求。

3.2.1. Background 腳本改為 V3 規(guī)則文件
3.2.1.1. V2 版本
chrome.webRequest.onBeforeRequest.addListener((e) => {
    console.log(e);
    return { redirectUrl: "https://developer.chrome.com/docs/extensions/mv3/intro/" };
  }, { 
    urls: [
      "https://developer.chrome.com/docs/extensions/mv2/"
    ]
  }, 
  ["blocking"]
);
3.2.1.2. V3 版本

對于 Manifest V3,請使用 "redirect" 操作類型。與之前一樣,"urlFilter" 會替換傳遞給 webRequest 監(jiān)聽器的 url 選項。請注意,在此示例中,規(guī)則文件的 "action" 對象包含一個 "redirect" 字段,其中包含要返回的網(wǎng)址,而不是要過濾的網(wǎng)址。

[
  {
    "id" : 1,
    "priority": 1,
    "action": {
      "type": "redirect",
      "redirect": { "url": "https://developer.chrome.com/docs/extensions/mv3/intro/" }
    },
    "condition": {
      "urlFilter": "https://developer.chrome.com/docs/extensions/mv2/",
      "resourceTypes": ["main_frame"]
    }
  }
3.2.2. 需要更改擴展程序的權(quán)限

"webRequestBlocking" 權(quán)限替換為 "declarativeNetRequest" 權(quán)限。系統(tǒng)再次將這些網(wǎng)址從 manifest.json 移到了規(guī)則文件中。請注意,除了主機權(quán)限之外,重定向還需要 "declarativeNetRequestWithHostAccess" 權(quán)限。

3.2.2.1. V2 版本
  "permissions": [
    "webRequestBlocking",
    "https://developer.chrome.com/docs/extensions/*",
    "https://developer.chrome.com/docs/extensions/reference"
  ]
3.2.2.2. V3 版本
  "permissions": [
    "declarativeNetRequestWithHostAccess"
  ],
  "host_permissions": [
    "https://developer.chrome.com/*"
  ]

3.3. 屏蔽 Cookie

Manifest V2 中,要屏蔽 Cookie,需要先攔截網(wǎng)絡(luò)請求標(biāo)頭,然后再發(fā)送這些標(biāo)頭并移除特定的 Cookie。

3.3.1. Background 腳本改為 V3 規(guī)則文件
3.3.1.1. V2 版本
chrome.webRequest.onBeforeSendHeaders.addListener(
  function(details) {
    removeHeader(details.requestHeaders, 'cookie');
    return {requestHeaders: details.requestHeaders};
  },
  // filters
  {urls: ['https://*/*', 'http://*/*']},
  // extraInfoSpec
  ['blocking', 'requestHeaders', 'extraHeaders']);
3.3.1.2. V3 版本

Manifest V3 也通過規(guī)則文件中的規(guī)則實現(xiàn)這一點。這次的操作類型為 "modifyHeaders"。該文件接受 "requestHeaders" 對象數(shù)組,用于指定要修改的標(biāo)頭以及如何修改這些標(biāo)頭。請注意,"condition" 對象僅包含 "resourceTypes" 數(shù)組。它支持的值與前面的示例相同。

[
  {
    "id": 1,
    "priority": 1,
    "action": {
      "type": "modifyHeaders",
      "requestHeaders": [
        { "header": "cookie", "operation": "remove" }
      ]
    },
    "condition": {
      "urlFilter": "|*?no-cookies=1",
      "resourceTypes": ["main_frame"]
    }
  }
]
3.3.2. 需要更新該擴展程序的權(quán)限。

"webRequestBlocking" 權(quán)限替換為 "declarativeNetRequest" 權(quán)限。

3.3.2.1. V2 版本
  "permissions": [
    "webRequestBlocking",
    "https://developer.chrome.com/docs/extensions/*",
    "https://developer.chrome.com/docs/extensions/reference"
  ]
3.3.2.2. V3 版本
  "permissions": [
    "declarativeNetRequestWithHostAccess"
  ],
  "host_permissions": [
    "https://developer.chrome.com/*"
  ]

引用

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容