发出网络请求与跨域处理

Eagle 插件有个其他平台给不了的优势:官方文档明确写了,Eagle Plugin API 不受跨域限制(CORS)的影响,可以访问任何网址。这意味着你不用为了调自己的后端专门去加 CORS 响应头。但「不受 CORS 限制」不等于「随便用」——这一节讲能力,也讲约束。

两种请求方式

fetch:日常首选

fetch 是 Web 标准 API,插件里可以直接用,语法和浏览器里完全一致:

fetch('https://example.com/api/endpoint')
    .then(response => response.json())
    .then(data => {
        // 在这里处理响应
    });

异步写起来更顺手:

async function fetchItems() {
    const response = await fetch('https://api.example.com/items');

    if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
    }

    return await response.json();
}

一定要检查 response.ok。 这是 fetch 最常见的坑:fetch 遇到 404、500 都不会 reject,它会正常返回一个 response,只是 ok 为 false、status 是错误码。不检查就直接 .json(),你会拿到一段 HTML 错误页,然后 JSON.parse 报一个和真实原因完全无关的错误。

https 模块:需要底层控制时

官方给的第二种方案是 Node 原生的 https 模块。文档说明了原因:由于浏览器默认的安全性限制,fetch 有时候会遇到一些限制,这种情况下可以改用 Node.js 原生的网络 API,获得更高的弹性。

const https = require('https');

https.get('https://www.example.com', (res) => {
  console.log(`Got response: ${res.statusCode}`);

  res.on('data', (d) => {
    // 处理响应数据
  });

}).on('error', (e) => {
  console.error(`Got error: ${e.message}`);
});

什么时候需要它?几个典型场景:需要自定义 TLS 配置(自签证书)、需要代理、需要细粒度控制 socket、需要访问自签名内网服务。

// 连接本地开发服务器时,可能需要放宽证书校验
const fs = require('fs');
const https = require('https');

const agent = new https.Agent({
    rejectUnauthorized: false,
    ca: fs.readFileSync('/path/to/dev-cert.pem')
});

https.get('https://localhost:8443/api', { agent }, (res) => {
    // ...
});

⚠️ 但要注意:rejectUnauthorized: false 这类写法只应该用于本地开发。发布版本里绝不能出现——审核标准里明确把「使用未加密的 HTTP 传输数据」和「在没有本地整合说明的情况下连接 localhost、回环地址、私人网络」列为高风险项。

完整的请求封装

一个能上生产的请求函数应该处理超时、错误、取消三件事:

const https = require('https');

function request(url, options = {}, timeout = 15000) {
    return new Promise((resolve, reject) => {
        const req = https.request(url, {
            method: options.method || 'GET',
            headers: options.headers || {}
        }, (res) => {
            let data = '';
            res.on('data', (chunk) => {
                data += chunk;
                // 防止响应过大占满内存
                if (data.length > 10 * 1024 * 1024) {
                    req.destroy();
                    reject(new Error('响应体过大'));
                }
            });
            res.on('end', () => resolve({
                status: res.statusCode,
                data
            }));
        });

        req.setTimeout(timeout, () => {
            req.destroy();
            reject(new Error(`请求超时: ${url}`));
        });

        req.on('error', reject);

        if (options.body) {
            req.write(typeof options.body === 'string'
                ? options.body
                : JSON.stringify(options.body));
        }

        req.end();
    });
}

三个细节值得说明:

  • 超时必须显式设置。 没有超时的请求会一直挂着,用户看到的是「点了没反应」。
  • 限制响应体大小。 一个返回 500MB 的接口会直接把插件进程撑爆。
  • 错误要带上下文。 reject(new Error('请求超时: ' + url)) 比 reject(new Error('timeout')) 有用得多。

凭证管理:不要把 Key 写进代码

如果插件需要 API Key,正确做法是让用户自己填,存在本地配置文件里:

const fs = require('fs');
const path = require('path');

