- Published on
从设计师的痛点到开发者的解决方案:手把手打造React Figma AI Chrome扩展
- Authors

- Name
- Stone
前言
“每次看到设计师在 Figma 里复制粘贴文案,我就想写个工具帮他们...”
现实中的开发困境
想象一下这个场景:你是一个前端开发工程师,设计师给你发来一个 Figma 设计稿,里面有几十个页面,每个页面都有大量的文案内容。而你的业务是面对国际化的,需要生成一个原始的 JSON 文案去找翻译的同学来帮你翻译成各个国家的语言 🤯 并且这个 JSON 要根据你具体的项目来调整格式。
很快,你需要做的任务如下:
- 手动复制文案 - 在 Figma 中一个个点击文本,复制粘贴到代码里
- 整理成 JSON 格式 - 手动整理成前端可用的多语言文件格式
- 翻译成多种语言 - 还要找翻译工具逐个翻译
- 保持数据结构一致 - 确保所有语言版本的键值对应正确
这个过程不仅效率低下,还容易出错。更要命的是,当设计师修改了文案,你又要重复一遍这个痛苦的过程...这无疑是很不高效的行为
那么这时候,作为一个有“懒癌”的程序员,我开始思考如何提高开发效率:
- 能否自动从 Figma 提取文案?
- 能否直接调用 AI 进行智能分析和翻译?
- 能否一键生成前端需要的多语言 JSON 文件?
于是,这个 React Figma AI Chrome 扩展项目就诞生了。
💡 从痛点到解决方案
作为一个经常被奇奇怪怪的需求“折磨”的开发者,我深知一个道理:好的工具都是从解决真实痛点开始的。现在让我们把刚才提到的痛点重新梳理一下,但这次我们要从“程序员思维”的角度来分析:
需求 1:自动化文案提取
- 痛点:手动复制粘贴,效率低下,特别是我开发的时候,有时候面对一些游戏规则页(比如一整页的文字说明搭配几个表格的那种),光是复制 + 整理成完整的 JSON 就已经足够痛苦了,基本上一个内容多的说明页,去创建一个符合规范的 JSON 就需要十分钟左右了 😭
- 解决思路:通过 Chrome 扩展直接访问 Figma API,自动提取页面文案,这样就节省掉我们复制粘贴的时间了
需求 2:智能文案处理
- 痛点:文案需要结构化整理,还要翻译。像往常我开发的时候,除了自己手动去整理以外,最常用的方式就是让 AI 去帮我整理成一个可以开发的 JSON,但往往需要手动去输入一大堆 Prompt,才可以生成一个勉强够用的。或者你会说:“哥们!你弄到备忘录里,需要的时候再弄不就成了吗?”那我问你,这样优雅吗?每次都需要从备忘录里找到 Prompt,然后调整项目的描述 or 项目需要的 JSON 格式,这也太繁琐了吧…
- 解决思路:接入 AI 服务,让 AI 帮我们干“脏活累活”,最好可以留出 调整项目描述和 JSON 格式的区域
需求 3:一键生成多语言 JSON
- 痛点:手动整理 JSON 格式,容易出错
- 解决思路:让 AI 直接输出标准化的 JSON 格式,并且支持多语言翻译
那么现在我们的开发思路就已经定好了:

