手把手教你开发一个 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 最大的变化是:

  1. 用 Service Worker 替代了 Background Page——后台脚本不再是常驻页面,而是事件驱动的 Worker,用完即销毁。这意味着你不能在全局变量中保存状态。
  2. 强制 host_permissions 分离——以前权限声明写在 permissions 数组里就行了,V3 必须用单独的 host_permissions 字段声明要访问的网站域名。
  3. 禁止远程代码——所有 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.comchat.doubao.com
/* 匹配任意路径 /chat/api/conversation 都行

注意*://*.ai/* 这样的泛匹配会匹配所有 .ai 域名的网站,包括 example.aiopen.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。
  • 如果脚本执行时页面已经加载完毕(interactivecomplete 状态),就立即执行。

为什么用 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,发现它的 positionrelative,返回它。按钮会相对于 .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: 500z-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 秒):按钮从蓝色变成绿色,给用户即时的"操作已收到"信号
  2. 文字变化(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
  • innerTexttextContent 的区别:innerText 会考虑 CSS 样式(比如不会返回 display: none 元素的内容),textContent 返回所有文本内容(包括隐藏元素的文本)。这里用 innerText 更合适,因为我们只关心用户看到的文本。

四、Prompt 增强算法:从规则引擎到智能补全

4.1 算法设计思路

增强算法的核心思路基于 Prompt Engineering 的最佳实践——好的 Prompt 应该包含 4 个要素

  1. 目标受众(Audience):写给谁看的?初学者、专业人士、管理者?
  2. 长度要求(Length):要写多少字、多少段、多少页?
  3. 内容结构(Structure):需要小标题、步骤、checklist 还是对比表格?
  4. 语气风格(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:通过 labelfor 属性和 textareaid 关联,点击标签自动聚焦输入框

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.jspopup.js 中各有一份。这是代码重复问题,在真正的生产项目中应该提取为共享模块:

方案一:Shared Module(推荐)

// shared/enhancer.js
export function enhancePrompt(prompt) { ... }

然后在 content.jspopup.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.parentElementelement.parentElementposition 没有被设置为 relative 代码中在 enhanceElement 函数里通过 getComputedStyle(positionElement).position 检查并设置 position: relative,但由于这个 Bug 提前返回了错误的父容器,按钮的 position: absolute 可能相对于错误容器定位。

修复:将 window.getComputedStyle(position) 改为 window.getComputedStyle(parent)


八、开发与调试工作流

8.1 本地开发环境搭建

  1. 在 Chrome 地址栏输入 chrome://extensions
  2. 打开右上角的"开发者模式"
  3. 点击"加载已解压的扩展程序"
  4. 选择 ai-prompt-enhancer/ 目录

8.2 调试技巧

Content Script 调试

Content Script 在目标页面的上下文中运行,但不在页面本身的 Sources 面板中。要调试 Content Script:

  1. 打开目标 AI 网站(如 doubao.com
  2. 打开 DevTools(F12)
  3. 在 Sources 面板中,找到 chrome-extension://[扩展ID]/content.js
  4. 在此设置断点

或者使用 console.log 输出,在目标页面的 Console 中查看——Content Script 的日志会出现在页面的 Console 中,并且会标明来源。

Popup 调试

Popup 弹窗的调试方式比较特殊:

  1. 在扩展图标上右键 → “审查弹出内容”
  2. 这会打开一个独立的 DevTools 窗口,专门用于调试 Popup

如果 Popup 失焦关闭,DevTools 窗口也会关闭。所以调试 Popup 时,可以先在代码中加 debugger; 语句,让执行暂停。

8.3 Hot Reload

每次修改 content.jspopup.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(内容安全策略)意味着:

  1. 不能使用 eval()——eval()new Function()setTimeout(string) 等都会报错
  2. 不能使用内联 <script>——所有脚本必须通过 <script src="..."> 引入外部文件
  3. 不能加载外部脚本——CDN 上的库(如 lodash、axios)不能直接在扩展中使用

9.2 对我们的影响

这个限制意味着:

  • 增强逻辑必须是自己写的(不能调用外部 AI API)
  • 如果需要接入 GPT API 来实现"AI 增强 AI",必须通过 HTTPS 请求(fetch)来调用,且 API key 必须硬编码在扩展中或通过用户设置输入
  • 不能使用 new Function() 做动态代码生成

9.3 CSS 注入的风险与防护

Content Script 注入的样式虽然只影响用户界面,但如果处理不当,可能被恶意站点利用。最佳实践:

  1. 使用 CSSStyleDeclaration 对象(内联样式)而非 <style> 标签
  2. 使用 !important 标记关键样式属性以防止站点覆盖
  3. 不在样式中使用 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 行


十一、未来可优化方向

短期优化(容易做、收益大)

  1. 接入 LLM API——用 GPT-4 / DeepSeek API 做真正的 AI 增强,而不是规则匹配
  2. 提取共享模块——将 enhancePrompt 提取为独立模块,通过 Service Worker 统一调用
  3. 修 Bug——修复 getPositionContainer 中的 getComputedStyle(position) 错误
  4. Shadow DOM 支持——检测并穿透组件的 Shadow DOM 边界

中期优化

  1. 用户自定义增强规则——用 chrome.storage.sync 存储用户的偏好设置
  2. 历史记录——保存增强前后的 Prompt 对比记录,支持回退
  3. 快捷键支持——Ctrl+Shift+E 一键增强当前输入框内容
  4. 多语言支持——英文、日文 Prompt 的增强规则

长期优化

  1. 上下文感知增强——根据 AI 网站的对话历史自动推断受众和风格
  2. Prompt 模板库——内置常见场景的 Prompt 模板(写作、编程、翻译、分析等)
  3. 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 分支查看。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