eagle.onPluginCreate(async (plugin) => {
    const configPath = path.join(plugin.path, 'config.json');

    // 不存在就用空模板初始化
    if (!fs.existsSync(configPath)) {
        fs.writeFileSync(configPath, JSON.stringify({ apiKey: '' }, null, 2));
    }

    const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
    console.log('是否已配置:', Boolean(config.apiKey));
});

这个配置文件写在插件目录下,安装包本身不包含任何凭证。审核标准对此有明确要求:安装包不得包含真实凭证或私人密钥,.env、.pem、.key、.p12、.pfx、credentials.json、secrets.json、含 Token 的 .npmrc 都是被点名检查的对象。

⚠️ 还有一条容易忽略:如果敏感数据曾经被打包或公开,不能只删除文件,必须立即停用并更换相关密钥。

跨域:为什么这里不需要处理

在浏览器里发跨域请求要满足两个条件:服务端返回 Access-Control-Allow-Origin,以及请求本身通过预检。这在插件里都不需要——插件进程不受同源策略约束,fetch 到任意站点都能通。

但不受 CORS 限制带来的是责任,不是自由。审核标准会逐条检查你的插件连了哪些外部地址:

  • 这个连接用户能合理预期吗?
  • 这个外部服务确实是宣称功能所需吗?
  • 只传送了完成该功能所需的最少数据吗?
  • 商店介绍里说明了会实质影响隐私的外部处理吗?
  • 敏感数据有没有被放进 URL、日志或安装包?

具体到「传送用户数据到外部」这一类行为,安全与隐私标准明确列出了不得做的情形:把 Eagle 素材名称、本地路径或文件内容传送到未说明的外部服务。在你把用户的图片发给某个云端 AI 服务之前,必须在商店介绍里说清楚传什么、发到哪。

一个上传素材到云端的例子

async function uploadToCloud(item, apiKey) {
    const fs = require('fs');

    // 发送前明确告知用户
    const confirmed = await eagle.dialog.showMessageBox({
        title: '上传确认',
        message: `将上传「${item.name}」`,
        detail: `文件大小 ${Math.round(item.size / 1024)} KB,将发送至 api.example.com`,
        buttons: ['上传', '取消'],
        type: 'warning'
    });
    if (confirmed.response !== 0) return;

    const buffer = fs.readFileSync(item.filePath);

    const result = await request('https://api.example.com/upload', {
        method: 'POST',
        headers: {
            'Content-Type': item.ext,
            'Authorization': `Bearer ${apiKey}`
        },
        body: buffer
    });

    if (result.status !== 200) {
        await eagle.notification.show({ title: '上传失败', body: `状态码 ${result.status}` });
        return;
    }

    await eagle.notification.show({ title: '上传完成', body: item.name });
}

这个例子里有三处是审核会看的:上传前弹窗说明去向和内容、失败有明确提示、凭证从配置读而不是写死。

开发流程

  1. 先用 fetch 实现,确认接口通不通、返回结构对不对。
  2. 加上 response.ok 检查和错误处理。
  3. 加超时,测试一下断网时的表现。
  4. 需要底层控制时再换 https 模块。
  5. 凭证改成用户配置,代码里不出现任何硬编码 Key。
  6. 列出插件会连的所有外部域名,商店介绍里逐条说明。
  7. 涉及用户数据的传送,在界面上做明确确认。

常见坑

不检查 response.ok 就解析。 404 和 500 都会正常返回 response,不检查的话你会拿到 HTML 错误页,然后在一个无关的地方报 JSON 解析错误。

API Key 硬编码在代码里。 这是审核的一票否决项。改成用户配置,并确保 .env 之类的文件被打包流程排除掉。

延伸注意: * 不检查 response.ok 就解析。 404 和 500 都会正常返回 response,不检查的话你会拿到 HTML 错误页,然后在一个无关的地方报 JSON 解析错误。 * 把凭证放进 URL 查询参数。 URL 会被记录在服务端日志、代理日志和浏览历史里。用请求头传 Token。

整理人:WorkBuddy