开发流程
初始化项目
mkdir figma-analyzer-extension
cd figma-analyzer-extension
npm init -y
开发技术栈选择方面,因为我先前一直是写 Vue 的,对 React 始终保持着好奇,但因为工作原因,一直没有机会去用到这个传奇的前端库,所以这次自己的小项目就选择了 React 来进行开发了。至于打包工具方面,就选择我们熟悉的 Vite 来进行打包构建就好~
除了这些主要的技术栈,我们还要根据我们的项目需求来选择一些有趣又有用的库,belike:
- `@crxjs/vite-plugin`:专为 Chrome 扩展优化的 Vite 插件(**开发 Chrome extension 的神器!**)
- `@types/chrome`:Chrome 扩展 API 的 TypeScript 类型定义
- `react-json-pretty`:用来美化显示 JSON 结果,方便我们直接在插件里浏览 AI 生成的 JSON,这个 JSON-pretty 足够 **轻量美观**
中间的一些细节就省略掉了,如果感兴趣的话可以查看 Chrome 扩展官方文档 ,基本上和我们的也大差不多。
在稍作调整后,我们的这个项目结构如下:
figma-analyzer-extension/
├── src/
│ ├── components/ # React 组件
│ │ └── FigmaAnalyzer.tsx
│ ├── manifest.json # Chrome 扩展配置文件(重要!)
│ ├── popup.html # 扩展弹窗的 HTML
│ ├── popup.tsx # React 应用入口
│ ├── popup.css # 样式文件
│ ├── background.ts # Service Worker(后台脚本)
│ ├── types.ts # TypeScript 类型定义
│ ├── constants.ts # 常量定义
│ ├── prompts.ts # AI 提示词模板
│ ├── figmaApi.ts # Figma API 服务
│ └── vite-env.d.ts # Vite 环境类型定义
├── package.json
├── tsconfig.json
├── tsconfig.node.json
├── vite.config.ts
└── README.md
这里之所以把 prompt 单独放在一个 ts 文件,是因为我们需要根据不同的功能来拆分不同的 prompt,并且需要根据用户的具体使用场景来调整 prompt。如果放在 React 组件中,会让整个项目看上去非常的臃肿,所以这里单独进行拆分了。
接入大模型之我全都要
Wait?!我们是不是忘记了一个很重要的事情?选择什么大模型呢?如果太贵了会不会得不偿失呢?市面上的大模型也太多了吧…选择困难症了 🤡
// 程序员的内心独白
const aiServices = {
openai: { price: '💰💰💰', quality: '🌟🌟🌟🌟🌟', speed: '🚀🚀🚀' },
claude: { price: '💰💰', quality: '🌟🌟🌟🌟🌟', speed: '🚀🚀' },
deepseek: { price: '💰', quality: '🌟🌟🌟🌟', speed: '🚀🚀🚀' },
ollama: { price: '🆓', quality: '🌟🌟🌟', speed: '🐌' },
};
// 最后决定:小孩才做选择,成年人表示,我全都要!
const solution = '让用户自己选择,我们都支持';

