├── .gitignore
├── README.md
├── images
├── api-setting.png
├── custom-tool-usage1.png
├── custom-tool-usage2.png
├── installation1.png
├── installation2.png
├── installation3.png
├── installation4.png
├── installation5.png
├── model-setting.png
└── quote-usage.png
├── package-lock.json
├── package.json
├── public
├── background.js
├── contentScript.js
├── floatButton.css
├── floatButton.js
├── icons
│ ├── icon128.png
│ ├── icon16.png
│ └── icon48.png
├── index.html
├── manifest.json
└── styles.css
├── src
├── api
│ └── chatApi.js
├── assets
│ └── styles.css
├── components
│ ├── App.js
│ ├── ChatPanel.js
│ ├── HistoryPanel.js
│ ├── MessageList.js
│ ├── ModelSelector.js
│ ├── SettingsPanel.js
│ ├── Tabs.js
│ └── ToolsPanel.js
├── index.js
└── utils
│ └── storage.js
└── webpack.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # 依赖
2 | /node_modules
3 | /.pnp
4 | .pnp.js
5 |
6 | # 测试
7 | /coverage
8 |
9 | # 构建输出
10 | /build
11 | /dist
12 |
13 | # 环境变量
14 | .env
15 | .env.local
16 | .env.development.local
17 | .env.test.local
18 | .env.production.local
19 |
20 | # 日志
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 | *.log
25 |
26 | # 编辑器配置
27 | .idea/
28 | .vscode/
29 | *.swp
30 | *.swo
31 | .DS_Store
32 |
33 | # 缓存
34 | .cache/
35 | .eslintcache
36 | .npm
37 |
38 | # 临时文件
39 | *.tmp
40 | *.temp
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # WebChat 浏览器侧边栏 AI 问答插件
2 |
3 | WebChat 是一个功能强大的 Chrome 扩展,可以帮助您在浏览网页时快速引用内容并与 AI 进行交互。
4 |
5 | 现有的网页侧边栏 AI 插件存在``几个不好用的硬伤``:
6 | 1. 不能自定义 api 设置自己想用的模型;
7 | 2. 不能对多段不连续的内容同时引用提问;
8 | 3. 不能自定义提示词。希望使用此功能:选中网页文本之后弹出自定义命令按钮,点击弹出的自定义的命令按钮就会``选中内容+自定义提示词``快速同时发送(比如:自定义“翻译”按钮,提示词是:“将上述内容翻译为英文”)。
9 |
10 | **项目地址:** [https://github.com/tangpan360/webchat](https://github.com/tangpan360/webchat)
11 |
12 | ## 使用方法
13 |
14 | ### 划线引用
15 | 1. 选中网页上的任意文本
16 | 2. 点击出现的"引用"按钮
17 | 3. 引用内容会自动添加到侧边栏中
18 | 4. 可以继续选择其他文本进行引用
19 | 5. 在输入框中添加问题或直接发送引用内容
20 |
21 | 
22 |
23 | ### 自定义工具
24 | 1. 点击"划线工具栏"选项卡
25 | 2. 添加新工具,设置名称和提示词
26 | 3. 选中网页文本时,新工具按钮会出现在引用按钮旁边
27 | 4. 点击自定义工具按钮,自动引用内容并添加提示词发送
28 |
29 | 
30 | 
31 |
32 | ### 自定义模型设置
33 | 1. 点击侧边栏右上角的设置图标
34 | 2. 在设置面板中选择"设置"选项卡
35 | 3. 选择您希望使用的模型
36 | 4. 可以添加自定义API密钥和自定义参数
37 |
38 | 
39 | 
40 |
41 | ## 主要功能
42 |
43 | ### 1. 划线引用功能
44 | - 选中网页文本后,会出现引用按钮工具栏
45 | - 支持多段不连续内容的引用
46 | - 所有引用内容显示在输入框上方,可随时编辑或删除
47 |
48 | ### 2. 自定义划线工具
49 | - 通过"划线工具栏"选项卡添加自定义工具
50 | - 每个工具可以配置名称和提示词
51 | - 选中文本后一键执行"引用+提示词+发送"的操作组合
52 |
53 | ### 3. 聊天功能
54 | - 支持 GPT-3.5、GPT-4 等多种模型
55 | - 流式响应,实时显示 AI 回复
56 | - 支持停止生成、重新生成等操作
57 |
58 | ### 4. 历史记录管理
59 | - 自动保存所有对话
60 | - 历史记录可随时查看和恢复
61 | - 支持编辑和删除历史消息
62 |
63 | ### 5. 界面特性
64 | - 悬浮按钮可拖动且紧贴屏幕边缘
65 | - 支持消息复制、删除等快捷操作
66 |
67 | ## 安装方法
68 |
69 | ### 方法一:直接安装发布版本(推荐)
70 |
71 | 1. 前往本项目的 [Releases](https://github.com/tangpan360/webchat/releases) 页面
72 | 2. 下载最新版本的 `webchat.zip` 文件
73 | 3. 解压下载的文件到本地文件夹
74 | 4. 在 Chrome 浏览器中打开 `chrome://extensions/`
75 | 5. 开启右上角的"开发者模式"
76 | 6. 点击"加载已解压的扩展程序"
77 | 7. 选择解压后的文件夹
78 | 8. 扩展安装完成后,图标将显示在浏览器工具栏
79 |
80 | 
81 | 
82 | 
83 | 
84 | 
85 |
86 | ### 方法二:从源码构建安装(开发者)
87 |
88 | 1. 首先,克隆或下载本仓库到本地:
89 | ```bash
90 | git clone https://github.com/tangpan360/webchat.git
91 | cd webchat
92 | ```
93 |
94 | 2. 安装项目依赖:
95 | ```bash
96 | npm install
97 | ```
98 |
99 | 3. 构建项目:
100 | ```bash
101 | npm run build
102 | ```
103 | 这将在项目根目录下生成一个 `dist` 文件夹,其中包含扩展所需的所有文件。
104 |
105 | 4. 在 Chrome 浏览器中加载扩展:
106 | - 打开 Chrome 浏览器,在地址栏输入 `chrome://extensions/`
107 | - 在右上角开启"开发者模式"
108 | - 点击"加载已解压的扩展程序"按钮
109 | - 选择项目中的 `dist` 目录
110 | - 成功后,扩展图标将出现在浏览器工具栏中
111 |
112 | ## 开发指南
113 |
114 | ### 开发模式
115 |
116 | 如果您想在开发模式下运行:
117 |
118 | ```bash
119 | npm start
120 | ```
121 |
122 | 这将启动开发服务器,当您修改代码时,扩展会自动重新构建。重新构建后,您需要在 `chrome://extensions/` 页面点击扩展卡片上的"刷新"按钮或重新加载扩展。
123 |
124 | ### 项目结构
125 |
126 | 本项目使用 React 构建,主要文件结构:
127 |
128 | - `/src` - 源代码目录
129 | - `/components` - React 组件
130 | - `/assets` - 样式和资源文件
131 | - `/utils` - 工具函数
132 | - `/api` - API 调用
133 | - `/public` - 静态资源和扩展配置
134 | - `/dist` - 构建输出目录(不包含在源码仓库中)
135 |
136 | ### 发布流程
137 |
138 | 如果您是项目维护者并想发布新版本:
139 |
140 | 1. 更新 `manifest.json` 中的版本号
141 | 2. 构建项目:`npm run build`
142 | 3. 将 `dist` 目录中的所有文件压缩为 zip 文件:
143 | ```bash
144 | cd dist
145 | zip -r ../webchat.zip *
146 | ```
147 | 4. 在 GitHub 创建新的 Release,上传 zip 文件作为附件
148 |
149 | ## 技术特性
150 |
151 | - 使用 React 构建用户界面
152 | - 使用 Chrome 扩展 API 实现跨页面通信
153 | - 支持 Markdown 渲染,包括代码高亮和公式
154 | - 本地存储聊天历史和用户配置
155 |
156 | ## 贡献指南
157 |
158 | 欢迎提交 Pull Request 或 Issue 来改进这个项目!
159 |
160 | 1. Fork 本仓库
161 | 2. 创建您的功能分支: `git checkout -b feature/amazing-feature`
162 | 3. 提交您的更改: `git commit -m '添加了一些很棒的功能'`
163 | 4. 推送到分支: `git push origin feature/amazing-feature`
164 | 5. 提交 Pull Request
165 |
--------------------------------------------------------------------------------
/images/api-setting.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tangpan360/webchat/6c01dfe6005798c2c275b03c3ce31cb1f33e1c40/images/api-setting.png
--------------------------------------------------------------------------------
/images/custom-tool-usage1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tangpan360/webchat/6c01dfe6005798c2c275b03c3ce31cb1f33e1c40/images/custom-tool-usage1.png
--------------------------------------------------------------------------------
/images/custom-tool-usage2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tangpan360/webchat/6c01dfe6005798c2c275b03c3ce31cb1f33e1c40/images/custom-tool-usage2.png
--------------------------------------------------------------------------------
/images/installation1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tangpan360/webchat/6c01dfe6005798c2c275b03c3ce31cb1f33e1c40/images/installation1.png
--------------------------------------------------------------------------------
/images/installation2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tangpan360/webchat/6c01dfe6005798c2c275b03c3ce31cb1f33e1c40/images/installation2.png
--------------------------------------------------------------------------------
/images/installation3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tangpan360/webchat/6c01dfe6005798c2c275b03c3ce31cb1f33e1c40/images/installation3.png
--------------------------------------------------------------------------------
/images/installation4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tangpan360/webchat/6c01dfe6005798c2c275b03c3ce31cb1f33e1c40/images/installation4.png
--------------------------------------------------------------------------------
/images/installation5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tangpan360/webchat/6c01dfe6005798c2c275b03c3ce31cb1f33e1c40/images/installation5.png
--------------------------------------------------------------------------------
/images/model-setting.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tangpan360/webchat/6c01dfe6005798c2c275b03c3ce31cb1f33e1c40/images/model-setting.png
--------------------------------------------------------------------------------
/images/quote-usage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tangpan360/webchat/6c01dfe6005798c2c275b03c3ce31cb1f33e1c40/images/quote-usage.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "webchat",
3 | "version": "1.0.5",
4 | "description": "浏览器侧边栏AI问答插件",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "webpack --watch --mode=development",
8 | "build": "webpack --mode=production",
9 | "test": "echo \"Error: no test specified\" && exit 1"
10 | },
11 | "author": "",
12 | "license": "MIT",
13 | "dependencies": {
14 | "katex": "^0.16.21",
15 | "react": "^18.2.0",
16 | "react-dom": "^18.2.0",
17 | "react-markdown": "^8.0.7",
18 | "react-syntax-highlighter": "^15.5.0",
19 | "rehype-katex": "^6.0.3",
20 | "remark-gfm": "^3.0.1",
21 | "remark-math": "^5.1.1"
22 | },
23 | "devDependencies": {
24 | "@babel/core": "^7.22.5",
25 | "@babel/preset-env": "^7.22.5",
26 | "@babel/preset-react": "^7.22.5",
27 | "babel-loader": "^9.1.2",
28 | "copy-webpack-plugin": "^11.0.0",
29 | "css-loader": "^6.8.1",
30 | "html-webpack-plugin": "^5.5.3",
31 | "style-loader": "^3.3.3",
32 | "webpack": "^5.88.0",
33 | "webpack-cli": "^5.1.4"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/public/background.js:
--------------------------------------------------------------------------------
1 | // 激活侧边栏功能
2 | chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });
3 |
4 | // 监听扩展安装或更新
5 | chrome.runtime.onInstalled.addListener((details) => {
6 | if (details.reason === "install") {
7 | console.log("WebChat扩展已安装");
8 | // 初始化工具栏设置
9 | chrome.storage.local.set({ 'toolbarSettings': { enabled: true } });
10 | } else if (details.reason === "update") {
11 | console.log(`WebChat扩展已更新到版本 ${chrome.runtime.getManifest().version}`);
12 | // 确保工具栏设置存在
13 | chrome.storage.local.get(['toolbarSettings'], (result) => {
14 | if (!result.toolbarSettings) {
15 | chrome.storage.local.set({ 'toolbarSettings': { enabled: true } });
16 | }
17 | });
18 | }
19 | });
20 |
21 | // 存储引用内容的数组
22 | let quotedTexts = [];
23 |
24 | // 存储自定义工具
25 | let customTools = [];
26 |
27 | // 工具栏设置
28 | let toolbarSettings = { enabled: true };
29 |
30 | // 状态变量
31 | let pendingActions = []; // 存储等待执行的操作
32 | let sidePanelReady = false; // 侧边栏是否已准备好
33 | let sidePanelOpening = false; // 侧边栏是否正在打开中
34 |
35 | // 初始化存储
36 | chrome.storage.local.get(['customTools', 'toolbarSettings'], (result) => {
37 | if (result.customTools) {
38 | customTools = result.customTools;
39 | }
40 | if (result.toolbarSettings) {
41 | toolbarSettings = result.toolbarSettings;
42 | }
43 | });
44 |
45 | // 跟踪侧边栏状态
46 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
47 | if (message.type === "sidePanelReady") {
48 | console.log("侧边栏已准备好接收消息");
49 | sidePanelReady = true;
50 | sidePanelOpening = false;
51 |
52 | // 处理所有待处理的操作
53 | if (pendingActions.length > 0) {
54 | console.log(`执行${pendingActions.length}个待处理操作`);
55 | pendingActions.forEach(action => {
56 | chrome.runtime.sendMessage(action);
57 | });
58 | pendingActions = []; // 清空待处理队列
59 | }
60 |
61 | if (sendResponse) {
62 | sendResponse({ status: "acknowledged" });
63 | }
64 | }
65 |
66 | // 当侧边栏关闭时重置状态
67 | if (message.type === "sidePanelClosed") {
68 | sidePanelReady = false;
69 | console.log("侧边栏已关闭");
70 | if (sendResponse) {
71 | sendResponse({ status: "acknowledged" });
72 | }
73 | }
74 | });
75 |
76 | // 监听来自content script的消息
77 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
78 | // 处理打开侧边栏的请求
79 | if (message.action === "openSidePanel") {
80 | // 标记侧边栏正在打开
81 | sidePanelOpening = true;
82 | sidePanelReady = false;
83 | chrome.sidePanel.open({ windowId: sender.tab?.windowId });
84 | }
85 |
86 | // 处理添加引用内容的请求
87 | if (message.type === "addQuote") {
88 | // 添加新的引用内容
89 | quotedTexts.push(message.quote);
90 |
91 | // 将引用内容发送给侧边栏
92 | chrome.runtime.sendMessage({
93 | type: "updateQuotes",
94 | quotes: quotedTexts
95 | });
96 |
97 | // 尝试打开侧边栏
98 | if (sender.tab) {
99 | chrome.sidePanel.open({ windowId: sender.tab.windowId });
100 | }
101 | }
102 |
103 | // 处理获取引用内容的请求
104 | if (message.type === "getQuotes") {
105 | // 直接回复当前引用内容
106 | chrome.runtime.sendMessage({
107 | type: "updateQuotes",
108 | quotes: quotedTexts
109 | });
110 |
111 | // 也返回响应(如果请求方使用了回调)
112 | if (sendResponse) {
113 | sendResponse({ quotes: quotedTexts });
114 | }
115 |
116 | return true; // 指示我们可能会异步回复
117 | }
118 |
119 | // 处理删除单个引用的请求
120 | if (message.type === "deleteQuote") {
121 | quotedTexts = quotedTexts.filter(quote => quote.id !== message.quoteId);
122 |
123 | // 将更新后的引用内容发送给侧边栏
124 | chrome.runtime.sendMessage({
125 | type: "updateQuotes",
126 | quotes: quotedTexts
127 | });
128 | }
129 |
130 | // 处理清空所有引用的请求
131 | if (message.type === "clearAllQuotes") {
132 | quotedTexts = [];
133 |
134 | // 通知侧边栏清空引用
135 | chrome.runtime.sendMessage({
136 | type: "updateQuotes",
137 | quotes: []
138 | });
139 | }
140 |
141 | // 获取自定义工具
142 | if (message.type === "getCustomTools") {
143 | chrome.storage.local.get(['customTools'], (result) => {
144 | if (result.customTools) {
145 | customTools = result.customTools;
146 | }
147 | sendResponse({ tools: customTools });
148 | });
149 | return true; // 指示我们会异步回复
150 | }
151 |
152 | // 更新自定义工具列表
153 | if (message.type === "updateCustomTools") {
154 | chrome.storage.local.get(['customTools'], (result) => {
155 | if (result.customTools) {
156 | customTools = result.customTools;
157 |
158 | // 通知所有内容脚本更新工具按钮
159 | chrome.tabs.query({}, (tabs) => {
160 | tabs.forEach(tab => {
161 | chrome.tabs.sendMessage(tab.id, { type: "updateCustomTools" })
162 | .catch(() => {}); // 忽略不支持的标签页错误
163 | });
164 | });
165 | }
166 | });
167 | }
168 |
169 | // 更新工具栏设置
170 | if (message.type === "updateToolbarSettings") {
171 | if (message.settings && message.settings.hasOwnProperty('enabled')) {
172 | toolbarSettings = message.settings;
173 |
174 | // 保存设置到存储
175 | chrome.storage.local.set({ 'toolbarSettings': toolbarSettings });
176 |
177 | // 通知所有内容脚本更新工具栏状态
178 | chrome.tabs.query({}, (tabs) => {
179 | tabs.forEach(tab => {
180 | chrome.tabs.sendMessage(tab.id, {
181 | type: "updateToolbarSettings",
182 | settings: toolbarSettings
183 | }).catch(() => {}); // 忽略不支持的标签页错误
184 | });
185 | });
186 |
187 | console.log('工具栏设置已更新:', toolbarSettings);
188 | }
189 | }
190 |
191 | // 执行工具操作(自动发送引用+提示词)
192 | if (message.type === "executeToolAction") {
193 | try {
194 | // 确保数据完整
195 | if (!message.data || !message.data.text || !message.data.prompt) {
196 | console.error('executeToolAction 数据不完整:', message.data);
197 | return;
198 | }
199 |
200 | // 生成唯一操作ID(如果没有)
201 | if (!message.data.actionId) {
202 | message.data.actionId = Date.now().toString() + Math.random().toString(36).substring(2, 8);
203 | console.log('为工具操作生成ID:', message.data.actionId);
204 | }
205 |
206 | console.log('background收到工具操作请求:', message.data.prompt, 'ID:', message.data.actionId);
207 |
208 | const actionMessage = {
209 | type: "executeToolAction",
210 | data: message.data
211 | };
212 |
213 | // 如果侧边栏已准备好,直接发送
214 | if (sidePanelReady) {
215 | console.log('侧边栏已准备好,直接发送工具操作', message.data.actionId);
216 | chrome.runtime.sendMessage(actionMessage);
217 | }
218 | // 侧边栏未打开或正在打开中,添加到待处理队列
219 | else {
220 | // 如果侧边栏未打开,尝试打开
221 | if (!sidePanelOpening) {
222 | console.log('侧边栏未打开,正在打开侧边栏');
223 | sidePanelOpening = true;
224 | if (sender.tab) {
225 | chrome.sidePanel.open({ windowId: sender.tab.windowId });
226 | }
227 | } else {
228 | console.log('侧边栏正在打开中');
229 | }
230 |
231 | // 将消息添加到队列(确保队列中没有重复的相同消息)
232 | // 先检查队列中是否已经有相同ID的消息
233 | const existingIndex = pendingActions.findIndex(
234 | act => act.data && act.data.actionId === message.data.actionId
235 | );
236 |
237 | if (existingIndex >= 0) {
238 | console.log('队列中已存在相同ID的消息,跳过添加');
239 | } else {
240 | console.log('将工具操作添加到待处理队列', message.data.actionId);
241 | pendingActions.push(actionMessage);
242 | }
243 | }
244 | } catch (error) {
245 | console.error('处理工具操作请求时出错:', error);
246 | }
247 |
248 | return true;
249 | }
250 |
251 | return true; // 允许异步响应
252 | });
--------------------------------------------------------------------------------
/public/contentScript.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 内容脚本,用于捕获网页中的选中文本
3 | * 这个脚本会被注入到浏览器扩展所访问的网页中
4 | */
5 |
6 | // 工具栏及按钮元素
7 | let toolsContainer = null;
8 | let customTools = []; // 自定义工具列表
9 | let toolbarEnabled = true; // 工具栏启用状态
10 |
11 | // 创建工具栏
12 | function createToolsContainer() {
13 | if (toolsContainer) return;
14 |
15 | // 创建工具栏容器
16 | toolsContainer = document.createElement('div');
17 | toolsContainer.className = 'webchat-tools-container webchat-extension-styles';
18 | // 添加不可选中属性
19 | toolsContainer.setAttribute('unselectable', 'on');
20 | toolsContainer.setAttribute('onselectstart', 'return false;');
21 |
22 | // 添加阻止事件冒泡
23 | toolsContainer.addEventListener('mousedown', function(e) {
24 | e.stopPropagation();
25 | });
26 |
27 | toolsContainer.addEventListener('click', function(e) {
28 | e.stopPropagation();
29 | });
30 |
31 | // 添加到文档中
32 | document.body.appendChild(toolsContainer);
33 |
34 | // 加载自定义工具
35 | loadCustomTools();
36 |
37 | // 加载工具栏设置
38 | loadToolbarSettings();
39 |
40 | // 初始隐藏工具栏
41 | hideToolsContainer();
42 | }
43 |
44 | // 加载自定义工具
45 | function loadCustomTools() {
46 | if (typeof chrome !== 'undefined' && chrome.runtime) {
47 | chrome.runtime.sendMessage({ type: 'getCustomTools' }, (response) => {
48 | if (response && response.tools) {
49 | customTools = response.tools;
50 | updateToolsButtons();
51 | }
52 | });
53 | }
54 | }
55 |
56 | // 加载工具栏设置
57 | function loadToolbarSettings() {
58 | if (typeof chrome !== 'undefined' && chrome.runtime && chrome.storage) {
59 | chrome.storage.local.get(['toolbarSettings'], (result) => {
60 | if (result && result.toolbarSettings) {
61 | toolbarEnabled = result.toolbarSettings.enabled;
62 | console.log('工具栏启用状态:', toolbarEnabled);
63 | }
64 | });
65 | }
66 | }
67 |
68 | // 更新工具按钮
69 | function updateToolsButtons() {
70 | // 清空现有按钮
71 | toolsContainer.innerHTML = '';
72 |
73 | // 添加默认的复制按钮
74 | const copyButton = createToolButton('复制', handleCopyButtonClick);
75 | toolsContainer.appendChild(copyButton);
76 |
77 | // 添加默认的引用按钮
78 | const quoteButton = createToolButton('引用', handleQuoteButtonClick);
79 | toolsContainer.appendChild(quoteButton);
80 |
81 | // 添加自定义工具按钮
82 | customTools.forEach(tool => {
83 | const button = createToolButton(tool.name, (event) => handleCustomToolClick(tool, event));
84 | toolsContainer.appendChild(button);
85 | });
86 | }
87 |
88 | // 创建工具按钮
89 | function createToolButton(text, clickHandler) {
90 | const button = document.createElement('button');
91 | button.className = 'webchat-tool-button';
92 | button.textContent = text;
93 | button.addEventListener('click', (event) => {
94 | // 阻止事件冒泡,防止选中内容被取消
95 | event.stopPropagation();
96 | event.preventDefault();
97 | clickHandler(event);
98 | });
99 | return button;
100 | }
101 |
102 | // 复制按钮点击处理函数
103 | function handleCopyButtonClick(event) {
104 | // 阻止事件冒泡和默认行为
105 | event.stopPropagation();
106 | event.preventDefault();
107 |
108 | const selection = window.getSelection();
109 | if (!selection || !selection.toString().trim()) return;
110 |
111 | const selectedText = selection.toString().trim();
112 | const button = event.target;
113 | const originalText = button.textContent;
114 | const originalBackground = button.style.background || '#1a73e8';
115 | const buttonWidth = button.offsetWidth;
116 | const buttonHeight = button.offsetHeight;
117 |
118 | // 尝试复制文本到剪贴板
119 | navigator.clipboard.writeText(selectedText)
120 | .then(() => {
121 | // 复制成功,显示对号并变绿
122 | button.style.width = `${buttonWidth}px`;
123 | button.style.height = `${buttonHeight}px`;
124 | button.innerHTML = '✓';
125 | button.style.background = '#52c41a';
126 |
127 | // 1秒后恢复原样
128 | setTimeout(() => {
129 | button.textContent = originalText;
130 | button.style.background = originalBackground;
131 | button.style.width = '';
132 | button.style.height = '';
133 | }, 1000);
134 | })
135 | .catch(error => {
136 | // 复制失败,显示错号并变红
137 | button.style.width = `${buttonWidth}px`;
138 | button.style.height = `${buttonHeight}px`;
139 | button.innerHTML = '✗';
140 | button.style.background = '#f44336';
141 |
142 | // 1秒后恢复原样
143 | setTimeout(() => {
144 | button.textContent = originalText;
145 | button.style.background = originalBackground;
146 | button.style.width = '';
147 | button.style.height = '';
148 | }, 1000);
149 |
150 | console.error('复制到剪贴板失败:', error);
151 | });
152 | }
153 |
154 | // 引用按钮点击处理函数
155 | function handleQuoteButtonClick(event) {
156 | // 阻止事件冒泡和默认行为
157 | event.stopPropagation();
158 | event.preventDefault();
159 |
160 | const selection = window.getSelection();
161 | if (!selection || !selection.toString().trim()) return;
162 |
163 | const selectedText = selection.toString().trim();
164 |
165 | // 获取当前时间作为引用ID
166 | const quoteId = Date.now().toString();
167 |
168 | // 向侧边栏应用发送消息
169 | window.postMessage({
170 | type: 'addQuote',
171 | quote: {
172 | id: quoteId,
173 | text: selectedText
174 | }
175 | }, '*');
176 |
177 | // 向浏览器扩展发送消息
178 | if (typeof chrome !== 'undefined' && chrome.runtime) {
179 | chrome.runtime.sendMessage({
180 | type: 'addQuote',
181 | quote: {
182 | id: quoteId,
183 | text: selectedText
184 | }
185 | });
186 | }
187 |
188 | // 自动打开侧边栏
189 | if (typeof chrome !== 'undefined' && chrome.runtime) {
190 | chrome.runtime.sendMessage({
191 | action: "openSidePanel"
192 | });
193 | }
194 |
195 | // 清除选区并隐藏工具栏
196 | selection.removeAllRanges();
197 | hideToolsContainer();
198 | }
199 |
200 | // 自定义工具按钮点击处理函数
201 | function handleCustomToolClick(tool, event) {
202 | // 阻止事件冒泡和默认行为
203 | event.stopPropagation();
204 | event.preventDefault();
205 |
206 | try {
207 | const selection = window.getSelection();
208 | if (!selection || !selection.toString().trim()) return;
209 |
210 | const selectedText = selection.toString().trim();
211 |
212 | // 获取当前时间作为引用ID
213 | const quoteId = Date.now().toString();
214 |
215 | // 为操作生成唯一ID
216 | const actionId = quoteId + Math.random().toString(36).substring(2, 8);
217 |
218 | // 引用选中内容并立即发送工具操作请求
219 | if (typeof chrome !== 'undefined' && chrome.runtime) {
220 | console.log('处理工具点击:', tool.name, 'ID:', actionId);
221 |
222 | // 首先添加引用
223 | chrome.runtime.sendMessage({
224 | type: 'addQuote',
225 | quote: {
226 | id: quoteId,
227 | text: selectedText
228 | }
229 | });
230 |
231 | // 然后打开侧边栏
232 | chrome.runtime.sendMessage({
233 | action: "openSidePanel"
234 | });
235 |
236 | // 最后发送工具操作
237 | console.log('发送工具操作请求:', tool.name);
238 | chrome.runtime.sendMessage({
239 | type: 'executeToolAction',
240 | data: {
241 | text: selectedText,
242 | prompt: tool.prompt,
243 | actionId: actionId // 添加唯一ID
244 | }
245 | });
246 | }
247 |
248 | // 清除选区并隐藏工具栏
249 | selection.removeAllRanges();
250 | hideToolsContainer();
251 | } catch (error) {
252 | console.error('工具操作执行失败:', error);
253 | // 仍然隐藏工具栏
254 | hideToolsContainer();
255 | }
256 | }
257 |
258 | // 显示工具栏
259 | function showToolsContainer(x, y) {
260 | // 如果工具栏被禁用,则不显示
261 | if (!toolbarEnabled) return;
262 |
263 | if (!toolsContainer) createToolsContainer();
264 |
265 | // 设置工具栏位置
266 | toolsContainer.style.left = `${x}px`;
267 | toolsContainer.style.top = `${y}px`;
268 | toolsContainer.style.display = 'flex';
269 | }
270 |
271 | // 隐藏工具栏
272 | function hideToolsContainer() {
273 | if (toolsContainer) {
274 | toolsContainer.style.display = 'none';
275 | }
276 | }
277 |
278 | // 处理文本选择
279 | function handleTextSelection(e) {
280 | setTimeout(() => {
281 | // 如果工具栏被禁用,则不显示
282 | if (!toolbarEnabled) return;
283 |
284 | const selection = window.getSelection();
285 | if (!selection || !selection.toString().trim()) {
286 | hideToolsContainer();
287 | return;
288 | }
289 |
290 | // 获取选区的位置
291 | const range = selection.getRangeAt(0);
292 | const rect = range.getBoundingClientRect();
293 |
294 | // 获取视口尺寸和滚动位置
295 | const viewportHeight = window.innerHeight;
296 | const viewportWidth = window.innerWidth;
297 | const scrollX = window.scrollX;
298 | const scrollY = window.scrollY;
299 |
300 | // 判断选中区域相对于视口的位置
301 | const rectTop = rect.top; // 选区顶部相对于视口顶部的位置
302 | const rectBottom = rect.bottom; // 选区底部相对于视口顶部的位置
303 | const isTopVisible = rectTop >= 0 && rectTop < viewportHeight;
304 | const isBottomVisible = rectBottom >= 0 && rectBottom < viewportHeight;
305 |
306 | // 计算工具栏的基础水平位置(居中或靠左)
307 | let x = scrollX + rect.left;
308 | if (rect.width > 100) {
309 | x += (rect.width / 2) - 50; // 大致居中
310 | }
311 |
312 | // 确保工具栏不会超出视口左右边界
313 | const maxX = viewportWidth - 100; // 假设最小宽度100px
314 | if (x > maxX) x = maxX - 5;
315 | if (x < 0) x = 5;
316 |
317 | let y;
318 |
319 | // 根据不同情况计算工具栏的垂直位置
320 | if (isBottomVisible) {
321 | // 情况1和3:下部可见,显示在下部
322 | y = scrollY + rectBottom + 10;
323 | } else if (isTopVisible) {
324 | // 情况2:上部可见,下部不可见,显示在上部
325 | y = scrollY + rectTop - 40; // 减去工具栏的估计高度和一些间距
326 | } else {
327 | // 情况4:上部和下部都不可见(选中内容太长,中间可见)
328 | // 在视口中间显示工具栏
329 | y = scrollY + (viewportHeight / 2);
330 | }
331 |
332 | // 确保工具栏在视口内可见
333 | if (y < scrollY) {
334 | y = scrollY + 10; // 如果太靠上,则放在页面顶部附近
335 | } else if (y > scrollY + viewportHeight - 50) {
336 | y = scrollY + viewportHeight - 50; // 如果太靠下,则放在页面底部附近
337 | }
338 |
339 | showToolsContainer(x, y);
340 | }, 10);
341 | }
342 |
343 | // 检查是否点击了工具栏之外的区域
344 | function handleDocumentClick(e) {
345 | // 如果工具栏存在,并且点击的不是工具栏或其子元素
346 | if (toolsContainer && e.target !== toolsContainer && !toolsContainer.contains(e.target)) {
347 | hideToolsContainer();
348 | }
349 | // 注意:这里不添加阻止冒泡,因为这是文档级别的点击,我们需要让普通点击正常工作
350 | }
351 |
352 | // 监听来自扩展后台的消息
353 | function handleExtensionMessages() {
354 | if (typeof chrome !== 'undefined' && chrome.runtime) {
355 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
356 | if (message.type === 'updateCustomTools') {
357 | loadCustomTools();
358 | } else if (message.type === 'updateToolbarSettings') {
359 | // 更新工具栏启用状态
360 | if (message.settings && message.settings.hasOwnProperty('enabled')) {
361 | toolbarEnabled = message.settings.enabled;
362 | console.log('工具栏启用状态已更新:', toolbarEnabled);
363 |
364 | // 如果禁用了工具栏,则隐藏
365 | if (!toolbarEnabled) {
366 | hideToolsContainer();
367 | }
368 | }
369 | }
370 | return true;
371 | });
372 | }
373 | }
374 |
375 | // 处理键盘选择文本
376 | function handleKeyboardSelection(e) {
377 | // 如果工具栏被禁用,则不显示
378 | if (!toolbarEnabled) return;
379 |
380 | // 检测常见的文本选择组合键
381 | const isTextSelectionKey = (
382 | // Ctrl+A (全选)
383 | (e.ctrlKey && e.key === 'a') ||
384 | // Shift+方向键
385 | (e.shiftKey && (e.key === 'ArrowUp' || e.key === 'ArrowDown' || e.key === 'ArrowLeft' || e.key === 'ArrowRight')) ||
386 | // Shift+Home/End
387 | (e.shiftKey && (e.key === 'Home' || e.key === 'End'))
388 | );
389 |
390 | if (isTextSelectionKey) {
391 | setTimeout(() => {
392 | const selection = window.getSelection();
393 | if (!selection || !selection.toString().trim()) {
394 | return;
395 | }
396 |
397 | // 特殊处理Ctrl+A全选的情况
398 | if (e.ctrlKey && e.key === 'a' && toolsContainer && toolsContainer.style.display === 'flex') {
399 | // 移除工具栏中的文本从选择中
400 | try {
401 | const selRange = selection.getRangeAt(0);
402 |
403 | // 如果工具栏可见,创建一个不包含工具栏的选择范围
404 | if (document.body.contains(toolsContainer)) {
405 | const toolsRange = document.createRange();
406 | toolsRange.selectNode(toolsContainer);
407 |
408 | // 检查是否有重叠
409 | if (selRange.intersectsNode(toolsContainer)) {
410 | // 工具栏在选择范围内,我们需要排除它
411 | selection.removeAllRanges(); // 清除当前选择
412 |
413 | // 重新创建选择,但排除工具栏
414 | // 注意:这是一个简化的处理方式,实际上可能需要更复杂的范围操作
415 | const newRange = document.createRange();
416 | newRange.selectNodeContents(document.body);
417 | selection.addRange(newRange);
418 |
419 | // 重新获取选区范围
420 | if (selection.rangeCount === 0) return;
421 | }
422 | }
423 | } catch (err) {
424 | console.error('调整选择范围时出错:', err);
425 | }
426 | }
427 |
428 | // 获取选区范围
429 | if (selection.rangeCount === 0) return;
430 | const range = selection.getRangeAt(0);
431 | const rect = range.getBoundingClientRect();
432 |
433 | // 获取视口尺寸和滚动位置
434 | const viewportHeight = window.innerHeight;
435 | const viewportWidth = window.innerWidth;
436 | const scrollX = window.scrollX;
437 | const scrollY = window.scrollY;
438 |
439 | // 判断选中区域相对于视口的位置
440 | const rectTop = rect.top;
441 | const rectBottom = rect.bottom;
442 | const isTopVisible = rectTop >= 0 && rectTop < viewportHeight;
443 | const isBottomVisible = rectBottom >= 0 && rectBottom < viewportHeight;
444 |
445 | // 计算工具栏的水平位置
446 | let x = scrollX + rect.left;
447 | if (rect.width > 100) {
448 | x += (rect.width / 2) - 50;
449 | }
450 |
451 | // 确保不超出视口边界
452 | const maxX = viewportWidth - 100;
453 | if (x > maxX) x = maxX - 5;
454 | if (x < 0) x = 5;
455 |
456 | let y;
457 |
458 | // 根据不同情况计算垂直位置
459 | if (isBottomVisible) {
460 | // 下部可见,显示在下部
461 | y = scrollY + rectBottom + 10;
462 | } else if (isTopVisible) {
463 | // 上部可见,显示在上部
464 | y = scrollY + rectTop - 40;
465 | } else {
466 | // 中间可见或全部不可见,显示在视口中间
467 | y = scrollY + (viewportHeight / 2);
468 | }
469 |
470 | // 确保在视口内可见
471 | if (y < scrollY) {
472 | y = scrollY + 10;
473 | } else if (y > scrollY + viewportHeight - 50) {
474 | y = scrollY + viewportHeight - 50;
475 | }
476 |
477 | showToolsContainer(x, y);
478 | }, 10);
479 | }
480 | }
481 |
482 | // 初始化
483 | function init() {
484 | createToolsContainer();
485 |
486 | // 添加事件监听器
487 | document.addEventListener('mouseup', handleTextSelection);
488 | document.addEventListener('mousedown', handleDocumentClick);
489 | document.addEventListener('keyup', handleKeyboardSelection);
490 | document.addEventListener('selectionchange', () => {
491 | const selection = window.getSelection();
492 | if (!selection || !selection.toString().trim()) {
493 | hideToolsContainer();
494 | }
495 | });
496 |
497 | // 监听扩展消息
498 | handleExtensionMessages();
499 |
500 | // 添加样式
501 | const style = document.createElement('style');
502 | style.textContent = `
503 | .webchat-tools-container {
504 | position: absolute;
505 | display: flex;
506 | align-items: center;
507 | background-color: #fff;
508 | border-radius: 4px;
509 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
510 | z-index: 10000;
511 | padding: 4px;
512 | gap: 4px;
513 | user-select: none;
514 | -webkit-user-select: none;
515 | -moz-user-select: none;
516 | -ms-user-select: none;
517 | pointer-events: auto;
518 | }
519 |
520 | .webchat-tool-button {
521 | padding: 4px 8px;
522 | background: #1a73e8;
523 | color: white;
524 | border: none;
525 | border-radius: 3px;
526 | font-size: 12px;
527 | cursor: pointer;
528 | font-family: system-ui, -apple-system, sans-serif;
529 | min-width: auto !important;
530 | max-width: none !important;
531 | width: auto !important;
532 | height: auto !important;
533 | line-height: normal !important;
534 | margin: 0 !important;
535 | transition: background-color 0.3s;
536 | display: inline-flex;
537 | justify-content: center;
538 | align-items: center;
539 | min-width: 40px !important;
540 | box-sizing: border-box !important;
541 | line-height: 1 !important;
542 | user-select: none;
543 | -webkit-user-select: none;
544 | -moz-user-select: none;
545 | -ms-user-select: none;
546 | }
547 |
548 | .webchat-tool-button:hover {
549 | background: #0d66d0;
550 | }
551 | `;
552 | document.head.appendChild(style);
553 |
554 | console.log('WebChat 划线工具栏已初始化');
555 | }
556 |
557 | // 当文档加载完成后初始化
558 | if (document.readyState === 'loading') {
559 | document.addEventListener('DOMContentLoaded', init);
560 | } else {
561 | init();
562 | }
--------------------------------------------------------------------------------
/public/floatButton.css:
--------------------------------------------------------------------------------
1 | /* 悬浮按钮样式 */
2 | .webchat-float-button {
3 | position: fixed;
4 | right: 0px;
5 | top: 50%;
6 | transform: translateY(-50%);
7 | width: 48px;
8 | height: 48px;
9 | border-radius: 50%;
10 | background-color: #1a73e8;
11 | color: white;
12 | display: flex;
13 | align-items: center;
14 | justify-content: center;
15 | cursor: pointer;
16 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
17 | z-index: 10000;
18 | transition: all 0.3s ease;
19 | border: none;
20 | outline: none;
21 | min-width: 48px !important;
22 | max-width: 48px !important;
23 | padding: 0 !important;
24 | margin: 0 !important;
25 | box-sizing: border-box !important;
26 | text-align: center !important;
27 | font-size: 16px !important;
28 | line-height: 48px !important;
29 | }
30 |
31 | .webchat-float-button:hover {
32 | background-color: #0d66d0;
33 | transform: translateY(-50%) scale(1.05);
34 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
35 | }
36 |
37 | .webchat-float-button:active {
38 | transform: translateY(-50%) scale(0.95);
39 | }
40 |
41 | /* 悬浮按钮图标 */
42 | .webchat-float-button svg {
43 | width: 24px;
44 | height: 24px;
45 | stroke: currentColor;
46 | display: block !important;
47 | margin: 0 auto !important;
48 | }
49 |
50 | /* 拖动时的样式 */
51 | .webchat-float-button.dragging {
52 | opacity: 0.8;
53 | background-color: #0d66d0;
54 | }
--------------------------------------------------------------------------------
/public/floatButton.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 浮动按钮脚本
3 | * 在页面右侧显示一个可拖动的悬浮按钮,点击后打开AI对话侧边栏
4 | */
5 |
6 | (function() {
7 | // 避免在扩展页面中运行
8 | if (window.location.href.includes('chrome-extension://')) {
9 | return;
10 | }
11 |
12 | // 存储按钮状态和位置的键
13 | const BUTTON_POSITION_KEY = 'webchat_float_button_position';
14 |
15 | // 创建浮动按钮
16 | function createFloatButton() {
17 | // 检查按钮是否已存在
18 | if (document.querySelector('.webchat-float-button')) {
19 | return;
20 | }
21 |
22 | // 创建按钮元素
23 | const floatButton = document.createElement('button');
24 | floatButton.className = 'webchat-float-button webchat-extension-styles';
25 | floatButton.setAttribute('title', '打开AI对话');
26 | floatButton.innerHTML = `
27 |
30 | `;
31 |
32 | // 添加到文档中
33 | document.body.appendChild(floatButton);
34 |
35 | // 载入保存的位置
36 | loadButtonPosition(floatButton);
37 |
38 | // 添加事件监听器
39 | setupDragEvents(floatButton);
40 | setupClickEvent(floatButton);
41 |
42 | console.log('WebChat 悬浮按钮已初始化');
43 | }
44 |
45 | // 设置拖动事件
46 | function setupDragEvents(button) {
47 | let isDragging = false;
48 | let initialY, initialTop;
49 |
50 | button.addEventListener('mousedown', function(e) {
51 | // 只处理左键点击
52 | if (e.button !== 0) return;
53 |
54 | // 阻止默认行为和冒泡
55 | e.preventDefault();
56 | e.stopPropagation();
57 |
58 | // 开始拖动
59 | isDragging = true;
60 | initialY = e.clientY;
61 | initialTop = parseInt(window.getComputedStyle(button).top, 10);
62 |
63 | // 添加拖动样式
64 | button.classList.add('dragging');
65 | });
66 |
67 | document.addEventListener('mousemove', function(e) {
68 | if (!isDragging) return;
69 |
70 | // 计算新位置
71 | const newTop = initialTop + (e.clientY - initialY);
72 |
73 | // 限制在窗口内
74 | const maxTop = window.innerHeight - button.offsetHeight;
75 | const limitedTop = Math.max(0, Math.min(newTop, maxTop));
76 |
77 | // 应用新位置
78 | button.style.top = `${limitedTop}px`;
79 | button.style.transform = 'none'; // 移除默认的居中垂直变换
80 | });
81 |
82 | document.addEventListener('mouseup', function() {
83 | if (!isDragging) return;
84 |
85 | // 结束拖动
86 | isDragging = false;
87 | button.classList.remove('dragging');
88 |
89 | // 存储按钮位置
90 | saveButtonPosition(button);
91 | });
92 | }
93 |
94 | // 设置点击事件
95 | function setupClickEvent(button) {
96 | button.addEventListener('click', function(e) {
97 | // 防止拖动后触发点击
98 | if (button.classList.contains('dragging')) {
99 | return;
100 | }
101 |
102 | // 阻止默认行为和冒泡
103 | e.preventDefault();
104 | e.stopPropagation();
105 |
106 | // 打开侧边栏
107 | if (chrome && chrome.runtime) {
108 | chrome.runtime.sendMessage({ action: 'openSidePanel' });
109 | }
110 | });
111 | }
112 |
113 | // 保存按钮位置到本地存储
114 | function saveButtonPosition(button) {
115 | const position = {
116 | top: button.style.top
117 | };
118 |
119 | if (chrome && chrome.storage) {
120 | chrome.storage.local.set({ [BUTTON_POSITION_KEY]: position });
121 | } else {
122 | localStorage.setItem(BUTTON_POSITION_KEY, JSON.stringify(position));
123 | }
124 | }
125 |
126 | // 从本地存储加载按钮位置
127 | function loadButtonPosition(button) {
128 | if (chrome && chrome.storage) {
129 | chrome.storage.local.get([BUTTON_POSITION_KEY], function(result) {
130 | if (result && result[BUTTON_POSITION_KEY]) {
131 | applyPosition(button, result[BUTTON_POSITION_KEY]);
132 | }
133 | });
134 | } else {
135 | const savedPosition = localStorage.getItem(BUTTON_POSITION_KEY);
136 | if (savedPosition) {
137 | applyPosition(button, JSON.parse(savedPosition));
138 | }
139 | }
140 | }
141 |
142 | // 应用保存的位置
143 | function applyPosition(button, position) {
144 | if (position.top) {
145 | button.style.top = position.top;
146 | button.style.transform = 'none'; // 移除默认的居中垂直变换
147 | }
148 | }
149 |
150 | // 等待页面加载完成后初始化
151 | if (document.readyState === 'loading') {
152 | document.addEventListener('DOMContentLoaded', createFloatButton);
153 | } else {
154 | createFloatButton();
155 | }
156 | })();
--------------------------------------------------------------------------------
/public/icons/icon128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tangpan360/webchat/6c01dfe6005798c2c275b03c3ce31cb1f33e1c40/public/icons/icon128.png
--------------------------------------------------------------------------------
/public/icons/icon16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tangpan360/webchat/6c01dfe6005798c2c275b03c3ce31cb1f33e1c40/public/icons/icon16.png
--------------------------------------------------------------------------------
/public/icons/icon48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tangpan360/webchat/6c01dfe6005798c2c275b03c3ce31cb1f33e1c40/public/icons/icon48.png
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | WebChat
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 3,
3 | "name": "WebChat",
4 | "version": "1.0.5",
5 | "description": "浏览器侧边栏AI问答插件",
6 | "action": {
7 | "default_title": "WebChat",
8 | "default_icon": {
9 | "16": "icons/icon16.png",
10 | "48": "icons/icon48.png",
11 | "128": "icons/icon128.png"
12 | }
13 | },
14 | "icons": {
15 | "16": "icons/icon16.png",
16 | "48": "icons/icon48.png",
17 | "128": "icons/icon128.png"
18 | },
19 | "permissions": [
20 | "storage",
21 | "sidePanel",
22 | "scripting"
23 | ],
24 | "host_permissions": [
25 | ""
26 | ],
27 | "side_panel": {
28 | "default_path": "index.html"
29 | },
30 | "background": {
31 | "service_worker": "background.js"
32 | },
33 | "content_scripts": [
34 | {
35 | "matches": [""],
36 | "js": ["contentScript.js", "floatButton.js"],
37 | "css": ["styles.css", "floatButton.css"]
38 | }
39 | ]
40 | }
--------------------------------------------------------------------------------
/public/styles.css:
--------------------------------------------------------------------------------
1 | /* 这些样式仅应用于扩展的自身元素,不影响网页内容 */
2 | .webchat-extension-styles {
3 | --webchat-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
4 | --webchat-bg-color: #f5f5f5;
5 | --webchat-text-color: #333;
6 | --webchat-border-color: #e0e0e0;
7 | --webchat-active-color: #1a73e8;
8 | }
9 |
10 | /* 只应用于侧边栏面板 */
11 | body.webchat-panel {
12 | font-family: var(--webchat-font-family);
13 | background-color: var(--webchat-bg-color);
14 | color: var(--webchat-text-color);
15 | margin: 0;
16 | padding: 0;
17 | box-sizing: border-box;
18 | height: 100vh;
19 | overflow: hidden;
20 | }
21 |
22 | body.webchat-panel * {
23 | box-sizing: border-box;
24 | }
25 |
26 | body.webchat-panel #app {
27 | height: 100vh;
28 | display: flex;
29 | flex-direction: column;
30 | }
31 |
32 | body.webchat-panel .tab-container {
33 | display: flex;
34 | border-bottom: 1px solid var(--webchat-border-color);
35 | background-color: #fff;
36 | }
37 |
38 | body.webchat-panel .tab {
39 | padding: 12px 16px;
40 | cursor: pointer;
41 | font-weight: 500;
42 | font-size: 14px;
43 | color: #666;
44 | border-bottom: 2px solid transparent;
45 | }
46 |
47 | body.webchat-panel .tab.active {
48 | color: var(--webchat-active-color);
49 | border-bottom: 2px solid var(--webchat-active-color);
50 | }
51 |
52 | body.webchat-panel .content {
53 | flex: 1;
54 | overflow: auto;
55 | padding: 16px;
56 | background-color: #fff;
57 | }
58 |
59 | body.webchat-panel .hidden {
60 | display: none;
61 | }
--------------------------------------------------------------------------------
/src/api/chatApi.js:
--------------------------------------------------------------------------------
1 | import { getStorage } from '../utils/storage';
2 |
3 | /**
4 | * 压缩消息内容以减少token使用
5 | * @param {string} content - 要压缩的内容
6 | * @param {number} maxLength - 最大长度
7 | * @returns {string} - 压缩后的内容
8 | */
9 | const compressContent = (content, maxLength) => {
10 | if (!content || content.length <= maxLength) return content;
11 |
12 | // 简单的压缩策略:保留前后部分,中间用...替代
13 | const firstPart = Math.floor(maxLength / 2);
14 | const secondPart = maxLength - firstPart - 3; // 减去"..."的长度
15 |
16 | return content.substring(0, firstPart) + '...' + content.substring(content.length - secondPart);
17 | };
18 |
19 | /**
20 | * 处理历史消息,控制数量和长度
21 | * @param {Array} messages - 历史消息数组
22 | * @param {Object} settings - 设置对象
23 | * @returns {Array} - 处理后的历史消息
24 | */
25 | const processMessages = (messages, settings) => {
26 | const maxMessages = settings.maxHistoryMessages || 10; // 默认保留10条
27 | const compressionThreshold = settings.compressionThreshold || 1000; // 默认1000字符压缩
28 |
29 | // 限制消息数量,保留最近的消息
30 | let processedMessages = [...messages];
31 |
32 | if (processedMessages.length > maxMessages) {
33 | // 始终保留系统消息
34 | const systemMessages = processedMessages.filter(msg => msg.role === 'system');
35 |
36 | // 取最近的用户和助手消息
37 | const recentMessages = processedMessages
38 | .filter(msg => msg.role !== 'system')
39 | .slice(-maxMessages);
40 |
41 | processedMessages = [...systemMessages, ...recentMessages];
42 | }
43 |
44 | // 压缩过长的消息
45 | if (compressionThreshold > 0) {
46 | processedMessages = processedMessages.map(msg => {
47 | if (msg.content && msg.content.length > compressionThreshold) {
48 | return {
49 | ...msg,
50 | content: compressContent(msg.content, compressionThreshold)
51 | };
52 | }
53 | return msg;
54 | });
55 | }
56 |
57 | return processedMessages;
58 | };
59 |
60 | /**
61 | * 发送消息到API并获取响应
62 | * @param {string} content - 用户消息内容
63 | * @param {string} modelId - 使用的模型ID
64 | * @param {Array} previousMessages - 之前的消息历史
65 | * @param {Function} onChunkReceived - 接收到内容块时的回调函数
66 | * @param {AbortSignal} signal - 用于中断请求的信号
67 | * @returns {Promise