You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Chrome扩展用GAPI操作Google文档遇CSP拦截,Offscreen Document无效

解决Chrome扩展加载GAPI被CSP拦截的问题

问题根源

  1. CSP限制:扩展当前的内容安全策略未允许加载https://apis.google.com的脚本,且你可能在内容脚本中尝试加载GAPI——内容脚本受宿主页面(Google Docs)的CSP限制,完全无法加载第三方外部脚本。
  2. Offscreen使用错误:创建Offscreen Document时指定的reasons: ['USER_MEDIA']不符合场景,USER_MEDIA仅用于媒体设备访问,加载外部API需使用匹配的原因类型。

分步解决方案

1. 更新扩展Manifest的CSP配置

在manifest.json中修改content_security_policy,允许扩展内部页面加载Google API脚本:

{
  "manifest_version": 3,
  // 其他扩展配置...
  "content_security_policy": {
    "extension_pages": "script-src 'self' 'wasm-unsafe-eval' 'inline-speculation-rules' http://localhost:* http://127.0.0.1:* https://apis.google.com; object-src 'none';",
    "content_scripts": "script-src 'self'; object-src 'none';"
  }
}
  • extension_pages覆盖Service Worker、Offscreen文档等扩展内部页面,添加https://apis.google.com到script-src白名单。

2. 修正Offscreen Document的创建逻辑

将reasons改为贴合场景的类型,同时明确justification描述:

async function createOffscreen() {
    if (await chrome.offscreen.hasDocument()) return;
    await chrome.offscreen.createDocument({
        url: 'offscreen-gapi.html', // 建议重命名为更贴合功能的文件名
        reasons: ['OTHER'],
        justification: '加载Google API并处理Google文档写入操作',
    });
}

3. 在Offscreen页面中封装GAPI逻辑

创建offscreen-gapi.html,在这个扩展内部页面中加载GAPI并处理消息交互:

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Security-Policy" content="script-src 'self' https://apis.google.com;">
</head>
<body>
    <script>
        // 异步加载GAPI
        function loadGAPI() {
            return new Promise((resolve, reject) => {
                const script = document.createElement('script');
                script.src = 'https://apis.google.com/js/api.js';
                script.onload = () => resolve(window.gapi);
                script.onerror = reject;
                document.body.appendChild(script);
            });
        }

        // 监听来自Service Worker的消息
        chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => {
            if (message.type === 'INIT_GAPI') {
                try {
                    const gapi = await loadGAPI();
                    // 初始化GAPI(替换为你的API密钥、客户端ID等信息)
                    await gapi.load('client:auth2', () => {
                        gapi.client.init({
                            apiKey: '你的API_KEY',
                            clientId: '你的CLIENT_ID',
                            discoveryDocs: ["https://www.googleapis.com/discovery/v1/apis/documents/v1/rest"],
                            scope: 'https://www.googleapis.com/auth/documents'
                        });
                    });
                    sendResponse({ success: true });
                } catch (err) {
                    sendResponse({ success: false, error: err.message });
                }
            } else if (message.type === 'WRITE_TO_DOC') {
                // 执行文档写入逻辑
                const { docId, content } = message;
                try {
                    const response = await gapi.client.documents.batchUpdate({
                        documentId: docId,
                        resource: {
                            requests: [
                                {
                                    insertText: {
                                        text: content,
                                        location: { index: 1 }
                                    }
                                }
                            ]
                        }
                    });
                    sendResponse({ success: true, data: response.result });
                } catch (err) {
                    sendResponse({ success: false, error: err.message });
                }
            }
            return true; // 保持消息通道开放以发送异步响应
        });
    </script>
</body>
</html>

4. 调整Service Worker的消息流转

让Service Worker负责协调Offscreen和内容脚本的交互,避免内容脚本直接接触GAPI:

chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
    if (changeInfo.status === 'complete' && tab.status === 'complete' && tab.url?.includes('https://docs.google.com/document')) {
        await createOffscreen();
        // 可靠提取文档ID(替代原有的substr方式)
        const url = new URL(tab.url);
        const docId = url.searchParams.get('d') || url.pathname.split('/')[3];
        
        // 通知Offscreen初始化GAPI
        const initRes = await chrome.runtime.sendMessage({ type: 'INIT_GAPI' });
        if (initRes.success) {
            // 告知内容脚本GAPI已就绪
            await chrome.tabs.sendMessage(tabId, {
                type: 'GAPI_READY',
                docId: docId
            });
        }
    }
});

// 转发内容脚本的写入请求到Offscreen
chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => {
    if (message.type === 'REQUEST_WRITE') {
        const writeRes = await chrome.runtime.sendMessage({
            type: 'WRITE_TO_DOC',
            docId: message.docId,
            content: message.content
        });
        sendResponse(writeRes);
    }
});

5. 内容脚本中触发写入操作

内容脚本仅负责发送请求,所有GAPI操作由Offscreen完成:

// 内容脚本代码
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
    if (message.type === 'GAPI_READY') {
        // 构造要写入的内容
        const writeContent = "这是通过Chrome扩展写入的测试内容";
        // 发送写入请求
        chrome.runtime.sendMessage({
            type: 'REQUEST_WRITE',
            docId: message.docId,
            content: writeContent
        }, (res) => {
            res.success ? console.log("写入成功") : console.error("写入失败:", res.error);
        });
    }
});

核心注意事项

  • 内容脚本完全受宿主页面CSP限制,绝不能在其中加载外部脚本,所有第三方API操作必须放在扩展内部页面(如Offscreen)执行。
  • Offscreen Document的reasons必须符合Chrome官方定义的合法值,避免因原因不匹配导致文档创建失败。
  • 确保GAPI授权流程配置正确,包括API密钥、客户端ID和权限范围的校验。

内容的提问来源于stack exchange,提问作者VladTbk321

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 22:20:46