那我们来简单实现一下 AI 调用模块:
// 支持多种 AI 服务的统一接口
const callAI = async (prompt, provider) => {
switch (provider) {
case 'openai':
return await callOpenAI(prompt);
case 'deepseek':
return await callDeepSeek(prompt);
// ... 其他服务
}
};
接着让我们实现一下接入 AI 大模型,这里选择性价比最高的 deepseek 来作为示例(其他的大模型同理):
function buildPrompt(request: AIAnalysisRequest): string {
const { operation, figmaData, projectDescription, targetLanguage } = request;
if (operation === 'translate') {
// 纯翻译模式
const textsToTranslate = figmaData.texts.map((t) => t.text).join('\n');
const targetLang = getLanguageName(targetLanguage || 'en');
const additionalInstruction = `\n\n**最终提醒**:以上共 ${figmaData.texts.length} 行文案,每行输出格式必须是:英文原文:${targetLang}译文`;
return (
TRANSLATION_PROMPT_TEMPLATE.replace(/\{targetLanguage\}/g, targetLang).replace(
'{textsToTranslate}',
textsToTranslate
) + additionalInstruction
);
} else if (operation === 'translate-and-structure') {
// 翻译 + 结构化模式
const allTextsFormatted = figmaData.texts
.map((text, index) => `${index + 1}. "${text.text}"`)
.join('\n');
const projectDesc = projectDescription || '网页界面设计项目';
const targetLang = getLanguageName(targetLanguage || 'en');
const strictReminder = `\n\n**再次强调**:请确保 JSON 中只包含上述 ${figmaData.totalTextCount} 条提取文案的翻译版本,不要添加任何额外内容!`;
return (
TRANSLATE_AND_STRUCTURE_PROMPT_TEMPLATE.replace(/\{targetLanguage\}/g, targetLang)
.replace('{textCount}', figmaData.totalTextCount.toString())
.replace('{allTexts}', allTextsFormatted)
.replace('{projectDescription}', projectDesc) + strictReminder
);
} else {
// 结构化 JSON 生成模式
const allTextsFormatted = figmaData.texts.map((text) => text.text).join('\n');
const projectDesc = projectDescription || '网页界面设计项目';
const strictReminder = `\n\n**再次强调**:请确保 JSON 中只包含上述 ${figmaData.totalTextCount} 条提取的文案,不要添加任何额外内容!`;
return (
ANALYSIS_PROMPT_TEMPLATE.replace('{textCount}', figmaData.totalTextCount.toString())
.replace('{allTexts}', allTextsFormatted)
.replace('{projectDescription}', projectDesc) + strictReminder
);
}
}
// DeepSeek API 调用实现
async function callDeepSeekAPI(prompt: string, apiKey: string): Promise<string> {
const requestBody = {
model: 'deepseek-chat',
messages: [{ role: 'user', content: prompt }],
temperature: 0.2, // 降低温度提高一致性
max_tokens: 2000,
};
console.log('🚀 发送到 DeepSeek 的请求:', requestBody);
const response = await fetch('https://api.deepseek.com/v1/chat/completions', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
});
if (!response.ok) {
const errorText = await response.text();
console.error('DeepSeek API Error:', errorText);
throw new Error(`DeepSeek API 错误: ${response.status}`);
}
const data = await response.json();
const content = data.choices[0]?.message?.content;
if (!content) {
throw new Error('DeepSeek API 返回空内容');
}
return content;
}
接入 Ollama 时,遇见阻碍
当我兴高采烈地接入一个又一个主流大模型的时候,也同时考虑到了这个免费的工具,不仅可以本地调用大模型,还可以保证信息的隐私,来处理一些敏感需求的话,Ollama 再合适不过了。但在接入的时候,遇见了第一个大坑:Error 403。
在查阅社区 issues 的时候,我发现了不少开发者也遇见了同样的问题:Ollama issue #4115。在查阅的过程中,才找到了解决方案:
macOS 上:
launchctl setenv OLLAMA_ORIGINS "*"
才刚解决完这个报错,又发现我下载的 deepseek-r1 8b 模型,一直会返回 think 部分:并且这时候,我们的解析接口返回也失效了:

