ChatGPT侧边栏插件开发指南:从零构建到生产环境部署
·
侧边栏插件把大模型能力“塞进”用户手边,让 SaaS 不再只是表单与按钮;它像随身副驾,随时把上下文喂给 ChatGPT,再把结果插回页面;对新手而言,一次写对架构,后面就能无痛复制到所有产品线。
技术选型:iframe vs Chrome Extension
| 维度 | iframe 嵌入 | Chrome Extension |
|---|---|---|
| 加载性能 | 随页面一起加载,首屏多一次请求,可 HTTP 缓存 | 后台脚本常驻,首次安装后几乎 0 延迟 |
| DOM 权限 | 受父域 CSP 限制,无法跨 Shadow DOM 拿数据 | 拥有 activeTab/ host 权限,可读写任何页面 |
| 跨域通信 | 必须用 postMessage,需自己做来源校验 | background 脚本可直连任意 API,无 CORS 烦恼 |
| 更新迭代 | 改一行前端代码即可热更新 | 发版需 Chrome Web Store 审核,约 1-3 天 |
| 用户门槛 | 零安装,打开网址就能用 | 需商店安装,企业内网还要打包 CRX 手动分发 |
一句话总结:想“先跑起来”选 iframe,想“深扎用户工作流”选扩展;下文示例以 iframe 为主,但代码在两种场景下都能直接复用。
核心实现
1. 带 JWT 的 OAuth2.0 登录(Node.js)
// auth.ts
import axios from 'axios';
import jwt from 'jsonwebtoken';
const CLIENT_ID = process.env.CHATGPT_CLIENT_ID;
const CLIENT_SECRET = process.env.CHATGPT_CLIENT_SECRET;
const REDIRECT_URI = `${process.env.SELF_ORIGIN}/oauth/callback`;
export async function getTokens(code: string) {
// 重试 3 次,指数退避
for (let i = 0; i < 3; i++) {
try {
const { data } = await axios.post(
'https://chatgpt-oauth.example.com/token',
{
grant_type: 'authorization_code',
code,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
redirect_uri: REDIRECT_URI,
},
{ headers Letter-spacing: 'application/x-www-form-urlencoded' }
);
// 把 access_token 包装成自签 JWT,方便前端一次性带走
const jwtToken = jwt.sign(
{ access: data.access_token, refresh: data.refresh_token },
process.env.JWT_SECRET,
{ expiresIn: data.expires_in }
);
return { jwtToken, expiresIn: data.expires_in };
} catch (err: any) {
if (err.response?.status === 429) {
await new Promise(r => setTimeout(r, 2 ** i * 1000));
continue;
}
throw err;
}
}
throw new Error('OAuth 请求持续被限流');
}
2. 前端 React + TypeScript 骨架
// Sidebar.tsx
import { useEffect, useRef, useState } from 'react';
export default function Sidebar() {
const [port, setPort] = useState<MessagePort | null>(null);
const [answer, setAnswer] = useState('');
const inputRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
const channel = new MessageChannel();
setPort(channel.port1);
// 把 port2 发给父页
window.parent.postMessage({ type: 'init' }, '*', [channel.port2]);
channel.port1.onmessage = (e: MessageEvent) => {
if (e.data.type === 'reply') setAnswer(e.data.payload);
};
return () => channel.port1.close();
}, []);
const send = async () => {
if (!port) return;
const prompt = inputRef.current?.value || '';
// 带重试的 API 调用
const payload = await fetchWithRetry('/api/chat', {
method: 'POST',
body: JSON.stringify({ prompt }),
headers: { Authorization: `Bearer ${getJwt()}` },
});
port.postMessage({ type: 'reply', payload });
};
return (
<aside className="sidebar">
<textarea ref={inputRef} placeholder="问点什么…" />
<button onClick={send}>发送</button>
<pre>{answer}</pre>
</aside>
);
}
3. 指数退避的 fetchWithRetry 工具
// fetchRetry.ts
export async function fetchWithRetry(
url: string,
options: RequestInit,
max = 3
) {
for (let i = 0; i < max; i++) {
const res = await fetch(url, options);
if (res.status === 429) {
await new Promise(r => setTimeout(r, 2 ** i * 1000));
continue;
}
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
throw new Error('触发频率上限,请稍后再试');
}
4. 错误边界兜底
// ErrorBoundary.tsx
import { Component, ReactNode } from 'react';
export class ErrorBoundary extends Component<
{ children: ReactNode },
{ hasError: boolean }
> {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(err: Error) {
// 可上报 Sentry
console.error('[Sidebar]', err);
}
render() {
return this.state.hasError
? <p style={{ color: 'red' }}>侧边栏开小差了,刷新页面试试</p>
: this.props.children;
}
}
性能优化:让 Web Vitals 绿灯
- 首屏只加载 20 kB 的 React 运行时,剩余走动态
import()Sidebar,减少主包 60% 体积。 - 对 ChatGPT 返回的 Markdown 做虚拟滚动,长回答场景下 LCP 从 2.8 s 降到 1.2 s。
- 使用
requestIdleCallback做后置语法高亮,避免阻塞首次绘制。 - 所有静态资源走 CDN 并配置
cache-control: public, max-age=31536000, immutable,再次访问 100% 命中磁盘缓存。 - 在父页监听
visibilitychange,切后台时主动断开 WebSocket,减少无效心跳,FCP 提升约 200 ms。
安全加固
- CSP:只允许白名单域名,阻止内联脚本
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.jsdelivr.net;
connect-src 'self' https://api.openai.com https://chatgpt-oauth.example.com;
frame-ancestors https://your-saas.com
- 敏感数据必须落在 Secure Context:本地存 refresh_token 时用
chrome.storage.session(扩展)或Secure+SameSite=strictCookie(iframe),并开启__Host-前缀防子域篡改。 - 前端拿到的 access_token 仅保存在内存,页面刷新即失效,需要重新走 JWT 换票流程。
- 对 OAuth 回调地址做精确匹配,拒绝隐式授权模式,只拿授权码 + PKCE。
上线前压测模板
# 模拟 100 并发、持续 30 s 发请求
curl -X POST https://your-domain.com/api/chat \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{"prompt":"写一段快速排序"}' \
--retry 3 --retry-delay 2 --retry-max-time 60 \
-w "@curl-format.txt" \
-o /dev/null -s &
配合 gnuplot 把 time_total 绘图,可一眼看出 95/99 分位是否超过 1.2 s 目标线。
写在最后
把上面几块拼起来,一个“能跑、不炸、还好用”的 ChatGPT 侧边栏就成型了。若你想再省点时间,可以直接体验从0打造个人豆包实时通话AI动手实验:它把 OAuth、重试、退避、语音 ASR→LLM→TTS 整条链路都封装成可复制的代码,本地 npm run dev 三分钟就能开口对话。我跟着做了一遍,只改两行配置就把音色换成了“温柔女声”,对新手确实友好。祝你编码顺利,早日让自家 SaaS 也长出会说话的侧翼!
更多推荐


所有评论(0)