基于Manifest V3的谷歌文档语音转录扩展开发疑问
Manifest V3下Chrome插件对接Google Docs API的修正方案
核心问题梳理
你的代码在MV3下存在几个关键问题:
- Manifest配置错误:权限划分混乱、OAuth2字段配置不符合规范、CSP限制了外部API脚本加载
- 内容脚本直接加载GAPI违反MV3安全策略,且无法直接调用Chrome身份验证API
- 未利用MV3的Service Worker处理后台逻辑,这是MV3替代background.html的核心方式
修正后的manifest.json
{ "name": "Docs Live Transcriber Plugin", "version": "1.0", "description": "A plugin for voice dictation and voice functions for GDocs.", "manifest_version": 3, "author": "", "icons": { "128": "img/logo.png" }, "permissions": [ "identity", "tabs", "storage", "management", "notifications", "unlimitedStorage" ], "oauth2": { "client_id": "你的客户端ID", "scopes": [ "https://www.googleapis.com/auth/userinfo.email", "profile", "https://www.googleapis.com/auth/documents" ] }, "content_scripts": [ { "matches": ["https://docs.google.com/*"], "js": ["foreground.js"] } ], "background": { "service_worker": "background.js" }, "content_security_policy": { "extension_pages": "script-src 'self' https://apis.google.com https://accounts.google.com; object-src 'self'", "content_scripts": "script-src 'self' 'unsafe-inline'; object-src 'self'" }, "host_permissions": [ "https://docs.google.com/*", "https://script.google.com/*", "https://www.googleapis.com/*", "https://accounts.google.com/*" ] }
改动说明:
- 把原
permissions里的URL移到host_permissions,MV3要求权限和主机权限分开 - 修正oauth2字段:移除不属于该字段的
api_key和discovery_docs,补充Docs操作所需的scope - 添加background service worker,用于处理GAPI初始化和OAuth验证
- 调整CSP,允许加载Google API和GIS的脚本源,同时给内容脚本放宽必要的inline脚本限制
新增background.js(替代原background.html)
let gapiInited = false; let tokenClient = null; const API_KEY = "你的API密钥"; const DISCOVERY_DOC = "https://docs.googleapis.com/$discovery/rest?version=v1"; const CLIENT_ID = "你的客户端ID"; const SCOPES = "https://www.googleapis.com/auth/userinfo.email profile https://www.googleapis.com/auth/documents"; // 初始化GAPI async function initGapi() { const script = document.createElement('script'); script.src = 'https://apis.google.com/js/api.js'; script.onload = async () => { await gapi.load('client', async () => { await gapi.client.init({ apiKey: API_KEY, discoveryDocs: [DISCOVERY_DOC], }); gapiInited = true; console.log("GAPI初始化完成"); }); }; document.head.appendChild(script); } // 初始化GIS(Google Identity Services) function initGis() { const script = document.createElement('script'); script.src = 'https://accounts.google.com/gsi/client'; script.onload = () => { tokenClient = google.accounts.oauth2.initTokenClient({ client_id: CLIENT_ID, scope: SCOPES, callback: handleTokenResponse, }); console.log("GIS初始化完成"); }; document.head.appendChild(script); } // 处理OAuth令牌响应 function handleTokenResponse(response) { if (response.error !== undefined) { throw response; } // 存储令牌到chrome.storage供内容脚本使用 chrome.storage.local.set({ accessToken: response.access_token }, () => { console.log("令牌已存储"); }); } // 监听内容脚本的登录请求 chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (request.type === "signIn") { if (!tokenClient) { initGis(); // 延迟调用确保GIS加载完成 setTimeout(() => tokenClient.requestAccessToken({ prompt: '' }), 1000); } else { tokenClient.requestAccessToken({ prompt: '' }); } sendResponse({ status: "登录请求已发起" }); } else if (request.type === "getDocsContent") { // 示例:调用Docs API获取文档内容 if (!gapiInited) { sendResponse({ error: "GAPI未初始化" }); return; } chrome.storage.local.get('accessToken', async (data) => { gapi.client.setToken({ access_token: data.accessToken }); try { const response = await gapi.client.documents.get({ documentId: request.documentId, }); sendResponse({ content: response.result }); } catch (err) { sendResponse({ error: err.message }); } }); return true; // 表示异步响应 } }); // 启动时初始化GAPI initGapi();
说明:
- 在Service Worker中加载GAPI和GIS,避开内容脚本的环境限制
- 用
chrome.runtime.onMessage和内容脚本通信,处理登录和API调用请求 - 把OAuth令牌存储到
chrome.storage,方便内容脚本获取使用
修正后的foreground.js
// 创建按钮的函数修正:参数错误(buttonId写成button_type) const createButton = (buttonId, eventListener, innerText) => { const button = document.createElement('button'); button.setAttribute('id', buttonId); button.addEventListener('click', eventListener); button.innerText = innerText; // 添加样式避免被Docs页面样式覆盖 button.style.padding = '8px 16px'; button.style.margin = '8px'; button.style.background = '#4285F4'; button.style.color = 'white'; button.style.border = 'none'; button.style.borderRadius = '4px'; button.style.cursor = 'pointer'; return button; }; // 插入登录按钮到Docs页面 const injectButton = () => { // 找Docs页面的工具栏区域插入按钮 const toolbar = document.querySelector('.docs-titlebar-buttons'); if (toolbar) { const signInBtn = createButton('signInBtn', handleSignIn, '登录并启用转录'); toolbar.appendChild(signInBtn); } }; // 处理登录请求:给后台发消息 const handleSignIn = async (e) => { e.preventDefault(); try { const response = await chrome.runtime.sendMessage({ type: "signIn" }); console.log(response.status); } catch (err) { alert(`登录失败:${err.message}`); } }; // 监听后台的令牌更新(可选) chrome.storage.onChanged.addListener((changes, area) => { if (area === 'local' && changes.accessToken) { console.log("已获取访问令牌,可以开始转录"); // 这里可以初始化语音转录逻辑 } }); // 页面加载完成后注入按钮 window.addEventListener('load', injectButton);
改动说明:
- 修正createButton函数的参数错误(原
button_type未定义,改为buttonId,改用addEventListener绑定事件) - 移除直接加载GAPI的代码,改为通过消息请求后台处理
- 添加页面元素注入逻辑,把按钮插入到Docs的工具栏
- 用
chrome.runtime.sendMessage和后台通信,处理登录请求
关键注意事项
- 确保在Google Cloud Console中正确配置OAuth客户端ID:
- 应用类型选"Chrome 应用"
- 填写插件的ID(在
chrome://extensions/中查看)
- 语音转录逻辑建议在内容脚本中处理,拿到令牌后直接调用Speech-to-Text API,再通过Docs API写入文档
- MV3的Service Worker是无状态的,注意令牌的持久化存储(用
chrome.storage.local)
内容的提问来源于stack exchange,提问作者VlAd TbK
相关产品推荐
相关产品推荐