同样在社区找到了相同的疑问:DeepSeek-R1 issue #582。DeepSeek 官方也并没有在 API 文档中说相关的内容…搜索了半天也没有找到结果,于是我尝试去了解 AI 的相关概念,比如:
- stream(流式输出)
- temperature(控制生成文本随机性的重要参数)
- think(深度思考)
哦!找到了,在 Ollama Thinking 博文 找到了答案:
手动将 think 设置成 false 即可!顺带一提,我个人不是很喜欢流式输出,即使现在很多的对话式 AI(如 ChatGPT 或者 DeepSeek)都选择了流式输出,但我们还是要根据自己的开发项目来设置。在我们这个需求中,直接获取到最终的结果就行,不需要关注生成的过程。
Figma 数据获取 - 从注入脚本到 REST API 的重构之路
最开始我思考的获取 Figma 文本的方式是注入脚本,通过在 Figma 页面中注入 JavaScript 代码来获取选中元素的数据:
// 早期的注入脚本方案(已弃用)
function getSelectedElements() {
// 直接访问 Figma 的内部 API
const selection = figma.currentPage.selection;
return selection.map((node) => ({
id: node.id,
name: node.name,
text: node.characters,
}));
}
最开始的时候我还沾沾自喜,认为自己的这个实现思路很完美。后面在获取元素的时候,发现经常出现“无法获取选中元素”的错误,这对用户的体验无疑是很差的。这时候,我想到了直接使用 Figma API:
export class FigmaApiService {
private apiToken: string;
private baseUrl = 'https://api.figma.com/v1';
constructor(apiToken: string) {
this.apiToken = apiToken;
}
// 获取 Figma 文件数据
async getFile(fileId: string): Promise<FigmaFileResponse> {
const response = await fetch(`${this.baseUrl}/files/${fileId}`, {
headers: {
'X-Figma-Token': this.apiToken,
},
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Figma API 错误 (${response.status}): ${errorText}`);
}
return (await response.json()) as FigmaFileResponse;
}
}
// 从 Figma URL 中提取文件 ID
static extractFileIdFromUrl(url: string): string | null {
const patterns = [
// 匹配 /file/ 或 /design/ 路径
/(?:www\.)?figma\.com\/(?:file|design)\/([a-zA-Z0-9-_]+)/,
// 备用模式:更宽松的匹配
/figma\.com\/[^/]+\/([a-zA-Z0-9-_]+)/,
];
for (const pattern of patterns) {
const match = url.match(pattern);
if (match && match[1]) {
return match[1];
}
}
return null;
}
// 从 URL 中提取节点 ID(当用户选中元素时)
static extractNodeIdFromUrl(url: string): string | null {
const nodeIdMatch = url.match(/[?&]node-id=([^&]+)/);
if (nodeIdMatch) {
let nodeId = decodeURIComponent(nodeIdMatch[1]);
nodeId = nodeId.replace('%3A', ':').replace('-', ':');
return nodeId;
}
return null;
}
// 递归提取节点中的所有文案
private extractTextsFromNode(node: FigmaNode, texts: FigmaTextInfo[] = []): FigmaTextInfo[] {
try {
// 如果是文本节点且有文案内容
if (node.type === 'TEXT' && node.characters) {
const boundingBox = node.absoluteBoundingBox || { x: 0, y: 0, width: 0, height: 0 };
texts.push({
id: node.id,
name: node.name,
text: node.characters,
fontSize: node.style?.fontSize || 16,
fontFamily: node.style?.fontFamily || 'Unknown',
x: boundingBox.x,
y: boundingBox.y,
width: boundingBox.width,
height: boundingBox.height,
});
}
// 递归处理子节点
if (node.children && node.children.length > 0) {
for (const child of node.children) {
this.extractTextsFromNode(child, texts);
}
}
} catch (error) {
console.warn('提取节点文案时出错:', error, node);
}
return texts;
}
注意,我们要使用 Figma API 的话,需要使用 Figma API token。这里给一个链接,方便用户点击后直接跳转去获取 Figma API Token:Figma API Access Tokens

AI Prompt 工程 - 让 AI 理解你的需求
第一版 Prompt 的失败经历
最初的 Prompt 设计得过于简单:
请分析以下文案并生成 JSON 格式的结果:
{文案内容}
结果 AI 经常返回格式不规范的内容,有时候还会添加额外的说明文字,即使我将 temperature 设置得足够低,也有很多奇怪的生成,导致 JSON 解析失败。
这里补充说明一下:
在 AI(尤其是语言模型)中,“temperature”(温度)是一个控制生成文本随机性的重要参数。
🎲 什么是 Temperature?
每次模型生成下一个 token(词或子词)时,会基于一组 logits(原始分数)通过 Softmax 函数将其转为概率分布。Temperature 会影响 Softmax 的平滑程度: 低温度(T < 1):概率分布更陡峭,模型更倾向于选择最高概率的词,也就是最“保守”“确定”的输出。 高温度(T > 1):概率分布更平坦,增加了选择概率较低词的机会,生成更“多样”“有创造性”的文本。
举个对比例子
在某一句话生成中,如果 logits 是 [2, 1, −1],Softmax 转换后会是大约 [0.67, 0.25, 0.08]。但如果引入不同 Temperature:
- T → 0:几乎总是选第一个 token,输出高度重复、确定。在日常中,对于技术写作、翻译或者说问答之类的就可以选择这种 Temperature
- T = 1:保留原始分布
- T > 1:分布变平,次优的词也有机会被采样,比如“旁门左道”出现的可能性更高。如果你有天马行空的想法,并且不是很在意会不会出错的话,就可以选择这种
这里关于 Temperature 的科普我们就讲到这里吧,如果对这个感兴趣的话,可以看 Medium 上的这篇文章,讲得非常详细:Temperature & LLMs
回到我们的项目吧,经过大量测试和优化,最终的 Prompt 模板是这样的:
export const ANALYSIS_PROMPT_TEMPLATE = `
你是一个专业的 UI/UX 文案分析师,请分析以下 {textCount} 条从设计稿中提取的文案内容。
项目描述:{projectDescription}
需要分析的文案:
{allTexts}
请严格按照以下要求输出 JSON 格式的分析结果:
1. 必须是有效的 JSON 格式,不要包含任何其他文字说明
2. 只分析上述提取的 {textCount} 条文案,不要添加额外内容
3. 结构化输出,包含页面标题、描述、建议等字段
输出格式示例:
{
"__page_title": "页面标题",
"button_text": "按钮文案",
"description": "描述文案",
"title": "主标题"
}
**再次强调**:请确保 JSON 中只包含上述 {textCount} 条提取的文案,不要添加任何额外内容!
`;
注意!这里只是针对于我的项目来写的内容,如果是你来做的话,可以稍微调整 JSON 格式,也算是一劳永逸的事情了。
效果展示
让我们看看最终的页面展示效果,用 Figma 官方的插件入门开发来作为演示:



graph TD
A["用户在Figma中选择元素"] --> B["Chrome扩展Popup界面"]
B --> C{"配置AI服务"}
C --> D["DeepSeek API"]
C --> E["OpenAI API"]
C --> F["Claude API"]
C --> G["Ollama本地模型"]
B --> H["提取Figma数据"]
H --> I["Figma REST API"]
I --> J{"智能检测模式"}
J --> K["从URL提取节点ID"]
J --> L["获取整个文件"]
K --> M["获取特定元素文案"]
L --> N["获取全部文案"]
M --> O["Background Script处理"]
N --> O
O --> P["AI分析处理"]
P --> Q["文案翻译"]
P --> R["结构化JSON生成"]
P --> S["文案分析"]
Q --> T["返回结果到Popup"]
R --> T
S --> T
T --> U["用户查看结果"]
开发思路
graph TD
A["开发挑战"] --> B["Chrome扩展架构设计"]
B --> C["Manifest V3配置"]
B --> D["React前端UI设计"]
B --> E["Background Service Worker"]
F["数据获取方案"] --> G["注入脚本方案(弃用)"]
F --> H["Figma REST API方案"]
H --> I["API Token验证"]
H --> J["文件ID提取"]
H --> K["节点ID智能检测"]
L["AI服务集成"] --> M["DeepSeek API"]
L --> N["OpenAI API"]
L --> O["Claude API"]
L --> P["Ollama本地服务"]
Q["核心功能实现"] --> R["文案智能提取"]
Q --> S["多语言翻译"]
Q --> T["结构化JSON生成"]
Q --> U["结果展示优化"]
V["用户体验优化"] --> W["智能检测选中元素"]
V --> X["简化操作流程"]
V --> Y["错误处理机制"]
V --> Z["性能优化"]
graph TB
subgraph "用户环境"
A["Figma设计稿"] --> B["用户选择元素"]
B --> C["Chrome浏览器"]
end
subgraph "Chrome扩展"
D["Popup界面<br/>(React + TypeScript)"]
E["Background Service Worker<br/>(消息处理 + API调用)"]
F["Chrome Storage API<br/>(配置存储)"]
D <--> E
D <--> F
end
subgraph "外部服务"
G["Figma REST API<br/>(文件数据获取)"]
H["DeepSeek API<br/>(AI分析)"]
I["OpenAI API<br/>(GPT模型)"]
J["Claude API<br/>(Anthropic)"]
K["Ollama服务<br/>(本地AI模型)"]
end
subgraph "数据流"
L["URL解析<br/>(文件ID + 节点ID)"]
M["文案提取<br/>(递归遍历节点)"]
N["AI处理<br/>(分析/翻译/结构化)"]
O["结果展示<br/>(JSON美化 + 复制)"]
end
C --> D
E --> G
E --> H
E --> I
E --> J
E --> K
G --> L
L --> M
M --> N
N --> O
O --> D
style A fill:#e1f5fe
style D fill:#f3e5f5
style E fill:#fff3e0
style G fill:#e8f5e8
style H fill:#fff8e1
style I fill:#f1f8e9
style J fill:#e3f2fd
style K fill:#fce4ec
