手把手教你开发一个 AI Prompt Enhancer Chrome 扩展
手把手教你开发一个 AI Prompt Enhancer Chrome 扩展
告别写提示词的迷茫,一键让你的 Prompt 更专业
前言
在使用豆包、通义千问、DeepSeek 等 AI 对话工具时,你是否曾遇到过这样的困扰:
- 输入一个简短的问题,AI 只给了三言两语的回复
- 忘记指定目标受众,AI 的输出方向完全跑偏
- 没有指定格式要求,结果收到一大段无法直接使用的文字
如果有,那么你并不孤单。Prompt Engineering(提示词工程) 已经成了一门学问——如何写出一个好的 Prompt,直接决定了 AI 输出的质量。
本文就带你从零开发一个 AI Prompt Enhancer Chrome 浏览器扩展,它能自动识别 AI 网站的输入框,一键增强你的提示词,让输出质量大幅提升。
全文约 5000 字,包含完整代码解读、架构设计思路、真实排坑经验,适合有 JavaScript 基础但第一次开发 Chrome 扩展的读者。
一、项目概览
项目名称:AI Prompt Enhancer
核心功能:
| 功能 | 说明 |
|---|---|
| 🎯 自动识别输入框 | 在豆包、通义千问、DeepSeek 等 AI 站点自动注入增强按钮 |
| ✨ 一键增强 | 点击按钮自动补全缺失的 Prompt 要素(受众、长度、结构、语气) |
| 📝 弹窗模式 | 支持在扩展弹窗中直接输入/增强提示词 |
| 📋 一键复制 | 增强后的提示词支持一键复制使用 |
技术栈:Vanilla JavaScript + Chrome Extension Manifest V3
代码规模:4 个核心文件,总计约 370 行
二、Chrome Extension Manifest V3 基础
2.1 从 manifest.json 看懂一个扩展的生命周期
在开始之前,我们先拆解一下扩展的配置文件 manifest.json,它是整个扩展的"身份证"和"权限清单":
{
"manifest_version": 3,
"name": "AI Prompt Enhancer",
"description": "Enhances your prompts for AI chat websites",
"version": "1.0"
}
manifest_version: 3 意味着什么?
Chrome 在 2020 年底发布了 Manifest V3,2022 年开始逐步淘汰 V2。V3 最大的变化是:
- 用 Service Worker 替代了 Background Page——后台脚本不再是常驻页面,而是事件驱动的 Worker,用完即销毁。这意味着你不能在全局变量中保存状态。
- 强制
host_permissions分离——以前权限声明写在permissions数组里就行了,V3 必须用单独的host_permissions字段声明要访问的网站域名。 - 禁止远程代码——所有 JavaScript 必须打包在扩展 ZIP 里,不能通过
eval()、new Function()或动态<script>标签加载外部脚本。
这些变化直接影响我们的开发方式,后文会逐一展开。
再看 permissions 和 host_permissions 的区别
{
"permissions": ["storage", "scripting"],
"host_permissions": [
"*://*.doubao.com/*",
"*://*.qwen.ai/*",
"*://*.deepseek.com/*",
"*://*.wenxin.baidu.com/*",
"*://*.chatglm.cn/*",
"*://*.moonshot.cn/*"
]
}
permissions:声明扩展自身需要的 API 能力。storage用于后续存储用户配置,scripting用于通过编程方式注入脚本(目前的 Content Script 声明式注入用不到,但为后续迭代预留)。host_permissions:声明扩展要读写哪些网站的数据。只声明permissions不够,Content Script 要想在目标页面运行,host_permissions是必填项。这是 V3 新增的安全隔离机制。
matches 通配符深入
"matches": [
"*://*.doubao.com/*",
"*://*.qwen.ai/*",
"*://*.deepseek.com/*",
"*://*.wenxin.baidu.com/*",
"*://*.chatglm.cn/*",
"*://*.moonshot.cn/*",
"*://*.ai/*",
"*://*.chat/*"
]
*://*.domain.com/* 这个通配模式有三层含义:
| 通配符 | 含义 | 匹配示例 |
|---|---|---|
*:// |
匹配 http 和 https 两种协议 | http:// 和 https:// 都行 |
*.doubao.com |
匹配所有子域名 | www.doubao.com、chat.doubao.com |
/* |
匹配任意路径 | /chat、/api/conversation 都行 |
注意:
*://*.ai/*这样的泛匹配会匹配所有.ai域名的网站,包括example.ai、open.ai等。在生产扩展中应该尽量精确,避免不必要的权限扩散——Chrome Web Store 审核时也会关注这一点。
2.2 Manifest V3 vs V2 关键区别速查
| 对比项 | V2 | V3 |
|---|---|---|
| 后台脚本 | background.js(常驻进程,持续占用内存) |
Service Worker(事件驱动,用完即销毁) |
| API 权限 | 直接在 permissions 中声明域名 |
需要单独的 host_permissions 字段 |
| 远程代码 | 允许(可通过 eval 或 CDN 加载) |
完全禁止(所有代码必须打包在扩展内) |
| 跨域请求 | 在 Background 中直接发起 | 需在 Service Worker 中用 fetch(遵循 CORS) |
| 内容安全策略 | 较为宽松 | 更严格,默认阻止 eval() |
| API 版本 | chrome.extension 大部分仍可用 |
推荐使用 chrome.runtime 等新 API |
三、Content Script:核心注入机制深度拆解
3.1 整体架构
Content Script 是整个扩展最核心的部分,负责在目标 AI 网站上注入增强按钮并处理用户交互。它的工作流程如下:
页面加载 → init() 执行
├─ DOMContentLoaded 就绪判断
├─ 立即遍历已有元素:enhanceInputElements(document.body)
└─ 启动 MutationObserver:监听后续动态添加的元素
↓
找到输入框 → 标记 data-prompt-enhanced
↓
注入 ✨ Enhance 按钮 → 绑定点击事件
↓
用户点击 → 读取输入框内容 → enhancePrompt() → 写回输入框
3.2 启动时机:DOMContentLoaded 的判断
(function() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
这段代码处理了一个容易被忽略的边界情况:
- 如果 Content Script 脚本执行时页面还在加载(
loading状态),就等到DOMContentLoaded触发再初始化,避免操作不完整的 DOM。 - 如果脚本执行时页面已经加载完毕(
interactive或complete状态),就立即执行。
为什么用 DOMContentLoaded 而不是 window.onload? 因为 DOMContentLoaded 在 HTML 解析完成后立即触发,不需要等待图片、样式表等资源加载完毕,用户体验更好——我们的按钮越快出现越好。
3.3 选择器设计:从暴力匹配到精准定位
const inputSelectors = [
'textarea[placeholder*="问" i]',
'textarea[placeholder*="输入" i]',
'textarea[placeholder*="Message" i]',
'textarea[placeholder*="Chat" i]',
'textarea[placeholder*="请输入" i]',
'div[contenteditable="true"][role="textbox"]',
'textarea',
'input[type="text"]'
];
这里有几个值得展开的设计决策:
(1) [attr*="value" i] 的 i 标志
CSS 属性选择器的 i 标志(case-insensitive)是 CSS Selectors Level 4 的特性,让匹配大小写不敏感。这意味着:
- 如果网站的 placeholder 是
"Ask me anything",[placeholder*="ask" i]能匹配到 - 如果 placeholder 是
"请输入问题",[placeholder*="输入" i]能匹配到
为什么不直接用 textarea 作为唯一选择器? 因为很多现代网站有多个 textarea,其中一些是搜索框、评论框等不需要增强的控件。通过 placeholder 文本过滤,我们可以更精准地定位到 AI 聊天输入框。
(2) Content Editable Div 的处理
'div[contenteditable="true"][role="textbox"]'
这是一个让很多开发者掉坑的地方。像 DeepSeek、通义千问等 AI 网站为了支持富文本(代码高亮、@提及等),会使用 contenteditable 的 <div> 而不是原生 <textarea>。我们必须同时支持这两种模式。
(3) 选择器的优先级顺序
选择器的排列顺序是一种性能优化策略:精确的选择器放在前面、兜底的选择器放在后面。当 querySelectorAll 遍历 DOM 时,精确选择器匹配的元素更少、遍历更快。'textarea' 作为最宽泛的选择器放在最后,是为了确保不会遗漏任何潜在目标。
3.4 MutationObserver:对付动态渲染的利器
现代前端框架(React、Vue、Svelte)都不会在 HTML 中直接渲染 DOM 元素,而是通过 JavaScript 动态创建。这意味着我们的 Content Script 在页面加载时扫描的元素,可能只是最终 DOM 的一部分——后续用户点击某个按钮、打开某个弹窗,才会渲染出真正的输入框。
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
mutation.addedNodes.forEach(function(node) {
if (node.nodeType === 1) { // ELEMENT_NODE
enhanceInputElements(node);
}
});
});
});
observer.observe(document.body, {
childList: true, // 监听子节点的增删
subtree: true // 监听所有后代节点(不限于直接子节点)
});
配置参数详解
| 参数 | 值 | 含义 |
|---|---|---|
childList |
true |
监听目标节点的直接子节点增删 |
subtree |
true |
递归监听所有后代节点,不只是直接子节点 |
attributes |
未设置 | 不监听属性变化(我们不需要) |
characterData |
未设置 | 不监听文本内容变化(我们不需要) |
为什么需要 subtree: true? 因为很多 AI 网站的输入框嵌套在多级容器中:<div class="chat-container"> → <div class="input-area"> → <div class="editor-wrapper"> → <textarea>。如果不设置 subtree: true,我们只能监听到直接子节点的变化,深层嵌套的元素变化会被遗漏。
性能考量
MutationObserver 是异步触发的,浏览器会将一段时间内的 DOM 变更合并成一次回调。但如果在短时间内有大量 DOM 变化(比如页面渲染一个复杂的组件树),enhanceInputElements 会被频繁调用。
一个常见的优化方案是防抖(debounce):
let debounceTimer;
const observer = new MutationObserver(function() {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
enhanceInputElements(document.body);
}, 200);
});
但在这个项目中,enhanceInputElements 本身已经通过 data-prompt-enhanced 做了防重复处理,即使被频繁调用也不会重复注入按钮,所以防抖不是必须的。
3.5 防重复处理:data 属性的妙用
if (element.getAttribute('data-prompt-enhanced') === 'true') {
return;
}
element.setAttribute('data-prompt-enhanced', 'true');
这是 Content Script 开发中最常用的幂等性(Idempotency) 处理模式:
- 检查:先看元素上是否有
data-prompt-enhanced="true"标记 - 标记:处理完后立即设置标记
- 跳过:下次遇到同一元素直接跳过
为什么用 data-* 属性而不是内存中的 Set/Map? 因为 MutationObserver 每次回调时都是一个独立的函数调用,不共享变量作用域。用 data-* 属性把状态持久化到 DOM 元素上是最可靠的方案——只要 DOM 元素存在,标记就存在。
潜在问题:如果目标网站使用了虚拟 DOM 或 DOM diff 算法(React、Vue 等),在组件重新渲染时,DOM 元素可能会被销毁重建。此时
data-prompt-enhanced标记会丢失,我们的按钮也需要重新注入。这正是 MutationObserver 的价值所在——它会捕获到旧元素被移除、新元素被添加的事件。
3.6 按钮定位:最棘手的 CSS 难题
function getPositionContainer(element) {
let parent = element.parentElement;
while (parent && parent !== document.body) {
const position = window.getComputedStyle(parent).position;
if (position === 'relative' || position === 'absolute' || position === 'fixed') {
return parent;
}
parent = parent.parentElement;
}
return element.parentElement; // 兜底方案
}
为什么需要这个函数?
我们的增强按钮使用绝对定位(position: absolute; right: 5px; top: 5px),这意味着它必须有一个已定位的祖先元素(position 不是 static)作为参照系。
目标网站的 DOM 结构各不相同:
情况 A:输入框包裹在一个 position: relative 的容器中
<div class="input-wrapper" style="position: relative;">
<textarea>...</textarea>
</div>
→ 按钮可以正确定位在输入框的右上角。✅
情况 B:输入框在多个静态定位的嵌套容器中
<div class="outer"> <!-- position: static -->
<div class="inner"> <!-- position: static -->
<textarea>...</textarea>
</div>
</div>
→ 我们需要将按钮的父容器设为 position: relative,否则按钮的 absolute 定位会相对于整个文档。因此代码会找到 element.parentElement 并设置其 position: relative。✅
情况 C:输入框在某个深层容器中,祖先已有定位
<div class="page" style="position: relative;">
<div class="panel">
<div class="input-area">
<textarea>...</textarea>
</div>
</div>
</div>
→ 函数会向上遍历到 .page,发现它的 position 是 relative,返回它。按钮会相对于 .page 定位——这可能导致按钮出现在页面角落而不是输入框旁边。❌
这就是为什么在代码中,我们还在按钮上设置了 right: 5px; top: 5px,并且把按钮 appendChild 到定位容器的尾部。但严格来说,最理想的做法是把按钮插入到距离输入框最近的定位祖元素中,或者直接插入到输入框的父元素中并设置其为 relative。
一个更好的定位方案
function injectButtonNearInput(inputElement, button) {
const parent = inputElement.parentElement;
const originalPosition = window.getComputedStyle(parent).position;
if (originalPosition === 'static') {
parent.style.position = 'relative';
}
// 插入按钮到输入框的后面(相邻兄弟)
inputElement.insertAdjacentElement('afterend', button);
// 或者更激进:覆盖输入框的 padding,让按钮浮动在输入框内部
button.style.position = 'absolute';
button.style.right = '8px';
button.style.bottom = '8px';
}
这个方案更加简洁可靠,但原代码使用循环遍历祖元素的策略也有其价值——它展示了如何适应不同网站的布局差异。
3.7 按钮的视觉设计策略
enhanceButton.style.cssText = `
position: absolute;
right: 5px;
top: 5px;
background: #007bff;
color: white;
border: none;
border-radius: 3px;
padding: 2px 6px;
font-size: 12px;
cursor: pointer;
z-index: 1000;
opacity: 0.8;
`;
这里有几个关键设计决策:
1. z-index: 1000
目标网站的样式可能会在输入框区域使用 z-index: 500、z-index: 999 等值。设为 1000 确保我们的按钮浮在所有内容之上。
2. opacity: 0.8
半透明设计让按钮不喧宾夺主。用户自然看得到,但不会被转移注意力。
3. 使用内联样式而非 CSS 文件
因为 Content Script 注入的样式优先级低于目标网站本身的 CSS。如果我们在 <style> 标签中定义样式,可能会被目标网站的规则覆盖。通过 JavaScript 直接设置 element.style.cssText,我们使用的是内联样式,优先级仅次于 !important,能最大程度避免被覆盖。
4. 视觉反馈机制
// 绿色闪烁反馈
const originalBg = enhanceButton.style.backgroundColor;
enhanceButton.style.backgroundColor = '#28a745';
setTimeout(() => {
enhanceButton.style.backgroundColor = originalBg;
}, 1000);
// 文字状态反馈
const originalText = enhanceButton.innerHTML;
enhanceButton.innerHTML = '✅ Done';
setTimeout(() => {
enhanceButton.innerHTML = originalText;
}, 1500);
这段代码实现了两步反馈:
- 颜色变化(1 秒):按钮从蓝色变成绿色,给用户即时的"操作已收到"信号
- 文字变化(1.5 秒):文字从"✨ Enhance"变成"✅ Done",然后恢复,给用户"操作已完成"的确认
需要注意的是,颜色恢复使用了 setTimeout 保存的原始值。如果以后我们支持自定义主题色,这里需要做相应调整。
3.8 读取输入框内容的兼容处理
let currentText = '';
if (element.tagName === 'TEXTAREA' || element.tagName === 'INPUT') {
currentText = element.value;
} else if (element.isContentEditable) {
currentText = element.innerText || element.textContent;
}
为什么需要区分标签类型?
<textarea>和<input>元素的内容存储在.value属性中contenteditable的<div>内容存储在.innerText或.textContent中innerText和textContent的区别:innerText会考虑 CSS 样式(比如不会返回display: none元素的内容),textContent返回所有文本内容(包括隐藏元素的文本)。这里用innerText更合适,因为我们只关心用户看到的文本。
四、Prompt 增强算法:从规则引擎到智能补全
4.1 算法设计思路
增强算法的核心思路基于 Prompt Engineering 的最佳实践——好的 Prompt 应该包含 4 个要素:
- 目标受众(Audience):写给谁看的?初学者、专业人士、管理者?
- 长度要求(Length):要写多少字、多少段、多少页?
- 内容结构(Structure):需要小标题、步骤、checklist 还是对比表格?
- 语气风格(Tone):正式书面语还是轻松口吻?专业严谨还是生动活泼?
判断逻辑的决策树如下:
用户输入
├─ 空或 < 5 字符 → 返回基础模板(引导用户补充)
├─ < 10 字符 → 简短短语填充模式
├─ 缺失要素检查
│ ├─ 缺少受众 → 添加"针对初学者"
│ ├─ 缺少长度 → 根据内容复杂度推测
│ ├─ 缺少结构 → 添加"包含三个小标题"
│ └─ 缺少语气 → 添加"请你以清晰易懂的口吻"
└─ 已包含完整要素 → 仅做格式润色(句号结尾等)
4.2 正则表达式的精细化设计
const hasAudience = /针对|面向|适合|对于|给.*看|给.*用/.test(enhanced);
const hasLength = /字|词|页|分钟|小时|段落/.test(enhanced);
const hasStructure = /标题|部分|章节|步骤|检查点|checklist|清单/.test(enhanced);
const hasTone = /口吻|风格|语气|专业|轻松|幽默|正式|随意/.test(enhanced);
每个正则都不是一拍脑门写的:
| 要素 | 检测关键词 | 设计思路 |
|---|---|---|
| 受众 | 针对、面向、适合、对于、给.*看、给.*用 |
覆盖中文用户表达受众的常见方式,包括"给…看/用"这种间隔结构 |
| 长度 | 字、词、页、分钟、小时、段落 |
“500字”、“2页”、"5分钟阅读"等都是常见长度描述 |
| 结构 | 标题、部分、章节、步骤、检查点、checklist、清单 |
中英文混写场景全覆盖 |
| 语气 | 口吻、风格、语气、专业、轻松、幽默、正式、随意 |
覆盖用户对输出风格的常见要求 |
4.3 增强内容的动态组装
这里有一段容易被忽略的代码逻辑:
// 组装增强内容
let tonePart = "";
let otherParts = additions.filter(part => !part.startsWith("请你以"));
if (additions.some(part => part.startsWith("请你以"))) {
tonePart = additions.find(part => part.startsWith("请你以")) + ",";
}
enhanced = tonePart + otherParts.join("、") + "," + enhanced;
这段代码为什么要特地把语气部分放在最前面?
因为中文表达习惯中,语气词(“请你以清晰易懂的口吻”)通常放在句子开头,而其他要素(“针对初学者”、“字数约800字”)通过顿号连接。组装后的文本读起来是这样的:
“请你以清晰易懂的口吻,针对初学者、字数约800字、包含三个小标题、结尾附上一个简洁的checklist,写一篇关于 XXX 的文章。”
如果不做这个重组,直接拼接会变成:
“针对初学者、字数约800字、请你以清晰易懂的口吻、…” —— 读起来很别扭。
4.4 边界情况处理
空输入和极短输入
if (!prompt || prompt.trim().length < 5) {
return `请你以专业且吸引人的方式,围绕[主题]展开论述。请指定:1)目标受众 2)所需字数/长度 3)内容结构 4)是否需要特殊格式`;
}
当用户输入为空或过短(少于 5 个字符)时,增强器返回一个模板,而不是报错。模板中的 [主题] 暗示用户替换为自己的关键词,四个要点引导用户思考应该提供什么信息。
句号补全
if (!enhanced.endsWith("。")) {
enhanced += "。";
}
这是一个很细节的优化——很多用户输入 Prompt 时不会写句号,但增强后的 Prompt 末尾应该有句号来保证语法完整性。
4.5 实际效果对比
| 输入 | 增强后 |
|---|---|
什么是机器学习 |
请你以清晰易懂的口吻,针对初学者、字数约500字、包含三个小标题、结尾附上一个简洁的checklist,什么是机器学习。 |
设计一个微服务架构方案,用于电商系统,要求高可用 |
请你以清晰易懂的口吻,针对初学者、字数约800字、包含三个小标题、结尾附上一个简洁的checklist,设计一个微服务架构方案,用于电商系统,要求高可用。 |
写一篇关于量子计算的文章,面向技术管理者,约1000字 |
请你以专业的角度,关于量子计算的文章,面向技术管理者,约1000字。(已包含受众和长度,仅做格式润色) |
五、Popup 模式:双通道体验设计
5.1 Popup 的独立闭环
除了 Content Script 的"即用即增强"模式,我们还提供了 Popup 弹窗。但这里有一个需要特别注意的问题:
Content Script 和 Popup 是在两个完全独立的环境中运行的:
Popup 环境
├─ 打开方式:点击扩展图标
├─ 生命周期:弹窗打开 → 用户操作 → 弹窗关闭
├─ 可访问的 API:完整的 chrome.* API
└─ 可访问的 DOM:popup.html 自己的 DOM
Content Script 环境
├─ 打开方式:目标页面加载时自动注入
├─ 生命周期:与目标页面共存亡
├─ 可访问的 API:有限的 chrome.* API
└─ 可访问的 DOM:目标页面的 DOM
这意味着:Popup 中的 enhancePrompt 函数与 Content Script 中的同名函数是两个独立的副本。它们的代码逻辑可能不同步——我们后面会讨论如何解决这个问题。
5.2 Popup HTML 结构分析
<div class="input-section">
<label for="originalPrompt">Original Prompt:</label>
<textarea id="originalPrompt" placeholder="Enter your prompt here..."></textarea>
</div>
<button id="enhanceBtn">Enhance Prompt</button>
<div class="input-section">
<label for="enhancedPrompt">Enhanced Prompt:</label>
<textarea id="enhancedPrompt" readonly></textarea>
</div>
<button id="copyBtn">Copy Enhanced Prompt</button>
布局特点:
- 两个 textarea:一个可编辑输入,一个只读输出(
readonly属性防止用户误修改) - 两个按钮:增强按钮和复制按钮,功能分离
- 标签 + textarea:通过
label的for属性和textarea的id关联,点击标签自动聚焦输入框
5.3 复制功能实现
copyBtn.addEventListener('click', function() {
enhancedPrompt.select();
document.execCommand('copy');
alert('已复制增强后的提示词');
});
这里使用了 document.execCommand('copy') —— 一个被标记为 deprecated 但仍然广泛支持的 API。更现代的做法是使用 Clipboard API:
// 更现代的复制方式(替代方案)
navigator.clipboard.writeText(enhancedPrompt.value)
.then(() => alert('已复制增强后的提示词'))
.catch(err => console.error('复制失败:', err));
但 navigator.clipboard 需要 HTTPS 环境或 clipboard-write 权限,在 Chrome Extension 的 Popup 中,document.execCommand('copy') 仍然是最简单可靠的选择。
5.4 代码复用问题
当前实现中,enhancePrompt() 函数在 content.js 和 popup.js 中各有一份。这是代码重复问题,在真正的生产项目中应该提取为共享模块:
方案一:Shared Module(推荐)
// shared/enhancer.js
export function enhancePrompt(prompt) { ... }
然后在 content.js 和 popup.js 中导入。但由于 Manifest V3 的 Content Script 不能直接使用 ES Module(需要打包工具),更实际的方案是:
方案二:Service Worker 作为中介
// content.js
chrome.runtime.sendMessage({ action: "enhance", text: prompt });
// service-worker.js
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === "enhance") {
sendResponse({ result: enhancePrompt(request.text) });
}
});
这样增强逻辑只维护一份,Content Script 和 Popup 都通过消息传递来调用。这也是当前实现可以优化的方向。
六、多站点兼容性设计深度分析
6.1 站点适配策略
当前扩展支持的站点:
| 站点 | 域名 | 输入框特征 |
|---|---|---|
| 豆包 | doubao.com | <textarea> |
| 通义千问 | qwen.ai | contenteditable div |
| DeepSeek | deepseek.com | contenteditable div |
| 文心一言 | wenxin.baidu.com | <textarea> |
| 智谱 ChatGLM | chatglm.cn | <textarea> |
| 月之暗面 Moonshot | moonshot.cn | <textarea> |
6.2 各站点的特殊处理
在实际测试中,我发现不同站点存在各种"幺蛾子":
通义千问(qwen.ai)
- 输入框是一个
contenteditable的<div>嵌套<p>标签 - 写回内容时,如果使用
innerText赋值,会破坏内部的<p>结构 - 解决方案:使用
innerHTML替代innerText,但要注意 XSS 风险——不过我们的增强结果只是纯文本,没有 HTML 标签,所以是安全的
DeepSeek(deepseek.com)
- 输入框上方有一个工具栏(代码块、公式等按钮),我们的增强按钮可能会和工具栏重叠
- 按钮定位需要避开工具栏区域
- 潜在解决方案:检测输入框上方是否有固定高度的工具栏,动态调整按钮的
top值
豆包(doubao.com)
- 使用 Shadow DOM 封装输入框组件
- Content Script 默认无法访问 Shadow DOM 内部的元素
- 潜在解决方案:通过
element.shadowRoot访问或使用open模式的 Shadow DOM
6.3 通用性 vs 特异性
这是 Content Script 开发中最根本的权衡:
- 通用选择器(如
textarea):覆盖广,但容易误伤(比如在评论框上也注入按钮) - 特定选择器(如
textarea[placeholder*="问" i]):精准,但可能漏掉新版 UI
我们的策略是通用优先,特例兜底:
先用通用选择器找出所有候选元素
└─ 再用 size 过滤(忽略高度 < 50px 的元素,大概率不是 AI 输入框)
└─ 最后用位置过滤(忽略不在视口中央的 textarea)
这个过滤策略可以作为后续优化的方向。
七、代码中的 Bug 分析(真实排查记录)
在阅读源代码时,我发现了一个 bug:
function getPositionContainer(element) {
let parent = element.parentElement;
while (parent && parent !== document.body) {
const position = window.getComputedStyle(position); // ← BUG!
// ^^^^^^^^
// 这里应该是 window.getComputedStyle(parent)
if (position && (position.position === 'relative' ||
position.position === 'absolute' ||
position.position === 'fixed')) {
return parent;
}
parent = parent.parentElement;
}
return element.parentElement;
}
问题:window.getComputedStyle(position) 中,position 是一个字符串('relative'、'absolute' 等),而不是 DOM 元素。window.getComputedStyle 期望的第一个参数是 Element,传入字符串会导致返回空对象或报错。
影响:这个函数会一直遍历到 document.body,最终返回 element.parentElement。但 element.parentElement 的 position 没有被设置为 relative! 代码中在 enhanceElement 函数里通过 getComputedStyle(positionElement).position 检查并设置 position: relative,但由于这个 Bug 提前返回了错误的父容器,按钮的 position: absolute 可能相对于错误容器定位。
修复:将 window.getComputedStyle(position) 改为 window.getComputedStyle(parent)。
八、开发与调试工作流
8.1 本地开发环境搭建
- 在 Chrome 地址栏输入
chrome://extensions - 打开右上角的"开发者模式"
- 点击"加载已解压的扩展程序"
- 选择
ai-prompt-enhancer/目录
8.2 调试技巧
Content Script 调试
Content Script 在目标页面的上下文中运行,但不在页面本身的 Sources 面板中。要调试 Content Script:
- 打开目标 AI 网站(如
doubao.com) - 打开 DevTools(F12)
- 在 Sources 面板中,找到
chrome-extension://[扩展ID]/content.js - 在此设置断点
或者使用 console.log 输出,在目标页面的 Console 中查看——Content Script 的日志会出现在页面的 Console 中,并且会标明来源。
Popup 调试
Popup 弹窗的调试方式比较特殊:
- 在扩展图标上右键 → “审查弹出内容”
- 这会打开一个独立的 DevTools 窗口,专门用于调试 Popup
如果 Popup 失焦关闭,DevTools 窗口也会关闭。所以调试 Popup 时,可以先在代码中加 debugger; 语句,让执行暂停。
8.3 Hot Reload
每次修改 content.js 或 popup.js 后,都需要在 chrome://extensions 页面点击刷新按钮。可以考虑使用 chrome.runtime.reload() 或借助 chrome-extensions-reloader 之类的工具。
九、安全注意事项(MV3 特有)
9.1 安全策略详解
{
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'"
}
}
(本项目的 manifest 中没有显式声明 content_security_policy,Chrome 会使用默认值 "script-src 'self'")
MV3 的默认 CSP(内容安全策略)意味着:
- 不能使用
eval()——eval()、new Function()、setTimeout(string)等都会报错 - 不能使用内联
<script>——所有脚本必须通过<script src="...">引入外部文件 - 不能加载外部脚本——CDN 上的库(如 lodash、axios)不能直接在扩展中使用
9.2 对我们的影响
这个限制意味着:
- 增强逻辑必须是自己写的(不能调用外部 AI API)
- 如果需要接入 GPT API 来实现"AI 增强 AI",必须通过 HTTPS 请求(
fetch)来调用,且 API key 必须硬编码在扩展中或通过用户设置输入 - 不能使用
new Function()做动态代码生成
9.3 CSS 注入的风险与防护
Content Script 注入的样式虽然只影响用户界面,但如果处理不当,可能被恶意站点利用。最佳实践:
- 使用
CSSStyleDeclaration对象(内联样式)而非<style>标签 - 使用
!important标记关键样式属性以防止站点覆盖 - 不在样式中使用
url()引用外部资源
十、完整代码结构与关键文件清单
ai-prompt-enhancer/
├── manifest.json # 扩展配置文件(46 行)
├── popup.html # 弹窗 UI 布局(71 行)
├── popup.js # 弹窗交互与增强逻辑(42 行)
├── content.js # Content Script 核心逻辑(219 行)
└── icons/
├── icon16.png # 工具栏图标 16x16
├── icon32.png # 工具栏图标 32x32
├── icon48.png # 管理页图标 48x48
└── icon128.png # Chrome Web Store 图标 128x128
总代码行数:约 378 行
十一、未来可优化方向
短期优化(容易做、收益大)
- 接入 LLM API——用 GPT-4 / DeepSeek API 做真正的 AI 增强,而不是规则匹配
- 提取共享模块——将
enhancePrompt提取为独立模块,通过 Service Worker 统一调用 - 修 Bug——修复
getPositionContainer中的getComputedStyle(position)错误 - Shadow DOM 支持——检测并穿透组件的 Shadow DOM 边界
中期优化
- 用户自定义增强规则——用
chrome.storage.sync存储用户的偏好设置 - 历史记录——保存增强前后的 Prompt 对比记录,支持回退
- 快捷键支持——
Ctrl+Shift+E一键增强当前输入框内容 - 多语言支持——英文、日文 Prompt 的增强规则
长期优化
- 上下文感知增强——根据 AI 网站的对话历史自动推断受众和风格
- Prompt 模板库——内置常见场景的 Prompt 模板(写作、编程、翻译、分析等)
- A/B 测试框架——对比不同增强策略对 AI 输出质量的影响
十二、总结
通过这个项目,我们完整走了一遍 Chrome Extension Manifest V3 的开发流程。
技术层面的收获:
- ✅ Content Script 的工作原理:DOM 注入、动态监听、幂等性处理
- ✅ MutationObserver 的配置参数和性能优化思路
- ✅ CSS 定位策略:
absolute定位的参照系、z-index管理 - ✅ Manifest V3 的安全模型:
host_permissions分离、CSP 限制 - ✅ 跨站点兼容性的处理策略:选择器设计、布局适配
工程层面的收获:
- ✅ 代码 Bug 的发现与修复思路
- ✅ Content Script 与 Popup 的通信与代码复用
- ✅ 浏览器扩展的调试工作流
- ✅ 增强算法的边界情况处理
整个项目代码不超过 400 行,却解决了每天使用 AI 时都会遇到的真实痛点。这就是工具类 Chrome 扩展的魅力所在——小而精,一次开发,持续受益。
如果你也遇到 Prompt 写不好的问题,不妨试试自己动手写一个。开发过程本身,就是对 Prompt Engineering 最好的学习。
如果你来扩展这个项目,你最想增加什么功能?欢迎在评论区分享你的想法!
项目地址:[GitHub 待上传] | 欢迎 Star ⭐
如果你对 Chrome 扩展开发或 Prompt Engineering 有更多想法,欢迎在评论区交流!
附:文中提到的 Bug 已于核查时记录,建议在正式发布前修复。完整的修正版代码可以在 GitHub 仓库的 fix/getPositionContainer 分支查看。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)