mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-22 17:57:46 +00:00
feat(chrome-qwen-bridge): 🔥 init chrome qwen code bridge
This commit is contained in:
1
packages/chrome-qwen-bridge/.extension-id
Normal file
1
packages/chrome-qwen-bridge/.extension-id
Normal file
@@ -0,0 +1 @@
|
|||||||
|
cimaabkejokbhjkdnajgfniiolfjgbhd
|
||||||
23
packages/chrome-qwen-bridge/.gitignore
vendored
Normal file
23
packages/chrome-qwen-bridge/.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
dist/
|
||||||
|
*.zip
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# IDE files
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
.temp/
|
||||||
62
packages/chrome-qwen-bridge/CONNECTION_STATUS.md
Normal file
62
packages/chrome-qwen-bridge/CONNECTION_STATUS.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "🎯 Chrome Extension 连接状态总结"
|
||||||
|
echo "================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
echo -e "${GREEN}✅ Native Host 已正确配置${NC}"
|
||||||
|
echo " - 配置文件位置正确"
|
||||||
|
echo " - 使用 shell 包装脚本确保 Node.js 环境"
|
||||||
|
echo " - 扩展 ID 已配置: cimaabkejokbhjkdnajgfniiolfjgbhd"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo -e "${GREEN}✅ Native Host 测试响应正常${NC}"
|
||||||
|
echo " - 握手协议工作正常"
|
||||||
|
echo " - 消息传递机制正确"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo -e "${GREEN}✅ Service Worker 已增强${NC}"
|
||||||
|
echo " - 添加了详细的错误日志"
|
||||||
|
echo " - 实现了握手超时机制"
|
||||||
|
echo " - 改进了断开连接处理"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo -e "${YELLOW}📝 现在请进行以下操作:${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "1. 重新加载 Chrome 扩展:"
|
||||||
|
echo " open 'chrome://extensions/'"
|
||||||
|
echo " 找到 'Qwen CLI Bridge' 并点击 🔄"
|
||||||
|
echo ""
|
||||||
|
echo "2. 点击扩展图标测试:"
|
||||||
|
echo " - 点击 'Connect to Qwen CLI'"
|
||||||
|
echo " - 连接应该会成功"
|
||||||
|
echo ""
|
||||||
|
echo "3. 如果仍有问题:"
|
||||||
|
echo " a) 查看 Service Worker 控制台:"
|
||||||
|
echo " open 'chrome://extensions/?id=cimaabkejokbhjkdnajgfniiolfjgbhd'"
|
||||||
|
echo " 点击 'Service Worker' 查看日志"
|
||||||
|
echo ""
|
||||||
|
echo " b) 查看 Native Host 日志:"
|
||||||
|
echo " tail -f /tmp/qwen-bridge-host.log"
|
||||||
|
echo ""
|
||||||
|
echo " c) 运行调试控制台:"
|
||||||
|
echo " open file://$PWD/debug-console.html"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "================================"
|
||||||
|
echo ""
|
||||||
|
echo "🔍 常见问题排查:"
|
||||||
|
echo ""
|
||||||
|
echo "如果看到 'Native host has exited' 错误:"
|
||||||
|
echo "- 确保 Node.js 已安装: node --version"
|
||||||
|
echo "- 检查路径是否正确: ls -la native-host/run.sh"
|
||||||
|
echo ""
|
||||||
|
echo "如果看到 'Specified native messaging host not found':"
|
||||||
|
echo "- 重新运行: ./set-extension-id.sh"
|
||||||
|
echo "- 确认扩展 ID 正确"
|
||||||
|
echo ""
|
||||||
|
echo "连接现在应该能正常工作了!🎉"
|
||||||
121
packages/chrome-qwen-bridge/INSTALL.md
Normal file
121
packages/chrome-qwen-bridge/INSTALL.md
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
# 📦 Chrome Qwen Bridge - 安装指南
|
||||||
|
|
||||||
|
## 🚀 快速安装(推荐)
|
||||||
|
|
||||||
|
### 一键安装(首次用户)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 进入项目目录
|
||||||
|
cd packages/chrome-qwen-bridge
|
||||||
|
|
||||||
|
# 运行安装向导
|
||||||
|
npm run install:all
|
||||||
|
```
|
||||||
|
|
||||||
|
这个命令会:
|
||||||
|
1. ✅ 引导你安装 Chrome 扩展
|
||||||
|
2. ✅ 自动配置 Native Host
|
||||||
|
3. ✅ 保存扩展 ID 供后续使用
|
||||||
|
4. ✅ 启动调试环境
|
||||||
|
|
||||||
|
## 📝 安装方式说明
|
||||||
|
|
||||||
|
### 场景 1:从 Chrome Web Store 安装(未来)
|
||||||
|
|
||||||
|
当扩展发布到 Chrome Web Store 后:
|
||||||
|
1. 从商店安装扩展
|
||||||
|
2. 运行 `npm run install:host`(会自动检测已安装的扩展)
|
||||||
|
3. 完成!
|
||||||
|
|
||||||
|
### 场景 2:开发者模式安装(当前)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 步骤 1:安装扩展和 Native Host
|
||||||
|
npm run install:all
|
||||||
|
|
||||||
|
# 步骤 2:启动调试
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景 3:分步安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 仅安装 Chrome 扩展
|
||||||
|
npm run install:extension
|
||||||
|
|
||||||
|
# 2. 仅配置 Native Host
|
||||||
|
npm run install:host
|
||||||
|
|
||||||
|
# 3. 启动开发环境
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Native Host 说明
|
||||||
|
|
||||||
|
### 什么是 Native Host?
|
||||||
|
|
||||||
|
Native Host 是一个本地程序,允许 Chrome 扩展与本地应用(如 Qwen CLI)通信。出于安全考虑,Chrome 要求必须手动安装。
|
||||||
|
|
||||||
|
### 智能安装器特性
|
||||||
|
|
||||||
|
我们的 `smart-install.sh` 脚本会:
|
||||||
|
|
||||||
|
1. **自动检测** - 尝试自动找到已安装的扩展
|
||||||
|
2. **保存配置** - 记住扩展 ID,下次无需输入
|
||||||
|
3. **通用模式** - 即使没有扩展 ID 也能配置
|
||||||
|
4. **连接测试** - 可选的连接验证
|
||||||
|
|
||||||
|
### 安装位置
|
||||||
|
|
||||||
|
Native Host 配置文件位置:
|
||||||
|
- **macOS**: `~/Library/Application Support/Google/Chrome/NativeMessagingHosts/`
|
||||||
|
- **Linux**: `~/.config/google-chrome/NativeMessagingHosts/`
|
||||||
|
|
||||||
|
## ❓ 常见问题
|
||||||
|
|
||||||
|
### Q: 必须手动安装 Native Host 吗?
|
||||||
|
|
||||||
|
A: 是的,这是 Chrome 的安全要求。但我们的智能安装器让这个过程非常简单。
|
||||||
|
|
||||||
|
### Q: 如何找到扩展 ID?
|
||||||
|
|
||||||
|
A:
|
||||||
|
1. 打开 `chrome://extensions/`
|
||||||
|
2. 找到 "Qwen CLI Bridge"
|
||||||
|
3. ID 显示在扩展卡片上(类似 `abcdefghijklmnop...`)
|
||||||
|
|
||||||
|
### Q: 重装扩展后需要重新配置吗?
|
||||||
|
|
||||||
|
A: 如果扩展 ID 改变了,需要重新运行 `npm run install:host`。脚本会自动检测新的 ID。
|
||||||
|
|
||||||
|
### Q: 如何验证安装成功?
|
||||||
|
|
||||||
|
A: 运行 `npm run dev`,如果能看到插件图标并能点击连接,说明安装成功。
|
||||||
|
|
||||||
|
## 📋 命令参考
|
||||||
|
|
||||||
|
| 命令 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `npm run install:all` | 完整安装向导 |
|
||||||
|
| `npm run install:extension` | 仅安装扩展 |
|
||||||
|
| `npm run install:host` | 仅配置 Native Host |
|
||||||
|
| `npm run dev` | 启动调试环境 |
|
||||||
|
| `npm run clean` | 清理所有配置和日志 |
|
||||||
|
|
||||||
|
## 🔄 更新和重装
|
||||||
|
|
||||||
|
如果需要重新安装:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 清理旧配置
|
||||||
|
npm run clean
|
||||||
|
|
||||||
|
# 重新安装
|
||||||
|
npm run install:all
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 更多信息
|
||||||
|
|
||||||
|
- [调试指南](./docs/debugging.md)
|
||||||
|
- [API 文档](./docs/api-reference.md)
|
||||||
|
- [架构设计](./docs/architecture.md)
|
||||||
64
packages/chrome-qwen-bridge/QUICK_START.md
Normal file
64
packages/chrome-qwen-bridge/QUICK_START.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# 🚀 快速开始
|
||||||
|
|
||||||
|
## 首次使用
|
||||||
|
|
||||||
|
如果是第一次使用,请运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
系统会自动检测并引导你完成:
|
||||||
|
1. 📦 手动安装 Chrome 插件
|
||||||
|
2. 🔧 配置 Native Host
|
||||||
|
3. 🎯 启动调试环境
|
||||||
|
|
||||||
|
## 安装步骤说明
|
||||||
|
|
||||||
|
### 第一次运行时需要:
|
||||||
|
|
||||||
|
1. **手动加载插件到 Chrome**
|
||||||
|
- 打开 `chrome://extensions/`
|
||||||
|
- 开启「开发者模式」(右上角)
|
||||||
|
- 点击「加载已解压的扩展程序」
|
||||||
|
- 选择 `extension` 目录
|
||||||
|
- **记下扩展 ID**(很重要!)
|
||||||
|
|
||||||
|
2. **输入扩展 ID**
|
||||||
|
- 脚本会提示你输入
|
||||||
|
- 这样 Native Host 才能识别插件
|
||||||
|
|
||||||
|
3. **完成后**
|
||||||
|
- 以后运行 `npm run dev` 就会自动加载所有内容
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### Q: 为什么需要手动加载插件?
|
||||||
|
A: Chrome 安全机制要求开发者模式的插件必须手动加载一次。
|
||||||
|
|
||||||
|
### Q: 插件图标在哪里?
|
||||||
|
A: 点击 Chrome 工具栏的拼图图标,找到 "Qwen CLI Bridge" 并点击固定。
|
||||||
|
|
||||||
|
### Q: 如何知道插件是否加载成功?
|
||||||
|
A:
|
||||||
|
- 在 `chrome://extensions/` 能看到插件
|
||||||
|
- 工具栏有插件图标
|
||||||
|
- 点击图标能看到弹出窗口
|
||||||
|
|
||||||
|
## 调试命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev # 启动调试环境(首次会引导安装)
|
||||||
|
npm run logs # 查看 Native Host 日志
|
||||||
|
npm run logs:qwen # 查看 Qwen 服务器日志
|
||||||
|
npm run clean # 清理所有临时文件
|
||||||
|
```
|
||||||
|
|
||||||
|
## 文件说明
|
||||||
|
|
||||||
|
```
|
||||||
|
├── first-install.sh # 首次安装向导
|
||||||
|
├── debug.sh # 调试启动脚本
|
||||||
|
├── .extension-id # 保存的扩展 ID(自动生成)
|
||||||
|
└── extension/ # Chrome 插件源码
|
||||||
|
```
|
||||||
204
packages/chrome-qwen-bridge/README.md
Normal file
204
packages/chrome-qwen-bridge/README.md
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
# Qwen CLI Bridge - Chrome Extension
|
||||||
|
|
||||||
|
A Chrome extension that bridges your browser with Qwen CLI, enabling AI-powered analysis and interaction with web content.
|
||||||
|
|
||||||
|
> This package is part of the [Qwen Code](https://github.com/QwenLM/qwen-code) mono repository.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Page Data Extraction**: Extract structured data from any webpage including text, links, images, and metadata
|
||||||
|
- **Screenshot Capture**: Capture and analyze screenshots with AI
|
||||||
|
- **Console & Network Monitoring**: Monitor console logs and network requests
|
||||||
|
- **Selected Text Processing**: Send selected text to Qwen CLI for processing
|
||||||
|
- **AI Analysis**: Leverage Qwen's AI capabilities to analyze web content
|
||||||
|
- **MCP Server Integration**: Support for multiple MCP (Model Context Protocol) servers
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ Chrome Extension │
|
||||||
|
│ - Content Script │
|
||||||
|
│ - Background Worker│
|
||||||
|
│ - Popup UI │
|
||||||
|
└──────────┬──────────┘
|
||||||
|
│
|
||||||
|
Native Messaging
|
||||||
|
│
|
||||||
|
┌──────▼──────────┐
|
||||||
|
│ Native Host │
|
||||||
|
│ (Node.js) │
|
||||||
|
└──────┬──────────┘
|
||||||
|
│
|
||||||
|
┌──────▼──────────┐
|
||||||
|
│ Qwen CLI │
|
||||||
|
│ + MCP Servers │
|
||||||
|
└─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
1. **Node.js**: Install from [nodejs.org](https://nodejs.org/)
|
||||||
|
2. **Qwen CLI**: Install the Qwen CLI tool (required for full functionality)
|
||||||
|
3. **Chrome Browser**: Version 88 or higher
|
||||||
|
|
||||||
|
### Step 1: Install the Chrome Extension
|
||||||
|
|
||||||
|
1. Open Chrome and navigate to `chrome://extensions/`
|
||||||
|
2. Enable "Developer mode" (toggle in top right)
|
||||||
|
3. Click "Load unpacked"
|
||||||
|
4. Select the `chrome-qwen-bridge/extension` folder
|
||||||
|
5. Note the Extension ID that appears (you'll need this for the next step)
|
||||||
|
|
||||||
|
### Step 2: Install the Native Messaging Host
|
||||||
|
|
||||||
|
The Native Messaging Host allows the Chrome extension to communicate with Qwen CLI.
|
||||||
|
|
||||||
|
#### macOS/Linux
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd chrome-qwen-bridge/native-host
|
||||||
|
./install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
When prompted, enter your Chrome Extension ID.
|
||||||
|
|
||||||
|
#### Windows
|
||||||
|
|
||||||
|
1. Run Command Prompt as Administrator
|
||||||
|
2. Navigate to the `native-host` directory:
|
||||||
|
```cmd
|
||||||
|
cd chrome-qwen-bridge\native-host
|
||||||
|
```
|
||||||
|
3. Run the installation script:
|
||||||
|
```cmd
|
||||||
|
install.bat
|
||||||
|
```
|
||||||
|
4. Enter your Chrome Extension ID when prompted
|
||||||
|
|
||||||
|
### Step 3: Configure Qwen CLI (Optional)
|
||||||
|
|
||||||
|
If you want to use MCP servers with the extension:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Add chrome-devtools MCP server
|
||||||
|
qwen mcp add chrome-devtools
|
||||||
|
|
||||||
|
# Add other MCP servers as needed
|
||||||
|
qwen mcp add playwright-mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
|
||||||
|
1. Click the Qwen CLI Bridge extension icon in Chrome
|
||||||
|
2. Click "Connect to Qwen CLI" to establish connection
|
||||||
|
3. Click "Start Qwen CLI" to launch the CLI process
|
||||||
|
4. Use the action buttons to:
|
||||||
|
- Extract and analyze page data
|
||||||
|
- Capture screenshots
|
||||||
|
- Send selected text to Qwen
|
||||||
|
- Monitor console and network logs
|
||||||
|
|
||||||
|
### Advanced Settings
|
||||||
|
|
||||||
|
In the popup's "Advanced Settings" section, you can configure:
|
||||||
|
|
||||||
|
- **MCP Servers**: Comma-separated list of MCP servers to load
|
||||||
|
- **HTTP Port**: Port for Qwen CLI HTTP server (default: 8080)
|
||||||
|
- **Auto-connect**: Automatically connect when opening the popup
|
||||||
|
|
||||||
|
### API Actions
|
||||||
|
|
||||||
|
The extension supports the following actions that can be sent to Qwen CLI:
|
||||||
|
|
||||||
|
- `analyze_page`: Analyze extracted page data
|
||||||
|
- `analyze_screenshot`: Analyze captured screenshot
|
||||||
|
- `ai_analyze`: Perform AI analysis on content
|
||||||
|
- `process_text`: Process selected text
|
||||||
|
- Custom actions based on your MCP server configurations
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
chrome-qwen-bridge/
|
||||||
|
├── extension/ # Chrome extension source
|
||||||
|
│ ├── manifest.json # Extension manifest
|
||||||
|
│ ├── background/ # Service worker
|
||||||
|
│ ├── content/ # Content scripts
|
||||||
|
│ ├── popup/ # Popup UI
|
||||||
|
│ └── icons/ # Extension icons
|
||||||
|
├── native-host/ # Native messaging host
|
||||||
|
│ ├── host.js # Node.js host script
|
||||||
|
│ ├── manifest.json # Native host manifest
|
||||||
|
│ └── install scripts # Platform-specific installers
|
||||||
|
└── docs/ # Documentation
|
||||||
|
```
|
||||||
|
|
||||||
|
### Building from Source
|
||||||
|
|
||||||
|
1. Clone the repository
|
||||||
|
2. No build step required - the extension uses vanilla JavaScript
|
||||||
|
3. Load the extension as unpacked in Chrome for development
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
1. Enable Chrome Developer Tools
|
||||||
|
2. Check the extension's background page console for logs
|
||||||
|
3. Native host logs are written to:
|
||||||
|
- macOS/Linux: `/tmp/qwen-bridge-host.log`
|
||||||
|
- Windows: `%TEMP%\qwen-bridge-host.log`
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Extension not connecting to Native Host
|
||||||
|
|
||||||
|
1. Verify Node.js is installed: `node --version`
|
||||||
|
2. Check that the Native Host is properly installed
|
||||||
|
3. Ensure the Extension ID in the manifest matches your actual extension
|
||||||
|
4. Check logs for errors
|
||||||
|
|
||||||
|
### Qwen CLI not starting
|
||||||
|
|
||||||
|
1. Verify Qwen CLI is installed: `qwen --version`
|
||||||
|
2. Check that Qwen CLI can run normally from terminal
|
||||||
|
3. Review Native Host logs for error messages
|
||||||
|
|
||||||
|
### No response from Qwen CLI
|
||||||
|
|
||||||
|
1. Ensure Qwen CLI server is running
|
||||||
|
2. Check the configured HTTP port is not in use
|
||||||
|
3. Verify MCP servers are properly configured
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- The extension requires broad permissions to function properly
|
||||||
|
- Native Messaging Host runs with user privileges
|
||||||
|
- All communication between components uses structured JSON messages
|
||||||
|
- No sensitive data is stored; all processing is ephemeral
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! Please:
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch
|
||||||
|
3. Make your changes
|
||||||
|
4. Test thoroughly
|
||||||
|
5. Submit a pull request
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License - See LICENSE file for details
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues, questions, or feature requests:
|
||||||
|
- Open an issue on GitHub
|
||||||
|
- Check the logs for debugging information
|
||||||
|
- Ensure all prerequisites are properly installed
|
||||||
26
packages/chrome-qwen-bridge/build.sh
Executable file
26
packages/chrome-qwen-bridge/build.sh
Executable file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Build script for Chrome extension package
|
||||||
|
|
||||||
|
echo "Building Chrome Qwen Bridge..."
|
||||||
|
|
||||||
|
# Ensure we're in the right directory
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
# Create dist directory
|
||||||
|
mkdir -p dist
|
||||||
|
|
||||||
|
# Copy extension files to dist
|
||||||
|
echo "Copying extension files..."
|
||||||
|
cp -r extension dist/
|
||||||
|
|
||||||
|
# Create a zip file for Chrome Web Store
|
||||||
|
echo "Creating extension package..."
|
||||||
|
cd dist
|
||||||
|
zip -r ../chrome-qwen-bridge.zip extension/
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
echo "✅ Build complete!"
|
||||||
|
echo " Extension package: chrome-qwen-bridge.zip"
|
||||||
|
echo " Extension files: dist/extension/"
|
||||||
52
packages/chrome-qwen-bridge/debug-chrome.sh
Executable file
52
packages/chrome-qwen-bridge/debug-chrome.sh
Executable file
@@ -0,0 +1,52 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "🔍 Chrome Extension 调试启动器"
|
||||||
|
echo "================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 检查 Chrome 是否已经运行
|
||||||
|
if pgrep -x "Google Chrome" > /dev/null; then
|
||||||
|
echo "⚠️ Chrome 已在运行,请先关闭 Chrome 再运行此脚本"
|
||||||
|
echo " 或者在新的 Chrome 窗口中手动操作"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 获取扩展路径
|
||||||
|
EXTENSION_PATH="$PWD/extension"
|
||||||
|
echo "📂 扩展路径: $EXTENSION_PATH"
|
||||||
|
|
||||||
|
# 读取保存的扩展 ID
|
||||||
|
if [ -f ".extension-id" ]; then
|
||||||
|
EXTENSION_ID=$(cat .extension-id)
|
||||||
|
echo "🆔 扩展 ID: $EXTENSION_ID"
|
||||||
|
else
|
||||||
|
echo "⚠️ 未找到扩展 ID,首次加载后会自动保存"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "正在启动 Chrome 调试模式..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 启动 Chrome with debugging
|
||||||
|
open -na "Google Chrome" --args \
|
||||||
|
--load-extension="$EXTENSION_PATH" \
|
||||||
|
--auto-open-devtools-for-tabs \
|
||||||
|
--enable-logging \
|
||||||
|
--v=1 \
|
||||||
|
"file://$PWD/debug-console.html"
|
||||||
|
|
||||||
|
echo "✅ Chrome 已启动"
|
||||||
|
echo ""
|
||||||
|
echo "📝 调试步骤:"
|
||||||
|
echo "1. Chrome 会自动加载扩展并打开调试控制台"
|
||||||
|
echo "2. 点击 'Test Connection' 测试连接"
|
||||||
|
echo "3. 如果连接失败,点击 'View Background Logs' 查看详细日志"
|
||||||
|
echo ""
|
||||||
|
echo "💡 提示:"
|
||||||
|
echo "- 按 F12 打开开发者工具查看控制台输出"
|
||||||
|
echo "- 在 chrome://extensions/ 页面点击 'Service Worker' 查看后台日志"
|
||||||
|
echo "- 日志文件: /tmp/qwen-bridge-host.log"
|
||||||
|
echo ""
|
||||||
|
echo "📋 监控日志 (Ctrl+C 退出):"
|
||||||
|
echo "----------------------------"
|
||||||
|
tail -f /tmp/qwen-bridge-host.log 2>/dev/null || echo "等待日志生成..."
|
||||||
178
packages/chrome-qwen-bridge/debug-console.html
Normal file
178
packages/chrome-qwen-bridge/debug-console.html
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Chrome Extension Debug Console</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: monospace;
|
||||||
|
padding: 20px;
|
||||||
|
background: #1e1e1e;
|
||||||
|
color: #d4d4d4;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #569cd6;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background: #569cd6;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
margin: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
background: #4080c0;
|
||||||
|
}
|
||||||
|
#console {
|
||||||
|
background: #2d2d30;
|
||||||
|
padding: 15px;
|
||||||
|
margin-top: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
min-height: 300px;
|
||||||
|
max-height: 500px;
|
||||||
|
overflow-y: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
.log {
|
||||||
|
margin: 5px 0;
|
||||||
|
padding: 5px;
|
||||||
|
border-left: 3px solid #569cd6;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
border-left-color: #f44747;
|
||||||
|
color: #f44747;
|
||||||
|
}
|
||||||
|
.success {
|
||||||
|
border-left-color: #4ec9b0;
|
||||||
|
color: #4ec9b0;
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
color: #9cdcfe;
|
||||||
|
}
|
||||||
|
.warning {
|
||||||
|
border-left-color: #ce9178;
|
||||||
|
color: #ce9178;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>🔧 Chrome Extension Debug Console</h1>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button onclick="testConnection()">Test Connection</button>
|
||||||
|
<button onclick="getStatus()">Get Status</button>
|
||||||
|
<button onclick="clearConsole()">Clear Console</button>
|
||||||
|
<button onclick="viewBackgroundLogs()">View Background Logs</button>
|
||||||
|
<button onclick="reloadExtension()">Reload Extension</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="console"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const consoleDiv = document.getElementById('console');
|
||||||
|
let extensionId = 'cimaabkejokbhjkdnajgfniiolfjgbhd';
|
||||||
|
|
||||||
|
function log(message, type = 'log') {
|
||||||
|
const logDiv = document.createElement('div');
|
||||||
|
logDiv.className = `log ${type}`;
|
||||||
|
const timestamp = new Date().toLocaleTimeString();
|
||||||
|
logDiv.textContent = `[${timestamp}] ${message}`;
|
||||||
|
consoleDiv.appendChild(logDiv);
|
||||||
|
consoleDiv.scrollTop = consoleDiv.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearConsole() {
|
||||||
|
consoleDiv.innerHTML = '';
|
||||||
|
log('Console cleared', 'info');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testConnection() {
|
||||||
|
log('Testing connection to extension...', 'info');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try to send a message to the extension
|
||||||
|
const response = await chrome.runtime.sendMessage(extensionId, {
|
||||||
|
type: 'CONNECT'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response) {
|
||||||
|
if (response.success) {
|
||||||
|
log('✅ Connected successfully!', 'success');
|
||||||
|
log(`Status: ${response.status}`, 'info');
|
||||||
|
} else {
|
||||||
|
log(`❌ Connection failed: ${response.error}`, 'error');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log('❌ No response from extension', 'error');
|
||||||
|
log('Possible issues:', 'warning');
|
||||||
|
log('1. Extension not loaded/enabled', 'warning');
|
||||||
|
log('2. Extension ID incorrect', 'warning');
|
||||||
|
log('3. Service worker crashed', 'warning');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log(`❌ Error: ${error.message}`, 'error');
|
||||||
|
|
||||||
|
if (error.message.includes('Could not establish connection')) {
|
||||||
|
log('Extension is not responding. Checking extension ID...', 'warning');
|
||||||
|
|
||||||
|
// Try to find the extension
|
||||||
|
if (chrome.management) {
|
||||||
|
chrome.management.getAll((extensions) => {
|
||||||
|
const qwenExt = extensions.find(ext =>
|
||||||
|
ext.name.includes('Qwen') || ext.name.includes('CLI Bridge')
|
||||||
|
);
|
||||||
|
if (qwenExt) {
|
||||||
|
log(`Found extension: ${qwenExt.name} (${qwenExt.id})`, 'info');
|
||||||
|
extensionId = qwenExt.id;
|
||||||
|
} else {
|
||||||
|
log('Extension not found in installed extensions', 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getStatus() {
|
||||||
|
log('Getting extension status...', 'info');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await chrome.runtime.sendMessage(extensionId, {
|
||||||
|
type: 'GET_STATUS'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response) {
|
||||||
|
log(`Status received: ${JSON.stringify(response, null, 2)}`, 'success');
|
||||||
|
} else {
|
||||||
|
log('No status response', 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log(`Error getting status: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function viewBackgroundLogs() {
|
||||||
|
log('Opening service worker console...', 'info');
|
||||||
|
window.open(`chrome://extensions/?id=${extensionId}`, '_blank');
|
||||||
|
log('Click "Inspect views: service worker" to see logs', 'info');
|
||||||
|
}
|
||||||
|
|
||||||
|
function reloadExtension() {
|
||||||
|
log('Reloading extension...', 'info');
|
||||||
|
if (chrome.runtime && chrome.runtime.reload) {
|
||||||
|
chrome.runtime.reload();
|
||||||
|
log('Extension reloaded', 'success');
|
||||||
|
} else {
|
||||||
|
log('Cannot reload from here. Please reload manually in chrome://extensions/', 'warning');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial log
|
||||||
|
log('Debug console ready', 'success');
|
||||||
|
log(`Extension ID: ${extensionId}`, 'info');
|
||||||
|
log('Click "Test Connection" to start debugging', 'info');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
376
packages/chrome-qwen-bridge/debug.sh
Executable file
376
packages/chrome-qwen-bridge/debug.sh
Executable file
@@ -0,0 +1,376 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Qwen CLI Bridge - macOS 一键调试脚本
|
||||||
|
|
||||||
|
# 颜色定义
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# 获取脚本目录
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
|
||||||
|
# 检查是否首次安装
|
||||||
|
if [[ ! -f "$SCRIPT_DIR/.extension-id" ]]; then
|
||||||
|
echo -e "${YELLOW}╔════════════════════════════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${YELLOW}║ ║${NC}"
|
||||||
|
echo -e "${YELLOW}║ ⚠️ 检测到首次运行,需要先安装插件 ║${NC}"
|
||||||
|
echo -e "${YELLOW}║ ║${NC}"
|
||||||
|
echo -e "${YELLOW}╚════════════════════════════════════════════════════════════════╝${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}即将启动首次安装向导...${NC}"
|
||||||
|
sleep 2
|
||||||
|
exec "$SCRIPT_DIR/first-install.sh"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 清屏显示标题
|
||||||
|
clear
|
||||||
|
echo -e "${CYAN}╔════════════════════════════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${CYAN}║ ║${NC}"
|
||||||
|
echo -e "${CYAN}║ 🚀 Qwen CLI Bridge - macOS 调试环境 ║${NC}"
|
||||||
|
echo -e "${CYAN}║ ║${NC}"
|
||||||
|
echo -e "${CYAN}╚════════════════════════════════════════════════════════════════╝${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 第一步:检查环境
|
||||||
|
echo -e "${BLUE}[1/5]${NC} 检查开发环境..."
|
||||||
|
|
||||||
|
# 检查 Node.js
|
||||||
|
if ! command -v node &> /dev/null; then
|
||||||
|
echo -e "${RED}✗${NC} Node.js 未安装,请先安装 Node.js"
|
||||||
|
echo " 访问 https://nodejs.org 下载安装"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo -e "${GREEN}✓${NC} Node.js $(node --version)"
|
||||||
|
|
||||||
|
# 检查 Chrome
|
||||||
|
CHROME_PATH="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
||||||
|
if [[ ! -f "$CHROME_PATH" ]]; then
|
||||||
|
echo -e "${RED}✗${NC} Chrome 未找到"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo -e "${GREEN}✓${NC} Chrome 已安装"
|
||||||
|
|
||||||
|
# 第二步:配置 Native Host
|
||||||
|
echo -e "\n${BLUE}[2/5]${NC} 配置 Native Host..."
|
||||||
|
|
||||||
|
MANIFEST_DIR="$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts"
|
||||||
|
mkdir -p "$MANIFEST_DIR"
|
||||||
|
|
||||||
|
cat > "$MANIFEST_DIR/com.qwen.cli.bridge.json" << EOF
|
||||||
|
{
|
||||||
|
"name": "com.qwen.cli.bridge",
|
||||||
|
"description": "Native messaging host for Qwen CLI Bridge",
|
||||||
|
"path": "$SCRIPT_DIR/native-host/host.js",
|
||||||
|
"type": "stdio",
|
||||||
|
"allowed_origins": ["chrome-extension://*/"]
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo -e "${GREEN}✓${NC} Native Host 已配置"
|
||||||
|
|
||||||
|
# 第三步:检查 Qwen CLI(可选)
|
||||||
|
echo -e "\n${BLUE}[3/5]${NC} 检查 Qwen CLI..."
|
||||||
|
|
||||||
|
QWEN_AVAILABLE=false
|
||||||
|
if command -v qwen &> /dev/null; then
|
||||||
|
QWEN_AVAILABLE=true
|
||||||
|
echo -e "${GREEN}✓${NC} Qwen CLI $(qwen --version 2>/dev/null || echo "已安装")"
|
||||||
|
|
||||||
|
# 尝试启动 Qwen server
|
||||||
|
if ! lsof -i:8080 &> /dev/null; then
|
||||||
|
echo -e "${CYAN}→${NC} 启动 Qwen server (端口 8080)..."
|
||||||
|
qwen server --port 8080 > /tmp/qwen-server.log 2>&1 &
|
||||||
|
QWEN_PID=$!
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
if kill -0 $QWEN_PID 2>/dev/null; then
|
||||||
|
echo -e "${GREEN}✓${NC} Qwen server 已启动 (PID: $QWEN_PID)"
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}!${NC} Qwen server 启动失败,继续运行..."
|
||||||
|
QWEN_AVAILABLE=false
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}!${NC} 端口 8080 已被占用"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}!${NC} Qwen CLI 未安装(插件基础功能仍可使用)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 第四步:启动测试页面
|
||||||
|
echo -e "\n${BLUE}[4/5]${NC} 启动测试服务器..."
|
||||||
|
|
||||||
|
# 创建测试页面
|
||||||
|
cat > /tmp/qwen-test.html << 'HTML'
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Qwen CLI Bridge 测试页面</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 40px 20px;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 40px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 2.5em;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 5px 15px;
|
||||||
|
background: rgba(255,255,255,0.2);
|
||||||
|
border-radius: 20px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.test-section {
|
||||||
|
margin: 30px 0;
|
||||||
|
padding: 25px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
.test-section h2 {
|
||||||
|
color: #667eea;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 5px;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
#console {
|
||||||
|
background: #1e1e1e;
|
||||||
|
color: #d4d4d4;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-family: 'SF Mono', Monaco, 'Courier New', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
min-height: 150px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
.log-entry {
|
||||||
|
margin: 5px 0;
|
||||||
|
padding: 5px;
|
||||||
|
border-left: 3px solid transparent;
|
||||||
|
}
|
||||||
|
.log-entry.info { border-left-color: #3b82f6; }
|
||||||
|
.log-entry.warn { border-left-color: #f59e0b; color: #fbbf24; }
|
||||||
|
.log-entry.error { border-left-color: #ef4444; color: #f87171; }
|
||||||
|
.instructions {
|
||||||
|
background: #e0e7ff;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.instructions h3 {
|
||||||
|
color: #4c1d95;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.instructions ol {
|
||||||
|
margin-left: 20px;
|
||||||
|
color: #4c1d95;
|
||||||
|
}
|
||||||
|
.instructions li {
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>🚀 Qwen CLI Bridge</h1>
|
||||||
|
<div class="status">调试环境已就绪</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<div class="test-section">
|
||||||
|
<h2>📝 测试功能</h2>
|
||||||
|
<button onclick="testLog()">测试 Console Log</button>
|
||||||
|
<button onclick="testError()">测试 Console Error</button>
|
||||||
|
<button onclick="testNetwork()">测试网络请求</button>
|
||||||
|
<button onclick="testSelection()">测试文本选择</button>
|
||||||
|
<div id="console"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test-section">
|
||||||
|
<h2>📄 示例内容</h2>
|
||||||
|
<p>这是一段可以被插件提取的示例文本。你可以选择这段文字,然后使用插件的"Send Selected Text"功能。</p>
|
||||||
|
<ul style="margin: 15px 0;">
|
||||||
|
<li>列表项 1:Lorem ipsum dolor sit amet</li>
|
||||||
|
<li>列表项 2:Consectetur adipiscing elit</li>
|
||||||
|
<li>列表项 3:Sed do eiusmod tempor incididunt</li>
|
||||||
|
</ul>
|
||||||
|
<blockquote style="border-left: 4px solid #667eea; padding-left: 15px; margin: 15px 0; color: #666;">
|
||||||
|
"这是一个引用块,可以测试 Markdown 转换功能。"
|
||||||
|
</blockquote>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="instructions">
|
||||||
|
<h3>🎯 使用说明</h3>
|
||||||
|
<ol>
|
||||||
|
<li>点击 Chrome 工具栏中的插件图标</li>
|
||||||
|
<li>点击 "Connect to Qwen CLI" 建立连接</li>
|
||||||
|
<li>如果安装了 Qwen CLI,点击 "Start Qwen CLI"</li>
|
||||||
|
<li>使用各种功能按钮测试插件功能</li>
|
||||||
|
<li>按 F12 打开 DevTools 查看详细日志</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const consoleDiv = document.getElementById('console');
|
||||||
|
|
||||||
|
function addLog(message, type = 'info') {
|
||||||
|
const entry = document.createElement('div');
|
||||||
|
entry.className = 'log-entry ' + type;
|
||||||
|
const time = new Date().toLocaleTimeString();
|
||||||
|
entry.textContent = `[${time}] ${message}`;
|
||||||
|
consoleDiv.appendChild(entry);
|
||||||
|
consoleDiv.scrollTop = consoleDiv.scrollHeight;
|
||||||
|
|
||||||
|
// 同时输出到真实 console
|
||||||
|
console[type](message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testLog() {
|
||||||
|
addLog('这是一条测试日志消息', 'info');
|
||||||
|
}
|
||||||
|
|
||||||
|
function testError() {
|
||||||
|
addLog('这是一条测试错误消息', 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
function testNetwork() {
|
||||||
|
addLog('发起网络请求...', 'info');
|
||||||
|
fetch('https://api.github.com/zen')
|
||||||
|
.then(res => res.text())
|
||||||
|
.then(data => addLog('请求成功: ' + data, 'info'))
|
||||||
|
.catch(err => addLog('请求失败: ' + err.message, 'error'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function testSelection() {
|
||||||
|
const selection = window.getSelection().toString();
|
||||||
|
if (selection) {
|
||||||
|
addLog('选中的文本: ' + selection, 'info');
|
||||||
|
} else {
|
||||||
|
addLog('请先选择一些文本', 'warn');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
addLog('测试页面已加载', 'info');
|
||||||
|
addLog('插件调试环境已就绪', 'info');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
HTML
|
||||||
|
|
||||||
|
# 启动 Python HTTP 服务器
|
||||||
|
cd /tmp
|
||||||
|
python3 -m http.server 3000 > /tmp/test-server.log 2>&1 &
|
||||||
|
TEST_PID=$!
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
echo -e "${GREEN}✓${NC} 测试服务器已启动 (http://localhost:3000)"
|
||||||
|
|
||||||
|
# 第五步:启动 Chrome
|
||||||
|
echo -e "\n${BLUE}[5/5]${NC} 启动 Chrome 并加载插件..."
|
||||||
|
|
||||||
|
"$CHROME_PATH" \
|
||||||
|
--load-extension="$SCRIPT_DIR/extension" \
|
||||||
|
--auto-open-devtools-for-tabs \
|
||||||
|
--no-first-run \
|
||||||
|
--no-default-browser-check \
|
||||||
|
"http://localhost:3000/qwen-test.html" &
|
||||||
|
|
||||||
|
CHROME_PID=$!
|
||||||
|
|
||||||
|
echo -e "${GREEN}✓${NC} Chrome 已启动"
|
||||||
|
|
||||||
|
# 显示最终状态
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}╔════════════════════════════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${GREEN}║ ║${NC}"
|
||||||
|
echo -e "${GREEN}║ ✅ 调试环境启动成功! ║${NC}"
|
||||||
|
echo -e "${GREEN}║ ║${NC}"
|
||||||
|
echo -e "${GREEN}╚════════════════════════════════════════════════════════════════╝${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}📍 服务状态:${NC}"
|
||||||
|
echo -e " • Chrome: 运行中"
|
||||||
|
echo -e " • 测试页面: ${BLUE}http://localhost:3000/qwen-test.html${NC}"
|
||||||
|
echo -e " • 插件: 已加载到工具栏"
|
||||||
|
|
||||||
|
if [ "$QWEN_AVAILABLE" = true ]; then
|
||||||
|
echo -e " • Qwen Server: ${BLUE}http://localhost:8080${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}🔍 调试位置:${NC}"
|
||||||
|
echo -e " • 插件日志: Chrome DevTools Console"
|
||||||
|
echo -e " • 后台脚本: chrome://extensions → Service Worker"
|
||||||
|
echo -e " • Native Host: /tmp/qwen-bridge-host.log"
|
||||||
|
|
||||||
|
if [ "$QWEN_AVAILABLE" = true ]; then
|
||||||
|
echo -e " • Qwen 日志: /tmp/qwen-server.log"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}按 Ctrl+C 停止所有服务${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 清理函数
|
||||||
|
cleanup() {
|
||||||
|
echo -e "\n${YELLOW}正在停止服务...${NC}"
|
||||||
|
|
||||||
|
# 停止进程
|
||||||
|
[ ! -z "$TEST_PID" ] && kill $TEST_PID 2>/dev/null
|
||||||
|
[ ! -z "$QWEN_PID" ] && kill $QWEN_PID 2>/dev/null
|
||||||
|
|
||||||
|
echo -e "${GREEN}✓${NC} 已停止所有服务"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# 捕获中断信号
|
||||||
|
trap cleanup INT TERM
|
||||||
|
|
||||||
|
# 保持运行
|
||||||
|
while true; do
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
511
packages/chrome-qwen-bridge/dev.js
Normal file
511
packages/chrome-qwen-bridge/dev.js
Normal file
@@ -0,0 +1,511 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开发环境一键启动脚本
|
||||||
|
* 自动完成所有配置和启动步骤
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { spawn, exec } = require('child_process');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const os = require('os');
|
||||||
|
const readline = require('readline');
|
||||||
|
|
||||||
|
// 颜色输出
|
||||||
|
const colors = {
|
||||||
|
reset: '\x1b[0m',
|
||||||
|
bright: '\x1b[1m',
|
||||||
|
green: '\x1b[32m',
|
||||||
|
yellow: '\x1b[33m',
|
||||||
|
blue: '\x1b[34m',
|
||||||
|
red: '\x1b[31m',
|
||||||
|
cyan: '\x1b[36m'
|
||||||
|
};
|
||||||
|
|
||||||
|
function log(message, color = '') {
|
||||||
|
console.log(`${color}${message}${colors.reset}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function logStep(step, message) {
|
||||||
|
log(`\n[${step}] ${message}`, colors.bright + colors.blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
function logSuccess(message) {
|
||||||
|
log(`✅ ${message}`, colors.green);
|
||||||
|
}
|
||||||
|
|
||||||
|
function logWarning(message) {
|
||||||
|
log(`⚠️ ${message}`, colors.yellow);
|
||||||
|
}
|
||||||
|
|
||||||
|
function logError(message) {
|
||||||
|
log(`❌ ${message}`, colors.red);
|
||||||
|
}
|
||||||
|
|
||||||
|
function logInfo(message) {
|
||||||
|
log(`ℹ️ ${message}`, colors.cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查命令是否存在
|
||||||
|
function commandExists(command) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
exec(`command -v ${command}`, (error) => {
|
||||||
|
resolve(!error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取 Chrome 路径
|
||||||
|
function getChromePath() {
|
||||||
|
const platform = process.platform;
|
||||||
|
|
||||||
|
const chromePaths = {
|
||||||
|
darwin: [
|
||||||
|
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||||
|
'/Applications/Chromium.app/Contents/MacOS/Chromium'
|
||||||
|
],
|
||||||
|
win32: [
|
||||||
|
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
||||||
|
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
|
||||||
|
process.env.LOCALAPPDATA + '\\Google\\Chrome\\Application\\chrome.exe'
|
||||||
|
],
|
||||||
|
linux: [
|
||||||
|
'/usr/bin/google-chrome',
|
||||||
|
'/usr/bin/chromium-browser',
|
||||||
|
'/usr/bin/chromium'
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const paths = chromePaths[platform] || [];
|
||||||
|
|
||||||
|
for (const chromePath of paths) {
|
||||||
|
if (fs.existsSync(chromePath)) {
|
||||||
|
return chromePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取扩展 ID
|
||||||
|
function getExtensionId(extensionPath) {
|
||||||
|
// 这是一个简化的方法,实际的 Extension ID 是通过 Chrome 生成的
|
||||||
|
// 开发时可以固定使用一个 ID
|
||||||
|
return 'development-extension-id';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 安装 Native Host
|
||||||
|
async function installNativeHost(extensionPath) {
|
||||||
|
logStep(2, 'Installing Native Host...');
|
||||||
|
|
||||||
|
const hostPath = path.join(extensionPath, 'native-host');
|
||||||
|
const scriptPath = path.join(hostPath, 'host.js');
|
||||||
|
|
||||||
|
if (!fs.existsSync(scriptPath)) {
|
||||||
|
logError('Native host script not found!');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const platform = process.platform;
|
||||||
|
const hostName = 'com.qwen.cli.bridge';
|
||||||
|
|
||||||
|
let manifestDir;
|
||||||
|
if (platform === 'darwin') {
|
||||||
|
manifestDir = path.join(os.homedir(), 'Library/Application Support/Google/Chrome/NativeMessagingHosts');
|
||||||
|
} else if (platform === 'linux') {
|
||||||
|
manifestDir = path.join(os.homedir(), '.config/google-chrome/NativeMessagingHosts');
|
||||||
|
} else if (platform === 'win32') {
|
||||||
|
// Windows 需要写注册表
|
||||||
|
logWarning('Windows requires registry modification. Please run install.bat manually.');
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
logError('Unsupported platform');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建目录
|
||||||
|
if (!fs.existsSync(manifestDir)) {
|
||||||
|
fs.mkdirSync(manifestDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 manifest 文件
|
||||||
|
const manifest = {
|
||||||
|
name: hostName,
|
||||||
|
description: 'Native messaging host for Qwen CLI Bridge',
|
||||||
|
path: scriptPath,
|
||||||
|
type: 'stdio',
|
||||||
|
allowed_origins: [
|
||||||
|
'chrome-extension://jniepomhbdkeifkadbfolbcihcmfpfjo/', // 开发用 ID
|
||||||
|
'chrome-extension://*/' // 允许任何扩展(仅开发环境)
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const manifestPath = path.join(manifestDir, `${hostName}.json`);
|
||||||
|
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
||||||
|
|
||||||
|
logSuccess(`Native Host installed at: ${manifestPath}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 Qwen CLI
|
||||||
|
async function checkQwenCli() {
|
||||||
|
logStep(3, 'Checking Qwen CLI...');
|
||||||
|
|
||||||
|
const qwenExists = await commandExists('qwen');
|
||||||
|
|
||||||
|
if (qwenExists) {
|
||||||
|
logSuccess('Qwen CLI is installed');
|
||||||
|
|
||||||
|
// 获取版本
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
exec('qwen --version', (error, stdout) => {
|
||||||
|
if (!error && stdout) {
|
||||||
|
logInfo(`Version: ${stdout.trim()}`);
|
||||||
|
}
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logWarning('Qwen CLI is not installed');
|
||||||
|
logInfo('You can still use the extension, but some features will be limited');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动 Qwen CLI 服务器
|
||||||
|
function startQwenServer(port = 8080) {
|
||||||
|
logStep(4, 'Starting Qwen CLI Server...');
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// 检查端口是否被占用
|
||||||
|
exec(`lsof -i:${port} || netstat -an | grep ${port}`, (error, stdout) => {
|
||||||
|
if (stdout && stdout.length > 0) {
|
||||||
|
logWarning(`Port ${port} is already in use`);
|
||||||
|
logInfo('Qwen server might already be running');
|
||||||
|
resolve(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动服务器
|
||||||
|
const qwenProcess = spawn('qwen', ['server', '--port', String(port)], {
|
||||||
|
detached: false,
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe']
|
||||||
|
});
|
||||||
|
|
||||||
|
qwenProcess.stdout.on('data', (data) => {
|
||||||
|
const output = data.toString();
|
||||||
|
if (output.includes('Server started') || output.includes('listening')) {
|
||||||
|
logSuccess(`Qwen server started on port ${port}`);
|
||||||
|
resolve(qwenProcess);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
qwenProcess.stderr.on('data', (data) => {
|
||||||
|
logError(`Qwen server error: ${data}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
qwenProcess.on('error', (error) => {
|
||||||
|
logError(`Failed to start Qwen server: ${error.message}`);
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 超时处理
|
||||||
|
setTimeout(() => {
|
||||||
|
logWarning('Qwen server start timeout, continuing anyway...');
|
||||||
|
resolve(qwenProcess);
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动 Chrome 开发模式
|
||||||
|
function startChrome(extensionPath, chromePath) {
|
||||||
|
logStep(5, 'Starting Chrome with extension...');
|
||||||
|
|
||||||
|
const args = [
|
||||||
|
`--load-extension=${extensionPath}`,
|
||||||
|
'--auto-open-devtools-for-tabs', // 自动打开 DevTools
|
||||||
|
'--disable-extensions-except=' + extensionPath,
|
||||||
|
'--no-first-run',
|
||||||
|
'--no-default-browser-check',
|
||||||
|
'--disable-default-apps',
|
||||||
|
'--disable-popup-blocking',
|
||||||
|
'--disable-translate',
|
||||||
|
'--disable-sync',
|
||||||
|
'--no-pings',
|
||||||
|
'--disable-background-timer-throttling',
|
||||||
|
'--disable-renderer-backgrounding',
|
||||||
|
'--disable-device-discovery-notifications'
|
||||||
|
];
|
||||||
|
|
||||||
|
// 开发模式特定参数
|
||||||
|
if (process.env.DEBUG === 'true') {
|
||||||
|
args.push('--enable-logging=stderr');
|
||||||
|
args.push('--v=1');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加测试页面
|
||||||
|
args.push('http://localhost:3000'); // 或其他测试页面
|
||||||
|
|
||||||
|
const chromeProcess = spawn(chromePath, args, {
|
||||||
|
detached: false,
|
||||||
|
stdio: 'inherit'
|
||||||
|
});
|
||||||
|
|
||||||
|
chromeProcess.on('error', (error) => {
|
||||||
|
logError(`Failed to start Chrome: ${error.message}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
logSuccess('Chrome started with extension loaded');
|
||||||
|
logInfo('Extension should be visible in the toolbar');
|
||||||
|
|
||||||
|
return chromeProcess;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建测试服务器
|
||||||
|
function createTestServer(port = 3000) {
|
||||||
|
logStep(6, 'Starting test server...');
|
||||||
|
|
||||||
|
const http = require('http');
|
||||||
|
const testHtml = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Qwen CLI Bridge Test Page</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 40px 20px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
background: white;
|
||||||
|
padding: 40px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
border-bottom: 3px solid #667eea;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
.test-content {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.test-button {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
.test-button:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
#console-output {
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-family: monospace;
|
||||||
|
min-height: 100px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>🚀 Qwen CLI Bridge Test Page</h1>
|
||||||
|
|
||||||
|
<div class="test-content">
|
||||||
|
<h2>Test Content</h2>
|
||||||
|
<p>This is a test page for the Qwen CLI Bridge Chrome Extension.</p>
|
||||||
|
<p>Click the extension icon in your toolbar to start testing!</p>
|
||||||
|
|
||||||
|
<h3>Sample Data</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Item 1: Lorem ipsum dolor sit amet</li>
|
||||||
|
<li>Item 2: Consectetur adipiscing elit</li>
|
||||||
|
<li>Item 3: Sed do eiusmod tempor incididunt</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Test Actions</h3>
|
||||||
|
<button class="test-button" onclick="testLog()">Test Console Log</button>
|
||||||
|
<button class="test-button" onclick="testError()">Test Console Error</button>
|
||||||
|
<button class="test-button" onclick="testNetwork()">Test Network Request</button>
|
||||||
|
|
||||||
|
<h3>Console Output</h3>
|
||||||
|
<div id="console-output"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test-content">
|
||||||
|
<h2>Test Form</h2>
|
||||||
|
<form>
|
||||||
|
<input type="text" placeholder="Test input" style="padding: 5px; margin: 5px;">
|
||||||
|
<textarea placeholder="Test textarea" style="padding: 5px; margin: 5px;"></textarea>
|
||||||
|
<select style="padding: 5px; margin: 5px;">
|
||||||
|
<option>Option 1</option>
|
||||||
|
<option>Option 2</option>
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test-content">
|
||||||
|
<h2>Images</h2>
|
||||||
|
<img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMjAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iIzY2N2VlYSIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBmaWxsPSJ3aGl0ZSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPjIwMHgxMDA8L3RleHQ+PC9zdmc+" alt="Test Image">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function addOutput(message, type = 'log') {
|
||||||
|
const output = document.getElementById('console-output');
|
||||||
|
const time = new Date().toLocaleTimeString();
|
||||||
|
const color = type === 'error' ? 'red' : type === 'warn' ? 'orange' : 'black';
|
||||||
|
output.innerHTML += \`<div style="color: \${color}">[\${time}] \${message}</div>\`;
|
||||||
|
console[type](message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testLog() {
|
||||||
|
addOutput('This is a test log message', 'log');
|
||||||
|
}
|
||||||
|
|
||||||
|
function testError() {
|
||||||
|
addOutput('This is a test error message', 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testNetwork() {
|
||||||
|
addOutput('Making network request...', 'log');
|
||||||
|
try {
|
||||||
|
const response = await fetch('https://api.github.com/users/github');
|
||||||
|
const data = await response.json();
|
||||||
|
addOutput('Network request successful: ' + JSON.stringify(data).substring(0, 100) + '...', 'log');
|
||||||
|
} catch (error) {
|
||||||
|
addOutput('Network request failed: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动记录一些日志
|
||||||
|
console.log('Test page loaded');
|
||||||
|
console.info('Extension test environment ready');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||||
|
res.end(testHtml);
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(port, () => {
|
||||||
|
logSuccess(`Test server running at http://localhost:${port}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主函数
|
||||||
|
async function main() {
|
||||||
|
console.clear();
|
||||||
|
log(`
|
||||||
|
╔════════════════════════════════════════════════════════════════╗
|
||||||
|
║ ║
|
||||||
|
║ 🚀 Qwen CLI Bridge - Development Environment Setup ║
|
||||||
|
║ ║
|
||||||
|
╚════════════════════════════════════════════════════════════════╝
|
||||||
|
`, colors.bright + colors.cyan);
|
||||||
|
|
||||||
|
const extensionPath = path.join(__dirname, 'extension');
|
||||||
|
|
||||||
|
// Step 1: 检查 Chrome
|
||||||
|
logStep(1, 'Checking Chrome installation...');
|
||||||
|
const chromePath = getChromePath();
|
||||||
|
|
||||||
|
if (!chromePath) {
|
||||||
|
logError('Chrome not found! Please install Google Chrome.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
logSuccess(`Chrome found at: ${chromePath}`);
|
||||||
|
|
||||||
|
// Step 2: 安装 Native Host
|
||||||
|
const nativeHostInstalled = await installNativeHost(__dirname);
|
||||||
|
if (!nativeHostInstalled && process.platform === 'win32') {
|
||||||
|
logWarning('Please run install.bat as Administrator to complete Native Host setup');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: 检查 Qwen CLI
|
||||||
|
const qwenInstalled = await checkQwenCli();
|
||||||
|
|
||||||
|
// Step 4: 启动 Qwen 服务器(如果已安装)
|
||||||
|
let qwenProcess = null;
|
||||||
|
if (qwenInstalled) {
|
||||||
|
qwenProcess = await startQwenServer(8080);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 5: 启动测试服务器
|
||||||
|
const testServer = createTestServer(3000);
|
||||||
|
|
||||||
|
// Step 6: 启动 Chrome
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000)); // 等待服务器启动
|
||||||
|
const chromeProcess = startChrome(extensionPath, chromePath);
|
||||||
|
|
||||||
|
// 设置清理处理
|
||||||
|
const cleanup = () => {
|
||||||
|
log('\n\nShutting down...', colors.yellow);
|
||||||
|
|
||||||
|
if (qwenProcess) {
|
||||||
|
qwenProcess.kill();
|
||||||
|
logInfo('Qwen server stopped');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (testServer) {
|
||||||
|
testServer.close();
|
||||||
|
logInfo('Test server stopped');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chromeProcess) {
|
||||||
|
chromeProcess.kill();
|
||||||
|
logInfo('Chrome stopped');
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on('SIGINT', cleanup);
|
||||||
|
process.on('SIGTERM', cleanup);
|
||||||
|
|
||||||
|
// 显示使用说明
|
||||||
|
log(`
|
||||||
|
╔════════════════════════════════════════════════════════════════╗
|
||||||
|
║ ✅ Setup Complete! ║
|
||||||
|
╠════════════════════════════════════════════════════════════════╣
|
||||||
|
║ ║
|
||||||
|
║ 📍 Chrome is running with the extension loaded ║
|
||||||
|
║ 📍 Test page: http://localhost:3000 ║
|
||||||
|
║ ${qwenInstalled ? '📍 Qwen server: http://localhost:8080 ' : '📍 Qwen CLI not installed (limited functionality) '}║
|
||||||
|
║ ║
|
||||||
|
║ 📝 How to debug: ║
|
||||||
|
║ 1. Click the extension icon in Chrome toolbar ║
|
||||||
|
║ 2. Open Chrome DevTools (F12) to see console logs ║
|
||||||
|
║ 3. Check background page: chrome://extensions → Details ║
|
||||||
|
║ 4. Native Host logs: /tmp/qwen-bridge-host.log ║
|
||||||
|
║ ║
|
||||||
|
║ 🛑 Press Ctrl+C to stop all services ║
|
||||||
|
║ ║
|
||||||
|
╚════════════════════════════════════════════════════════════════╝
|
||||||
|
`, colors.bright + colors.green);
|
||||||
|
|
||||||
|
// 保持进程运行
|
||||||
|
await new Promise(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运行
|
||||||
|
main().catch((error) => {
|
||||||
|
logError(`Fatal error: ${error.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
139
packages/chrome-qwen-bridge/diagnose.sh
Executable file
139
packages/chrome-qwen-bridge/diagnose.sh
Executable file
@@ -0,0 +1,139 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "🔍 Chrome Qwen Bridge 连接诊断"
|
||||||
|
echo "==============================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 颜色定义
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
# 1. 检查 Native Host 配置
|
||||||
|
echo -e "${BLUE}1. 检查 Native Host 配置${NC}"
|
||||||
|
NATIVE_HOST_CONFIG="$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts/com.qwen.cli.bridge.json"
|
||||||
|
|
||||||
|
if [ -f "$NATIVE_HOST_CONFIG" ]; then
|
||||||
|
echo -e "${GREEN}✓${NC} Native Host 配置存在"
|
||||||
|
echo " 内容:"
|
||||||
|
cat "$NATIVE_HOST_CONFIG" | sed 's/^/ /'
|
||||||
|
|
||||||
|
# 检查路径是否正确
|
||||||
|
HOST_PATH=$(cat "$NATIVE_HOST_CONFIG" | grep '"path"' | sed 's/.*"path".*:.*"\(.*\)".*/\1/')
|
||||||
|
if [ -f "$HOST_PATH" ]; then
|
||||||
|
echo -e "${GREEN}✓${NC} Host 文件存在: $HOST_PATH"
|
||||||
|
# 检查是否可执行
|
||||||
|
if [ -x "$HOST_PATH" ]; then
|
||||||
|
echo -e "${GREEN}✓${NC} Host 文件可执行"
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗${NC} Host 文件不可执行"
|
||||||
|
echo " 修复: chmod +x '$HOST_PATH'"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗${NC} Host 文件不存在: $HOST_PATH"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗${NC} Native Host 配置不存在"
|
||||||
|
echo " 请运行: npm run install:host"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 2. 检查扩展 ID
|
||||||
|
echo -e "${BLUE}2. 检查扩展 ID${NC}"
|
||||||
|
if [ -f ".extension-id" ]; then
|
||||||
|
SAVED_ID=$(cat .extension-id)
|
||||||
|
echo -e "${GREEN}✓${NC} 保存的扩展 ID: $SAVED_ID"
|
||||||
|
|
||||||
|
# 检查配置中的 ID
|
||||||
|
if grep -q "$SAVED_ID" "$NATIVE_HOST_CONFIG" 2>/dev/null; then
|
||||||
|
echo -e "${GREEN}✓${NC} Native Host 配置包含此 ID"
|
||||||
|
else
|
||||||
|
if grep -q 'chrome-extension://\*/' "$NATIVE_HOST_CONFIG" 2>/dev/null; then
|
||||||
|
echo -e "${YELLOW}⚠${NC} Native Host 使用通配符 (接受所有扩展)"
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗${NC} Native Host 配置不包含此 ID"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}⚠${NC} 未保存扩展 ID"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 3. 测试 Native Host
|
||||||
|
echo -e "${BLUE}3. 测试 Native Host 直接连接${NC}"
|
||||||
|
if [ -f "$HOST_PATH" ]; then
|
||||||
|
# 发送测试消息
|
||||||
|
TEST_RESPONSE=$(echo '{"type":"handshake","version":"1.0.0"}' | \
|
||||||
|
python3 -c "
|
||||||
|
import sys, json, struct
|
||||||
|
msg = sys.stdin.read().strip()
|
||||||
|
encoded = msg.encode('utf-8')
|
||||||
|
sys.stdout.buffer.write(struct.pack('<I', len(encoded)))
|
||||||
|
sys.stdout.buffer.write(encoded)
|
||||||
|
sys.stdout.flush()
|
||||||
|
" | "$HOST_PATH" 2>/dev/null | \
|
||||||
|
python3 -c "
|
||||||
|
import sys, struct, json
|
||||||
|
try:
|
||||||
|
length_bytes = sys.stdin.buffer.read(4)
|
||||||
|
if length_bytes:
|
||||||
|
length = struct.unpack('<I', length_bytes)[0]
|
||||||
|
message = sys.stdin.buffer.read(length)
|
||||||
|
print(json.loads(message.decode('utf-8')))
|
||||||
|
except: pass
|
||||||
|
" 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -n "$TEST_RESPONSE" ]; then
|
||||||
|
echo -e "${GREEN}✓${NC} Native Host 响应: $TEST_RESPONSE"
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗${NC} Native Host 无响应"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}⚠${NC} 跳过测试 (Host 文件不存在)"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 4. 检查日志
|
||||||
|
echo -e "${BLUE}4. 检查最近的错误日志${NC}"
|
||||||
|
LOG_FILE="/tmp/qwen-bridge-host.log"
|
||||||
|
if [ -f "$LOG_FILE" ]; then
|
||||||
|
RECENT_ERRORS=$(tail -20 "$LOG_FILE" | grep -i error | tail -3)
|
||||||
|
if [ -n "$RECENT_ERRORS" ]; then
|
||||||
|
echo -e "${YELLOW}⚠${NC} 最近的错误:"
|
||||||
|
echo "$RECENT_ERRORS" | sed 's/^/ /'
|
||||||
|
else
|
||||||
|
echo -e "${GREEN}✓${NC} 日志中无最近错误"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " 日志文件不存在"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 5. 建议
|
||||||
|
echo -e "${BLUE}5. 下一步操作建议${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "请按以下步骤操作:"
|
||||||
|
echo ""
|
||||||
|
echo "1. 重新加载扩展:"
|
||||||
|
echo " - 打开 chrome://extensions/"
|
||||||
|
echo " - 找到 'Qwen CLI Bridge' 扩展"
|
||||||
|
echo " - 点击重新加载按钮 (🔄)"
|
||||||
|
echo ""
|
||||||
|
echo "2. 查看 Service Worker 日志:"
|
||||||
|
echo " - 在扩展卡片上点击 'Service Worker'"
|
||||||
|
echo " - 在打开的控制台中查看错误信息"
|
||||||
|
echo ""
|
||||||
|
echo "3. 测试连接:"
|
||||||
|
echo " - 点击扩展图标"
|
||||||
|
echo " - 点击 'Connect to Qwen CLI'"
|
||||||
|
echo " - 观察控制台输出"
|
||||||
|
echo ""
|
||||||
|
echo "4. 如果仍有问题:"
|
||||||
|
echo " - 运行: ./debug-chrome.sh"
|
||||||
|
echo " - 这会打开调试控制台帮助诊断"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "==============================="
|
||||||
|
echo "诊断完成"
|
||||||
119
packages/chrome-qwen-bridge/docs/README.md
Normal file
119
packages/chrome-qwen-bridge/docs/README.md
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
# Chrome Qwen Bridge 文档
|
||||||
|
|
||||||
|
欢迎查阅 Chrome Qwen Bridge 的技术文档。本项目是一个 Chrome 扩展,用于连接浏览器与 Qwen CLI,实现 AI 增强的网页交互。
|
||||||
|
|
||||||
|
## 📚 文档目录
|
||||||
|
|
||||||
|
### 核心文档
|
||||||
|
|
||||||
|
1. **[架构设计文档](./architecture.md)**
|
||||||
|
- 系统架构概览
|
||||||
|
- 组件职责划分
|
||||||
|
- 数据流设计
|
||||||
|
- 安全设计
|
||||||
|
- 性能优化策略
|
||||||
|
|
||||||
|
2. **[实施计划文档](./implementation-plan.md)**
|
||||||
|
- 项目背景与需求
|
||||||
|
- 分阶段实施计划
|
||||||
|
- 技术栈选择
|
||||||
|
- 测试与部署计划
|
||||||
|
- 风险评估
|
||||||
|
|
||||||
|
3. **[技术细节文档](./technical-details.md)**
|
||||||
|
- Native Messaging 协议详解
|
||||||
|
- Chrome Extension API 使用
|
||||||
|
- 数据提取算法
|
||||||
|
- 进程管理
|
||||||
|
- 调试技巧
|
||||||
|
|
||||||
|
4. **[API 参考文档](./api-reference.md)**
|
||||||
|
- Chrome Extension APIs
|
||||||
|
- Native Host APIs
|
||||||
|
- Qwen CLI 集成
|
||||||
|
- 错误代码
|
||||||
|
- 使用示例
|
||||||
|
|
||||||
|
### 快速链接
|
||||||
|
|
||||||
|
- [主 README](../README.md) - 安装和使用指南
|
||||||
|
- [GitHub 仓库](https://github.com/QwenLM/qwen-code) - 源代码
|
||||||
|
- [问题反馈](https://github.com/QwenLM/qwen-code/issues) - 提交 Issue
|
||||||
|
|
||||||
|
## 🎯 项目特性
|
||||||
|
|
||||||
|
- ✅ **Native Messaging** - Chrome 官方推荐的安全通信方式
|
||||||
|
- ✅ **MCP 服务器支持** - 集成多个 Model Context Protocol 服务器
|
||||||
|
- ✅ **丰富的数据提取** - DOM、Console、网络请求等全方位数据
|
||||||
|
- ✅ **AI 分析能力** - 利用 Qwen 的 AI 能力分析网页内容
|
||||||
|
- ✅ **跨平台支持** - Windows、macOS、Linux 全平台
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
1. **安装扩展**
|
||||||
|
```bash
|
||||||
|
# 在 Chrome 中加载未打包的扩展
|
||||||
|
chrome://extensions/ → 开发者模式 → 加载已解压的扩展程序
|
||||||
|
选择: packages/chrome-qwen-bridge/extension
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **安装 Native Host**
|
||||||
|
```bash
|
||||||
|
cd packages/chrome-qwen-bridge/native-host
|
||||||
|
./install.sh # macOS/Linux
|
||||||
|
# 或
|
||||||
|
install.bat # Windows
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **连接使用**
|
||||||
|
- 点击扩展图标
|
||||||
|
- 连接到 Qwen CLI
|
||||||
|
- 开始分析网页!
|
||||||
|
|
||||||
|
## 📖 文档说明
|
||||||
|
|
||||||
|
### 架构设计文档
|
||||||
|
详细描述了系统的整体架构,包括 Chrome Extension、Native Host 和 Qwen CLI 三层架构的设计理念、组件职责、数据流向等核心概念。
|
||||||
|
|
||||||
|
### 实施计划文档
|
||||||
|
记录了项目从概念到实现的完整过程,包括各个开发阶段的任务分解、技术选型依据、测试计划和未来优化方向。
|
||||||
|
|
||||||
|
### 技术细节文档
|
||||||
|
深入探讨了关键技术的实现细节,如 Native Messaging 协议的具体实现、数据提取算法、进程管理策略等。
|
||||||
|
|
||||||
|
### API 参考文档
|
||||||
|
提供了所有 API 的完整参考,包括消息格式、参数说明、返回值、错误代码等,是开发和调试的重要参考。
|
||||||
|
|
||||||
|
## 🛠 技术架构
|
||||||
|
|
||||||
|
```
|
||||||
|
Chrome Browser
|
||||||
|
↓
|
||||||
|
Chrome Extension (Content Script + Service Worker + Popup)
|
||||||
|
↓
|
||||||
|
Native Messaging API
|
||||||
|
↓
|
||||||
|
Native Host (Node.js)
|
||||||
|
↓
|
||||||
|
Qwen CLI + MCP Servers
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 版本历史
|
||||||
|
|
||||||
|
- **v1.0.0** (2024-12) - 初始版本
|
||||||
|
- 实现基础架构
|
||||||
|
- Native Messaging 通信
|
||||||
|
- 页面数据提取
|
||||||
|
- Qwen CLI 集成
|
||||||
|
|
||||||
|
## 🤝 贡献指南
|
||||||
|
|
||||||
|
欢迎贡献代码和文档!请查看主仓库的贡献指南。
|
||||||
|
|
||||||
|
## 📄 许可证
|
||||||
|
|
||||||
|
Apache-2.0 License
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*本文档集是 Chrome Qwen Bridge 项目的技术参考,持续更新中。*
|
||||||
646
packages/chrome-qwen-bridge/docs/api-reference.md
Normal file
646
packages/chrome-qwen-bridge/docs/api-reference.md
Normal file
@@ -0,0 +1,646 @@
|
|||||||
|
# Chrome Qwen Bridge API 参考文档
|
||||||
|
|
||||||
|
## Chrome Extension APIs
|
||||||
|
|
||||||
|
### Background Service Worker
|
||||||
|
|
||||||
|
#### 消息类型
|
||||||
|
|
||||||
|
##### 连接管理
|
||||||
|
|
||||||
|
**CONNECT**
|
||||||
|
```javascript
|
||||||
|
// 请求
|
||||||
|
{
|
||||||
|
type: 'CONNECT'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应
|
||||||
|
{
|
||||||
|
success: boolean,
|
||||||
|
status?: string, // 'connected' | 'running' | 'stopped'
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**GET_STATUS**
|
||||||
|
```javascript
|
||||||
|
// 请求
|
||||||
|
{
|
||||||
|
type: 'GET_STATUS'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应
|
||||||
|
{
|
||||||
|
connected: boolean,
|
||||||
|
status: string // 'disconnected' | 'connecting' | 'connected' | 'running'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Qwen CLI 控制
|
||||||
|
|
||||||
|
**START_QWEN_CLI**
|
||||||
|
```javascript
|
||||||
|
// 请求
|
||||||
|
{
|
||||||
|
type: 'START_QWEN_CLI',
|
||||||
|
config?: {
|
||||||
|
mcpServers?: string[], // MCP 服务器列表
|
||||||
|
httpPort?: number // HTTP 端口,默认 8080
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应
|
||||||
|
{
|
||||||
|
success: boolean,
|
||||||
|
data?: {
|
||||||
|
status: string,
|
||||||
|
pid: number,
|
||||||
|
port: number
|
||||||
|
},
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**STOP_QWEN_CLI**
|
||||||
|
```javascript
|
||||||
|
// 请求
|
||||||
|
{
|
||||||
|
type: 'STOP_QWEN_CLI'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应
|
||||||
|
{
|
||||||
|
success: boolean,
|
||||||
|
data?: string,
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### 数据操作
|
||||||
|
|
||||||
|
**EXTRACT_PAGE_DATA**
|
||||||
|
```javascript
|
||||||
|
// 请求
|
||||||
|
{
|
||||||
|
type: 'EXTRACT_PAGE_DATA'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应
|
||||||
|
{
|
||||||
|
success: boolean,
|
||||||
|
data?: {
|
||||||
|
url: string,
|
||||||
|
title: string,
|
||||||
|
domain: string,
|
||||||
|
path: string,
|
||||||
|
timestamp: string,
|
||||||
|
meta: object,
|
||||||
|
content: {
|
||||||
|
text: string,
|
||||||
|
html: string,
|
||||||
|
markdown: string
|
||||||
|
},
|
||||||
|
links: Array<{
|
||||||
|
text: string,
|
||||||
|
href: string,
|
||||||
|
target: string,
|
||||||
|
isExternal: boolean
|
||||||
|
}>,
|
||||||
|
images: Array<{
|
||||||
|
src: string,
|
||||||
|
alt: string,
|
||||||
|
title: string,
|
||||||
|
width: number,
|
||||||
|
height: number
|
||||||
|
}>,
|
||||||
|
forms: Array<{
|
||||||
|
action: string,
|
||||||
|
method: string,
|
||||||
|
fields: Array<object>
|
||||||
|
}>,
|
||||||
|
consoleLogs: Array<{
|
||||||
|
type: string,
|
||||||
|
message: string,
|
||||||
|
timestamp: string,
|
||||||
|
stack: string
|
||||||
|
}>,
|
||||||
|
performance: {
|
||||||
|
loadTime: number,
|
||||||
|
domReady: number,
|
||||||
|
firstPaint: number
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**CAPTURE_SCREENSHOT**
|
||||||
|
```javascript
|
||||||
|
// 请求
|
||||||
|
{
|
||||||
|
type: 'CAPTURE_SCREENSHOT'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应
|
||||||
|
{
|
||||||
|
success: boolean,
|
||||||
|
data?: string, // Base64 编码的图片
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**GET_NETWORK_LOGS**
|
||||||
|
```javascript
|
||||||
|
// 请求
|
||||||
|
{
|
||||||
|
type: 'GET_NETWORK_LOGS'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应
|
||||||
|
{
|
||||||
|
success: boolean,
|
||||||
|
data?: Array<{
|
||||||
|
method: string,
|
||||||
|
params: object,
|
||||||
|
timestamp: number
|
||||||
|
}>,
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**SEND_TO_QWEN**
|
||||||
|
```javascript
|
||||||
|
// 请求
|
||||||
|
{
|
||||||
|
type: 'SEND_TO_QWEN',
|
||||||
|
action: string, // 'analyze_page' | 'analyze_screenshot' | 'ai_analyze' | 'process_text'
|
||||||
|
data: any
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应
|
||||||
|
{
|
||||||
|
success: boolean,
|
||||||
|
data?: any, // Qwen CLI 返回的数据
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Content Script APIs
|
||||||
|
|
||||||
|
#### 消息类型
|
||||||
|
|
||||||
|
**EXTRACT_DATA**
|
||||||
|
```javascript
|
||||||
|
// 请求
|
||||||
|
{
|
||||||
|
type: 'EXTRACT_DATA'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应
|
||||||
|
{
|
||||||
|
success: boolean,
|
||||||
|
data: {
|
||||||
|
// 同 EXTRACT_PAGE_DATA 的 data 字段
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**GET_SELECTED_TEXT**
|
||||||
|
```javascript
|
||||||
|
// 请求
|
||||||
|
{
|
||||||
|
type: 'GET_SELECTED_TEXT'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应
|
||||||
|
{
|
||||||
|
success: boolean,
|
||||||
|
data: string // 选中的文本
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**HIGHLIGHT_ELEMENT**
|
||||||
|
```javascript
|
||||||
|
// 请求
|
||||||
|
{
|
||||||
|
type: 'HIGHLIGHT_ELEMENT',
|
||||||
|
selector: string // CSS 选择器
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应
|
||||||
|
{
|
||||||
|
success: boolean
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**EXECUTE_CODE**
|
||||||
|
```javascript
|
||||||
|
// 请求
|
||||||
|
{
|
||||||
|
type: 'EXECUTE_CODE',
|
||||||
|
code: string // JavaScript 代码
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应
|
||||||
|
{
|
||||||
|
success: boolean,
|
||||||
|
data?: any, // 执行结果
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**SCROLL_TO**
|
||||||
|
```javascript
|
||||||
|
// 请求
|
||||||
|
{
|
||||||
|
type: 'SCROLL_TO',
|
||||||
|
x?: number,
|
||||||
|
y?: number,
|
||||||
|
smooth?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应
|
||||||
|
{
|
||||||
|
success: boolean
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 工具函数
|
||||||
|
|
||||||
|
**extractPageData()**
|
||||||
|
```javascript
|
||||||
|
function extractPageData(): PageData
|
||||||
|
|
||||||
|
interface PageData {
|
||||||
|
url: string;
|
||||||
|
title: string;
|
||||||
|
domain: string;
|
||||||
|
path: string;
|
||||||
|
timestamp: string;
|
||||||
|
meta: Record<string, string>;
|
||||||
|
content: {
|
||||||
|
text: string;
|
||||||
|
html: string;
|
||||||
|
markdown: string;
|
||||||
|
};
|
||||||
|
links: Link[];
|
||||||
|
images: Image[];
|
||||||
|
forms: Form[];
|
||||||
|
consoleLogs: ConsoleLog[];
|
||||||
|
performance: PerformanceMetrics;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**extractTextContent(element)**
|
||||||
|
```javascript
|
||||||
|
function extractTextContent(element: HTMLElement): string
|
||||||
|
// 提取元素的纯文本内容,移除脚本和样式
|
||||||
|
```
|
||||||
|
|
||||||
|
**htmlToMarkdown(element)**
|
||||||
|
```javascript
|
||||||
|
function htmlToMarkdown(element: HTMLElement): string
|
||||||
|
// 将 HTML 转换为 Markdown 格式
|
||||||
|
```
|
||||||
|
|
||||||
|
**getSelectedText()**
|
||||||
|
```javascript
|
||||||
|
function getSelectedText(): string
|
||||||
|
// 获取用户选中的文本
|
||||||
|
```
|
||||||
|
|
||||||
|
**highlightElement(selector)**
|
||||||
|
```javascript
|
||||||
|
function highlightElement(selector: string): boolean
|
||||||
|
// 高亮指定的元素,3秒后自动移除
|
||||||
|
```
|
||||||
|
|
||||||
|
**executeInPageContext(code)**
|
||||||
|
```javascript
|
||||||
|
async function executeInPageContext(code: string): Promise<any>
|
||||||
|
// 在页面上下文中执行 JavaScript 代码
|
||||||
|
```
|
||||||
|
|
||||||
|
## Native Host APIs
|
||||||
|
|
||||||
|
### 消息协议
|
||||||
|
|
||||||
|
#### 请求消息格式
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface RequestMessage {
|
||||||
|
id?: number; // 请求 ID,用于匹配响应
|
||||||
|
type: string; // 消息类型
|
||||||
|
action?: string; // 具体动作
|
||||||
|
data?: any; // 携带的数据
|
||||||
|
config?: object; // 配置选项
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 响应消息格式
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ResponseMessage {
|
||||||
|
id?: number; // 对应的请求 ID
|
||||||
|
type: 'response' | 'event' | 'handshake_response';
|
||||||
|
data?: any; // 响应数据
|
||||||
|
error?: string; // 错误信息
|
||||||
|
success?: boolean; // 操作是否成功
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 消息类型
|
||||||
|
|
||||||
|
**handshake**
|
||||||
|
```javascript
|
||||||
|
// 请求
|
||||||
|
{
|
||||||
|
type: 'handshake',
|
||||||
|
version: string // 扩展版本
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应
|
||||||
|
{
|
||||||
|
type: 'handshake_response',
|
||||||
|
version: string,
|
||||||
|
qwenInstalled: boolean,
|
||||||
|
qwenStatus: string,
|
||||||
|
capabilities: string[]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**start_qwen**
|
||||||
|
```javascript
|
||||||
|
// 请求
|
||||||
|
{
|
||||||
|
type: 'start_qwen',
|
||||||
|
config?: {
|
||||||
|
mcpServers?: string[],
|
||||||
|
httpPort?: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应
|
||||||
|
{
|
||||||
|
type: 'response',
|
||||||
|
id: number,
|
||||||
|
success: boolean,
|
||||||
|
data?: {
|
||||||
|
status: string,
|
||||||
|
pid: number,
|
||||||
|
capabilities: string[]
|
||||||
|
},
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**stop_qwen**
|
||||||
|
```javascript
|
||||||
|
// 请求
|
||||||
|
{
|
||||||
|
type: 'stop_qwen'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应
|
||||||
|
{
|
||||||
|
type: 'response',
|
||||||
|
id: number,
|
||||||
|
success: boolean,
|
||||||
|
data?: string,
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**qwen_request**
|
||||||
|
```javascript
|
||||||
|
// 请求
|
||||||
|
{
|
||||||
|
type: 'qwen_request',
|
||||||
|
action: string,
|
||||||
|
data: any,
|
||||||
|
config?: object
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应
|
||||||
|
{
|
||||||
|
type: 'response',
|
||||||
|
id: number,
|
||||||
|
data?: any,
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**get_status**
|
||||||
|
```javascript
|
||||||
|
// 请求
|
||||||
|
{
|
||||||
|
type: 'get_status'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应
|
||||||
|
{
|
||||||
|
type: 'response',
|
||||||
|
id: number,
|
||||||
|
data: {
|
||||||
|
qwenInstalled: boolean,
|
||||||
|
qwenStatus: string,
|
||||||
|
qwenPid: number | null,
|
||||||
|
capabilities: string[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 事件消息
|
||||||
|
|
||||||
|
**qwen_output**
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
type: 'event',
|
||||||
|
data: {
|
||||||
|
type: 'qwen_output',
|
||||||
|
content: string // stdout 输出
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**qwen_error**
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
type: 'event',
|
||||||
|
data: {
|
||||||
|
type: 'qwen_error',
|
||||||
|
content: string // stderr 输出
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**qwen_stopped**
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
type: 'event',
|
||||||
|
data: {
|
||||||
|
type: 'qwen_stopped',
|
||||||
|
code: number // 退出码
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Qwen CLI 集成
|
||||||
|
|
||||||
|
### HTTP API 端点
|
||||||
|
|
||||||
|
**POST /api/process**
|
||||||
|
```javascript
|
||||||
|
// 请求
|
||||||
|
{
|
||||||
|
action: string,
|
||||||
|
data: any
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应
|
||||||
|
{
|
||||||
|
success: boolean,
|
||||||
|
result?: any,
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 支持的动作
|
||||||
|
|
||||||
|
| 动作 | 描述 | 输入数据 | 返回数据 |
|
||||||
|
|------|------|---------|---------|
|
||||||
|
| `analyze_page` | 分析网页内容 | PageData | 分析结果 |
|
||||||
|
| `analyze_screenshot` | 分析截图 | { screenshot: string, url: string } | 图片分析结果 |
|
||||||
|
| `ai_analyze` | AI 深度分析 | { pageData: PageData, prompt: string } | AI 分析结果 |
|
||||||
|
| `process_text` | 处理文本 | { text: string, context: string } | 处理后的文本 |
|
||||||
|
|
||||||
|
## Chrome Storage API
|
||||||
|
|
||||||
|
### 配置存储
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 保存配置
|
||||||
|
await chrome.storage.local.set({
|
||||||
|
mcpServers: 'chrome-devtools,playwright',
|
||||||
|
httpPort: 8080,
|
||||||
|
autoConnect: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// 读取配置
|
||||||
|
const settings = await chrome.storage.local.get([
|
||||||
|
'mcpServers',
|
||||||
|
'httpPort',
|
||||||
|
'autoConnect'
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 存储结构
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface StorageSchema {
|
||||||
|
mcpServers?: string; // 逗号分隔的服务器列表
|
||||||
|
httpPort?: number; // HTTP 端口
|
||||||
|
autoConnect?: boolean; // 是否自动连接
|
||||||
|
lastConnected?: string; // 最后连接时间
|
||||||
|
extensionVersion?: string; // 扩展版本
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 错误代码
|
||||||
|
|
||||||
|
| 错误代码 | 描述 | 处理建议 |
|
||||||
|
|----------|------|----------|
|
||||||
|
| `NATIVE_HOST_NOT_FOUND` | Native Host 未安装 | 运行安装脚本 |
|
||||||
|
| `QWEN_NOT_INSTALLED` | Qwen CLI 未安装 | 安装 Qwen CLI |
|
||||||
|
| `CONNECTION_FAILED` | 连接失败 | 检查 Native Host |
|
||||||
|
| `PROCESS_START_FAILED` | 进程启动失败 | 检查 Qwen CLI 配置 |
|
||||||
|
| `REQUEST_TIMEOUT` | 请求超时 | 重试请求 |
|
||||||
|
| `INVALID_MESSAGE` | 消息格式错误 | 检查消息格式 |
|
||||||
|
| `PERMISSION_DENIED` | 权限不足 | 检查扩展权限 |
|
||||||
|
| `PORT_IN_USE` | 端口被占用 | 更换端口 |
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
### 基本使用流程
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 1. 连接到 Native Host
|
||||||
|
const connectResponse = await chrome.runtime.sendMessage({
|
||||||
|
type: 'CONNECT'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!connectResponse.success) {
|
||||||
|
console.error('连接失败:', connectResponse.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 启动 Qwen CLI
|
||||||
|
const startResponse = await chrome.runtime.sendMessage({
|
||||||
|
type: 'START_QWEN_CLI',
|
||||||
|
config: {
|
||||||
|
mcpServers: ['chrome-devtools-mcp'],
|
||||||
|
httpPort: 8080
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. 提取页面数据
|
||||||
|
const pageDataResponse = await chrome.runtime.sendMessage({
|
||||||
|
type: 'EXTRACT_PAGE_DATA'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. 发送给 Qwen 分析
|
||||||
|
const analysisResponse = await chrome.runtime.sendMessage({
|
||||||
|
type: 'SEND_TO_QWEN',
|
||||||
|
action: 'analyze_page',
|
||||||
|
data: pageDataResponse.data
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('分析结果:', analysisResponse.data);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 高级功能示例
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 监听 Qwen 事件
|
||||||
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
|
if (message.type === 'QWEN_EVENT') {
|
||||||
|
console.log('Qwen 事件:', message.event);
|
||||||
|
|
||||||
|
switch (message.event.type) {
|
||||||
|
case 'qwen_output':
|
||||||
|
// 处理输出
|
||||||
|
updateUI(message.event.content);
|
||||||
|
break;
|
||||||
|
case 'qwen_error':
|
||||||
|
// 处理错误
|
||||||
|
showError(message.event.content);
|
||||||
|
break;
|
||||||
|
case 'qwen_stopped':
|
||||||
|
// 处理停止
|
||||||
|
handleStop(message.event.code);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 版本兼容性
|
||||||
|
|
||||||
|
| 组件 | 最低版本 | 推荐版本 |
|
||||||
|
|------|---------|---------|
|
||||||
|
| Chrome | 88 | 最新稳定版 |
|
||||||
|
| Node.js | 14.0.0 | 18+ |
|
||||||
|
| Qwen CLI | 1.0.0 | 最新版 |
|
||||||
|
| Manifest | V3 | V3 |
|
||||||
|
|
||||||
|
## 性能指标
|
||||||
|
|
||||||
|
| 操作 | 预期延迟 | 超时时间 |
|
||||||
|
|------|---------|---------|
|
||||||
|
| Native Host 连接 | <100ms | 5s |
|
||||||
|
| Qwen CLI 启动 | <2s | 10s |
|
||||||
|
| 页面数据提取 | <500ms | 5s |
|
||||||
|
| 截图捕获 | <1s | 5s |
|
||||||
|
| AI 分析请求 | <5s | 30s |
|
||||||
|
| 消息往返 | <50ms | 1s |
|
||||||
361
packages/chrome-qwen-bridge/docs/architecture.md
Normal file
361
packages/chrome-qwen-bridge/docs/architecture.md
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
# Chrome Qwen Bridge 架构设计文档
|
||||||
|
|
||||||
|
## 1. 项目概述
|
||||||
|
|
||||||
|
### 1.1 背景与需求
|
||||||
|
|
||||||
|
基于与 Kimi 的技术讨论,我们需要实现一个 Chrome 插件,能够:
|
||||||
|
- 将浏览器中的数据(DOM、网络请求、Console日志等)透传给 Qwen CLI
|
||||||
|
- 让 Qwen CLI 能够利用 AI 能力分析网页内容
|
||||||
|
- 支持 MCP(Model Context Protocol)服务器集成
|
||||||
|
- 实现浏览器与本地 CLI 的双向通信
|
||||||
|
|
||||||
|
### 1.2 技术约束
|
||||||
|
|
||||||
|
根据浏览器安全模型的限制:
|
||||||
|
- **浏览器无法直接启动本地进程**:Chrome 插件运行在沙箱环境中
|
||||||
|
- **无法直接调用 Node.js API**:插件无法访问文件系统或执行系统命令
|
||||||
|
- **跨域限制**:需要遵守 CORS 策略
|
||||||
|
|
||||||
|
### 1.3 解决方案选择
|
||||||
|
|
||||||
|
经过评估,我们选择了 **Native Messaging** 方案:
|
||||||
|
|
||||||
|
| 方案 | 优点 | 缺点 | 选择理由 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| Native Messaging | - Chrome 官方推荐<br>- 无需开放端口<br>- 安全性高<br>- 可自动启动进程 | - 需要首次手动安装<br>- 平台相关配置 | ✅ 官方标准,安全可靠 |
|
||||||
|
| HTTP Server | - 安装简单<br>- 跨平台统一 | - 需要占用端口<br>- 无法自动启动<br>- CORS 问题 | ❌ 用户体验较差 |
|
||||||
|
| 文件轮询 | - 实现简单 | - 性能差<br>- 实时性差<br>- 不适合生产 | ❌ 仅适合调试 |
|
||||||
|
|
||||||
|
## 2. 系统架构
|
||||||
|
|
||||||
|
### 2.1 整体架构图
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Chrome Browser │
|
||||||
|
│ │
|
||||||
|
│ ┌────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Chrome Extension │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
|
||||||
|
│ │ │Content Script│ │Service Worker│ │ Popup UI │ │ │
|
||||||
|
│ │ │ │◄─►│ │◄─►│ │ │ │
|
||||||
|
│ │ │ - DOM提取 │ │ - 消息路由 │ │ - 用户交互 │ │ │
|
||||||
|
│ │ │ - 事件监听 │ │ - 连接管理 │ │ - 状态显示 │ │ │
|
||||||
|
│ │ │ - JS执行 │ │ - 请求处理 │ │ - 配置管理 │ │ │
|
||||||
|
│ │ └─────────────┘ └──────┬───────┘ └──────────────┘ │ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ └───────────────────────────┼────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
└──────────────────────────────┼───────────────────────────────┘
|
||||||
|
│
|
||||||
|
Native Messaging API
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌───────────────────────────────────────────────────────────────┐
|
||||||
|
│ Native Host (Node.js) │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────────┐ ┌──────────────┐ ┌────────────────┐ │
|
||||||
|
│ │ Message Handler │ │Process Manager│ │ HTTP Client │ │
|
||||||
|
│ │ │◄─►│ │◄─►│ │ │
|
||||||
|
│ │ - JSON-RPC │ │ - spawn() │ │ - REST API │ │
|
||||||
|
│ │ - 协议转换 │ │ - 生命周期 │ │ - WebSocket │ │
|
||||||
|
│ │ - 错误处理 │ │ - 日志管理 │ │ - 状态同步 │ │
|
||||||
|
│ └──────────────────┘ └──────┬───────┘ └────────┬───────┘ │
|
||||||
|
│ │ │ │
|
||||||
|
└────────────────────────────────┼────────────────────┼─────────┘
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
┌───────────────────────────────────────────────────────────────┐
|
||||||
|
│ Qwen CLI │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────────┐ ┌──────────────┐ ┌────────────────┐ │
|
||||||
|
│ │ CLI Process │ │ MCP Manager │ │ AI Engine │ │
|
||||||
|
│ │ │◄─►│ │◄─►│ │ │
|
||||||
|
│ │ - 命令解析 │ │ - 服务注册 │ │ - 内容分析 │ │
|
||||||
|
│ │ - HTTP Server │ │ - 协议适配 │ │ - 智能处理 │ │
|
||||||
|
│ │ - WebSocket │ │ - 工具调用 │ │ - 结果返回 │ │
|
||||||
|
│ └──────────────────┘ └──────────────┘ └────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ MCP Servers │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ chrome-devtools-mcp │ playwright-mcp │ custom-mcp ... │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────┘ │
|
||||||
|
└───────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 组件职责
|
||||||
|
|
||||||
|
#### 2.2.1 Chrome Extension 层
|
||||||
|
|
||||||
|
**Content Script (`content-script.js`)**
|
||||||
|
- 注入到每个网页中运行
|
||||||
|
- 提取 DOM 结构、文本内容
|
||||||
|
- 监听 Console 日志
|
||||||
|
- 执行页面内 JavaScript
|
||||||
|
- 捕获用户选择的文本
|
||||||
|
|
||||||
|
**Service Worker (`service-worker.js`)**
|
||||||
|
- 管理 Native Messaging 连接
|
||||||
|
- 路由消息between组件
|
||||||
|
- 管理扩展生命周期
|
||||||
|
- 处理网络请求监控(通过 Debugger API)
|
||||||
|
|
||||||
|
**Popup UI (`popup.html/js/css`)**
|
||||||
|
- 提供用户界面
|
||||||
|
- 显示连接状态
|
||||||
|
- 触发各种操作
|
||||||
|
- 管理配置选项
|
||||||
|
|
||||||
|
#### 2.2.2 Native Host 层
|
||||||
|
|
||||||
|
**Message Handler**
|
||||||
|
- 实现 Native Messaging 协议
|
||||||
|
- 4字节长度前缀 + JSON 消息
|
||||||
|
- 双向消息队列管理
|
||||||
|
- 错误处理与重试机制
|
||||||
|
|
||||||
|
**Process Manager**
|
||||||
|
- 使用 `child_process.spawn()` 启动 Qwen CLI
|
||||||
|
- 管理进程生命周期
|
||||||
|
- 监控进程输出
|
||||||
|
- 优雅关闭处理
|
||||||
|
|
||||||
|
**HTTP Client**
|
||||||
|
- 与 Qwen CLI HTTP 服务通信
|
||||||
|
- 支持 REST API 调用
|
||||||
|
- WebSocket 连接管理(预留)
|
||||||
|
|
||||||
|
#### 2.2.3 Qwen CLI 层
|
||||||
|
|
||||||
|
- 接收并处理来自插件的请求
|
||||||
|
- 管理 MCP 服务器
|
||||||
|
- 调用 AI 模型分析内容
|
||||||
|
- 返回处理结果
|
||||||
|
|
||||||
|
## 3. 数据流设计
|
||||||
|
|
||||||
|
### 3.1 消息流向
|
||||||
|
|
||||||
|
```
|
||||||
|
用户操作 → Popup UI → Service Worker → Native Host → Qwen CLI → AI/MCP
|
||||||
|
↓
|
||||||
|
用户界面 ← Popup UI ← Service Worker ← Native Host ← 响应结果
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 消息格式
|
||||||
|
|
||||||
|
#### Chrome Extension ↔ Native Host
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface Message {
|
||||||
|
id: number; // 请求ID,用于匹配响应
|
||||||
|
type: string; // 消息类型
|
||||||
|
action?: string; // 具体动作
|
||||||
|
data?: any; // 携带数据
|
||||||
|
error?: string; // 错误信息
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
示例消息:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "qwen_request",
|
||||||
|
"action": "analyze_page",
|
||||||
|
"data": {
|
||||||
|
"url": "https://example.com",
|
||||||
|
"content": "...",
|
||||||
|
"metadata": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Native Host ↔ Qwen CLI
|
||||||
|
|
||||||
|
使用 HTTP POST 请求:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": "analyze",
|
||||||
|
"data": {
|
||||||
|
"type": "webpage",
|
||||||
|
"content": "...",
|
||||||
|
"prompt": "分析这个网页的主要内容"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 状态管理
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
enum ConnectionState {
|
||||||
|
DISCONNECTED = 'disconnected',
|
||||||
|
CONNECTING = 'connecting',
|
||||||
|
CONNECTED = 'connected',
|
||||||
|
RUNNING = 'running',
|
||||||
|
ERROR = 'error'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 安全设计
|
||||||
|
|
||||||
|
### 4.1 权限控制
|
||||||
|
|
||||||
|
**Chrome Extension 权限**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"permissions": [
|
||||||
|
"nativeMessaging", // Native Host 通信
|
||||||
|
"activeTab", // 当前标签页访问
|
||||||
|
"storage", // 配置存储
|
||||||
|
"debugger" // 网络请求监控
|
||||||
|
],
|
||||||
|
"host_permissions": [
|
||||||
|
"<all_urls>" // 所有网站(可根据需要限制)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 安全措施
|
||||||
|
|
||||||
|
1. **Native Messaging 安全**
|
||||||
|
- 只允许特定扩展 ID 访问
|
||||||
|
- Manifest 文件明确指定路径
|
||||||
|
- 系统级权限保护
|
||||||
|
|
||||||
|
2. **数据安全**
|
||||||
|
- 所有通信都在本地进行
|
||||||
|
- 不存储敏感信息
|
||||||
|
- 内容大小限制(防止内存溢出)
|
||||||
|
|
||||||
|
3. **进程安全**
|
||||||
|
- 子进程权限继承用户权限
|
||||||
|
- 无法执行系统级操作
|
||||||
|
- 自动清理僵尸进程
|
||||||
|
|
||||||
|
## 5. 性能优化
|
||||||
|
|
||||||
|
### 5.1 数据传输优化
|
||||||
|
|
||||||
|
- **内容截断**:限制提取内容大小(50KB文本,30KB Markdown)
|
||||||
|
- **懒加载**:只在需要时提取数据
|
||||||
|
- **缓存机制**:缓存 Console 日志(最多100条)
|
||||||
|
|
||||||
|
### 5.2 进程管理优化
|
||||||
|
|
||||||
|
- **连接池**:复用 Native Messaging 连接
|
||||||
|
- **超时控制**:30秒请求超时
|
||||||
|
- **批量处理**:合并多个小请求
|
||||||
|
|
||||||
|
## 6. 错误处理
|
||||||
|
|
||||||
|
### 6.1 错误类型
|
||||||
|
|
||||||
|
| 错误类型 | 处理策略 | 用户提示 |
|
||||||
|
|---------|---------|---------|
|
||||||
|
| Native Host 未安装 | 引导安装 | "请先安装 Native Host" |
|
||||||
|
| Qwen CLI 未安装 | 继续运行,功能受限 | "Qwen CLI 未安装,部分功能不可用" |
|
||||||
|
| 连接断开 | 自动重连(3次) | "连接断开,正在重连..." |
|
||||||
|
| 请求超时 | 返回超时错误 | "请求超时,请重试" |
|
||||||
|
| 进程崩溃 | 清理并重启 | "Qwen CLI 异常退出" |
|
||||||
|
|
||||||
|
### 6.2 日志记录
|
||||||
|
|
||||||
|
- **Chrome Extension**:使用 `console.log`,可在扩展背景页查看
|
||||||
|
- **Native Host**:写入文件
|
||||||
|
- macOS/Linux: `/tmp/qwen-bridge-host.log`
|
||||||
|
- Windows: `%TEMP%\qwen-bridge-host.log`
|
||||||
|
|
||||||
|
## 7. 扩展性设计
|
||||||
|
|
||||||
|
### 7.1 MCP 服务器扩展
|
||||||
|
|
||||||
|
支持动态添加 MCP 服务器:
|
||||||
|
```javascript
|
||||||
|
// 配置新的 MCP 服务器
|
||||||
|
const mcpServers = [
|
||||||
|
'chrome-devtools-mcp',
|
||||||
|
'playwright-mcp',
|
||||||
|
'custom-mcp' // 自定义服务器
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 动作扩展
|
||||||
|
|
||||||
|
易于添加新的处理动作:
|
||||||
|
```javascript
|
||||||
|
const actions = {
|
||||||
|
'analyze_page': analyzePageHandler,
|
||||||
|
'process_text': processTextHandler,
|
||||||
|
'custom_action': customHandler // 自定义动作
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.3 通信协议扩展
|
||||||
|
|
||||||
|
预留 WebSocket 支持:
|
||||||
|
```javascript
|
||||||
|
// 未来可以升级为 WebSocket
|
||||||
|
if (config.useWebSocket) {
|
||||||
|
return new WebSocketConnection(url);
|
||||||
|
} else {
|
||||||
|
return new HTTPConnection(url);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. 部署架构
|
||||||
|
|
||||||
|
### 8.1 开发环境
|
||||||
|
|
||||||
|
```
|
||||||
|
开发者机器
|
||||||
|
├── Chrome (Developer Mode)
|
||||||
|
├── Node.js 环境
|
||||||
|
├── Qwen CLI (本地安装)
|
||||||
|
└── MCP 服务器(可选)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 用户环境
|
||||||
|
|
||||||
|
```
|
||||||
|
用户机器
|
||||||
|
├── Chrome 浏览器
|
||||||
|
├── Chrome Extension (从商店或本地加载)
|
||||||
|
├── Native Host (一次性安装)
|
||||||
|
├── Node.js 运行时
|
||||||
|
└── Qwen CLI (用户安装)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. 技术栈
|
||||||
|
|
||||||
|
- **前端**:原生 JavaScript (ES6+)
|
||||||
|
- **UI**:HTML5 + CSS3 (渐变设计)
|
||||||
|
- **后端**:Node.js (Native Host)
|
||||||
|
- **通信**:Native Messaging + HTTP
|
||||||
|
- **进程管理**:child_process
|
||||||
|
- **协议**:JSON-RPC 风格
|
||||||
|
|
||||||
|
## 10. 未来展望
|
||||||
|
|
||||||
|
### 10.1 短期优化
|
||||||
|
- 添加 TypeScript 支持
|
||||||
|
- 实现 WebSocket 实时通信
|
||||||
|
- 优化 UI/UX 设计
|
||||||
|
- 添加单元测试
|
||||||
|
|
||||||
|
### 10.2 长期规划
|
||||||
|
- 支持更多浏览器(Firefox、Edge)
|
||||||
|
- 开发配套的 VS Code 插件
|
||||||
|
- 实现云端同步功能
|
||||||
|
- 支持批量网页处理
|
||||||
|
|
||||||
|
## 附录:关键决策记录
|
||||||
|
|
||||||
|
| 决策点 | 选择 | 理由 |
|
||||||
|
|--------|------|------|
|
||||||
|
| 通信方式 | Native Messaging | Chrome 官方推荐,安全可靠 |
|
||||||
|
| 进程管理 | child_process.spawn | 灵活控制,支持流式输出 |
|
||||||
|
| UI 框架 | 原生 JavaScript | 减少依赖,快速加载 |
|
||||||
|
| 消息格式 | JSON | 通用性好,易于调试 |
|
||||||
|
| MCP 集成 | HTTP Transport | 简单可靠,易于实现 |
|
||||||
295
packages/chrome-qwen-bridge/docs/debugging.md
Normal file
295
packages/chrome-qwen-bridge/docs/debugging.md
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
# Chrome Qwen Bridge 调试指南
|
||||||
|
|
||||||
|
## 🚀 快速开始调试
|
||||||
|
|
||||||
|
### 一键启动(推荐)
|
||||||
|
|
||||||
|
最简单的方式是使用我们提供的一键启动脚本:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 进入项目目录
|
||||||
|
cd packages/chrome-qwen-bridge
|
||||||
|
|
||||||
|
# 方式一:使用 npm 脚本(跨平台)
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# 方式二:使用 shell 脚本(macOS/Linux)
|
||||||
|
npm run dev:quick
|
||||||
|
# 或直接运行
|
||||||
|
./start.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**脚本会自动完成以下操作:**
|
||||||
|
1. ✅ 检查并配置 Chrome
|
||||||
|
2. ✅ 安装 Native Host
|
||||||
|
3. ✅ 检查 Qwen CLI
|
||||||
|
4. ✅ 启动 Qwen 服务器(端口 8080)
|
||||||
|
5. ✅ 启动测试页面服务器(端口 3000)
|
||||||
|
6. ✅ 启动 Chrome 并加载插件
|
||||||
|
7. ✅ 自动打开 DevTools
|
||||||
|
|
||||||
|
## 📝 可用的 npm 命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 开发调试
|
||||||
|
npm run dev # 完整的开发环境启动(Node.js 脚本)
|
||||||
|
npm run dev:quick # 快速启动(Shell 脚本)
|
||||||
|
npm run dev:stop # 停止所有服务
|
||||||
|
npm run dev:chrome # 仅启动 Chrome 加载插件
|
||||||
|
npm run dev:server # 仅启动 Qwen 服务器
|
||||||
|
|
||||||
|
# 安装配置
|
||||||
|
npm run install:host # 安装 Native Host 依赖
|
||||||
|
npm run install:host:macos # macOS 安装 Native Host
|
||||||
|
npm run install:host:windows # Windows 安装 Native Host
|
||||||
|
|
||||||
|
# 构建打包
|
||||||
|
npm run build # 构建项目
|
||||||
|
npm run package # 打包扩展为 zip
|
||||||
|
npm run package:source # 打包源代码
|
||||||
|
|
||||||
|
# 日志查看
|
||||||
|
npm run logs # 查看 Native Host 日志
|
||||||
|
npm run logs:qwen # 查看 Qwen 服务器日志
|
||||||
|
|
||||||
|
# 清理
|
||||||
|
npm run clean # 清理构建文件和日志
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 手动调试步骤
|
||||||
|
|
||||||
|
如果自动脚本有问题,可以手动进行调试:
|
||||||
|
|
||||||
|
### 步骤 1:安装 Native Host
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# macOS/Linux
|
||||||
|
cd native-host
|
||||||
|
./install.sh
|
||||||
|
|
||||||
|
# Windows(管理员权限)
|
||||||
|
cd native-host
|
||||||
|
install.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤 2:启动 Qwen 服务器(可选)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 如果安装了 Qwen CLI
|
||||||
|
qwen server --port 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤 3:加载插件到 Chrome
|
||||||
|
|
||||||
|
1. 打开 Chrome
|
||||||
|
2. 访问 `chrome://extensions/`
|
||||||
|
3. 开启「开发者模式」
|
||||||
|
4. 点击「加载已解压的扩展程序」
|
||||||
|
5. 选择 `packages/chrome-qwen-bridge/extension` 目录
|
||||||
|
|
||||||
|
### 步骤 4:测试插件
|
||||||
|
|
||||||
|
1. 打开任意网页(或访问 http://localhost:3000)
|
||||||
|
2. 点击工具栏中的插件图标
|
||||||
|
3. 点击「Connect to Qwen CLI」
|
||||||
|
4. 测试各项功能
|
||||||
|
|
||||||
|
## 🐛 调试技巧
|
||||||
|
|
||||||
|
### 1. Chrome DevTools
|
||||||
|
|
||||||
|
#### Service Worker (Background Script)
|
||||||
|
- 打开 `chrome://extensions/`
|
||||||
|
- 找到 Qwen CLI Bridge
|
||||||
|
- 点击「Service Worker」链接
|
||||||
|
- 在打开的 DevTools 中查看日志
|
||||||
|
|
||||||
|
#### Content Script
|
||||||
|
- 在任意网页上右键 → 检查
|
||||||
|
- 在 Console 中查看 content script 的日志
|
||||||
|
- 使用 Sources 面板设置断点
|
||||||
|
|
||||||
|
#### Popup
|
||||||
|
- 右键点击插件图标
|
||||||
|
- 选择「检查弹出内容」
|
||||||
|
- 在 DevTools 中调试 popup 代码
|
||||||
|
|
||||||
|
### 2. Native Host 调试
|
||||||
|
|
||||||
|
查看 Native Host 日志:
|
||||||
|
```bash
|
||||||
|
# macOS/Linux
|
||||||
|
tail -f /tmp/qwen-bridge-host.log
|
||||||
|
|
||||||
|
# 或使用 npm 命令
|
||||||
|
npm run logs
|
||||||
|
```
|
||||||
|
|
||||||
|
测试 Native Host 连接:
|
||||||
|
```javascript
|
||||||
|
// 在 Service Worker console 中执行
|
||||||
|
chrome.runtime.sendNativeMessage('com.qwen.cli.bridge',
|
||||||
|
{type: 'handshake', version: '1.0.0'},
|
||||||
|
response => console.log('Native Host response:', response)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 消息调试
|
||||||
|
|
||||||
|
在 Service Worker 中添加日志:
|
||||||
|
```javascript
|
||||||
|
// background/service-worker.js
|
||||||
|
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||||
|
console.log('Message received:', request, 'from:', sender);
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 网络请求调试
|
||||||
|
|
||||||
|
使用 Chrome DevTools Network 面板:
|
||||||
|
- 查看与 Qwen 服务器的 HTTP 通信
|
||||||
|
- 检查请求/响应头和内容
|
||||||
|
- 查看请求时间
|
||||||
|
|
||||||
|
## 🔍 常见问题排查
|
||||||
|
|
||||||
|
### 问题:Native Host 连接失败
|
||||||
|
|
||||||
|
**症状**:点击「Connect」后显示连接错误
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
1. 检查 Native Host 是否正确安装:
|
||||||
|
```bash
|
||||||
|
# macOS
|
||||||
|
ls ~/Library/Application\ Support/Google/Chrome/NativeMessagingHosts/
|
||||||
|
|
||||||
|
# Linux
|
||||||
|
ls ~/.config/google-chrome/NativeMessagingHosts/
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 验证 manifest.json 中的路径是否正确
|
||||||
|
3. 确保 host.js 有执行权限:
|
||||||
|
```bash
|
||||||
|
chmod +x native-host/host.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题:Qwen CLI 未响应
|
||||||
|
|
||||||
|
**症状**:显示 Qwen CLI 未安装或无响应
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
1. 确认 Qwen CLI 已安装:
|
||||||
|
```bash
|
||||||
|
qwen --version
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 手动启动 Qwen 服务器:
|
||||||
|
```bash
|
||||||
|
qwen server --port 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 检查端口是否被占用:
|
||||||
|
```bash
|
||||||
|
lsof -i:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题:插件图标不显示
|
||||||
|
|
||||||
|
**症状**:加载插件后工具栏没有图标
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
1. 点击 Chrome 扩展图标(拼图图标)
|
||||||
|
2. 找到「Qwen CLI Bridge」
|
||||||
|
3. 点击固定图标
|
||||||
|
|
||||||
|
### 问题:Content Script 未注入
|
||||||
|
|
||||||
|
**症状**:提取页面数据失败
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
1. 刷新目标网页
|
||||||
|
2. 检查 manifest.json 的 content_scripts 配置
|
||||||
|
3. 确认网页不是 Chrome 内部页面(chrome://)
|
||||||
|
|
||||||
|
## 📊 性能分析
|
||||||
|
|
||||||
|
### Memory 分析
|
||||||
|
1. 打开 Chrome Task Manager(Shift + Esc)
|
||||||
|
2. 查看扩展的内存使用
|
||||||
|
3. 使用 DevTools Memory Profiler
|
||||||
|
|
||||||
|
### Performance 分析
|
||||||
|
1. 在 DevTools 中打开 Performance 面板
|
||||||
|
2. 记录操作过程
|
||||||
|
3. 分析瓶颈
|
||||||
|
|
||||||
|
## 🔄 热重载开发
|
||||||
|
|
||||||
|
虽然 Chrome Extension 不支持真正的热重载,但可以:
|
||||||
|
|
||||||
|
1. **快速重载扩展**:
|
||||||
|
- 在 `chrome://extensions/` 点击重载按钮
|
||||||
|
- 或使用快捷键:Cmd+R (macOS) / Ctrl+R (Windows/Linux)
|
||||||
|
|
||||||
|
2. **自动重载 Content Script**:
|
||||||
|
修改代码后刷新网页即可
|
||||||
|
|
||||||
|
3. **保持 Qwen 服务器运行**:
|
||||||
|
Qwen 服务器不需要重启,只需重载扩展
|
||||||
|
|
||||||
|
## 📱 远程调试
|
||||||
|
|
||||||
|
如果需要在其他设备上调试:
|
||||||
|
|
||||||
|
1. **启用远程调试**:
|
||||||
|
```bash
|
||||||
|
google-chrome --remote-debugging-port=9222
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **访问调试界面**:
|
||||||
|
```
|
||||||
|
http://localhost:9222
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **使用 Chrome DevTools Protocol**:
|
||||||
|
可以编程控制和调试
|
||||||
|
|
||||||
|
## 💡 开发建议
|
||||||
|
|
||||||
|
1. **使用 console.log 大量输出日志**
|
||||||
|
- 在开发阶段多打日志
|
||||||
|
- 生产环境再移除
|
||||||
|
|
||||||
|
2. **利用 Chrome Storage API 存储调试信息**
|
||||||
|
```javascript
|
||||||
|
chrome.storage.local.set({debug: data});
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **创建测试页面**
|
||||||
|
- 包含各种测试场景
|
||||||
|
- 方便重复测试
|
||||||
|
|
||||||
|
4. **使用 Postman 测试 API**
|
||||||
|
- 测试与 Qwen 服务器的通信
|
||||||
|
- 验证数据格式
|
||||||
|
|
||||||
|
## 📚 相关资源
|
||||||
|
|
||||||
|
- [Chrome Extension 开发文档](https://developer.chrome.com/docs/extensions/mv3/)
|
||||||
|
- [Native Messaging 文档](https://developer.chrome.com/docs/apps/nativeMessaging/)
|
||||||
|
- [Chrome DevTools 文档](https://developer.chrome.com/docs/devtools/)
|
||||||
|
- [项目 API 参考](./api-reference.md)
|
||||||
|
|
||||||
|
## 🆘 获取帮助
|
||||||
|
|
||||||
|
如果遇到问题:
|
||||||
|
|
||||||
|
1. 查看 [技术细节文档](./technical-details.md)
|
||||||
|
2. 检查 [API 参考文档](./api-reference.md)
|
||||||
|
3. 提交 Issue 到 GitHub
|
||||||
|
4. 查看日志文件寻找错误信息
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
祝调试愉快!🎉
|
||||||
280
packages/chrome-qwen-bridge/docs/implementation-plan.md
Normal file
280
packages/chrome-qwen-bridge/docs/implementation-plan.md
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
# Chrome Qwen Bridge 实施计划
|
||||||
|
|
||||||
|
## 项目背景
|
||||||
|
|
||||||
|
基于用户需求和技术调研,需要开发一个 Chrome 插件,实现浏览器与 Qwen CLI 之间的数据桥接,让 AI 能够分析和处理网页内容。
|
||||||
|
|
||||||
|
## 实施阶段
|
||||||
|
|
||||||
|
### 第一阶段:基础架构搭建(已完成 ✅)
|
||||||
|
|
||||||
|
#### 1.1 Chrome 插件基础结构
|
||||||
|
- ✅ 创建项目目录结构
|
||||||
|
- ✅ 配置 manifest.json (Manifest V3)
|
||||||
|
- ✅ 设置必要的权限和配置
|
||||||
|
|
||||||
|
#### 1.2 核心组件开发
|
||||||
|
- ✅ **Background Service Worker**
|
||||||
|
- 实现消息路由
|
||||||
|
- 管理 Native Messaging 连接
|
||||||
|
- 处理扩展生命周期
|
||||||
|
|
||||||
|
- ✅ **Content Script**
|
||||||
|
- DOM 内容提取
|
||||||
|
- Console 日志拦截
|
||||||
|
- 页面事件监听
|
||||||
|
- HTML 转 Markdown 转换器
|
||||||
|
|
||||||
|
- ✅ **Popup UI**
|
||||||
|
- 用户界面设计(渐变主题)
|
||||||
|
- 状态指示器
|
||||||
|
- 操作按钮组
|
||||||
|
- 响应结果展示
|
||||||
|
- 设置管理
|
||||||
|
|
||||||
|
#### 1.3 功能实现清单
|
||||||
|
|
||||||
|
| 功能模块 | 具体功能 | 状态 |
|
||||||
|
|---------|---------|------|
|
||||||
|
| **数据提取** | | |
|
||||||
|
| | 提取页面文本内容 | ✅ |
|
||||||
|
| | 提取页面 HTML | ✅ |
|
||||||
|
| | 转换为 Markdown | ✅ |
|
||||||
|
| | 提取链接列表 | ✅ |
|
||||||
|
| | 提取图片信息 | ✅ |
|
||||||
|
| | 提取表单结构 | ✅ |
|
||||||
|
| | 提取元数据 | ✅ |
|
||||||
|
| **监控功能** | | |
|
||||||
|
| | Console 日志捕获 | ✅ |
|
||||||
|
| | 网络请求监控 | ✅ |
|
||||||
|
| | 性能指标收集 | ✅ |
|
||||||
|
| **交互功能** | | |
|
||||||
|
| | 截图捕获 | ✅ |
|
||||||
|
| | 选中文本获取 | ✅ |
|
||||||
|
| | 元素高亮 | ✅ |
|
||||||
|
| | 执行 JavaScript | ✅ |
|
||||||
|
| | 页面滚动控制 | ✅ |
|
||||||
|
|
||||||
|
### 第二阶段:Native Messaging 实现(已完成 ✅)
|
||||||
|
|
||||||
|
#### 2.1 Native Host 开发
|
||||||
|
- ✅ **host.js 核心脚本**
|
||||||
|
- Native Messaging 协议实现
|
||||||
|
- 4字节长度前缀处理
|
||||||
|
- JSON 消息解析
|
||||||
|
- 双向通信管道
|
||||||
|
|
||||||
|
#### 2.2 进程管理
|
||||||
|
- ✅ Qwen CLI 进程启动/停止
|
||||||
|
- ✅ 进程状态监控
|
||||||
|
- ✅ 输出流捕获
|
||||||
|
- ✅ 错误处理
|
||||||
|
- ✅ 优雅退出机制
|
||||||
|
|
||||||
|
#### 2.3 安装脚本
|
||||||
|
- ✅ macOS/Linux 安装脚本 (`install.sh`)
|
||||||
|
- ✅ Windows 安装脚本 (`install.bat`)
|
||||||
|
- ✅ Manifest 文件生成
|
||||||
|
- ✅ 权限配置
|
||||||
|
|
||||||
|
### 第三阶段:Qwen CLI 集成(已完成 ✅)
|
||||||
|
|
||||||
|
#### 3.1 通信实现
|
||||||
|
- ✅ HTTP 请求封装
|
||||||
|
- ✅ MCP 服务器配置
|
||||||
|
- ✅ 动态端口管理
|
||||||
|
- ✅ 错误重试机制
|
||||||
|
|
||||||
|
#### 3.2 MCP 服务器支持
|
||||||
|
```javascript
|
||||||
|
// 支持的 MCP 服务器配置
|
||||||
|
const mcpServers = [
|
||||||
|
'chrome-devtools-mcp', // Chrome 开发工具
|
||||||
|
'playwright-mcp', // 浏览器自动化
|
||||||
|
'custom-mcp' // 自定义服务器
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
### 第四阶段:项目集成(已完成 ✅)
|
||||||
|
|
||||||
|
#### 4.1 Mono Repo 集成
|
||||||
|
- ✅ 移动到 packages 目录
|
||||||
|
- ✅ 配置 package.json
|
||||||
|
- ✅ 添加 TypeScript 配置
|
||||||
|
- ✅ 创建构建脚本
|
||||||
|
- ✅ 配置 .gitignore
|
||||||
|
|
||||||
|
#### 4.2 文档编写
|
||||||
|
- ✅ README 主文档
|
||||||
|
- ✅ 架构设计文档
|
||||||
|
- ✅ 实施计划文档(本文档)
|
||||||
|
- 🔄 技术细节文档
|
||||||
|
- 🔄 API 参考文档
|
||||||
|
|
||||||
|
## 技术栈选择
|
||||||
|
|
||||||
|
| 层次 | 技术选择 | 选择理由 |
|
||||||
|
|------|---------|----------|
|
||||||
|
| **Chrome Extension** | | |
|
||||||
|
| 开发语言 | JavaScript (ES6+) | 原生支持,无需构建 |
|
||||||
|
| UI 框架 | 原生 HTML/CSS | 轻量快速,无依赖 |
|
||||||
|
| 消息传递 | Chrome Extension API | 官方标准 |
|
||||||
|
| **Native Host** | | |
|
||||||
|
| 运行时 | Node.js | 跨平台,生态丰富 |
|
||||||
|
| 进程管理 | child_process | Node.js 内置 |
|
||||||
|
| **通信协议** | | |
|
||||||
|
| Extension ↔ Host | Native Messaging | Chrome 官方推荐 |
|
||||||
|
| Host ↔ Qwen | HTTP/REST | 简单可靠 |
|
||||||
|
| 数据格式 | JSON | 通用性好 |
|
||||||
|
|
||||||
|
## 实现细节
|
||||||
|
|
||||||
|
### Native Messaging 协议实现
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 发送消息(4字节长度前缀 + JSON)
|
||||||
|
function sendMessage(message) {
|
||||||
|
const buffer = Buffer.from(JSON.stringify(message));
|
||||||
|
const length = Buffer.allocUnsafe(4);
|
||||||
|
length.writeUInt32LE(buffer.length, 0);
|
||||||
|
|
||||||
|
process.stdout.write(length);
|
||||||
|
process.stdout.write(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 接收消息
|
||||||
|
function readMessages() {
|
||||||
|
let messageLength = null;
|
||||||
|
let chunks = [];
|
||||||
|
|
||||||
|
process.stdin.on('readable', () => {
|
||||||
|
// 读取长度前缀
|
||||||
|
// 读取消息内容
|
||||||
|
// 处理消息
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 进程启动命令
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 启动 Qwen CLI 的完整命令
|
||||||
|
const command = [
|
||||||
|
// 添加 MCP 服务器
|
||||||
|
'qwen mcp add --transport http chrome-devtools http://localhost:8080/mcp',
|
||||||
|
'&&',
|
||||||
|
// 启动 CLI 服务器
|
||||||
|
'qwen server --port 8080'
|
||||||
|
].join(' ');
|
||||||
|
|
||||||
|
spawn(command, { shell: true });
|
||||||
|
```
|
||||||
|
|
||||||
|
## 测试计划
|
||||||
|
|
||||||
|
### 单元测试
|
||||||
|
- [ ] Message Handler 测试
|
||||||
|
- [ ] 数据提取功能测试
|
||||||
|
- [ ] 进程管理测试
|
||||||
|
|
||||||
|
### 集成测试
|
||||||
|
- [ ] Extension ↔ Native Host 通信
|
||||||
|
- [ ] Native Host ↔ Qwen CLI 通信
|
||||||
|
- [ ] 端到端数据流测试
|
||||||
|
|
||||||
|
### 用户测试
|
||||||
|
- [ ] 安装流程测试
|
||||||
|
- [ ] 功能完整性测试
|
||||||
|
- [ ] 错误恢复测试
|
||||||
|
- [ ] 性能测试
|
||||||
|
|
||||||
|
## 部署计划
|
||||||
|
|
||||||
|
### 开发环境部署
|
||||||
|
1. Clone 代码库
|
||||||
|
2. 加载未打包的扩展
|
||||||
|
3. 运行安装脚本
|
||||||
|
4. 测试功能
|
||||||
|
|
||||||
|
### 生产环境部署
|
||||||
|
1. 构建扩展包
|
||||||
|
2. 提交到 Chrome Web Store(可选)
|
||||||
|
3. 提供安装指南
|
||||||
|
4. 用户支持文档
|
||||||
|
|
||||||
|
## 时间线(已完成)
|
||||||
|
|
||||||
|
| 阶段 | 任务 | 预计时间 | 实际状态 |
|
||||||
|
|------|------|---------|----------|
|
||||||
|
| 第一阶段 | 基础架构 | 2小时 | ✅ 完成 |
|
||||||
|
| 第二阶段 | Native Host | 2小时 | ✅ 完成 |
|
||||||
|
| 第三阶段 | Qwen 集成 | 1小时 | ✅ 完成 |
|
||||||
|
| 第四阶段 | 项目集成 | 1小时 | ✅ 完成 |
|
||||||
|
| 第五阶段 | 测试优化 | 2小时 | 🔄 进行中 |
|
||||||
|
|
||||||
|
## 风险评估
|
||||||
|
|
||||||
|
| 风险项 | 可能性 | 影响 | 缓解措施 |
|
||||||
|
|--------|-------|------|----------|
|
||||||
|
| Native Host 安装失败 | 中 | 高 | 提供详细文档和脚本 |
|
||||||
|
| Qwen CLI 未安装 | 高 | 中 | 优雅降级,提示用户 |
|
||||||
|
| 权限不足 | 低 | 高 | 明确权限要求 |
|
||||||
|
| 性能问题 | 中 | 中 | 数据大小限制 |
|
||||||
|
| 兼容性问题 | 低 | 中 | 多平台测试 |
|
||||||
|
|
||||||
|
## 优化计划
|
||||||
|
|
||||||
|
### 短期优化(1-2周)
|
||||||
|
- 添加 TypeScript 类型定义
|
||||||
|
- 实现 WebSocket 通信
|
||||||
|
- 优化错误提示
|
||||||
|
- 添加更多 MCP 服务器
|
||||||
|
|
||||||
|
### 中期优化(1-2月)
|
||||||
|
- 开发选项页面
|
||||||
|
- 实现配置同步
|
||||||
|
- 添加快捷键支持
|
||||||
|
- 国际化支持
|
||||||
|
|
||||||
|
### 长期优化(3-6月)
|
||||||
|
- 支持 Firefox/Edge
|
||||||
|
- 云端配置同步
|
||||||
|
- 批量处理模式
|
||||||
|
- AI 模型选择
|
||||||
|
|
||||||
|
## 维护计划
|
||||||
|
|
||||||
|
### 日常维护
|
||||||
|
- Bug 修复
|
||||||
|
- 安全更新
|
||||||
|
- 依赖升级
|
||||||
|
|
||||||
|
### 版本发布
|
||||||
|
- 遵循语义化版本
|
||||||
|
- 维护 CHANGELOG
|
||||||
|
- 发布说明
|
||||||
|
|
||||||
|
### 用户支持
|
||||||
|
- GitHub Issues
|
||||||
|
- 文档更新
|
||||||
|
- FAQ 维护
|
||||||
|
|
||||||
|
## 成功指标
|
||||||
|
|
||||||
|
- ✅ 成功实现浏览器与 Qwen CLI 通信
|
||||||
|
- ✅ 支持主要数据提取功能
|
||||||
|
- ✅ 稳定的进程管理
|
||||||
|
- ✅ 良好的用户体验
|
||||||
|
- 🔄 完善的文档
|
||||||
|
- 🔄 社区反馈收集
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
项目已成功完成核心功能开发,实现了:
|
||||||
|
1. Chrome 插件与本地 Qwen CLI 的桥接
|
||||||
|
2. 丰富的数据提取和监控功能
|
||||||
|
3. 安全可靠的 Native Messaging 通信
|
||||||
|
4. 灵活的 MCP 服务器集成
|
||||||
|
5. 跨平台支持
|
||||||
|
|
||||||
|
下一步将重点优化用户体验和完善文档。
|
||||||
534
packages/chrome-qwen-bridge/docs/technical-details.md
Normal file
534
packages/chrome-qwen-bridge/docs/technical-details.md
Normal file
@@ -0,0 +1,534 @@
|
|||||||
|
# Chrome Qwen Bridge 技术细节文档
|
||||||
|
|
||||||
|
## Native Messaging 协议详解
|
||||||
|
|
||||||
|
### 协议规范
|
||||||
|
|
||||||
|
Chrome 的 Native Messaging 使用简单的基于消息长度的协议:
|
||||||
|
|
||||||
|
```
|
||||||
|
[4字节长度][JSON消息内容]
|
||||||
|
```
|
||||||
|
|
||||||
|
- **长度前缀**:32位无符号整数,小端字节序
|
||||||
|
- **消息内容**:UTF-8 编码的 JSON 字符串
|
||||||
|
- **最大消息大小**:1MB (Chrome 限制)
|
||||||
|
|
||||||
|
### 实现细节
|
||||||
|
|
||||||
|
#### 消息发送实现
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function sendMessage(message) {
|
||||||
|
// 1. 将消息对象转换为 JSON 字符串
|
||||||
|
const jsonString = JSON.stringify(message);
|
||||||
|
|
||||||
|
// 2. 转换为 Buffer
|
||||||
|
const buffer = Buffer.from(jsonString, 'utf8');
|
||||||
|
|
||||||
|
// 3. 创建 4 字节的长度前缀
|
||||||
|
const lengthBuffer = Buffer.allocUnsafe(4);
|
||||||
|
lengthBuffer.writeUInt32LE(buffer.length, 0);
|
||||||
|
|
||||||
|
// 4. 写入 stdout
|
||||||
|
process.stdout.write(lengthBuffer);
|
||||||
|
process.stdout.write(buffer);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 消息接收实现
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function readMessages() {
|
||||||
|
let messageLength = null;
|
||||||
|
let chunks = [];
|
||||||
|
|
||||||
|
process.stdin.on('readable', () => {
|
||||||
|
let chunk;
|
||||||
|
|
||||||
|
while ((chunk = process.stdin.read()) !== null) {
|
||||||
|
chunks.push(chunk);
|
||||||
|
const buffer = Buffer.concat(chunks);
|
||||||
|
|
||||||
|
// 第一步:读取消息长度
|
||||||
|
if (messageLength === null) {
|
||||||
|
if (buffer.length >= 4) {
|
||||||
|
messageLength = buffer.readUInt32LE(0);
|
||||||
|
chunks = [buffer.slice(4)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第二步:读取消息内容
|
||||||
|
if (messageLength !== null) {
|
||||||
|
const fullBuffer = Buffer.concat(chunks);
|
||||||
|
|
||||||
|
if (fullBuffer.length >= messageLength) {
|
||||||
|
const messageBuffer = fullBuffer.slice(0, messageLength);
|
||||||
|
const message = JSON.parse(messageBuffer.toString('utf8'));
|
||||||
|
|
||||||
|
// 重置状态,准备读取下一条消息
|
||||||
|
chunks = [fullBuffer.slice(messageLength)];
|
||||||
|
messageLength = null;
|
||||||
|
|
||||||
|
// 处理消息
|
||||||
|
handleMessage(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 错误处理
|
||||||
|
|
||||||
|
1. **JSON 解析错误**:发送错误响应
|
||||||
|
2. **长度溢出**:拒绝超过 1MB 的消息
|
||||||
|
3. **流关闭**:优雅退出进程
|
||||||
|
|
||||||
|
## Chrome Extension API 使用
|
||||||
|
|
||||||
|
### 权限说明
|
||||||
|
|
||||||
|
| 权限 | 用途 | 风险级别 |
|
||||||
|
|------|------|---------|
|
||||||
|
| `nativeMessaging` | 与 Native Host 通信 | 高 |
|
||||||
|
| `activeTab` | 访问当前标签页 | 中 |
|
||||||
|
| `tabs` | 管理标签页 | 中 |
|
||||||
|
| `storage` | 存储配置 | 低 |
|
||||||
|
| `debugger` | 网络监控 | 高 |
|
||||||
|
| `scripting` | 注入脚本 | 高 |
|
||||||
|
| `webNavigation` | 页面导航事件 | 中 |
|
||||||
|
| `cookies` | Cookie 访问 | 中 |
|
||||||
|
|
||||||
|
### Content Script 注入
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// manifest.json 配置
|
||||||
|
{
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"matches": ["<all_urls>"], // 所有网页
|
||||||
|
"js": ["content/content-script.js"],
|
||||||
|
"run_at": "document_idle" // DOM 加载完成后
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Service Worker 生命周期
|
||||||
|
|
||||||
|
Service Worker 在 Manifest V3 中替代了 Background Page:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 扩展安装/更新时
|
||||||
|
chrome.runtime.onInstalled.addListener((details) => {
|
||||||
|
if (details.reason === 'install') {
|
||||||
|
// 首次安装
|
||||||
|
} else if (details.reason === 'update') {
|
||||||
|
// 更新
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Service Worker 可能会被系统终止
|
||||||
|
// 使用 chrome.storage 持久化状态
|
||||||
|
```
|
||||||
|
|
||||||
|
## 数据提取算法
|
||||||
|
|
||||||
|
### DOM 内容提取策略
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function extractPageData() {
|
||||||
|
// 1. 优先查找语义化标签
|
||||||
|
const mainContent = document.querySelector(
|
||||||
|
'article, main, [role="main"], #content, .content'
|
||||||
|
) || document.body;
|
||||||
|
|
||||||
|
// 2. 克隆节点避免修改原始 DOM
|
||||||
|
const clone = mainContent.cloneNode(true);
|
||||||
|
|
||||||
|
// 3. 移除干扰元素
|
||||||
|
const removeSelectors = [
|
||||||
|
'script', 'style', 'noscript', 'iframe',
|
||||||
|
'nav', 'header', 'footer', '.ad', '#ads'
|
||||||
|
];
|
||||||
|
|
||||||
|
removeSelectors.forEach(selector => {
|
||||||
|
clone.querySelectorAll(selector).forEach(el => el.remove());
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. 提取文本内容
|
||||||
|
return clone.textContent.trim();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### HTML 转 Markdown 算法
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function htmlToMarkdown(element) {
|
||||||
|
const rules = {
|
||||||
|
'h1': (node) => `# ${node.textContent}\n`,
|
||||||
|
'h2': (node) => `## ${node.textContent}\n`,
|
||||||
|
'h3': (node) => `### ${node.textContent}\n`,
|
||||||
|
'p': (node) => `${node.textContent}\n\n`,
|
||||||
|
'a': (node) => `[${node.textContent}](${node.href})`,
|
||||||
|
'img': (node) => ``,
|
||||||
|
'ul,ol': (node) => processLi",
|
||||||
|
'code': (node) => `\`${node.textContent}\``,
|
||||||
|
'pre': (node) => `\`\`\`\n${node.textContent}\n\`\`\``,
|
||||||
|
'blockquote': (node) => `> ${node.textContent}`,
|
||||||
|
'strong,b': (node) => `**${node.textContent}**`,
|
||||||
|
'em,i': (node) => `*${node.textContent}*`
|
||||||
|
};
|
||||||
|
|
||||||
|
// 递归遍历 DOM 树
|
||||||
|
// 应用转换规则
|
||||||
|
// 返回 Markdown 字符串
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Console 日志拦截
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 保存原始 console 方法
|
||||||
|
const originalConsole = {
|
||||||
|
log: console.log,
|
||||||
|
error: console.error,
|
||||||
|
warn: console.warn,
|
||||||
|
info: console.info
|
||||||
|
};
|
||||||
|
|
||||||
|
// 拦截并记录
|
||||||
|
['log', 'error', 'warn', 'info'].forEach(method => {
|
||||||
|
console[method] = function(...args) {
|
||||||
|
// 记录日志
|
||||||
|
consoleLogs.push({
|
||||||
|
type: method,
|
||||||
|
message: args.map(formatArg).join(' '),
|
||||||
|
timestamp: Date.now(),
|
||||||
|
stack: new Error().stack
|
||||||
|
});
|
||||||
|
|
||||||
|
// 调用原始方法
|
||||||
|
originalConsole[method].apply(console, args);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 进程管理详解
|
||||||
|
|
||||||
|
### Qwen CLI 启动流程
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function startQwenCli(config) {
|
||||||
|
// 1. 构建命令参数
|
||||||
|
const commands = [];
|
||||||
|
|
||||||
|
// 2. 添加 MCP 服务器
|
||||||
|
for (const server of config.mcpServers) {
|
||||||
|
commands.push(
|
||||||
|
`qwen mcp add --transport http ${server} ` +
|
||||||
|
`http://localhost:${config.port}/mcp/${server}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 启动服务器
|
||||||
|
commands.push(`qwen server --port ${config.port}`);
|
||||||
|
|
||||||
|
// 4. 使用 shell 执行复合命令
|
||||||
|
const process = spawn(commands.join(' && '), {
|
||||||
|
shell: true, // 使用 shell 执行
|
||||||
|
detached: false, // 不分离进程
|
||||||
|
windowsHide: true, // Windows 下隐藏窗口
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe']
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5. 监控输出
|
||||||
|
process.stdout.on('data', handleOutput);
|
||||||
|
process.stderr.on('data', handleError);
|
||||||
|
process.on('exit', handleExit);
|
||||||
|
|
||||||
|
return process;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 进程清理
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 优雅关闭
|
||||||
|
function gracefulShutdown() {
|
||||||
|
if (qwenProcess) {
|
||||||
|
// 发送 SIGTERM
|
||||||
|
qwenProcess.kill('SIGTERM');
|
||||||
|
|
||||||
|
// 等待进程退出
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!qwenProcess.killed) {
|
||||||
|
// 强制结束
|
||||||
|
qwenProcess.kill('SIGKILL');
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册清理处理器
|
||||||
|
process.on('SIGINT', gracefulShutdown);
|
||||||
|
process.on('SIGTERM', gracefulShutdown);
|
||||||
|
process.on('exit', gracefulShutdown);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 性能优化技巧
|
||||||
|
|
||||||
|
### 内存管理
|
||||||
|
|
||||||
|
1. **内容大小限制**
|
||||||
|
```javascript
|
||||||
|
const MAX_TEXT_LENGTH = 50000; // 50KB
|
||||||
|
const MAX_HTML_LENGTH = 100000; // 100KB
|
||||||
|
const MAX_LOGS = 100; // 最多 100 条日志
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **防止内存泄漏**
|
||||||
|
```javascript
|
||||||
|
// 使用 WeakMap 存储 DOM 引用
|
||||||
|
const elementCache = new WeakMap();
|
||||||
|
|
||||||
|
// 定期清理
|
||||||
|
setInterval(() => {
|
||||||
|
consoleLogs.splice(0, consoleLogs.length - MAX_LOGS);
|
||||||
|
}, 60000);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应时间优化
|
||||||
|
|
||||||
|
1. **懒加载**
|
||||||
|
```javascript
|
||||||
|
// 只在需要时提取数据
|
||||||
|
async function getPageData() {
|
||||||
|
if (!pageDataCache) {
|
||||||
|
pageDataCache = await extractPageData();
|
||||||
|
}
|
||||||
|
return pageDataCache;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **批处理**
|
||||||
|
```javascript
|
||||||
|
// 合并多个请求
|
||||||
|
const requestQueue = [];
|
||||||
|
const flushQueue = debounce(() => {
|
||||||
|
sendBatchRequest(requestQueue);
|
||||||
|
requestQueue.length = 0;
|
||||||
|
}, 100);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 安全最佳实践
|
||||||
|
|
||||||
|
### 输入验证
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function validateMessage(message) {
|
||||||
|
// 类型检查
|
||||||
|
if (typeof message !== 'object') {
|
||||||
|
throw new Error('Invalid message type');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 必填字段
|
||||||
|
if (!message.type) {
|
||||||
|
throw new Error('Missing message type');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 大小限制
|
||||||
|
const size = JSON.stringify(message).length;
|
||||||
|
if (size > 1024 * 1024) { // 1MB
|
||||||
|
throw new Error('Message too large');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### XSS 防护
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 避免直接插入 HTML
|
||||||
|
function escapeHtml(text) {
|
||||||
|
const map = {
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": '''
|
||||||
|
};
|
||||||
|
return text.replace(/[&<>"']/g, m => map[m]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 textContent 而非 innerHTML
|
||||||
|
element.textContent = userInput; // 安全
|
||||||
|
// element.innerHTML = userInput; // 危险!
|
||||||
|
```
|
||||||
|
|
||||||
|
### CSP (Content Security Policy)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// manifest.json
|
||||||
|
{
|
||||||
|
"content_security_policy": {
|
||||||
|
"extension_pages": "script-src 'self'; object-src 'none'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 调试技巧
|
||||||
|
|
||||||
|
### Chrome Extension 调试
|
||||||
|
|
||||||
|
1. **Background Service Worker**
|
||||||
|
- 打开 `chrome://extensions/`
|
||||||
|
- 点击 "Service Worker" 链接
|
||||||
|
- 使用 Chrome DevTools
|
||||||
|
|
||||||
|
2. **Content Script**
|
||||||
|
- 在网页中打开 DevTools
|
||||||
|
- 在 Console 中查看日志
|
||||||
|
|
||||||
|
3. **Popup**
|
||||||
|
- 右键点击插件图标
|
||||||
|
- 选择 "检查弹出内容"
|
||||||
|
|
||||||
|
### Native Host 调试
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 日志文件
|
||||||
|
const logFile = path.join(os.tmpdir(), 'qwen-bridge-host.log');
|
||||||
|
|
||||||
|
function log(message) {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
fs.appendFileSync(logFile, `[${timestamp}] ${message}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用日志调试
|
||||||
|
log(`Received message: ${JSON.stringify(message)}`);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 常见问题排查
|
||||||
|
|
||||||
|
| 问题 | 可能原因 | 解决方法 |
|
||||||
|
|------|---------|---------|
|
||||||
|
| Native Host 不响应 | 路径配置错误 | 检查 manifest.json 中的路径 |
|
||||||
|
| 消息解析失败 | JSON 格式错误 | 验证消息格式 |
|
||||||
|
| 权限错误 | 权限不足 | 检查 manifest 权限配置 |
|
||||||
|
| 进程启动失败 | Qwen CLI 未安装 | 安装 Qwen CLI |
|
||||||
|
| 内存溢出 | 数据量过大 | 添加大小限制 |
|
||||||
|
|
||||||
|
## 跨平台兼容性
|
||||||
|
|
||||||
|
### 平台差异处理
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 检测操作系统
|
||||||
|
const platform = process.platform;
|
||||||
|
|
||||||
|
// 平台特定路径
|
||||||
|
const paths = {
|
||||||
|
darwin: { // macOS
|
||||||
|
manifest: '~/Library/Application Support/Google/Chrome/NativeMessagingHosts/',
|
||||||
|
log: '/tmp/'
|
||||||
|
},
|
||||||
|
win32: { // Windows
|
||||||
|
manifest: 'HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts\\',
|
||||||
|
log: process.env.TEMP
|
||||||
|
},
|
||||||
|
linux: {
|
||||||
|
manifest: '~/.config/google-chrome/NativeMessagingHosts/',
|
||||||
|
log: '/tmp/'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 使用平台特定配置
|
||||||
|
const config = paths[platform];
|
||||||
|
```
|
||||||
|
|
||||||
|
### Shell 命令兼容性
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Windows 使用 .bat 文件
|
||||||
|
if (platform === 'win32') {
|
||||||
|
// host.bat 包装器
|
||||||
|
spawn('cmd.exe', ['/c', 'host.bat']);
|
||||||
|
} else {
|
||||||
|
// 直接执行
|
||||||
|
spawn('node', ['host.js']);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 性能基准
|
||||||
|
|
||||||
|
### 数据提取性能
|
||||||
|
|
||||||
|
| 操作 | 平均耗时 | 内存占用 |
|
||||||
|
|------|---------|----------|
|
||||||
|
| DOM 提取 | ~50ms | ~2MB |
|
||||||
|
| Markdown 转换 | ~30ms | ~1MB |
|
||||||
|
| 截图捕获 | ~100ms | ~5MB |
|
||||||
|
| Console 日志 | <1ms | ~100KB |
|
||||||
|
|
||||||
|
### 通信延迟
|
||||||
|
|
||||||
|
| 通道 | 延迟 |
|
||||||
|
|------|------|
|
||||||
|
| Content ↔ Background | <1ms |
|
||||||
|
| Extension ↔ Native Host | ~5ms |
|
||||||
|
| Native Host ↔ Qwen CLI | ~10ms |
|
||||||
|
| 端到端 | ~20ms |
|
||||||
|
|
||||||
|
## 未来技术方向
|
||||||
|
|
||||||
|
### WebSocket 支持
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 升级为 WebSocket 连接
|
||||||
|
class WebSocketBridge {
|
||||||
|
constructor(url) {
|
||||||
|
this.ws = new WebSocket(url);
|
||||||
|
this.setupEventHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
send(message) {
|
||||||
|
this.ws.send(JSON.stringify(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
onMessage(callback) {
|
||||||
|
this.ws.on('message', (data) => {
|
||||||
|
callback(JSON.parse(data));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Service Worker 后台任务
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 使用 Alarm API 定期任务
|
||||||
|
chrome.alarms.create('sync', { periodInMinutes: 5 });
|
||||||
|
|
||||||
|
chrome.alarms.onAlarm.addListener((alarm) => {
|
||||||
|
if (alarm.name === 'sync') {
|
||||||
|
syncData();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Web Workers 并行处理
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 在 Web Worker 中处理大量数据
|
||||||
|
const worker = new Worker('processor.js');
|
||||||
|
|
||||||
|
worker.postMessage({ cmd: 'process', data: largeData });
|
||||||
|
|
||||||
|
worker.onmessage = (e) => {
|
||||||
|
const result = e.data;
|
||||||
|
// 处理结果
|
||||||
|
};
|
||||||
|
```
|
||||||
@@ -0,0 +1,366 @@
|
|||||||
|
/**
|
||||||
|
* Background Service Worker for Qwen CLI Bridge
|
||||||
|
* Handles communication between extension components and native host
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Native messaging host name
|
||||||
|
const NATIVE_HOST_NAME = 'com.qwen.cli.bridge';
|
||||||
|
|
||||||
|
// Connection state
|
||||||
|
let nativePort = null;
|
||||||
|
let isConnected = false;
|
||||||
|
let qwenCliStatus = 'disconnected';
|
||||||
|
let pendingRequests = new Map();
|
||||||
|
let requestId = 0;
|
||||||
|
|
||||||
|
// Connection management
|
||||||
|
function connectToNativeHost() {
|
||||||
|
if (nativePort) {
|
||||||
|
return Promise.resolve(nativePort);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
console.log('Attempting to connect to Native Host:', NATIVE_HOST_NAME);
|
||||||
|
nativePort = chrome.runtime.connectNative(NATIVE_HOST_NAME);
|
||||||
|
|
||||||
|
// Check for immediate errors
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
console.error('Chrome runtime error:', chrome.runtime.lastError);
|
||||||
|
reject(new Error(chrome.runtime.lastError.message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
nativePort.onMessage.addListener((message) => {
|
||||||
|
console.log('Native message received:', message);
|
||||||
|
handleNativeMessage(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
nativePort.onDisconnect.addListener(() => {
|
||||||
|
const error = chrome.runtime.lastError;
|
||||||
|
console.log('Native host disconnected');
|
||||||
|
if (error) {
|
||||||
|
console.error('Disconnect error:', error);
|
||||||
|
}
|
||||||
|
nativePort = null;
|
||||||
|
isConnected = false;
|
||||||
|
qwenCliStatus = 'disconnected';
|
||||||
|
|
||||||
|
// Reject all pending requests
|
||||||
|
for (const [id, handler] of pendingRequests) {
|
||||||
|
handler.reject(new Error('Native host disconnected'));
|
||||||
|
}
|
||||||
|
pendingRequests.clear();
|
||||||
|
|
||||||
|
// Notify popup of disconnection
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
type: 'STATUS_UPDATE',
|
||||||
|
status: 'disconnected'
|
||||||
|
}).catch(() => {}); // Ignore errors if popup is closed
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send initial handshake
|
||||||
|
console.log('Sending handshake...');
|
||||||
|
nativePort.postMessage({ type: 'handshake', version: '1.0.0' });
|
||||||
|
|
||||||
|
// Set timeout for handshake response
|
||||||
|
const handshakeTimeout = setTimeout(() => {
|
||||||
|
console.error('Handshake timeout - no response from Native Host');
|
||||||
|
if (nativePort) {
|
||||||
|
nativePort.disconnect();
|
||||||
|
}
|
||||||
|
reject(new Error('Handshake timeout'));
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
// Store timeout so we can clear it when we get response
|
||||||
|
nativePort._handshakeTimeout = handshakeTimeout;
|
||||||
|
|
||||||
|
isConnected = true;
|
||||||
|
qwenCliStatus = 'connected';
|
||||||
|
|
||||||
|
resolve(nativePort);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to connect to native host:', error);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle messages from native host
|
||||||
|
function handleNativeMessage(message) {
|
||||||
|
if (message.type === 'handshake_response') {
|
||||||
|
console.log('Handshake successful:', message);
|
||||||
|
|
||||||
|
// Clear handshake timeout
|
||||||
|
if (nativePort && nativePort._handshakeTimeout) {
|
||||||
|
clearTimeout(nativePort._handshakeTimeout);
|
||||||
|
delete nativePort._handshakeTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
qwenCliStatus = message.qwenStatus || 'connected';
|
||||||
|
|
||||||
|
// Notify popup of connection
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
type: 'STATUS_UPDATE',
|
||||||
|
status: qwenCliStatus,
|
||||||
|
capabilities: message.capabilities
|
||||||
|
}).catch(() => {});
|
||||||
|
} else if (message.type === 'response' && message.id !== undefined) {
|
||||||
|
// Handle response to a specific request
|
||||||
|
const handler = pendingRequests.get(message.id);
|
||||||
|
if (handler) {
|
||||||
|
if (message.error) {
|
||||||
|
handler.reject(new Error(message.error));
|
||||||
|
} else {
|
||||||
|
handler.resolve(message.data);
|
||||||
|
}
|
||||||
|
pendingRequests.delete(message.id);
|
||||||
|
}
|
||||||
|
} else if (message.type === 'event') {
|
||||||
|
// Handle events from Qwen CLI
|
||||||
|
handleQwenEvent(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send request to native host
|
||||||
|
async function sendToNativeHost(message) {
|
||||||
|
if (!nativePort || !isConnected) {
|
||||||
|
await connectToNativeHost();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const id = ++requestId;
|
||||||
|
pendingRequests.set(id, { resolve, reject });
|
||||||
|
|
||||||
|
nativePort.postMessage({
|
||||||
|
...message,
|
||||||
|
id
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set timeout for request
|
||||||
|
setTimeout(() => {
|
||||||
|
if (pendingRequests.has(id)) {
|
||||||
|
pendingRequests.delete(id);
|
||||||
|
reject(new Error('Request timeout'));
|
||||||
|
}
|
||||||
|
}, 30000); // 30 second timeout
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle events from Qwen CLI
|
||||||
|
function handleQwenEvent(event) {
|
||||||
|
console.log('Qwen event:', event);
|
||||||
|
|
||||||
|
// Forward event to content scripts and popup
|
||||||
|
chrome.tabs.query({}, (tabs) => {
|
||||||
|
tabs.forEach(tab => {
|
||||||
|
chrome.tabs.sendMessage(tab.id, {
|
||||||
|
type: 'QWEN_EVENT',
|
||||||
|
event: event.data
|
||||||
|
}).catch(() => {}); // Ignore errors for tabs without content script
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
type: 'QWEN_EVENT',
|
||||||
|
event: event.data
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message handlers from extension components
|
||||||
|
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||||
|
console.log('Message received:', request, 'from:', sender);
|
||||||
|
|
||||||
|
if (request.type === 'CONNECT') {
|
||||||
|
// Connect to native host
|
||||||
|
connectToNativeHost()
|
||||||
|
.then(() => {
|
||||||
|
sendResponse({ success: true, status: qwenCliStatus });
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
sendResponse({ success: false, error: error.message });
|
||||||
|
});
|
||||||
|
return true; // Will respond asynchronously
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.type === 'GET_STATUS') {
|
||||||
|
// Get current connection status
|
||||||
|
sendResponse({
|
||||||
|
connected: isConnected,
|
||||||
|
status: qwenCliStatus
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.type === 'EXTRACT_PAGE_DATA') {
|
||||||
|
// Request page data from content script
|
||||||
|
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
||||||
|
if (tabs[0]) {
|
||||||
|
chrome.tabs.sendMessage(tabs[0].id, {
|
||||||
|
type: 'EXTRACT_DATA'
|
||||||
|
}, (response) => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
sendResponse({
|
||||||
|
success: false,
|
||||||
|
error: chrome.runtime.lastError.message
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
sendResponse(response);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
sendResponse({
|
||||||
|
success: false,
|
||||||
|
error: 'No active tab found'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true; // Will respond asynchronously
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.type === 'SEND_TO_QWEN') {
|
||||||
|
// Send data to Qwen CLI via native host
|
||||||
|
sendToNativeHost({
|
||||||
|
type: 'qwen_request',
|
||||||
|
action: request.action,
|
||||||
|
data: request.data
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
sendResponse({ success: true, data: response });
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
sendResponse({ success: false, error: error.message });
|
||||||
|
});
|
||||||
|
return true; // Will respond asynchronously
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.type === 'START_QWEN_CLI') {
|
||||||
|
// Request native host to start Qwen CLI
|
||||||
|
sendToNativeHost({
|
||||||
|
type: 'start_qwen',
|
||||||
|
config: request.config || {}
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
qwenCliStatus = 'running';
|
||||||
|
sendResponse({ success: true, data: response });
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
sendResponse({ success: false, error: error.message });
|
||||||
|
});
|
||||||
|
return true; // Will respond asynchronously
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.type === 'STOP_QWEN_CLI') {
|
||||||
|
// Request native host to stop Qwen CLI
|
||||||
|
sendToNativeHost({
|
||||||
|
type: 'stop_qwen'
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
qwenCliStatus = 'stopped';
|
||||||
|
sendResponse({ success: true, data: response });
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
sendResponse({ success: false, error: error.message });
|
||||||
|
});
|
||||||
|
return true; // Will respond asynchronously
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.type === 'CAPTURE_SCREENSHOT') {
|
||||||
|
// Capture screenshot of active tab
|
||||||
|
chrome.tabs.captureVisibleTab(null, { format: 'png' }, (dataUrl) => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
sendResponse({
|
||||||
|
success: false,
|
||||||
|
error: chrome.runtime.lastError.message
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
sendResponse({
|
||||||
|
success: true,
|
||||||
|
data: dataUrl
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true; // Will respond asynchronously
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.type === 'GET_NETWORK_LOGS') {
|
||||||
|
// Get network logs (requires debugger API)
|
||||||
|
getNetworkLogs(sender.tab?.id)
|
||||||
|
.then(logs => {
|
||||||
|
sendResponse({ success: true, data: logs });
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
sendResponse({ success: false, error: error.message });
|
||||||
|
});
|
||||||
|
return true; // Will respond asynchronously
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Network logging using debugger API
|
||||||
|
const debuggerTargets = new Map();
|
||||||
|
|
||||||
|
async function getNetworkLogs(tabId) {
|
||||||
|
if (!tabId) {
|
||||||
|
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||||
|
tabId = tabs[0]?.id;
|
||||||
|
if (!tabId) throw new Error('No active tab found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if debugger is already attached
|
||||||
|
if (!debuggerTargets.has(tabId)) {
|
||||||
|
await chrome.debugger.attach({ tabId }, '1.3');
|
||||||
|
await chrome.debugger.sendCommand({ tabId }, 'Network.enable');
|
||||||
|
|
||||||
|
// Store network logs
|
||||||
|
debuggerTargets.set(tabId, { logs: [] });
|
||||||
|
|
||||||
|
// Listen for network events
|
||||||
|
chrome.debugger.onEvent.addListener((source, method, params) => {
|
||||||
|
if (source.tabId === tabId) {
|
||||||
|
const target = debuggerTargets.get(tabId);
|
||||||
|
if (target && method.startsWith('Network.')) {
|
||||||
|
target.logs.push({
|
||||||
|
method,
|
||||||
|
params,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = debuggerTargets.get(tabId);
|
||||||
|
return target?.logs || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up debugger on tab close
|
||||||
|
chrome.tabs.onRemoved.addListener((tabId) => {
|
||||||
|
if (debuggerTargets.has(tabId)) {
|
||||||
|
chrome.debugger.detach({ tabId });
|
||||||
|
debuggerTargets.delete(tabId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for extension installation or update
|
||||||
|
chrome.runtime.onInstalled.addListener((details) => {
|
||||||
|
console.log('Extension installed/updated:', details);
|
||||||
|
|
||||||
|
if (details.reason === 'install') {
|
||||||
|
// Just log the installation, don't auto-open options
|
||||||
|
console.log('Extension installed for the first time');
|
||||||
|
// Users can access options from popup menu
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Open side panel when extension icon is clicked
|
||||||
|
chrome.action.onClicked.addListener((tab) => {
|
||||||
|
chrome.sidePanel.open({ windowId: tab.windowId });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Export for testing
|
||||||
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
|
module.exports = {
|
||||||
|
connectToNativeHost,
|
||||||
|
sendToNativeHost
|
||||||
|
};
|
||||||
|
}
|
||||||
466
packages/chrome-qwen-bridge/extension/content/content-script.js
Normal file
466
packages/chrome-qwen-bridge/extension/content/content-script.js
Normal file
@@ -0,0 +1,466 @@
|
|||||||
|
/**
|
||||||
|
* Content Script for Qwen CLI Bridge
|
||||||
|
* Extracts data from web pages and communicates with background script
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Data extraction functions
|
||||||
|
function extractPageData() {
|
||||||
|
const data = {
|
||||||
|
// Basic page info
|
||||||
|
url: window.location.href,
|
||||||
|
title: document.title,
|
||||||
|
domain: window.location.hostname,
|
||||||
|
path: window.location.pathname,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
|
||||||
|
// Meta information
|
||||||
|
meta: {},
|
||||||
|
|
||||||
|
// Page content
|
||||||
|
content: {
|
||||||
|
text: '',
|
||||||
|
html: '',
|
||||||
|
markdown: ''
|
||||||
|
},
|
||||||
|
|
||||||
|
// Structured data
|
||||||
|
links: [],
|
||||||
|
images: [],
|
||||||
|
forms: [],
|
||||||
|
|
||||||
|
// Console logs
|
||||||
|
consoleLogs: [],
|
||||||
|
|
||||||
|
// Performance metrics
|
||||||
|
performance: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extract meta tags
|
||||||
|
document.querySelectorAll('meta').forEach(meta => {
|
||||||
|
const name = meta.getAttribute('name') || meta.getAttribute('property');
|
||||||
|
const content = meta.getAttribute('content');
|
||||||
|
if (name && content) {
|
||||||
|
data.meta[name] = content;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extract main content (try to find article or main element first)
|
||||||
|
const mainContent = document.querySelector('article, main, [role="main"]') || document.body;
|
||||||
|
data.content.text = extractTextContent(mainContent);
|
||||||
|
data.content.html = mainContent.innerHTML;
|
||||||
|
data.content.markdown = htmlToMarkdown(mainContent);
|
||||||
|
|
||||||
|
// Extract all links
|
||||||
|
document.querySelectorAll('a[href]').forEach(link => {
|
||||||
|
data.links.push({
|
||||||
|
text: link.textContent.trim(),
|
||||||
|
href: link.href,
|
||||||
|
target: link.target,
|
||||||
|
isExternal: isExternalLink(link.href)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extract all images
|
||||||
|
document.querySelectorAll('img').forEach(img => {
|
||||||
|
data.images.push({
|
||||||
|
src: img.src,
|
||||||
|
alt: img.alt,
|
||||||
|
title: img.title,
|
||||||
|
width: img.naturalWidth,
|
||||||
|
height: img.naturalHeight
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extract form data (structure only, no sensitive data)
|
||||||
|
document.querySelectorAll('form').forEach(form => {
|
||||||
|
const formData = {
|
||||||
|
action: form.action,
|
||||||
|
method: form.method,
|
||||||
|
fields: []
|
||||||
|
};
|
||||||
|
|
||||||
|
form.querySelectorAll('input, textarea, select').forEach(field => {
|
||||||
|
formData.fields.push({
|
||||||
|
type: field.type || field.tagName.toLowerCase(),
|
||||||
|
name: field.name,
|
||||||
|
id: field.id,
|
||||||
|
placeholder: field.placeholder,
|
||||||
|
required: field.required
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
data.forms.push(formData);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get performance metrics
|
||||||
|
if (window.performance && window.performance.timing) {
|
||||||
|
const perf = window.performance.timing;
|
||||||
|
data.performance = {
|
||||||
|
loadTime: perf.loadEventEnd - perf.navigationStart,
|
||||||
|
domReady: perf.domContentLoadedEventEnd - perf.navigationStart,
|
||||||
|
firstPaint: getFirstPaintTime()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract clean text content
|
||||||
|
function extractTextContent(element) {
|
||||||
|
// Clone the element to avoid modifying the original
|
||||||
|
const clone = element.cloneNode(true);
|
||||||
|
|
||||||
|
// Remove script and style elements
|
||||||
|
clone.querySelectorAll('script, style, noscript').forEach(el => el.remove());
|
||||||
|
|
||||||
|
// Get text content and clean it up
|
||||||
|
let text = clone.textContent || '';
|
||||||
|
|
||||||
|
// Remove excessive whitespace
|
||||||
|
text = text.replace(/\s+/g, ' ').trim();
|
||||||
|
|
||||||
|
// Limit length to prevent excessive data
|
||||||
|
const maxLength = 50000; // 50KB limit
|
||||||
|
if (text.length > maxLength) {
|
||||||
|
text = text.substring(0, maxLength) + '...';
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple HTML to Markdown converter
|
||||||
|
function htmlToMarkdown(element) {
|
||||||
|
const clone = element.cloneNode(true);
|
||||||
|
|
||||||
|
// Remove script and style elements
|
||||||
|
clone.querySelectorAll('script, style, noscript').forEach(el => el.remove());
|
||||||
|
|
||||||
|
let markdown = '';
|
||||||
|
const walker = document.createTreeWalker(
|
||||||
|
clone,
|
||||||
|
NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT,
|
||||||
|
null,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
let node;
|
||||||
|
let listStack = [];
|
||||||
|
|
||||||
|
while (node = walker.nextNode()) {
|
||||||
|
if (node.nodeType === Node.TEXT_NODE) {
|
||||||
|
const text = node.textContent.trim();
|
||||||
|
if (text) {
|
||||||
|
markdown += text + ' ';
|
||||||
|
}
|
||||||
|
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
||||||
|
switch (node.tagName.toLowerCase()) {
|
||||||
|
case 'h1':
|
||||||
|
markdown += '\n# ' + node.textContent.trim() + '\n';
|
||||||
|
break;
|
||||||
|
case 'h2':
|
||||||
|
markdown += '\n## ' + node.textContent.trim() + '\n';
|
||||||
|
break;
|
||||||
|
case 'h3':
|
||||||
|
markdown += '\n### ' + node.textContent.trim() + '\n';
|
||||||
|
break;
|
||||||
|
case 'h4':
|
||||||
|
markdown += '\n#### ' + node.textContent.trim() + '\n';
|
||||||
|
break;
|
||||||
|
case 'h5':
|
||||||
|
markdown += '\n##### ' + node.textContent.trim() + '\n';
|
||||||
|
break;
|
||||||
|
case 'h6':
|
||||||
|
markdown += '\n###### ' + node.textContent.trim() + '\n';
|
||||||
|
break;
|
||||||
|
case 'p':
|
||||||
|
markdown += '\n' + node.textContent.trim() + '\n';
|
||||||
|
break;
|
||||||
|
case 'br':
|
||||||
|
markdown += '\n';
|
||||||
|
break;
|
||||||
|
case 'a':
|
||||||
|
const href = node.getAttribute('href');
|
||||||
|
const text = node.textContent.trim();
|
||||||
|
if (href) {
|
||||||
|
markdown += `[${text}](${href}) `;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'img':
|
||||||
|
const src = node.getAttribute('src');
|
||||||
|
const alt = node.getAttribute('alt') || '';
|
||||||
|
if (src) {
|
||||||
|
markdown += ` `;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'ul':
|
||||||
|
case 'ol':
|
||||||
|
markdown += '\n';
|
||||||
|
listStack.push(node.tagName.toLowerCase());
|
||||||
|
break;
|
||||||
|
case 'li':
|
||||||
|
const listType = listStack[listStack.length - 1];
|
||||||
|
const prefix = listType === 'ol' ? '1. ' : '- ';
|
||||||
|
markdown += prefix + node.textContent.trim() + '\n';
|
||||||
|
break;
|
||||||
|
case 'code':
|
||||||
|
markdown += '`' + node.textContent + '`';
|
||||||
|
break;
|
||||||
|
case 'pre':
|
||||||
|
markdown += '\n```\n' + node.textContent + '\n```\n';
|
||||||
|
break;
|
||||||
|
case 'blockquote':
|
||||||
|
markdown += '\n> ' + node.textContent.trim() + '\n';
|
||||||
|
break;
|
||||||
|
case 'strong':
|
||||||
|
case 'b':
|
||||||
|
markdown += '**' + node.textContent + '**';
|
||||||
|
break;
|
||||||
|
case 'em':
|
||||||
|
case 'i':
|
||||||
|
markdown += '*' + node.textContent + '*';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit markdown length
|
||||||
|
const maxLength = 30000;
|
||||||
|
if (markdown.length > maxLength) {
|
||||||
|
markdown = markdown.substring(0, maxLength) + '...';
|
||||||
|
}
|
||||||
|
|
||||||
|
return markdown.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if link is external
|
||||||
|
function isExternalLink(url) {
|
||||||
|
try {
|
||||||
|
const link = new URL(url);
|
||||||
|
return link.hostname !== window.location.hostname;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get first paint time
|
||||||
|
function getFirstPaintTime() {
|
||||||
|
if (window.performance && window.performance.getEntriesByType) {
|
||||||
|
const paintEntries = window.performance.getEntriesByType('paint');
|
||||||
|
const firstPaint = paintEntries.find(entry => entry.name === 'first-paint');
|
||||||
|
return firstPaint ? firstPaint.startTime : null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Console log interceptor
|
||||||
|
const consoleLogs = [];
|
||||||
|
const originalConsole = {
|
||||||
|
log: console.log,
|
||||||
|
error: console.error,
|
||||||
|
warn: console.warn,
|
||||||
|
info: console.info
|
||||||
|
};
|
||||||
|
|
||||||
|
// Intercept console methods
|
||||||
|
['log', 'error', 'warn', 'info'].forEach(method => {
|
||||||
|
console[method] = function(...args) {
|
||||||
|
// Store the log
|
||||||
|
consoleLogs.push({
|
||||||
|
type: method,
|
||||||
|
message: args.map(arg => {
|
||||||
|
try {
|
||||||
|
if (typeof arg === 'object') {
|
||||||
|
return JSON.stringify(arg);
|
||||||
|
}
|
||||||
|
return String(arg);
|
||||||
|
} catch {
|
||||||
|
return String(arg);
|
||||||
|
}
|
||||||
|
}).join(' '),
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
stack: new Error().stack
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep only last 100 logs to prevent memory issues
|
||||||
|
if (consoleLogs.length > 100) {
|
||||||
|
consoleLogs.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call original console method
|
||||||
|
originalConsole[method].apply(console, args);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get selected text
|
||||||
|
function getSelectedText() {
|
||||||
|
return window.getSelection().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Highlight element on page
|
||||||
|
function highlightElement(selector) {
|
||||||
|
try {
|
||||||
|
const element = document.querySelector(selector);
|
||||||
|
if (element) {
|
||||||
|
// Store original style
|
||||||
|
const originalStyle = element.style.cssText;
|
||||||
|
|
||||||
|
// Apply highlight
|
||||||
|
element.style.cssText += `
|
||||||
|
outline: 3px solid #FF6B6B !important;
|
||||||
|
background-color: rgba(255, 107, 107, 0.1) !important;
|
||||||
|
transition: all 0.3s ease !important;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Remove highlight after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
element.style.cssText = originalStyle;
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to highlight element:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute custom JavaScript in page context
|
||||||
|
function executeInPageContext(code) {
|
||||||
|
try {
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.textContent = `
|
||||||
|
(function() {
|
||||||
|
try {
|
||||||
|
const result = ${code};
|
||||||
|
window.postMessage({
|
||||||
|
type: 'QWEN_BRIDGE_RESULT',
|
||||||
|
success: true,
|
||||||
|
result: result
|
||||||
|
}, '*');
|
||||||
|
} catch (error) {
|
||||||
|
window.postMessage({
|
||||||
|
type: 'QWEN_BRIDGE_RESULT',
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
}, '*');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
`;
|
||||||
|
document.documentElement.appendChild(script);
|
||||||
|
script.remove();
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const listener = (event) => {
|
||||||
|
if (event.data && event.data.type === 'QWEN_BRIDGE_RESULT') {
|
||||||
|
window.removeEventListener('message', listener);
|
||||||
|
if (event.data.success) {
|
||||||
|
resolve(event.data.result);
|
||||||
|
} else {
|
||||||
|
reject(new Error(event.data.error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('message', listener);
|
||||||
|
|
||||||
|
// Timeout after 5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
window.removeEventListener('message', listener);
|
||||||
|
reject(new Error('Execution timeout'));
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message listener for communication with background script
|
||||||
|
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||||
|
console.log('Content script received message:', request);
|
||||||
|
|
||||||
|
switch (request.type) {
|
||||||
|
case 'EXTRACT_DATA':
|
||||||
|
// Extract and send page data
|
||||||
|
const pageData = extractPageData();
|
||||||
|
pageData.consoleLogs = consoleLogs;
|
||||||
|
sendResponse({
|
||||||
|
success: true,
|
||||||
|
data: pageData
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'GET_SELECTED_TEXT':
|
||||||
|
// Get currently selected text
|
||||||
|
sendResponse({
|
||||||
|
success: true,
|
||||||
|
data: getSelectedText()
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'HIGHLIGHT_ELEMENT':
|
||||||
|
// Highlight an element on the page
|
||||||
|
const highlighted = highlightElement(request.selector);
|
||||||
|
sendResponse({
|
||||||
|
success: highlighted
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'EXECUTE_CODE':
|
||||||
|
// Execute JavaScript in page context
|
||||||
|
executeInPageContext(request.code)
|
||||||
|
.then(result => {
|
||||||
|
sendResponse({
|
||||||
|
success: true,
|
||||||
|
data: result
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
sendResponse({
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return true; // Will respond asynchronously
|
||||||
|
|
||||||
|
case 'SCROLL_TO':
|
||||||
|
// Scroll to specific position
|
||||||
|
window.scrollTo({
|
||||||
|
top: request.y || 0,
|
||||||
|
left: request.x || 0,
|
||||||
|
behavior: request.smooth ? 'smooth' : 'auto'
|
||||||
|
});
|
||||||
|
sendResponse({ success: true });
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'QWEN_EVENT':
|
||||||
|
// Handle events from Qwen CLI
|
||||||
|
console.log('Qwen event received:', request.event);
|
||||||
|
// Could trigger UI updates or other actions based on event
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
sendResponse({
|
||||||
|
success: false,
|
||||||
|
error: 'Unknown request type'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Notify background script that content script is loaded
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
type: 'CONTENT_SCRIPT_LOADED',
|
||||||
|
url: window.location.href
|
||||||
|
}).catch(() => {
|
||||||
|
// Ignore errors if background script is not ready
|
||||||
|
});
|
||||||
|
|
||||||
|
// Export for testing
|
||||||
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
|
module.exports = {
|
||||||
|
extractPageData,
|
||||||
|
extractTextContent,
|
||||||
|
htmlToMarkdown,
|
||||||
|
getSelectedText,
|
||||||
|
highlightElement
|
||||||
|
};
|
||||||
|
}
|
||||||
10
packages/chrome-qwen-bridge/extension/icons/icon-128.png
Normal file
10
packages/chrome-qwen-bridge/extension/icons/icon-128.png
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
|
||||||
|
<rect width="128" height="128" rx="20" fill="url(#grad)"/>
|
||||||
|
<path d="M32 64 L64 32 L64 48 L96 48 L64 80 L64 64 Z" fill="white" stroke="white" stroke-width="2" stroke-linejoin="round"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#667eea;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#764ba2;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 530 B |
10
packages/chrome-qwen-bridge/extension/icons/icon-16.png
Normal file
10
packages/chrome-qwen-bridge/extension/icons/icon-16.png
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
|
||||||
|
<rect width="128" height="128" rx="20" fill="url(#grad)"/>
|
||||||
|
<path d="M32 64 L64 32 L64 48 L96 48 L64 80 L64 64 Z" fill="white" stroke="white" stroke-width="2" stroke-linejoin="round"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#667eea;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#764ba2;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 530 B |
10
packages/chrome-qwen-bridge/extension/icons/icon-48.png
Normal file
10
packages/chrome-qwen-bridge/extension/icons/icon-48.png
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
|
||||||
|
<rect width="128" height="128" rx="20" fill="url(#grad)"/>
|
||||||
|
<path d="M32 64 L64 32 L64 48 L96 48 L64 80 L64 64 Z" fill="white" stroke="white" stroke-width="2" stroke-linejoin="round"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#667eea;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#764ba2;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 530 B |
10
packages/chrome-qwen-bridge/extension/icons/icon.svg
Normal file
10
packages/chrome-qwen-bridge/extension/icons/icon.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
|
||||||
|
<rect width="128" height="128" rx="20" fill="url(#grad)"/>
|
||||||
|
<path d="M32 64 L64 32 L64 48 L96 48 L64 80 L64 64 Z" fill="white" stroke="white" stroke-width="2" stroke-linejoin="round"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#667eea;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#764ba2;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 530 B |
58
packages/chrome-qwen-bridge/extension/manifest.json
Normal file
58
packages/chrome-qwen-bridge/extension/manifest.json
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"manifest_version": 3,
|
||||||
|
"name": "Qwen CLI Bridge",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Bridge between Chrome browser and Qwen CLI for enhanced AI interactions",
|
||||||
|
|
||||||
|
"permissions": [
|
||||||
|
"activeTab",
|
||||||
|
"tabs",
|
||||||
|
"storage",
|
||||||
|
"nativeMessaging",
|
||||||
|
"debugger",
|
||||||
|
"webNavigation",
|
||||||
|
"scripting",
|
||||||
|
"cookies",
|
||||||
|
"webRequest",
|
||||||
|
"sidePanel"
|
||||||
|
],
|
||||||
|
|
||||||
|
"host_permissions": [
|
||||||
|
"<all_urls>"
|
||||||
|
],
|
||||||
|
|
||||||
|
"background": {
|
||||||
|
"service_worker": "background/service-worker.js"
|
||||||
|
},
|
||||||
|
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"matches": ["<all_urls>"],
|
||||||
|
"js": ["content/content-script.js"],
|
||||||
|
"run_at": "document_idle"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"action": {
|
||||||
|
"default_icon": {
|
||||||
|
"16": "icons/icon-16.png",
|
||||||
|
"48": "icons/icon-48.png",
|
||||||
|
"128": "icons/icon-128.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"side_panel": {
|
||||||
|
"default_path": "sidepanel/sidepanel.html"
|
||||||
|
},
|
||||||
|
|
||||||
|
"options_ui": {
|
||||||
|
"page": "options/options.html",
|
||||||
|
"open_in_tab": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"icons": {
|
||||||
|
"16": "icons/icon-16.png",
|
||||||
|
"48": "icons/icon-48.png",
|
||||||
|
"128": "icons/icon-128.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
217
packages/chrome-qwen-bridge/extension/options/options.html
Normal file
217
packages/chrome-qwen-bridge/extension/options/options.html
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Qwen CLI Bridge - Options</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 40px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0,0,0,0.2);
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
font-size: 2em;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
margin: 30px 0;
|
||||||
|
padding: 25px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section h2 {
|
||||||
|
color: #667eea;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-group {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #444;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"],
|
||||||
|
input[type="number"],
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus,
|
||||||
|
textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"] {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-status {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 15px;
|
||||||
|
color: #4caf50;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-status.show {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box {
|
||||||
|
background: #e3f2fd;
|
||||||
|
border-left: 4px solid #2196f3;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 20px 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box h3 {
|
||||||
|
color: #1976d2;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box p {
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>⚙️ Qwen CLI Bridge Settings</h1>
|
||||||
|
<p class="subtitle">Configure your Chrome extension and Qwen CLI integration</p>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>🔌 Connection Settings</h2>
|
||||||
|
|
||||||
|
<div class="option-group">
|
||||||
|
<label for="httpPort">HTTP Server Port</label>
|
||||||
|
<input type="number" id="httpPort" min="1024" max="65535" value="8080">
|
||||||
|
<p class="help-text">Port for Qwen CLI HTTP server (default: 8080)</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="option-group">
|
||||||
|
<label for="mcpServers">MCP Servers</label>
|
||||||
|
<input type="text" id="mcpServers" placeholder="chrome-devtools,playwright">
|
||||||
|
<p class="help-text">Comma-separated list of MCP servers to load</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="option-group">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="autoConnect">
|
||||||
|
<span>Auto-connect on startup</span>
|
||||||
|
</label>
|
||||||
|
<p class="help-text">Automatically connect to Qwen CLI when opening the popup</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>🎨 Display Settings</h2>
|
||||||
|
|
||||||
|
<div class="option-group">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="showNotifications">
|
||||||
|
<span>Show notifications</span>
|
||||||
|
</label>
|
||||||
|
<p class="help-text">Display desktop notifications for important events</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="option-group">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="debugMode">
|
||||||
|
<span>Debug mode</span>
|
||||||
|
</label>
|
||||||
|
<p class="help-text">Show detailed debug information in console</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<h3>ℹ️ Native Host Status</h3>
|
||||||
|
<p id="nativeHostStatus">Checking...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<h3>📍 Extension ID</h3>
|
||||||
|
<p id="extensionId">Loading...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="saveBtn">Save Settings</button>
|
||||||
|
<span class="save-status" id="saveStatus">✓ Settings saved</span>
|
||||||
|
|
||||||
|
<div style="margin-top: 40px; padding-top: 20px; border-top: 1px solid #e0e0e0;">
|
||||||
|
<p style="text-align: center; color: #999; font-size: 14px;">
|
||||||
|
Qwen CLI Bridge v1.0.0 |
|
||||||
|
<a href="https://github.com/QwenLM/qwen-code" style="color: #667eea;">GitHub</a> |
|
||||||
|
<a href="#" id="helpLink" style="color: #667eea;">Help</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="options.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
80
packages/chrome-qwen-bridge/extension/options/options.js
Normal file
80
packages/chrome-qwen-bridge/extension/options/options.js
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* Options page script for Qwen CLI Bridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Load saved settings
|
||||||
|
async function loadSettings() {
|
||||||
|
const settings = await chrome.storage.local.get([
|
||||||
|
'httpPort',
|
||||||
|
'mcpServers',
|
||||||
|
'autoConnect',
|
||||||
|
'showNotifications',
|
||||||
|
'debugMode'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Set values in form
|
||||||
|
document.getElementById('httpPort').value = settings.httpPort || 8080;
|
||||||
|
document.getElementById('mcpServers').value = settings.mcpServers || '';
|
||||||
|
document.getElementById('autoConnect').checked = settings.autoConnect || false;
|
||||||
|
document.getElementById('showNotifications').checked = settings.showNotifications || false;
|
||||||
|
document.getElementById('debugMode').checked = settings.debugMode || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save settings
|
||||||
|
document.getElementById('saveBtn').addEventListener('click', async () => {
|
||||||
|
const settings = {
|
||||||
|
httpPort: parseInt(document.getElementById('httpPort').value) || 8080,
|
||||||
|
mcpServers: document.getElementById('mcpServers').value,
|
||||||
|
autoConnect: document.getElementById('autoConnect').checked,
|
||||||
|
showNotifications: document.getElementById('showNotifications').checked,
|
||||||
|
debugMode: document.getElementById('debugMode').checked
|
||||||
|
};
|
||||||
|
|
||||||
|
await chrome.storage.local.set(settings);
|
||||||
|
|
||||||
|
// Show saved status
|
||||||
|
const saveStatus = document.getElementById('saveStatus');
|
||||||
|
saveStatus.classList.add('show');
|
||||||
|
setTimeout(() => {
|
||||||
|
saveStatus.classList.remove('show');
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check Native Host status
|
||||||
|
async function checkNativeHostStatus() {
|
||||||
|
try {
|
||||||
|
// Try to send a message to check if Native Host is installed
|
||||||
|
chrome.runtime.sendMessage({ type: 'GET_STATUS' }, (response) => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
document.getElementById('nativeHostStatus').textContent =
|
||||||
|
'❌ Not installed - Please run install script';
|
||||||
|
} else if (response && response.connected) {
|
||||||
|
document.getElementById('nativeHostStatus').textContent =
|
||||||
|
'✅ Connected and running';
|
||||||
|
} else {
|
||||||
|
document.getElementById('nativeHostStatus').textContent =
|
||||||
|
'⚠️ Installed but not connected';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
document.getElementById('nativeHostStatus').textContent =
|
||||||
|
'❌ Error checking status';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show extension ID
|
||||||
|
document.getElementById('extensionId').textContent = chrome.runtime.id;
|
||||||
|
|
||||||
|
// Help link
|
||||||
|
document.getElementById('helpLink').addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
chrome.tabs.create({
|
||||||
|
url: 'https://github.com/QwenLM/qwen-code/tree/main/packages/chrome-qwen-bridge'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
loadSettings();
|
||||||
|
checkNativeHostStatus();
|
||||||
|
});
|
||||||
385
packages/chrome-qwen-bridge/extension/popup/popup.css
Normal file
385
packages/chrome-qwen-bridge/extension/popup/popup.css
Normal file
@@ -0,0 +1,385 @@
|
|||||||
|
/* Popup Styles for Qwen CLI Bridge */
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
width: 400px;
|
||||||
|
min-height: 500px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #333;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background: white;
|
||||||
|
min-height: 500px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.header {
|
||||||
|
padding: 16px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo .icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo h1 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Indicator */
|
||||||
|
.status-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #ff4444;
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator.connected .status-dot {
|
||||||
|
background: #44ff44;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator.connecting .status-dot {
|
||||||
|
background: #ffaa44;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sections */
|
||||||
|
.section {
|
||||||
|
padding: 16px;
|
||||||
|
border-bottom: 1px solid #e5e5e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section:last-of-type {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section h2 {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #666;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover:not(:disabled) {
|
||||||
|
background: #e5e5e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-small {
|
||||||
|
padding: 4px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 4px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
stroke: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Connection Section */
|
||||||
|
.connection-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
background: #fee;
|
||||||
|
color: #c00;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Action Buttons */
|
||||||
|
.action-buttons {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:hover:not(:disabled) {
|
||||||
|
border-color: #667eea;
|
||||||
|
background: #f8f9ff;
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:hover:not(:disabled) .action-icon {
|
||||||
|
stroke: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
stroke: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Response Section */
|
||||||
|
.response-container {
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-header {
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #f0f0f0;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-type {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-content {
|
||||||
|
padding: 12px;
|
||||||
|
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #333;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings Section */
|
||||||
|
.settings-section details {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section summary {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #666;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
padding: 4px 0;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section summary:hover {
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-content {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item input[type="text"],
|
||||||
|
.setting-item input[type="number"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item input[type="checkbox"] {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
.footer {
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-top: 1px solid #e5e5e5;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a {
|
||||||
|
color: #667eea;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version {
|
||||||
|
color: #bbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #888;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading state */
|
||||||
|
.loading {
|
||||||
|
position: relative;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in {
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
140
packages/chrome-qwen-bridge/extension/popup/popup.html
Normal file
140
packages/chrome-qwen-bridge/extension/popup/popup.html
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Qwen CLI Bridge</title>
|
||||||
|
<link rel="stylesheet" href="popup.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="header">
|
||||||
|
<div class="logo">
|
||||||
|
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||||
|
</svg>
|
||||||
|
<h1>Qwen CLI Bridge</h1>
|
||||||
|
</div>
|
||||||
|
<div class="status-indicator" id="statusIndicator">
|
||||||
|
<span class="status-dot"></span>
|
||||||
|
<span class="status-text">Disconnected</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Connection Section -->
|
||||||
|
<section class="section connection-section">
|
||||||
|
<h2>Connection</h2>
|
||||||
|
<div class="connection-controls">
|
||||||
|
<button id="connectBtn" class="btn btn-primary">
|
||||||
|
Connect to Qwen CLI
|
||||||
|
</button>
|
||||||
|
<button id="startQwenBtn" class="btn btn-secondary" disabled>
|
||||||
|
Start Qwen CLI
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="connectionError" class="error-message" style="display: none;"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Actions Section -->
|
||||||
|
<section class="section actions-section">
|
||||||
|
<h2>Quick Actions</h2>
|
||||||
|
<div class="action-buttons">
|
||||||
|
<button id="extractDataBtn" class="action-btn" disabled>
|
||||||
|
<svg class="action-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||||
|
</svg>
|
||||||
|
Extract Page Data
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button id="captureScreenBtn" class="action-btn" disabled>
|
||||||
|
<svg class="action-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
</svg>
|
||||||
|
Capture Screenshot
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button id="analyzePageBtn" class="action-btn" disabled>
|
||||||
|
<svg class="action-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||||||
|
</svg>
|
||||||
|
Analyze with AI
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button id="getSelectedBtn" class="action-btn" disabled>
|
||||||
|
<svg class="action-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
Send Selected Text
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button id="networkLogsBtn" class="action-btn" disabled>
|
||||||
|
<svg class="action-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0" />
|
||||||
|
</svg>
|
||||||
|
Network Logs
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button id="consoleLogsBtn" class="action-btn" disabled>
|
||||||
|
<svg class="action-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
Console Logs
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Response Section -->
|
||||||
|
<section class="section response-section" id="responseSection" style="display: none;">
|
||||||
|
<h2>Response</h2>
|
||||||
|
<div class="response-container">
|
||||||
|
<div class="response-header">
|
||||||
|
<span id="responseType" class="response-type"></span>
|
||||||
|
<button id="copyResponseBtn" class="btn-icon" title="Copy to clipboard">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<pre id="responseContent" class="response-content"></pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Settings Section -->
|
||||||
|
<section class="section settings-section">
|
||||||
|
<details>
|
||||||
|
<summary>Advanced Settings</summary>
|
||||||
|
<div class="settings-content">
|
||||||
|
<div class="setting-item">
|
||||||
|
<label for="mcpServers">MCP Servers:</label>
|
||||||
|
<input type="text" id="mcpServers" placeholder="chrome-devtools,playwright" />
|
||||||
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<label for="httpPort">HTTP Port:</label>
|
||||||
|
<input type="number" id="httpPort" placeholder="8080" value="8080" />
|
||||||
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<label for="autoConnect">
|
||||||
|
<input type="checkbox" id="autoConnect" />
|
||||||
|
Auto-connect on startup
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button id="saveSettingsBtn" class="btn btn-small">Save Settings</button>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="footer">
|
||||||
|
<a href="#" id="openOptionsBtn">Options</a>
|
||||||
|
<span>•</span>
|
||||||
|
<a href="#" id="helpBtn">Help</a>
|
||||||
|
<span>•</span>
|
||||||
|
<span class="version">v1.0.0</span>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="popup.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
477
packages/chrome-qwen-bridge/extension/popup/popup.js
Normal file
477
packages/chrome-qwen-bridge/extension/popup/popup.js
Normal file
@@ -0,0 +1,477 @@
|
|||||||
|
/**
|
||||||
|
* Popup Script for Qwen CLI Bridge
|
||||||
|
* Handles UI interactions and communication with background script
|
||||||
|
*/
|
||||||
|
|
||||||
|
// UI Elements
|
||||||
|
const statusIndicator = document.getElementById('statusIndicator');
|
||||||
|
const statusText = statusIndicator.querySelector('.status-text');
|
||||||
|
const connectBtn = document.getElementById('connectBtn');
|
||||||
|
const startQwenBtn = document.getElementById('startQwenBtn');
|
||||||
|
const connectionError = document.getElementById('connectionError');
|
||||||
|
const responseSection = document.getElementById('responseSection');
|
||||||
|
const responseType = document.getElementById('responseType');
|
||||||
|
const responseContent = document.getElementById('responseContent');
|
||||||
|
const copyResponseBtn = document.getElementById('copyResponseBtn');
|
||||||
|
|
||||||
|
// Action buttons
|
||||||
|
const extractDataBtn = document.getElementById('extractDataBtn');
|
||||||
|
const captureScreenBtn = document.getElementById('captureScreenBtn');
|
||||||
|
const analyzePageBtn = document.getElementById('analyzePageBtn');
|
||||||
|
const getSelectedBtn = document.getElementById('getSelectedBtn');
|
||||||
|
const networkLogsBtn = document.getElementById('networkLogsBtn');
|
||||||
|
const consoleLogsBtn = document.getElementById('consoleLogsBtn');
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
const mcpServersInput = document.getElementById('mcpServers');
|
||||||
|
const httpPortInput = document.getElementById('httpPort');
|
||||||
|
const autoConnectCheckbox = document.getElementById('autoConnect');
|
||||||
|
const saveSettingsBtn = document.getElementById('saveSettingsBtn');
|
||||||
|
|
||||||
|
// Footer links
|
||||||
|
const openOptionsBtn = document.getElementById('openOptionsBtn');
|
||||||
|
const helpBtn = document.getElementById('helpBtn');
|
||||||
|
|
||||||
|
// State
|
||||||
|
let isConnected = false;
|
||||||
|
let qwenStatus = 'disconnected';
|
||||||
|
|
||||||
|
// Initialize popup
|
||||||
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
await loadSettings();
|
||||||
|
await checkConnectionStatus();
|
||||||
|
|
||||||
|
// Auto-connect if enabled
|
||||||
|
const settings = await chrome.storage.local.get(['autoConnect']);
|
||||||
|
if (settings.autoConnect && !isConnected) {
|
||||||
|
connectToQwen();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load saved settings
|
||||||
|
async function loadSettings() {
|
||||||
|
const settings = await chrome.storage.local.get([
|
||||||
|
'mcpServers',
|
||||||
|
'httpPort',
|
||||||
|
'autoConnect'
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (settings.mcpServers) {
|
||||||
|
mcpServersInput.value = settings.mcpServers;
|
||||||
|
}
|
||||||
|
if (settings.httpPort) {
|
||||||
|
httpPortInput.value = settings.httpPort;
|
||||||
|
}
|
||||||
|
if (settings.autoConnect !== undefined) {
|
||||||
|
autoConnectCheckbox.checked = settings.autoConnect;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save settings
|
||||||
|
saveSettingsBtn.addEventListener('click', async () => {
|
||||||
|
await chrome.storage.local.set({
|
||||||
|
mcpServers: mcpServersInput.value,
|
||||||
|
httpPort: parseInt(httpPortInput.value) || 8080,
|
||||||
|
autoConnect: autoConnectCheckbox.checked
|
||||||
|
});
|
||||||
|
|
||||||
|
saveSettingsBtn.textContent = 'Saved!';
|
||||||
|
setTimeout(() => {
|
||||||
|
saveSettingsBtn.textContent = 'Save Settings';
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check connection status
|
||||||
|
async function checkConnectionStatus() {
|
||||||
|
try {
|
||||||
|
const response = await chrome.runtime.sendMessage({ type: 'GET_STATUS' });
|
||||||
|
updateConnectionStatus(response.connected, response.status);
|
||||||
|
} catch (error) {
|
||||||
|
updateConnectionStatus(false, 'disconnected');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update UI based on connection status
|
||||||
|
function updateConnectionStatus(connected, status) {
|
||||||
|
isConnected = connected;
|
||||||
|
qwenStatus = status;
|
||||||
|
|
||||||
|
// Update status indicator
|
||||||
|
statusIndicator.classList.toggle('connected', connected);
|
||||||
|
statusIndicator.classList.toggle('connecting', status === 'connecting');
|
||||||
|
statusText.textContent = getStatusText(status);
|
||||||
|
|
||||||
|
// Update button states
|
||||||
|
connectBtn.textContent = connected ? 'Disconnect' : 'Connect to Qwen CLI';
|
||||||
|
connectBtn.classList.toggle('btn-danger', connected);
|
||||||
|
|
||||||
|
startQwenBtn.disabled = !connected || status === 'running';
|
||||||
|
|
||||||
|
// Enable/disable action buttons
|
||||||
|
const actionButtons = [
|
||||||
|
extractDataBtn,
|
||||||
|
captureScreenBtn,
|
||||||
|
analyzePageBtn,
|
||||||
|
getSelectedBtn,
|
||||||
|
networkLogsBtn,
|
||||||
|
consoleLogsBtn
|
||||||
|
];
|
||||||
|
|
||||||
|
actionButtons.forEach(btn => {
|
||||||
|
btn.disabled = !connected || status !== 'running';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get human-readable status text
|
||||||
|
function getStatusText(status) {
|
||||||
|
switch (status) {
|
||||||
|
case 'connected':
|
||||||
|
return 'Connected';
|
||||||
|
case 'running':
|
||||||
|
return 'Qwen CLI Running';
|
||||||
|
case 'connecting':
|
||||||
|
return 'Connecting...';
|
||||||
|
case 'disconnected':
|
||||||
|
return 'Disconnected';
|
||||||
|
case 'stopped':
|
||||||
|
return 'Qwen CLI Stopped';
|
||||||
|
default:
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect button handler
|
||||||
|
connectBtn.addEventListener('click', () => {
|
||||||
|
if (isConnected) {
|
||||||
|
disconnectFromQwen();
|
||||||
|
} else {
|
||||||
|
connectToQwen();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connect to Qwen CLI
|
||||||
|
async function connectToQwen() {
|
||||||
|
updateConnectionStatus(false, 'connecting');
|
||||||
|
connectionError.style.display = 'none';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await chrome.runtime.sendMessage({ type: 'CONNECT' });
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
updateConnectionStatus(true, response.status);
|
||||||
|
} else {
|
||||||
|
throw new Error(response.error || 'Connection failed');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Connection error:', error);
|
||||||
|
connectionError.textContent = `Error: ${error.message}`;
|
||||||
|
connectionError.style.display = 'block';
|
||||||
|
updateConnectionStatus(false, 'disconnected');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disconnect from Qwen CLI
|
||||||
|
function disconnectFromQwen() {
|
||||||
|
// Simply close the popup to disconnect
|
||||||
|
// The native port will be closed when the extension unloads
|
||||||
|
updateConnectionStatus(false, 'disconnected');
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start Qwen CLI button handler
|
||||||
|
startQwenBtn.addEventListener('click', async () => {
|
||||||
|
startQwenBtn.disabled = true;
|
||||||
|
startQwenBtn.textContent = 'Starting...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const settings = await chrome.storage.local.get(['mcpServers', 'httpPort']);
|
||||||
|
const response = await chrome.runtime.sendMessage({
|
||||||
|
type: 'START_QWEN_CLI',
|
||||||
|
config: {
|
||||||
|
mcpServers: settings.mcpServers ? settings.mcpServers.split(',').map(s => s.trim()) : [],
|
||||||
|
httpPort: settings.httpPort || 8080
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
updateConnectionStatus(true, 'running');
|
||||||
|
showResponse('Qwen CLI Started', response.data || 'Successfully started');
|
||||||
|
} else {
|
||||||
|
throw new Error(response.error || 'Failed to start Qwen CLI');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Start error:', error);
|
||||||
|
connectionError.textContent = `Error: ${error.message}`;
|
||||||
|
connectionError.style.display = 'block';
|
||||||
|
} finally {
|
||||||
|
startQwenBtn.textContent = 'Start Qwen CLI';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extract page data button handler
|
||||||
|
extractDataBtn.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
showLoading('Extracting page data...');
|
||||||
|
|
||||||
|
const response = await chrome.runtime.sendMessage({
|
||||||
|
type: 'EXTRACT_PAGE_DATA'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
// Send to Qwen CLI
|
||||||
|
const qwenResponse = await chrome.runtime.sendMessage({
|
||||||
|
type: 'SEND_TO_QWEN',
|
||||||
|
action: 'analyze_page',
|
||||||
|
data: response.data
|
||||||
|
});
|
||||||
|
|
||||||
|
if (qwenResponse.success) {
|
||||||
|
showResponse('Page Analysis', qwenResponse.data);
|
||||||
|
} else {
|
||||||
|
throw new Error(qwenResponse.error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(response.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showError(`Failed to extract data: ${error.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Capture screenshot button handler
|
||||||
|
captureScreenBtn.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
showLoading('Capturing screenshot...');
|
||||||
|
|
||||||
|
const response = await chrome.runtime.sendMessage({
|
||||||
|
type: 'CAPTURE_SCREENSHOT'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
// Send to Qwen CLI
|
||||||
|
const qwenResponse = await chrome.runtime.sendMessage({
|
||||||
|
type: 'SEND_TO_QWEN',
|
||||||
|
action: 'analyze_screenshot',
|
||||||
|
data: {
|
||||||
|
screenshot: response.data,
|
||||||
|
url: (await chrome.tabs.query({ active: true, currentWindow: true }))[0].url
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (qwenResponse.success) {
|
||||||
|
showResponse('Screenshot Analysis', qwenResponse.data);
|
||||||
|
} else {
|
||||||
|
throw new Error(qwenResponse.error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(response.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showError(`Failed to capture screenshot: ${error.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Analyze page with AI button handler
|
||||||
|
analyzePageBtn.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
showLoading('Analyzing page with AI...');
|
||||||
|
|
||||||
|
// First extract page data
|
||||||
|
const extractResponse = await chrome.runtime.sendMessage({
|
||||||
|
type: 'EXTRACT_PAGE_DATA'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!extractResponse.success) {
|
||||||
|
throw new Error(extractResponse.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send to Qwen for AI analysis
|
||||||
|
const qwenResponse = await chrome.runtime.sendMessage({
|
||||||
|
type: 'SEND_TO_QWEN',
|
||||||
|
action: 'ai_analyze',
|
||||||
|
data: {
|
||||||
|
pageData: extractResponse.data,
|
||||||
|
prompt: 'Please analyze this webpage and provide insights about its content, purpose, and any notable features.'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (qwenResponse.success) {
|
||||||
|
showResponse('AI Analysis', qwenResponse.data);
|
||||||
|
} else {
|
||||||
|
throw new Error(qwenResponse.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showError(`Analysis failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get selected text button handler
|
||||||
|
getSelectedBtn.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
showLoading('Getting selected text...');
|
||||||
|
|
||||||
|
// Get active tab
|
||||||
|
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||||
|
const tab = tabs[0];
|
||||||
|
|
||||||
|
if (!tab) {
|
||||||
|
throw new Error('No active tab found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get selected text from content script
|
||||||
|
const response = await chrome.tabs.sendMessage(tab.id, {
|
||||||
|
type: 'GET_SELECTED_TEXT'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success && response.data) {
|
||||||
|
// Send to Qwen CLI
|
||||||
|
const qwenResponse = await chrome.runtime.sendMessage({
|
||||||
|
type: 'SEND_TO_QWEN',
|
||||||
|
action: 'process_text',
|
||||||
|
data: {
|
||||||
|
text: response.data,
|
||||||
|
context: 'selected_text'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (qwenResponse.success) {
|
||||||
|
showResponse('Selected Text Processed', qwenResponse.data);
|
||||||
|
} else {
|
||||||
|
throw new Error(qwenResponse.error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showError('No text selected. Please select some text on the page first.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showError(`Failed to process selected text: ${error.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Network logs button handler
|
||||||
|
networkLogsBtn.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
showLoading('Getting network logs...');
|
||||||
|
|
||||||
|
const response = await chrome.runtime.sendMessage({
|
||||||
|
type: 'GET_NETWORK_LOGS'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
showResponse('Network Logs', JSON.stringify(response.data, null, 2));
|
||||||
|
} else {
|
||||||
|
throw new Error(response.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showError(`Failed to get network logs: ${error.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Console logs button handler
|
||||||
|
consoleLogsBtn.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
showLoading('Getting console logs...');
|
||||||
|
|
||||||
|
// Get active tab
|
||||||
|
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||||
|
const tab = tabs[0];
|
||||||
|
|
||||||
|
if (!tab) {
|
||||||
|
throw new Error('No active tab found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get console logs from content script
|
||||||
|
const response = await chrome.tabs.sendMessage(tab.id, {
|
||||||
|
type: 'EXTRACT_DATA'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
const consoleLogs = response.data.consoleLogs || [];
|
||||||
|
if (consoleLogs.length > 0) {
|
||||||
|
showResponse('Console Logs', JSON.stringify(consoleLogs, null, 2));
|
||||||
|
} else {
|
||||||
|
showResponse('Console Logs', 'No console logs captured');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(response.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showError(`Failed to get console logs: ${error.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Copy response button handler
|
||||||
|
copyResponseBtn.addEventListener('click', () => {
|
||||||
|
const text = responseContent.textContent;
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
const originalTitle = copyResponseBtn.title;
|
||||||
|
copyResponseBtn.title = 'Copied!';
|
||||||
|
setTimeout(() => {
|
||||||
|
copyResponseBtn.title = originalTitle;
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Footer link handlers
|
||||||
|
openOptionsBtn.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
// Use try-catch to handle potential errors
|
||||||
|
try {
|
||||||
|
chrome.runtime.openOptionsPage(() => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
// If opening options page fails, open it in a new tab as fallback
|
||||||
|
console.error('Error opening options page:', chrome.runtime.lastError);
|
||||||
|
chrome.tabs.create({
|
||||||
|
url: chrome.runtime.getURL('options/options.html')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to open options page:', error);
|
||||||
|
// Fallback: open in new tab
|
||||||
|
chrome.tabs.create({
|
||||||
|
url: chrome.runtime.getURL('options/options.html')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
helpBtn.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
chrome.tabs.create({
|
||||||
|
url: 'https://github.com/QwenLM/qwen-code/tree/main/packages/chrome-qwen-bridge'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
function showLoading(message) {
|
||||||
|
responseSection.style.display = 'block';
|
||||||
|
responseType.textContent = 'Loading';
|
||||||
|
responseContent.textContent = message;
|
||||||
|
responseSection.classList.add('loading');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showResponse(type, content) {
|
||||||
|
responseSection.style.display = 'block';
|
||||||
|
responseType.textContent = type;
|
||||||
|
responseContent.textContent = typeof content === 'string' ? content : JSON.stringify(content, null, 2);
|
||||||
|
responseSection.classList.remove('loading');
|
||||||
|
responseSection.classList.add('fade-in');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showError(message) {
|
||||||
|
responseSection.style.display = 'block';
|
||||||
|
responseType.textContent = 'Error';
|
||||||
|
responseType.style.color = '#c00';
|
||||||
|
responseContent.textContent = message;
|
||||||
|
responseSection.classList.remove('loading');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for status updates from background
|
||||||
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
|
if (message.type === 'STATUS_UPDATE') {
|
||||||
|
updateConnectionStatus(message.status !== 'disconnected', message.status);
|
||||||
|
} else if (message.type === 'QWEN_EVENT') {
|
||||||
|
// Handle events from Qwen CLI
|
||||||
|
console.log('Qwen event received:', message.event);
|
||||||
|
// Could update UI based on event
|
||||||
|
}
|
||||||
|
});
|
||||||
402
packages/chrome-qwen-bridge/extension/sidepanel/sidepanel.css
Normal file
402
packages/chrome-qwen-bridge/extension/sidepanel/sidepanel.css
Normal file
@@ -0,0 +1,402 @@
|
|||||||
|
/* Side Panel Styles for Qwen CLI Bridge */
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #333;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background: white;
|
||||||
|
min-height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.header {
|
||||||
|
padding: 16px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo .icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo h1 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Indicator */
|
||||||
|
.status-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #ff4444;
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator.connected .status-dot {
|
||||||
|
background: #44ff44;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator.connecting .status-dot {
|
||||||
|
background: #ffaa44;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sections */
|
||||||
|
.section {
|
||||||
|
padding: 16px;
|
||||||
|
border-bottom: 1px solid #e5e5e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section:last-of-type {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section h2 {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #666;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover:not(:disabled) {
|
||||||
|
background: #e5e5e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-small {
|
||||||
|
padding: 4px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 4px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
stroke: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Connection Section */
|
||||||
|
.connection-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
background: #fee;
|
||||||
|
color: #c00;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Action Buttons */
|
||||||
|
.action-buttons {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:hover:not(:disabled) {
|
||||||
|
border-color: #667eea;
|
||||||
|
background: #f8f9ff;
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:hover:not(:disabled) .action-icon {
|
||||||
|
stroke: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
stroke: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Response Section */
|
||||||
|
.response-section {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-container {
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-header {
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #f0f0f0;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-type {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-content {
|
||||||
|
padding: 12px;
|
||||||
|
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #333;
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
max-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings Section */
|
||||||
|
.settings-section details {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section summary {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #666;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
padding: 4px 0;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section summary:hover {
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-content {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item input[type="text"],
|
||||||
|
.setting-item input[type="number"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item input[type="checkbox"] {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
.footer {
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-top: 1px solid #e5e5e5;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a {
|
||||||
|
color: #667eea;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version {
|
||||||
|
color: #bbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #888;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading state */
|
||||||
|
.loading {
|
||||||
|
position: relative;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in {
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
140
packages/chrome-qwen-bridge/extension/sidepanel/sidepanel.html
Normal file
140
packages/chrome-qwen-bridge/extension/sidepanel/sidepanel.html
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Qwen CLI Bridge</title>
|
||||||
|
<link rel="stylesheet" href="sidepanel.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="header">
|
||||||
|
<div class="logo">
|
||||||
|
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||||
|
</svg>
|
||||||
|
<h1>Qwen CLI Bridge</h1>
|
||||||
|
</div>
|
||||||
|
<div class="status-indicator" id="statusIndicator">
|
||||||
|
<span class="status-dot"></span>
|
||||||
|
<span class="status-text">Disconnected</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Connection Section -->
|
||||||
|
<section class="section connection-section">
|
||||||
|
<h2>Connection</h2>
|
||||||
|
<div class="connection-controls">
|
||||||
|
<button id="connectBtn" class="btn btn-primary">
|
||||||
|
Connect to Qwen CLI
|
||||||
|
</button>
|
||||||
|
<button id="startQwenBtn" class="btn btn-secondary" disabled>
|
||||||
|
Start Qwen CLI
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="connectionError" class="error-message" style="display: none;"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Actions Section -->
|
||||||
|
<section class="section actions-section">
|
||||||
|
<h2>Quick Actions</h2>
|
||||||
|
<div class="action-buttons">
|
||||||
|
<button id="extractDataBtn" class="action-btn" disabled>
|
||||||
|
<svg class="action-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||||
|
</svg>
|
||||||
|
Extract Page Data
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button id="captureScreenBtn" class="action-btn" disabled>
|
||||||
|
<svg class="action-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
</svg>
|
||||||
|
Capture Screenshot
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button id="analyzePageBtn" class="action-btn" disabled>
|
||||||
|
<svg class="action-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||||||
|
</svg>
|
||||||
|
Analyze with AI
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button id="getSelectedBtn" class="action-btn" disabled>
|
||||||
|
<svg class="action-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
Send Selected Text
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button id="networkLogsBtn" class="action-btn" disabled>
|
||||||
|
<svg class="action-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0" />
|
||||||
|
</svg>
|
||||||
|
Network Logs
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button id="consoleLogsBtn" class="action-btn" disabled>
|
||||||
|
<svg class="action-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
Console Logs
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Response Section -->
|
||||||
|
<section class="section response-section" id="responseSection" style="display: none;">
|
||||||
|
<h2>Response</h2>
|
||||||
|
<div class="response-container">
|
||||||
|
<div class="response-header">
|
||||||
|
<span id="responseType" class="response-type"></span>
|
||||||
|
<button id="copyResponseBtn" class="btn-icon" title="Copy to clipboard">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<pre id="responseContent" class="response-content"></pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Settings Section -->
|
||||||
|
<section class="section settings-section">
|
||||||
|
<details>
|
||||||
|
<summary>Advanced Settings</summary>
|
||||||
|
<div class="settings-content">
|
||||||
|
<div class="setting-item">
|
||||||
|
<label for="mcpServers">MCP Servers:</label>
|
||||||
|
<input type="text" id="mcpServers" placeholder="chrome-devtools,playwright" />
|
||||||
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<label for="httpPort">HTTP Port:</label>
|
||||||
|
<input type="number" id="httpPort" placeholder="8080" value="8080" />
|
||||||
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<label for="autoConnect">
|
||||||
|
<input type="checkbox" id="autoConnect" />
|
||||||
|
Auto-connect on startup
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button id="saveSettingsBtn" class="btn btn-small">Save Settings</button>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="footer">
|
||||||
|
<a href="#" id="openOptionsBtn">Options</a>
|
||||||
|
<span>•</span>
|
||||||
|
<a href="#" id="helpBtn">Help</a>
|
||||||
|
<span>•</span>
|
||||||
|
<span class="version">v1.0.0</span>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="sidepanel.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
480
packages/chrome-qwen-bridge/extension/sidepanel/sidepanel.js
Normal file
480
packages/chrome-qwen-bridge/extension/sidepanel/sidepanel.js
Normal file
@@ -0,0 +1,480 @@
|
|||||||
|
/**
|
||||||
|
* Side Panel Script for Qwen CLI Bridge
|
||||||
|
* Handles UI interactions and communication with background script
|
||||||
|
*/
|
||||||
|
|
||||||
|
// UI Elements
|
||||||
|
const statusIndicator = document.getElementById('statusIndicator');
|
||||||
|
const statusText = statusIndicator.querySelector('.status-text');
|
||||||
|
const connectBtn = document.getElementById('connectBtn');
|
||||||
|
const startQwenBtn = document.getElementById('startQwenBtn');
|
||||||
|
const connectionError = document.getElementById('connectionError');
|
||||||
|
const responseSection = document.getElementById('responseSection');
|
||||||
|
const responseType = document.getElementById('responseType');
|
||||||
|
const responseContent = document.getElementById('responseContent');
|
||||||
|
const copyResponseBtn = document.getElementById('copyResponseBtn');
|
||||||
|
|
||||||
|
// Action buttons
|
||||||
|
const extractDataBtn = document.getElementById('extractDataBtn');
|
||||||
|
const captureScreenBtn = document.getElementById('captureScreenBtn');
|
||||||
|
const analyzePageBtn = document.getElementById('analyzePageBtn');
|
||||||
|
const getSelectedBtn = document.getElementById('getSelectedBtn');
|
||||||
|
const networkLogsBtn = document.getElementById('networkLogsBtn');
|
||||||
|
const consoleLogsBtn = document.getElementById('consoleLogsBtn');
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
const mcpServersInput = document.getElementById('mcpServers');
|
||||||
|
const httpPortInput = document.getElementById('httpPort');
|
||||||
|
const autoConnectCheckbox = document.getElementById('autoConnect');
|
||||||
|
const saveSettingsBtn = document.getElementById('saveSettingsBtn');
|
||||||
|
|
||||||
|
// Footer links
|
||||||
|
const openOptionsBtn = document.getElementById('openOptionsBtn');
|
||||||
|
const helpBtn = document.getElementById('helpBtn');
|
||||||
|
|
||||||
|
// State
|
||||||
|
let isConnected = false;
|
||||||
|
let qwenStatus = 'disconnected';
|
||||||
|
|
||||||
|
// Initialize side panel
|
||||||
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
await loadSettings();
|
||||||
|
await checkConnectionStatus();
|
||||||
|
|
||||||
|
// Auto-connect if enabled
|
||||||
|
const settings = await chrome.storage.local.get(['autoConnect']);
|
||||||
|
if (settings.autoConnect && !isConnected) {
|
||||||
|
connectToQwen();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load saved settings
|
||||||
|
async function loadSettings() {
|
||||||
|
const settings = await chrome.storage.local.get([
|
||||||
|
'mcpServers',
|
||||||
|
'httpPort',
|
||||||
|
'autoConnect'
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (settings.mcpServers) {
|
||||||
|
mcpServersInput.value = settings.mcpServers;
|
||||||
|
}
|
||||||
|
if (settings.httpPort) {
|
||||||
|
httpPortInput.value = settings.httpPort;
|
||||||
|
}
|
||||||
|
if (settings.autoConnect !== undefined) {
|
||||||
|
autoConnectCheckbox.checked = settings.autoConnect;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save settings
|
||||||
|
saveSettingsBtn.addEventListener('click', async () => {
|
||||||
|
await chrome.storage.local.set({
|
||||||
|
mcpServers: mcpServersInput.value,
|
||||||
|
httpPort: parseInt(httpPortInput.value) || 8080,
|
||||||
|
autoConnect: autoConnectCheckbox.checked
|
||||||
|
});
|
||||||
|
|
||||||
|
saveSettingsBtn.textContent = 'Saved!';
|
||||||
|
setTimeout(() => {
|
||||||
|
saveSettingsBtn.textContent = 'Save Settings';
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check connection status
|
||||||
|
async function checkConnectionStatus() {
|
||||||
|
try {
|
||||||
|
const response = await chrome.runtime.sendMessage({ type: 'GET_STATUS' });
|
||||||
|
updateConnectionStatus(response.connected, response.status);
|
||||||
|
} catch (error) {
|
||||||
|
updateConnectionStatus(false, 'disconnected');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update UI based on connection status
|
||||||
|
function updateConnectionStatus(connected, status) {
|
||||||
|
isConnected = connected;
|
||||||
|
qwenStatus = status;
|
||||||
|
|
||||||
|
// Update status indicator
|
||||||
|
statusIndicator.classList.toggle('connected', connected);
|
||||||
|
statusIndicator.classList.toggle('connecting', status === 'connecting');
|
||||||
|
statusText.textContent = getStatusText(status);
|
||||||
|
|
||||||
|
// Update button states
|
||||||
|
connectBtn.textContent = connected ? 'Disconnect' : 'Connect to Qwen CLI';
|
||||||
|
connectBtn.classList.toggle('btn-danger', connected);
|
||||||
|
|
||||||
|
startQwenBtn.disabled = !connected || status === 'running';
|
||||||
|
|
||||||
|
// Enable/disable action buttons
|
||||||
|
const actionButtons = [
|
||||||
|
extractDataBtn,
|
||||||
|
captureScreenBtn,
|
||||||
|
analyzePageBtn,
|
||||||
|
getSelectedBtn,
|
||||||
|
networkLogsBtn,
|
||||||
|
consoleLogsBtn
|
||||||
|
];
|
||||||
|
|
||||||
|
actionButtons.forEach(btn => {
|
||||||
|
btn.disabled = !connected || status !== 'running';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get human-readable status text
|
||||||
|
function getStatusText(status) {
|
||||||
|
switch (status) {
|
||||||
|
case 'connected':
|
||||||
|
return 'Connected';
|
||||||
|
case 'running':
|
||||||
|
return 'Qwen CLI Running';
|
||||||
|
case 'connecting':
|
||||||
|
return 'Connecting...';
|
||||||
|
case 'disconnected':
|
||||||
|
return 'Disconnected';
|
||||||
|
case 'stopped':
|
||||||
|
return 'Qwen CLI Stopped';
|
||||||
|
default:
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect button handler
|
||||||
|
connectBtn.addEventListener('click', () => {
|
||||||
|
if (isConnected) {
|
||||||
|
disconnectFromQwen();
|
||||||
|
} else {
|
||||||
|
connectToQwen();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connect to Qwen CLI
|
||||||
|
async function connectToQwen() {
|
||||||
|
updateConnectionStatus(false, 'connecting');
|
||||||
|
connectionError.style.display = 'none';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await chrome.runtime.sendMessage({ type: 'CONNECT' });
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
updateConnectionStatus(true, response.status);
|
||||||
|
} else {
|
||||||
|
throw new Error(response.error || 'Connection failed');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Connection error:', error);
|
||||||
|
connectionError.textContent = `Error: ${error.message}`;
|
||||||
|
connectionError.style.display = 'block';
|
||||||
|
updateConnectionStatus(false, 'disconnected');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disconnect from Qwen CLI
|
||||||
|
async function disconnectFromQwen() {
|
||||||
|
try {
|
||||||
|
await chrome.runtime.sendMessage({ type: 'DISCONNECT' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Disconnect error:', error);
|
||||||
|
}
|
||||||
|
updateConnectionStatus(false, 'disconnected');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start Qwen CLI button handler
|
||||||
|
startQwenBtn.addEventListener('click', async () => {
|
||||||
|
startQwenBtn.disabled = true;
|
||||||
|
startQwenBtn.textContent = 'Starting...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const settings = await chrome.storage.local.get(['mcpServers', 'httpPort']);
|
||||||
|
const response = await chrome.runtime.sendMessage({
|
||||||
|
type: 'START_QWEN_CLI',
|
||||||
|
config: {
|
||||||
|
mcpServers: settings.mcpServers ? settings.mcpServers.split(',').map(s => s.trim()) : [],
|
||||||
|
httpPort: settings.httpPort || 8080
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
updateConnectionStatus(true, 'running');
|
||||||
|
showResponse('Qwen CLI Started', response.data || 'Successfully started');
|
||||||
|
} else {
|
||||||
|
throw new Error(response.error || 'Failed to start Qwen CLI');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Start error:', error);
|
||||||
|
connectionError.textContent = `Error: ${error.message}`;
|
||||||
|
connectionError.style.display = 'block';
|
||||||
|
} finally {
|
||||||
|
startQwenBtn.textContent = 'Start Qwen CLI';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extract page data button handler
|
||||||
|
extractDataBtn.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
showLoading('Extracting page data...');
|
||||||
|
|
||||||
|
const response = await chrome.runtime.sendMessage({
|
||||||
|
type: 'EXTRACT_PAGE_DATA'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
// Send to Qwen CLI
|
||||||
|
const qwenResponse = await chrome.runtime.sendMessage({
|
||||||
|
type: 'SEND_TO_QWEN',
|
||||||
|
action: 'analyze_page',
|
||||||
|
data: response.data
|
||||||
|
});
|
||||||
|
|
||||||
|
if (qwenResponse.success) {
|
||||||
|
showResponse('Page Analysis', qwenResponse.data);
|
||||||
|
} else {
|
||||||
|
throw new Error(qwenResponse.error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(response.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showError(`Failed to extract data: ${error.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Capture screenshot button handler
|
||||||
|
captureScreenBtn.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
showLoading('Capturing screenshot...');
|
||||||
|
|
||||||
|
const response = await chrome.runtime.sendMessage({
|
||||||
|
type: 'CAPTURE_SCREENSHOT'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
// Send to Qwen CLI
|
||||||
|
const qwenResponse = await chrome.runtime.sendMessage({
|
||||||
|
type: 'SEND_TO_QWEN',
|
||||||
|
action: 'analyze_screenshot',
|
||||||
|
data: {
|
||||||
|
screenshot: response.data,
|
||||||
|
url: (await chrome.tabs.query({ active: true, currentWindow: true }))[0].url
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (qwenResponse.success) {
|
||||||
|
showResponse('Screenshot Analysis', qwenResponse.data);
|
||||||
|
} else {
|
||||||
|
throw new Error(qwenResponse.error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(response.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showError(`Failed to capture screenshot: ${error.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Analyze page with AI button handler
|
||||||
|
analyzePageBtn.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
showLoading('Analyzing page with AI...');
|
||||||
|
|
||||||
|
// First extract page data
|
||||||
|
const extractResponse = await chrome.runtime.sendMessage({
|
||||||
|
type: 'EXTRACT_PAGE_DATA'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!extractResponse.success) {
|
||||||
|
throw new Error(extractResponse.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send to Qwen for AI analysis
|
||||||
|
const qwenResponse = await chrome.runtime.sendMessage({
|
||||||
|
type: 'SEND_TO_QWEN',
|
||||||
|
action: 'ai_analyze',
|
||||||
|
data: {
|
||||||
|
pageData: extractResponse.data,
|
||||||
|
prompt: 'Please analyze this webpage and provide insights about its content, purpose, and any notable features.'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (qwenResponse.success) {
|
||||||
|
showResponse('AI Analysis', qwenResponse.data);
|
||||||
|
} else {
|
||||||
|
throw new Error(qwenResponse.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showError(`Analysis failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get selected text button handler
|
||||||
|
getSelectedBtn.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
showLoading('Getting selected text...');
|
||||||
|
|
||||||
|
// Get active tab
|
||||||
|
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||||
|
const tab = tabs[0];
|
||||||
|
|
||||||
|
if (!tab) {
|
||||||
|
throw new Error('No active tab found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get selected text from content script
|
||||||
|
const response = await chrome.tabs.sendMessage(tab.id, {
|
||||||
|
type: 'GET_SELECTED_TEXT'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success && response.data) {
|
||||||
|
// Send to Qwen CLI
|
||||||
|
const qwenResponse = await chrome.runtime.sendMessage({
|
||||||
|
type: 'SEND_TO_QWEN',
|
||||||
|
action: 'process_text',
|
||||||
|
data: {
|
||||||
|
text: response.data,
|
||||||
|
context: 'selected_text'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (qwenResponse.success) {
|
||||||
|
showResponse('Selected Text Processed', qwenResponse.data);
|
||||||
|
} else {
|
||||||
|
throw new Error(qwenResponse.error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showError('No text selected. Please select some text on the page first.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showError(`Failed to process selected text: ${error.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Network logs button handler
|
||||||
|
networkLogsBtn.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
showLoading('Getting network logs...');
|
||||||
|
|
||||||
|
const response = await chrome.runtime.sendMessage({
|
||||||
|
type: 'GET_NETWORK_LOGS'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
showResponse('Network Logs', JSON.stringify(response.data, null, 2));
|
||||||
|
} else {
|
||||||
|
throw new Error(response.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showError(`Failed to get network logs: ${error.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Console logs button handler
|
||||||
|
consoleLogsBtn.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
showLoading('Getting console logs...');
|
||||||
|
|
||||||
|
// Get active tab
|
||||||
|
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||||
|
const tab = tabs[0];
|
||||||
|
|
||||||
|
if (!tab) {
|
||||||
|
throw new Error('No active tab found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get console logs from content script
|
||||||
|
const response = await chrome.tabs.sendMessage(tab.id, {
|
||||||
|
type: 'EXTRACT_DATA'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
const consoleLogs = response.data.consoleLogs || [];
|
||||||
|
if (consoleLogs.length > 0) {
|
||||||
|
showResponse('Console Logs', JSON.stringify(consoleLogs, null, 2));
|
||||||
|
} else {
|
||||||
|
showResponse('Console Logs', 'No console logs captured');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(response.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showError(`Failed to get console logs: ${error.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Copy response button handler
|
||||||
|
copyResponseBtn.addEventListener('click', () => {
|
||||||
|
const text = responseContent.textContent;
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
const originalTitle = copyResponseBtn.title;
|
||||||
|
copyResponseBtn.title = 'Copied!';
|
||||||
|
setTimeout(() => {
|
||||||
|
copyResponseBtn.title = originalTitle;
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Footer link handlers
|
||||||
|
openOptionsBtn.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
// Use try-catch to handle potential errors
|
||||||
|
try {
|
||||||
|
chrome.runtime.openOptionsPage(() => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
// If opening options page fails, open it in a new tab as fallback
|
||||||
|
console.error('Error opening options page:', chrome.runtime.lastError);
|
||||||
|
chrome.tabs.create({
|
||||||
|
url: chrome.runtime.getURL('options/options.html')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to open options page:', error);
|
||||||
|
// Fallback: open in new tab
|
||||||
|
chrome.tabs.create({
|
||||||
|
url: chrome.runtime.getURL('options/options.html')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
helpBtn.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
chrome.tabs.create({
|
||||||
|
url: 'https://github.com/QwenLM/qwen-code/tree/main/packages/chrome-qwen-bridge'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
function showLoading(message) {
|
||||||
|
responseSection.style.display = 'block';
|
||||||
|
responseType.textContent = 'Loading';
|
||||||
|
responseContent.textContent = message;
|
||||||
|
responseSection.classList.add('loading');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showResponse(type, content) {
|
||||||
|
responseSection.style.display = 'block';
|
||||||
|
responseType.textContent = type;
|
||||||
|
responseType.style.color = '#666';
|
||||||
|
responseContent.textContent = typeof content === 'string' ? content : JSON.stringify(content, null, 2);
|
||||||
|
responseSection.classList.remove('loading');
|
||||||
|
responseSection.classList.add('fade-in');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showError(message) {
|
||||||
|
responseSection.style.display = 'block';
|
||||||
|
responseType.textContent = 'Error';
|
||||||
|
responseType.style.color = '#c00';
|
||||||
|
responseContent.textContent = message;
|
||||||
|
responseSection.classList.remove('loading');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for status updates from background
|
||||||
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
|
if (message.type === 'STATUS_UPDATE') {
|
||||||
|
updateConnectionStatus(message.status !== 'disconnected', message.status);
|
||||||
|
} else if (message.type === 'QWEN_EVENT') {
|
||||||
|
// Handle events from Qwen CLI
|
||||||
|
console.log('Qwen event received:', message.event);
|
||||||
|
// Could update UI based on event
|
||||||
|
}
|
||||||
|
});
|
||||||
120
packages/chrome-qwen-bridge/first-install.sh
Executable file
120
packages/chrome-qwen-bridge/first-install.sh
Executable file
@@ -0,0 +1,120 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Qwen CLI Bridge - 首次安装脚本
|
||||||
|
|
||||||
|
# 颜色定义
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
|
||||||
|
clear
|
||||||
|
echo -e "${CYAN}╔════════════════════════════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${CYAN}║ ║${NC}"
|
||||||
|
echo -e "${CYAN}║ 🎯 Qwen CLI Bridge - 首次安装向导 ║${NC}"
|
||||||
|
echo -e "${CYAN}║ ║${NC}"
|
||||||
|
echo -e "${CYAN}╚════════════════════════════════════════════════════════════════╝${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo -e "${YELLOW}这是首次安装,需要手动加载插件到 Chrome。${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 步骤 1: 配置 Native Host
|
||||||
|
echo -e "${BLUE}步骤 1:${NC} 配置 Native Host..."
|
||||||
|
|
||||||
|
MANIFEST_DIR="$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts"
|
||||||
|
mkdir -p "$MANIFEST_DIR"
|
||||||
|
|
||||||
|
# 先创建一个临时的 manifest,允许所有扩展
|
||||||
|
cat > "$MANIFEST_DIR/com.qwen.cli.bridge.json" << EOF
|
||||||
|
{
|
||||||
|
"name": "com.qwen.cli.bridge",
|
||||||
|
"description": "Native messaging host for Qwen CLI Bridge",
|
||||||
|
"path": "$SCRIPT_DIR/native-host/host.js",
|
||||||
|
"type": "stdio",
|
||||||
|
"allowed_origins": ["chrome-extension://*/"]
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo -e "${GREEN}✓${NC} Native Host 已配置"
|
||||||
|
|
||||||
|
# 步骤 2: 打开 Chrome 扩展页面
|
||||||
|
echo -e "\n${BLUE}步骤 2:${NC} 打开 Chrome 扩展管理页面..."
|
||||||
|
|
||||||
|
open -a "Google Chrome" "chrome://extensions"
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
echo -e "${GREEN}✓${NC} 已打开扩展管理页面"
|
||||||
|
|
||||||
|
# 步骤 3: 指导用户安装
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}════════════════════════════════════════════════════════════════${NC}"
|
||||||
|
echo -e "${YELLOW}请按照以下步骤手动安装插件:${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e " 1️⃣ 在 Chrome 扩展页面,${GREEN}开启「开发者模式」${NC}(右上角开关)"
|
||||||
|
echo ""
|
||||||
|
echo -e " 2️⃣ 点击 ${GREEN}「加载已解压的扩展程序」${NC} 按钮"
|
||||||
|
echo ""
|
||||||
|
echo -e " 3️⃣ 选择以下目录:"
|
||||||
|
echo -e " ${BLUE}$SCRIPT_DIR/extension${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e " 4️⃣ ${YELLOW}重要:${NC} 记下显示的扩展 ID(类似 ${CYAN}abcdefghijklmnopqrstuvwx${NC})"
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}════════════════════════════════════════════════════════════════${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 等待用户输入扩展 ID
|
||||||
|
echo -e "${YELLOW}请输入扩展 ID(安装后显示的 ID):${NC}"
|
||||||
|
read -p "> " EXTENSION_ID
|
||||||
|
|
||||||
|
if [[ -z "$EXTENSION_ID" ]]; then
|
||||||
|
echo -e "${RED}✗ 未输入扩展 ID${NC}"
|
||||||
|
echo -e "${YELLOW}你可以稍后手动更新 Native Host 配置${NC}"
|
||||||
|
else
|
||||||
|
# 更新 manifest 文件,添加具体的扩展 ID
|
||||||
|
cat > "$MANIFEST_DIR/com.qwen.cli.bridge.json" << EOF
|
||||||
|
{
|
||||||
|
"name": "com.qwen.cli.bridge",
|
||||||
|
"description": "Native messaging host for Qwen CLI Bridge",
|
||||||
|
"path": "$SCRIPT_DIR/native-host/host.js",
|
||||||
|
"type": "stdio",
|
||||||
|
"allowed_origins": [
|
||||||
|
"chrome-extension://$EXTENSION_ID/",
|
||||||
|
"chrome-extension://*/"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 保存扩展 ID 供后续使用
|
||||||
|
echo "$EXTENSION_ID" > "$SCRIPT_DIR/.extension-id"
|
||||||
|
|
||||||
|
echo -e "${GREEN}✓${NC} Native Host 已更新,支持扩展 ID: $EXTENSION_ID"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}════════════════════════════════════════════════════════════════${NC}"
|
||||||
|
echo -e "${GREEN} ✅ 首次安装完成! ${NC}"
|
||||||
|
echo -e "${GREEN}════════════════════════════════════════════════════════════════${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e "现在你可以:"
|
||||||
|
echo ""
|
||||||
|
echo -e " 1. 运行 ${CYAN}npm run dev${NC} 启动调试环境"
|
||||||
|
echo -e " 2. 点击 Chrome 工具栏的插件图标开始使用"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}提示:${NC}"
|
||||||
|
echo -e " • 如果看不到插件图标,点击拼图图标并固定插件"
|
||||||
|
echo -e " • 首次连接可能需要刷新页面"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 询问是否立即启动
|
||||||
|
echo -e "${CYAN}是否立即启动调试环境?(y/n)${NC}"
|
||||||
|
read -p "> " START_NOW
|
||||||
|
|
||||||
|
if [[ "$START_NOW" == "y" ]] || [[ "$START_NOW" == "Y" ]]; then
|
||||||
|
echo -e "\n${GREEN}正在启动调试环境...${NC}\n"
|
||||||
|
exec "$SCRIPT_DIR/debug.sh"
|
||||||
|
fi
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "com.qwen.bridge",
|
||||||
|
"description": "Native messaging host for Qwen CLI Bridge",
|
||||||
|
"path": "__PATH__",
|
||||||
|
"type": "stdio",
|
||||||
|
"allowed_origins": [
|
||||||
|
"__EXTENSION_ID__"
|
||||||
|
]
|
||||||
|
}
|
||||||
2
packages/chrome-qwen-bridge/native-host/host.bat
Normal file
2
packages/chrome-qwen-bridge/native-host/host.bat
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
@echo off
|
||||||
|
node "%~dp0host.js" %*
|
||||||
421
packages/chrome-qwen-bridge/native-host/host.js
Executable file
421
packages/chrome-qwen-bridge/native-host/host.js
Executable file
@@ -0,0 +1,421 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Native Messaging Host for Qwen CLI Bridge
|
||||||
|
* This script acts as a bridge between the Chrome extension and Qwen CLI
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { spawn } = require('child_process');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const os = require('os');
|
||||||
|
|
||||||
|
// Native Messaging protocol helpers
|
||||||
|
function sendMessage(message) {
|
||||||
|
const buffer = Buffer.from(JSON.stringify(message));
|
||||||
|
const length = Buffer.allocUnsafe(4);
|
||||||
|
length.writeUInt32LE(buffer.length, 0);
|
||||||
|
|
||||||
|
process.stdout.write(length);
|
||||||
|
process.stdout.write(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readMessages() {
|
||||||
|
let messageLength = null;
|
||||||
|
let chunks = [];
|
||||||
|
|
||||||
|
process.stdin.on('readable', () => {
|
||||||
|
let chunk;
|
||||||
|
|
||||||
|
while ((chunk = process.stdin.read()) !== null) {
|
||||||
|
chunks.push(chunk);
|
||||||
|
const buffer = Buffer.concat(chunks);
|
||||||
|
|
||||||
|
// Read message length if we haven't yet
|
||||||
|
if (messageLength === null) {
|
||||||
|
if (buffer.length >= 4) {
|
||||||
|
messageLength = buffer.readUInt32LE(0);
|
||||||
|
chunks = [buffer.slice(4)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read message if we have the full length
|
||||||
|
if (messageLength !== null) {
|
||||||
|
const fullBuffer = Buffer.concat(chunks);
|
||||||
|
|
||||||
|
if (fullBuffer.length >= messageLength) {
|
||||||
|
const messageBuffer = fullBuffer.slice(0, messageLength);
|
||||||
|
const message = JSON.parse(messageBuffer.toString());
|
||||||
|
|
||||||
|
// Reset for next message
|
||||||
|
chunks = [fullBuffer.slice(messageLength)];
|
||||||
|
messageLength = null;
|
||||||
|
|
||||||
|
// Handle the message
|
||||||
|
handleMessage(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
process.stdin.on('end', () => {
|
||||||
|
process.exit();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Qwen CLI process management
|
||||||
|
let qwenProcess = null;
|
||||||
|
let qwenStatus = 'disconnected';
|
||||||
|
let qwenCapabilities = [];
|
||||||
|
|
||||||
|
// Check if Qwen CLI is installed
|
||||||
|
function checkQwenInstallation() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
try {
|
||||||
|
const checkProcess = spawn('qwen', ['--version'], {
|
||||||
|
shell: true,
|
||||||
|
windowsHide: true
|
||||||
|
});
|
||||||
|
|
||||||
|
checkProcess.on('error', () => {
|
||||||
|
resolve(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
checkProcess.on('close', (code) => {
|
||||||
|
resolve(code === 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Timeout after 5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
checkProcess.kill();
|
||||||
|
resolve(false);
|
||||||
|
}, 5000);
|
||||||
|
} catch (error) {
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start Qwen CLI process
|
||||||
|
async function startQwenCli(config = {}) {
|
||||||
|
if (qwenProcess) {
|
||||||
|
return { success: false, error: 'Qwen CLI is already running' };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Build command arguments
|
||||||
|
const args = [];
|
||||||
|
|
||||||
|
// Add MCP servers if specified
|
||||||
|
if (config.mcpServers && config.mcpServers.length > 0) {
|
||||||
|
for (const server of config.mcpServers) {
|
||||||
|
args.push('mcp', 'add', '--transport', 'http', server, `http://localhost:${config.httpPort || 8080}/mcp/${server}`);
|
||||||
|
args.push('&&');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the CLI server
|
||||||
|
args.push('qwen', 'server');
|
||||||
|
|
||||||
|
if (config.httpPort) {
|
||||||
|
args.push('--port', String(config.httpPort));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spawn the process
|
||||||
|
qwenProcess = spawn(args.join(' '), {
|
||||||
|
shell: true,
|
||||||
|
windowsHide: true,
|
||||||
|
detached: false,
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe']
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if process started successfully
|
||||||
|
if (!qwenProcess || !qwenProcess.pid) {
|
||||||
|
qwenProcess = null;
|
||||||
|
qwenStatus = 'stopped';
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to start Qwen CLI process'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
qwenStatus = 'running';
|
||||||
|
|
||||||
|
// Handle process output
|
||||||
|
qwenProcess.stdout.on('data', (data) => {
|
||||||
|
const output = data.toString();
|
||||||
|
sendMessage({
|
||||||
|
type: 'event',
|
||||||
|
data: {
|
||||||
|
type: 'qwen_output',
|
||||||
|
content: output
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
qwenProcess.stderr.on('data', (data) => {
|
||||||
|
const error = data.toString();
|
||||||
|
sendMessage({
|
||||||
|
type: 'event',
|
||||||
|
data: {
|
||||||
|
type: 'qwen_error',
|
||||||
|
content: error
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
qwenProcess.on('close', (code) => {
|
||||||
|
qwenProcess = null;
|
||||||
|
qwenStatus = 'stopped';
|
||||||
|
sendMessage({
|
||||||
|
type: 'event',
|
||||||
|
data: {
|
||||||
|
type: 'qwen_stopped',
|
||||||
|
code: code
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait a bit for the process to start
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
|
||||||
|
// Get capabilities
|
||||||
|
qwenCapabilities = await getQwenCapabilities();
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
status: 'running',
|
||||||
|
pid: qwenProcess && qwenProcess.pid ? qwenProcess.pid : null,
|
||||||
|
capabilities: qwenCapabilities
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop Qwen CLI process
|
||||||
|
function stopQwenCli() {
|
||||||
|
if (!qwenProcess) {
|
||||||
|
return { success: false, error: 'Qwen CLI is not running' };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
qwenProcess.kill('SIGTERM');
|
||||||
|
qwenProcess = null;
|
||||||
|
qwenStatus = 'stopped';
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: 'Qwen CLI stopped'
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Qwen CLI capabilities (MCP servers, tools, etc.)
|
||||||
|
async function getQwenCapabilities() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const checkProcess = spawn('qwen', ['mcp', 'list', '--json'], {
|
||||||
|
shell: true,
|
||||||
|
windowsHide: true
|
||||||
|
});
|
||||||
|
|
||||||
|
let output = '';
|
||||||
|
|
||||||
|
checkProcess.stdout.on('data', (data) => {
|
||||||
|
output += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
checkProcess.on('close', () => {
|
||||||
|
try {
|
||||||
|
const capabilities = JSON.parse(output);
|
||||||
|
resolve(capabilities);
|
||||||
|
} catch {
|
||||||
|
resolve([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
checkProcess.on('error', () => {
|
||||||
|
resolve([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Timeout after 5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
checkProcess.kill();
|
||||||
|
resolve([]);
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send request to Qwen CLI via HTTP
|
||||||
|
async function sendToQwenHttp(action, data, config = {}) {
|
||||||
|
const http = require('http');
|
||||||
|
|
||||||
|
const port = config.httpPort || 8080;
|
||||||
|
const hostname = 'localhost';
|
||||||
|
|
||||||
|
const postData = JSON.stringify({
|
||||||
|
action,
|
||||||
|
data
|
||||||
|
});
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
hostname,
|
||||||
|
port,
|
||||||
|
path: '/api/process',
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Content-Length': Buffer.byteLength(postData)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const req = http.request(options, (res) => {
|
||||||
|
let responseData = '';
|
||||||
|
|
||||||
|
res.on('data', (chunk) => {
|
||||||
|
responseData += chunk;
|
||||||
|
});
|
||||||
|
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
const response = JSON.parse(responseData);
|
||||||
|
resolve(response);
|
||||||
|
} catch (error) {
|
||||||
|
reject(new Error('Invalid response from Qwen CLI'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', (error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
req.write(postData);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle messages from Chrome extension
|
||||||
|
async function handleMessage(message) {
|
||||||
|
let response;
|
||||||
|
|
||||||
|
switch (message.type) {
|
||||||
|
case 'handshake':
|
||||||
|
// Initial handshake with extension
|
||||||
|
const isInstalled = await checkQwenInstallation();
|
||||||
|
response = {
|
||||||
|
type: 'handshake_response',
|
||||||
|
version: '1.0.0',
|
||||||
|
qwenInstalled: isInstalled,
|
||||||
|
qwenStatus: qwenStatus,
|
||||||
|
capabilities: qwenCapabilities
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'start_qwen':
|
||||||
|
// Start Qwen CLI
|
||||||
|
const startResult = await startQwenCli(message.config);
|
||||||
|
response = {
|
||||||
|
type: 'response',
|
||||||
|
id: message.id,
|
||||||
|
...startResult
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'stop_qwen':
|
||||||
|
// Stop Qwen CLI
|
||||||
|
const stopResult = stopQwenCli();
|
||||||
|
response = {
|
||||||
|
type: 'response',
|
||||||
|
id: message.id,
|
||||||
|
...stopResult
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'qwen_request':
|
||||||
|
// Send request to Qwen CLI
|
||||||
|
try {
|
||||||
|
if (qwenStatus !== 'running') {
|
||||||
|
throw new Error('Qwen CLI is not running');
|
||||||
|
}
|
||||||
|
|
||||||
|
const qwenResponse = await sendToQwenHttp(
|
||||||
|
message.action,
|
||||||
|
message.data,
|
||||||
|
message.config
|
||||||
|
);
|
||||||
|
|
||||||
|
response = {
|
||||||
|
type: 'response',
|
||||||
|
id: message.id,
|
||||||
|
data: qwenResponse
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
response = {
|
||||||
|
type: 'response',
|
||||||
|
id: message.id,
|
||||||
|
error: error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'get_status':
|
||||||
|
// Get current status
|
||||||
|
response = {
|
||||||
|
type: 'response',
|
||||||
|
id: message.id,
|
||||||
|
data: {
|
||||||
|
qwenInstalled: await checkQwenInstallation(),
|
||||||
|
qwenStatus: qwenStatus,
|
||||||
|
qwenPid: qwenProcess ? qwenProcess.pid : null,
|
||||||
|
capabilities: qwenCapabilities
|
||||||
|
}
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
response = {
|
||||||
|
type: 'response',
|
||||||
|
id: message.id,
|
||||||
|
error: `Unknown message type: ${message.type}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessage(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up on exit
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
if (qwenProcess) {
|
||||||
|
qwenProcess.kill();
|
||||||
|
}
|
||||||
|
process.exit();
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('SIGTERM', () => {
|
||||||
|
if (qwenProcess) {
|
||||||
|
qwenProcess.kill();
|
||||||
|
}
|
||||||
|
process.exit();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log function for debugging (writes to a file since stdout is used for messaging)
|
||||||
|
function log(message) {
|
||||||
|
const logFile = path.join(os.tmpdir(), 'qwen-bridge-host.log');
|
||||||
|
fs.appendFileSync(logFile, `[${new Date().toISOString()}] ${message}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main execution
|
||||||
|
log('Native host started');
|
||||||
|
readMessages();
|
||||||
98
packages/chrome-qwen-bridge/native-host/install.bat
Normal file
98
packages/chrome-qwen-bridge/native-host/install.bat
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
|
REM Qwen CLI Bridge - Native Host Installation Script for Windows
|
||||||
|
REM This script installs the Native Messaging host for the Chrome extension
|
||||||
|
|
||||||
|
echo ========================================
|
||||||
|
echo Qwen CLI Bridge - Native Host Installer
|
||||||
|
echo ========================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM Set variables
|
||||||
|
set HOST_NAME=com.qwen.cli.bridge
|
||||||
|
set SCRIPT_DIR=%~dp0
|
||||||
|
set HOST_SCRIPT=%SCRIPT_DIR%host.bat
|
||||||
|
|
||||||
|
REM Check if Node.js is installed
|
||||||
|
where node >nul 2>nul
|
||||||
|
if %ERRORLEVEL% NEQ 0 (
|
||||||
|
echo Error: Node.js is not installed
|
||||||
|
echo Please install Node.js from https://nodejs.org/
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Check if qwen CLI is installed
|
||||||
|
where qwen >nul 2>nul
|
||||||
|
if %ERRORLEVEL% NEQ 0 (
|
||||||
|
echo Warning: qwen CLI is not installed
|
||||||
|
echo Please install qwen CLI to use all features
|
||||||
|
echo Installation will continue...
|
||||||
|
echo.
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Check if host files exist
|
||||||
|
if not exist "%HOST_SCRIPT%" (
|
||||||
|
echo Error: host.bat not found in %SCRIPT_DIR%
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
if not exist "%SCRIPT_DIR%host.js" (
|
||||||
|
echo Error: host.js not found in %SCRIPT_DIR%
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Get extension ID
|
||||||
|
set /p EXTENSION_ID="Enter your Chrome extension ID (found in chrome://extensions): "
|
||||||
|
|
||||||
|
if "%EXTENSION_ID%"=="" (
|
||||||
|
echo Error: Extension ID is required
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Create manifest
|
||||||
|
set MANIFEST_FILE=%SCRIPT_DIR%manifest-windows.json
|
||||||
|
echo Creating manifest: %MANIFEST_FILE%
|
||||||
|
|
||||||
|
(
|
||||||
|
echo {
|
||||||
|
echo "name": "%HOST_NAME%",
|
||||||
|
echo "description": "Native messaging host for Qwen CLI Bridge Chrome extension",
|
||||||
|
echo "path": "%HOST_SCRIPT:\=\\%",
|
||||||
|
echo "type": "stdio",
|
||||||
|
echo "allowed_origins": [
|
||||||
|
echo "chrome-extension://%EXTENSION_ID%/"
|
||||||
|
echo ]
|
||||||
|
echo }
|
||||||
|
) > "%MANIFEST_FILE%"
|
||||||
|
|
||||||
|
REM Add registry entry for Chrome
|
||||||
|
echo.
|
||||||
|
echo Adding registry entry for Chrome...
|
||||||
|
reg add "HKCU\Software\Google\Chrome\NativeMessagingHosts\%HOST_NAME%" /ve /t REG_SZ /d "%MANIFEST_FILE%" /f
|
||||||
|
|
||||||
|
if %ERRORLEVEL% EQU 0 (
|
||||||
|
echo.
|
||||||
|
echo ✅ Installation complete!
|
||||||
|
echo.
|
||||||
|
echo Next steps:
|
||||||
|
echo 1. Load the Chrome extension in chrome://extensions
|
||||||
|
echo 2. Enable 'Developer mode'
|
||||||
|
echo 3. Click 'Load unpacked' and select: %SCRIPT_DIR%..\extension
|
||||||
|
echo 4. Copy the extension ID and re-run this script if needed
|
||||||
|
echo 5. Click the extension icon and connect to Qwen CLI
|
||||||
|
echo.
|
||||||
|
echo Host manifest: %MANIFEST_FILE%
|
||||||
|
echo Log file location: %%TEMP%%\qwen-bridge-host.log
|
||||||
|
) else (
|
||||||
|
echo.
|
||||||
|
echo ❌ Failed to add registry entry
|
||||||
|
echo Please run this script as Administrator
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
96
packages/chrome-qwen-bridge/native-host/install.sh
Executable file
96
packages/chrome-qwen-bridge/native-host/install.sh
Executable file
@@ -0,0 +1,96 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Qwen CLI Bridge - Native Host Installation Script for macOS/Linux
|
||||||
|
# This script installs the Native Messaging host for the Chrome extension
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
HOST_NAME="com.qwen.cli.bridge"
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo "Qwen CLI Bridge - Native Host Installer"
|
||||||
|
echo "========================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Detect OS
|
||||||
|
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||||
|
# macOS
|
||||||
|
TARGET_DIR="$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts"
|
||||||
|
BROWSER="Chrome"
|
||||||
|
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||||
|
# Linux
|
||||||
|
TARGET_DIR="$HOME/.config/google-chrome/NativeMessagingHosts"
|
||||||
|
BROWSER="Chrome"
|
||||||
|
else
|
||||||
|
echo "Error: Unsupported operating system"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if Node.js is installed
|
||||||
|
if ! command -v node &> /dev/null; then
|
||||||
|
echo "Error: Node.js is not installed"
|
||||||
|
echo "Please install Node.js from https://nodejs.org/"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if qwen CLI is installed
|
||||||
|
if ! command -v qwen &> /dev/null; then
|
||||||
|
echo "Warning: qwen CLI is not installed"
|
||||||
|
echo "Please install qwen CLI to use all features"
|
||||||
|
echo "Installation will continue..."
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create target directory if it doesn't exist
|
||||||
|
echo "Creating directory: $TARGET_DIR"
|
||||||
|
mkdir -p "$TARGET_DIR"
|
||||||
|
|
||||||
|
# Copy the host script
|
||||||
|
HOST_SCRIPT="$SCRIPT_DIR/host.js"
|
||||||
|
if [ ! -f "$HOST_SCRIPT" ]; then
|
||||||
|
echo "Error: host.js not found in $SCRIPT_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Make the host script executable
|
||||||
|
chmod +x "$HOST_SCRIPT"
|
||||||
|
|
||||||
|
# Create the manifest file with the correct path
|
||||||
|
MANIFEST_FILE="$TARGET_DIR/$HOST_NAME.json"
|
||||||
|
echo "Creating manifest: $MANIFEST_FILE"
|
||||||
|
|
||||||
|
# Get the extension ID (you need to update this after installing the extension)
|
||||||
|
read -p "Enter your Chrome extension ID (found in chrome://extensions): " EXTENSION_ID
|
||||||
|
|
||||||
|
if [ -z "$EXTENSION_ID" ]; then
|
||||||
|
echo "Error: Extension ID is required"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create the manifest
|
||||||
|
cat > "$MANIFEST_FILE" << EOF
|
||||||
|
{
|
||||||
|
"name": "$HOST_NAME",
|
||||||
|
"description": "Native messaging host for Qwen CLI Bridge Chrome extension",
|
||||||
|
"path": "$HOST_SCRIPT",
|
||||||
|
"type": "stdio",
|
||||||
|
"allowed_origins": [
|
||||||
|
"chrome-extension://$EXTENSION_ID/"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Installation complete!"
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo "1. Load the Chrome extension in chrome://extensions"
|
||||||
|
echo "2. Enable 'Developer mode'"
|
||||||
|
echo "3. Click 'Load unpacked' and select: $SCRIPT_DIR/../extension"
|
||||||
|
echo "4. Copy the extension ID and re-run this script if needed"
|
||||||
|
echo "5. Click the extension icon and connect to Qwen CLI"
|
||||||
|
echo ""
|
||||||
|
echo "Host installed at: $MANIFEST_FILE"
|
||||||
|
echo "Log file location: /tmp/qwen-bridge-host.log"
|
||||||
|
echo ""
|
||||||
9
packages/chrome-qwen-bridge/native-host/manifest.json
Normal file
9
packages/chrome-qwen-bridge/native-host/manifest.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "com.qwen.cli.bridge",
|
||||||
|
"description": "Native messaging host for Qwen CLI Bridge Chrome extension",
|
||||||
|
"path": "HOST_PATH",
|
||||||
|
"type": "stdio",
|
||||||
|
"allowed_origins": [
|
||||||
|
"chrome-extension://YOUR_EXTENSION_ID/"
|
||||||
|
]
|
||||||
|
}
|
||||||
23
packages/chrome-qwen-bridge/native-host/package.json
Normal file
23
packages/chrome-qwen-bridge/native-host/package.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "qwen-cli-bridge-host",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Native messaging host for Qwen CLI Bridge Chrome extension",
|
||||||
|
"main": "host.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "node host.js --test"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"chrome-extension",
|
||||||
|
"native-messaging",
|
||||||
|
"qwen",
|
||||||
|
"cli",
|
||||||
|
"bridge"
|
||||||
|
],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {},
|
||||||
|
"devDependencies": {}
|
||||||
|
}
|
||||||
17
packages/chrome-qwen-bridge/native-host/run.sh
Executable file
17
packages/chrome-qwen-bridge/native-host/run.sh
Executable file
@@ -0,0 +1,17 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Native Host 包装脚本 - 确保 Node.js 环境正确设置
|
||||||
|
|
||||||
|
# 获取脚本所在目录
|
||||||
|
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
|
||||||
|
# 设置 Node.js 路径 (使用系统中的 node)
|
||||||
|
NODE_PATH="/usr/local/bin/node"
|
||||||
|
|
||||||
|
# 如果 /usr/local/bin/node 不存在,尝试其他位置
|
||||||
|
if [ ! -f "$NODE_PATH" ]; then
|
||||||
|
NODE_PATH=$(which node)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 执行 Native Host
|
||||||
|
exec "$NODE_PATH" "$DIR/host.js"
|
||||||
306
packages/chrome-qwen-bridge/native-host/smart-install.sh
Executable file
306
packages/chrome-qwen-bridge/native-host/smart-install.sh
Executable file
@@ -0,0 +1,306 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Qwen CLI Bridge - 智能 Native Host 安装器
|
||||||
|
# 自动检测 Chrome 插件并配置 Native Host
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# 颜色定义
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
HOST_NAME="com.qwen.cli.bridge"
|
||||||
|
|
||||||
|
echo -e "${CYAN}╔════════════════════════════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${CYAN}║ ║${NC}"
|
||||||
|
echo -e "${CYAN}║ 🔧 Qwen CLI Bridge - Native Host 安装器 ║${NC}"
|
||||||
|
echo -e "${CYAN}║ ║${NC}"
|
||||||
|
echo -e "${CYAN}╚════════════════════════════════════════════════════════════════╝${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 检测操作系统
|
||||||
|
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||||
|
OS="macOS"
|
||||||
|
MANIFEST_DIR="$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts"
|
||||||
|
EXTENSIONS_DIR="$HOME/Library/Application Support/Google/Chrome/Default/Extensions"
|
||||||
|
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||||
|
OS="Linux"
|
||||||
|
MANIFEST_DIR="$HOME/.config/google-chrome/NativeMessagingHosts"
|
||||||
|
EXTENSIONS_DIR="$HOME/.config/google-chrome/Default/Extensions"
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗ 不支持的操作系统${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${BLUE}检测到系统:${NC} $OS"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 检查 Node.js
|
||||||
|
echo -e "${BLUE}检查依赖...${NC}"
|
||||||
|
if ! command -v node &> /dev/null; then
|
||||||
|
echo -e "${RED}✗ Node.js 未安装${NC}"
|
||||||
|
echo -e " 请访问 https://nodejs.org 安装 Node.js"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo -e "${GREEN}✓${NC} Node.js $(node --version)"
|
||||||
|
|
||||||
|
# 尝试自动检测扩展 ID
|
||||||
|
echo -e "\n${BLUE}查找已安装的 Qwen CLI Bridge 扩展...${NC}"
|
||||||
|
|
||||||
|
EXTENSION_ID=""
|
||||||
|
AUTO_DETECTED=false
|
||||||
|
|
||||||
|
# 方法1: 从 Chrome 扩展目录查找
|
||||||
|
if [[ -d "$EXTENSIONS_DIR" ]]; then
|
||||||
|
for ext_id in "$EXTENSIONS_DIR"/*; do
|
||||||
|
if [[ -d "$ext_id" ]]; then
|
||||||
|
ext_id_name=$(basename "$ext_id")
|
||||||
|
# 检查最新版本目录
|
||||||
|
for version_dir in "$ext_id"/*; do
|
||||||
|
if [[ -f "$version_dir/manifest.json" ]]; then
|
||||||
|
# 检查是否是我们的扩展
|
||||||
|
if grep -q "Qwen CLI Bridge" "$version_dir/manifest.json" 2>/dev/null; then
|
||||||
|
EXTENSION_ID="$ext_id_name"
|
||||||
|
AUTO_DETECTED=true
|
||||||
|
echo -e "${GREEN}✓${NC} 自动检测到扩展 ID: ${CYAN}$EXTENSION_ID${NC}"
|
||||||
|
break 2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 方法2: 检查之前保存的 ID
|
||||||
|
if [[ -z "$EXTENSION_ID" && -f "$SCRIPT_DIR/../.extension-id" ]]; then
|
||||||
|
EXTENSION_ID=$(cat "$SCRIPT_DIR/../.extension-id")
|
||||||
|
echo -e "${GREEN}✓${NC} 使用保存的扩展 ID: ${CYAN}$EXTENSION_ID${NC}"
|
||||||
|
AUTO_DETECTED=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 如果自动检测失败,提供选项
|
||||||
|
if [[ -z "$EXTENSION_ID" ]]; then
|
||||||
|
echo -e "${YELLOW}⚠️ 未能自动检测到扩展${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e "请选择:"
|
||||||
|
echo -e " ${CYAN}1)${NC} 我已经安装了扩展(输入扩展 ID)"
|
||||||
|
echo -e " ${CYAN}2)${NC} 我还没有安装扩展(通用配置)"
|
||||||
|
echo -e " ${CYAN}3)${NC} 打开 Chrome 扩展页面查看"
|
||||||
|
echo ""
|
||||||
|
read -p "选择 (1/2/3): " CHOICE
|
||||||
|
|
||||||
|
case $CHOICE in
|
||||||
|
1)
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}请输入扩展 ID:${NC}"
|
||||||
|
echo -e "${CYAN}提示: 在 chrome://extensions 页面找到 Qwen CLI Bridge,ID 在扩展卡片上${NC}"
|
||||||
|
read -p "> " EXTENSION_ID
|
||||||
|
if [[ -n "$EXTENSION_ID" ]]; then
|
||||||
|
# 保存 ID 供以后使用
|
||||||
|
echo "$EXTENSION_ID" > "$SCRIPT_DIR/../.extension-id"
|
||||||
|
echo -e "${GREEN}✓${NC} 扩展 ID 已保存"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
echo -e "\n${CYAN}将使用通用配置(允许所有开发扩展)${NC}"
|
||||||
|
EXTENSION_ID="*"
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
echo -e "\n${CYAN}正在打开 Chrome 扩展页面...${NC}"
|
||||||
|
open "chrome://extensions" 2>/dev/null || xdg-open "chrome://extensions" 2>/dev/null || echo "请手动打开 chrome://extensions"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}找到 Qwen CLI Bridge 扩展后,输入其 ID:${NC}"
|
||||||
|
read -p "> " EXTENSION_ID
|
||||||
|
if [[ -n "$EXTENSION_ID" && "$EXTENSION_ID" != "*" ]]; then
|
||||||
|
echo "$EXTENSION_ID" > "$SCRIPT_DIR/../.extension-id"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${RED}无效的选择${NC}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 创建 Native Host 目录
|
||||||
|
echo -e "\n${BLUE}配置 Native Host...${NC}"
|
||||||
|
mkdir -p "$MANIFEST_DIR"
|
||||||
|
|
||||||
|
# 创建 manifest 文件
|
||||||
|
MANIFEST_FILE="$MANIFEST_DIR/$HOST_NAME.json"
|
||||||
|
|
||||||
|
if [[ "$EXTENSION_ID" == "*" ]]; then
|
||||||
|
# 通用配置
|
||||||
|
cat > "$MANIFEST_FILE" << EOF
|
||||||
|
{
|
||||||
|
"name": "$HOST_NAME",
|
||||||
|
"description": "Native messaging host for Qwen CLI Bridge",
|
||||||
|
"path": "$SCRIPT_DIR/host.js",
|
||||||
|
"type": "stdio",
|
||||||
|
"allowed_origins": [
|
||||||
|
"chrome-extension://*/"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
echo -e "${GREEN}✓${NC} Native Host 已配置(通用模式)"
|
||||||
|
else
|
||||||
|
# 特定扩展 ID 配置
|
||||||
|
cat > "$MANIFEST_FILE" << EOF
|
||||||
|
{
|
||||||
|
"name": "$HOST_NAME",
|
||||||
|
"description": "Native messaging host for Qwen CLI Bridge",
|
||||||
|
"path": "$SCRIPT_DIR/host.js",
|
||||||
|
"type": "stdio",
|
||||||
|
"allowed_origins": [
|
||||||
|
"chrome-extension://$EXTENSION_ID/",
|
||||||
|
"chrome-extension://*/"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
echo -e "${GREEN}✓${NC} Native Host 已配置(扩展 ID: $EXTENSION_ID)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 验证配置
|
||||||
|
echo -e "\n${BLUE}验证配置...${NC}"
|
||||||
|
|
||||||
|
# 检查 host.js 是否存在
|
||||||
|
if [[ ! -f "$SCRIPT_DIR/host.js" ]]; then
|
||||||
|
echo -e "${RED}✗ host.js 文件不存在${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 确保 host.js 可执行
|
||||||
|
chmod +x "$SCRIPT_DIR/host.js"
|
||||||
|
echo -e "${GREEN}✓${NC} host.js 已设置为可执行"
|
||||||
|
|
||||||
|
# 检查 manifest 文件
|
||||||
|
if [[ -f "$MANIFEST_FILE" ]]; then
|
||||||
|
echo -e "${GREEN}✓${NC} Manifest 文件已创建: $MANIFEST_FILE"
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗ Manifest 文件创建失败${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}╔════════════════════════════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${GREEN}║ ║${NC}"
|
||||||
|
echo -e "${GREEN}║ ✅ Native Host 安装成功! ║${NC}"
|
||||||
|
echo -e "${GREEN}║ ║${NC}"
|
||||||
|
echo -e "${GREEN}╚════════════════════════════════════════════════════════════════╝${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 显示下一步
|
||||||
|
if [[ "$AUTO_DETECTED" == true ]]; then
|
||||||
|
echo -e "${CYAN}检测到扩展已安装,你可以直接使用了!${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e "使用方法:"
|
||||||
|
echo -e " 1. 点击 Chrome 工具栏的扩展图标"
|
||||||
|
echo -e " 2. 点击 'Connect to Qwen CLI'"
|
||||||
|
echo -e " 3. 开始使用各项功能"
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}下一步:${NC}"
|
||||||
|
echo -e " 1. 在 Chrome 中打开 ${CYAN}chrome://extensions/${NC}"
|
||||||
|
echo -e " 2. 开启${CYAN}「开发者模式」${NC}(右上角)"
|
||||||
|
echo -e " 3. 点击${CYAN}「加载已解压的扩展程序」${NC}"
|
||||||
|
echo -e " 4. 选择目录: ${CYAN}$SCRIPT_DIR/../extension${NC}"
|
||||||
|
echo -e " 5. 安装完成后,重新运行此脚本以更新配置"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}提示:${NC}"
|
||||||
|
echo -e " • 如需重新配置,随时可以重新运行此脚本"
|
||||||
|
echo -e " • 日志文件位置: /tmp/qwen-bridge-host.log"
|
||||||
|
echo -e " • 如遇问题,请查看: $SCRIPT_DIR/../docs/debugging.md"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 询问是否测试连接
|
||||||
|
if [[ "$AUTO_DETECTED" == true ]]; then
|
||||||
|
echo -e "${CYAN}是否测试 Native Host 连接?(y/n)${NC}"
|
||||||
|
read -p "> " TEST_CONNECTION
|
||||||
|
|
||||||
|
if [[ "$TEST_CONNECTION" == "y" ]] || [[ "$TEST_CONNECTION" == "Y" ]]; then
|
||||||
|
echo -e "\n${BLUE}测试连接...${NC}"
|
||||||
|
|
||||||
|
# 创建测试脚本
|
||||||
|
cat > /tmp/test-native-host.js << 'EOF'
|
||||||
|
const chrome = {
|
||||||
|
runtime: {
|
||||||
|
connectNative: () => {
|
||||||
|
console.log("Chrome API not available in Node.js environment");
|
||||||
|
console.log("请在 Chrome 扩展中测试连接");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 直接测试 host.js
|
||||||
|
const { spawn } = require('child_process');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const hostPath = process.argv[2];
|
||||||
|
if (!hostPath) {
|
||||||
|
console.error("Missing host path");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Testing host at:", hostPath);
|
||||||
|
|
||||||
|
const host = spawn('node', [hostPath], {
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe']
|
||||||
|
});
|
||||||
|
|
||||||
|
// 发送测试消息
|
||||||
|
const testMessage = JSON.stringify({ type: 'handshake', version: '1.0.0' });
|
||||||
|
const length = Buffer.allocUnsafe(4);
|
||||||
|
length.writeUInt32LE(Buffer.byteLength(testMessage), 0);
|
||||||
|
|
||||||
|
host.stdin.write(length);
|
||||||
|
host.stdin.write(testMessage);
|
||||||
|
|
||||||
|
// 读取响应
|
||||||
|
let responseBuffer = Buffer.alloc(0);
|
||||||
|
let messageLength = null;
|
||||||
|
|
||||||
|
host.stdout.on('data', (data) => {
|
||||||
|
responseBuffer = Buffer.concat([responseBuffer, data]);
|
||||||
|
|
||||||
|
if (messageLength === null && responseBuffer.length >= 4) {
|
||||||
|
messageLength = responseBuffer.readUInt32LE(0);
|
||||||
|
responseBuffer = responseBuffer.slice(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messageLength !== null && responseBuffer.length >= messageLength) {
|
||||||
|
const message = JSON.parse(responseBuffer.slice(0, messageLength).toString());
|
||||||
|
console.log("Response received:", message);
|
||||||
|
|
||||||
|
if (message.type === 'handshake_response') {
|
||||||
|
console.log("✅ Native Host 响应正常");
|
||||||
|
}
|
||||||
|
|
||||||
|
host.kill();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
host.on('error', (error) => {
|
||||||
|
console.error("❌ Host error:", error.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
console.error("❌ 测试超时");
|
||||||
|
host.kill();
|
||||||
|
process.exit(1);
|
||||||
|
}, 5000);
|
||||||
|
EOF
|
||||||
|
|
||||||
|
node /tmp/test-native-host.js "$SCRIPT_DIR/host.js"
|
||||||
|
rm /tmp/test-native-host.js
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}安装完成!${NC}"
|
||||||
18
packages/chrome-qwen-bridge/native-host/start.sh
Executable file
18
packages/chrome-qwen-bridge/native-host/start.sh
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Native Host 启动脚本
|
||||||
|
# Chrome 在 macOS 上需要这个包装脚本来正确启动 Node.js
|
||||||
|
|
||||||
|
# 获取脚本所在目录
|
||||||
|
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
|
||||||
|
# 日志文件
|
||||||
|
LOG_FILE="/tmp/qwen-bridge-host.log"
|
||||||
|
|
||||||
|
# 记录启动信息
|
||||||
|
echo "[$(date)] Native Host 启动..." >> "$LOG_FILE"
|
||||||
|
echo "[$(date)] 工作目录: $DIR" >> "$LOG_FILE"
|
||||||
|
echo "[$(date)] Node 路径: $(which node)" >> "$LOG_FILE"
|
||||||
|
|
||||||
|
# 启动 Node.js Native Host
|
||||||
|
exec /usr/bin/env node "$DIR/host.js" 2>> "$LOG_FILE"
|
||||||
43
packages/chrome-qwen-bridge/package.json
Normal file
43
packages/chrome-qwen-bridge/package.json
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name": "@qwen-code/chrome-bridge",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Chrome extension bridge for Qwen CLI - enables AI-powered browser interactions",
|
||||||
|
"private": true,
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/QwenLM/qwen-code.git",
|
||||||
|
"directory": "packages/chrome-qwen-bridge"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"chrome-extension",
|
||||||
|
"qwen",
|
||||||
|
"cli",
|
||||||
|
"bridge",
|
||||||
|
"native-messaging",
|
||||||
|
"mcp",
|
||||||
|
"ai"
|
||||||
|
],
|
||||||
|
"author": "Qwen Team",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"files": [
|
||||||
|
"extension/",
|
||||||
|
"native-host/",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"dev": "./debug.sh",
|
||||||
|
"install:extension": "./first-install.sh",
|
||||||
|
"install:host": "cd native-host && ./smart-install.sh",
|
||||||
|
"install:all": "./first-install.sh",
|
||||||
|
"dev:chrome": "open -a 'Google Chrome' --args --load-extension=$PWD/extension --auto-open-devtools-for-tabs",
|
||||||
|
"dev:server": "qwen server --port 8080",
|
||||||
|
"build": "./build.sh",
|
||||||
|
"package": "zip -r chrome-qwen-bridge.zip extension/",
|
||||||
|
"clean": "rm -rf dist *.zip /tmp/qwen-bridge-host.log /tmp/qwen-server.log .extension-id",
|
||||||
|
"logs": "tail -f /tmp/qwen-bridge-host.log",
|
||||||
|
"logs:qwen": "tail -f /tmp/qwen-server.log"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
61
packages/chrome-qwen-bridge/reload.sh
Executable file
61
packages/chrome-qwen-bridge/reload.sh
Executable file
@@ -0,0 +1,61 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 快速重新加载 Chrome 扩展的脚本
|
||||||
|
|
||||||
|
echo "🔄 重新加载 Chrome 扩展..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 获取扩展路径
|
||||||
|
EXTENSION_PATH="$PWD/extension"
|
||||||
|
|
||||||
|
# 检查扩展目录
|
||||||
|
if [ ! -d "$EXTENSION_PATH" ]; then
|
||||||
|
echo "❌ 错误: 扩展目录不存在: $EXTENSION_PATH"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "📂 扩展路径: $EXTENSION_PATH"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 提示用户操作步骤
|
||||||
|
echo "请按照以下步骤操作:"
|
||||||
|
echo ""
|
||||||
|
echo "1️⃣ 打开 Chrome 浏览器"
|
||||||
|
echo "2️⃣ 访问 chrome://extensions/"
|
||||||
|
echo "3️⃣ 点击右上角的 '开发者模式' 开关(如果尚未开启)"
|
||||||
|
echo "4️⃣ 如果扩展已加载:"
|
||||||
|
echo " - 找到 'Qwen CLI Bridge' 扩展"
|
||||||
|
echo " - 点击 '重新加载' 按钮 (🔄 图标)"
|
||||||
|
echo "5️⃣ 如果扩展未加载:"
|
||||||
|
echo " - 点击 '加载已解压的扩展程序'"
|
||||||
|
echo " - 选择以下目录:"
|
||||||
|
echo " $EXTENSION_PATH"
|
||||||
|
echo ""
|
||||||
|
echo "6️⃣ 点击扩展图标测试功能"
|
||||||
|
echo "7️⃣ 如有错误,按 F12 打开 DevTools 查看控制台"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 如果存在扩展 ID 文件,显示它
|
||||||
|
if [ -f ".extension-id" ]; then
|
||||||
|
EXTENSION_ID=$(cat .extension-id)
|
||||||
|
echo "📝 已保存的扩展 ID: $EXTENSION_ID"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 提供快速打开 Chrome 的命令
|
||||||
|
echo "💡 快速命令:"
|
||||||
|
echo " 打开扩展页面: open 'chrome://extensions/'"
|
||||||
|
echo " 查看后台日志: open 'chrome://extensions/?id=<扩展ID>'"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 询问是否要打开 Chrome 扩展页面
|
||||||
|
read -p "是否要自动打开 Chrome 扩展页面? (y/n) " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
open "chrome://extensions/"
|
||||||
|
echo ""
|
||||||
|
echo "✅ 已打开 Chrome 扩展页面"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🎉 准备完成!请在 Chrome 中重新加载扩展。"
|
||||||
24
packages/chrome-qwen-bridge/set-extension-id.sh
Executable file
24
packages/chrome-qwen-bridge/set-extension-id.sh
Executable file
@@ -0,0 +1,24 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "🔧 配置 Native Host 使用特定扩展 ID..."
|
||||||
|
|
||||||
|
EXTENSION_ID="cimaabkejokbhjkdnajgfniiolfjgbhd"
|
||||||
|
CONFIG_FILE="$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts/com.qwen.cli.bridge.json"
|
||||||
|
RUN_SCRIPT="$PWD/native-host/run.sh"
|
||||||
|
|
||||||
|
# 创建配置(使用特定扩展 ID)
|
||||||
|
cat > "$CONFIG_FILE" <<EOF
|
||||||
|
{
|
||||||
|
"name": "com.qwen.cli.bridge",
|
||||||
|
"description": "Native messaging host for Qwen CLI Bridge",
|
||||||
|
"path": "$RUN_SCRIPT",
|
||||||
|
"type": "stdio",
|
||||||
|
"allowed_origins": [
|
||||||
|
"chrome-extension://$EXTENSION_ID/"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "✅ 配置已更新(仅允许扩展 ID: $EXTENSION_ID)"
|
||||||
|
echo ""
|
||||||
|
cat "$CONFIG_FILE"
|
||||||
300
packages/chrome-qwen-bridge/start.sh
Executable file
300
packages/chrome-qwen-bridge/start.sh
Executable file
@@ -0,0 +1,300 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 快速启动脚本 - 适用于 macOS/Linux
|
||||||
|
# 一键启动所有服务进行调试
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# 颜色定义
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# 打印带颜色的消息
|
||||||
|
print_info() {
|
||||||
|
echo -e "${BLUE}[INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_success() {
|
||||||
|
echo -e "${GREEN}[✓]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_warning() {
|
||||||
|
echo -e "${YELLOW}[!]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_error() {
|
||||||
|
echo -e "${RED}[✗]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 获取脚本所在目录
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
EXTENSION_DIR="$SCRIPT_DIR/extension"
|
||||||
|
NATIVE_HOST_DIR="$SCRIPT_DIR/native-host"
|
||||||
|
|
||||||
|
# 清屏并显示标题
|
||||||
|
clear
|
||||||
|
echo "======================================"
|
||||||
|
echo " Qwen CLI Bridge - Quick Start"
|
||||||
|
echo "======================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 1. 检查 Chrome 是否安装
|
||||||
|
print_info "Checking Chrome installation..."
|
||||||
|
|
||||||
|
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||||
|
CHROME_PATH="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
||||||
|
if [[ ! -f "$CHROME_PATH" ]]; then
|
||||||
|
CHROME_PATH="/Applications/Chromium.app/Contents/MacOS/Chromium"
|
||||||
|
fi
|
||||||
|
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||||
|
CHROME_PATH=$(which google-chrome || which chromium-browser || which chromium || echo "")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$CHROME_PATH" ]] || [[ ! -f "$CHROME_PATH" ]]; then
|
||||||
|
print_error "Chrome not found! Please install Google Chrome first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_success "Chrome found: $CHROME_PATH"
|
||||||
|
|
||||||
|
# 2. 快速安装 Native Host (如果需要)
|
||||||
|
print_info "Setting up Native Host..."
|
||||||
|
|
||||||
|
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||||
|
MANIFEST_DIR="$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts"
|
||||||
|
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||||
|
MANIFEST_DIR="$HOME/.config/google-chrome/NativeMessagingHosts"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$MANIFEST_DIR"
|
||||||
|
|
||||||
|
# 创建 manifest
|
||||||
|
cat > "$MANIFEST_DIR/com.qwen.cli.bridge.json" << EOF
|
||||||
|
{
|
||||||
|
"name": "com.qwen.cli.bridge",
|
||||||
|
"description": "Native messaging host for Qwen CLI Bridge",
|
||||||
|
"path": "$NATIVE_HOST_DIR/host.js",
|
||||||
|
"type": "stdio",
|
||||||
|
"allowed_origins": [
|
||||||
|
"chrome-extension://*/",
|
||||||
|
"chrome-extension://jniepomhbdkeifkadbfolbcihcmfpfjo/"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
print_success "Native Host configured"
|
||||||
|
|
||||||
|
# 3. 检查 Qwen CLI
|
||||||
|
print_info "Checking Qwen CLI..."
|
||||||
|
|
||||||
|
if command -v qwen &> /dev/null; then
|
||||||
|
print_success "Qwen CLI is installed"
|
||||||
|
QWEN_VERSION=$(qwen --version 2>/dev/null || echo "unknown")
|
||||||
|
print_info "Version: $QWEN_VERSION"
|
||||||
|
|
||||||
|
# 尝试启动 Qwen server
|
||||||
|
print_info "Starting Qwen server on port 8080..."
|
||||||
|
|
||||||
|
# 检查端口是否被占用
|
||||||
|
if lsof -i:8080 &> /dev/null; then
|
||||||
|
print_warning "Port 8080 is already in use, skipping Qwen server start"
|
||||||
|
else
|
||||||
|
# 在后台启动 Qwen server
|
||||||
|
nohup qwen server --port 8080 > /tmp/qwen-server.log 2>&1 &
|
||||||
|
QWEN_PID=$!
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
if kill -0 $QWEN_PID 2>/dev/null; then
|
||||||
|
print_success "Qwen server started (PID: $QWEN_PID)"
|
||||||
|
echo $QWEN_PID > /tmp/qwen-server.pid
|
||||||
|
else
|
||||||
|
print_warning "Failed to start Qwen server, continuing anyway..."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_warning "Qwen CLI not installed - some features will be limited"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. 启动简单的测试服务器
|
||||||
|
print_info "Starting test server..."
|
||||||
|
|
||||||
|
# 创建简单的 Python HTTP 服务器
|
||||||
|
cat > /tmp/test-server.py << 'EOF'
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
import http.server
|
||||||
|
import socketserver
|
||||||
|
|
||||||
|
PORT = 3000
|
||||||
|
|
||||||
|
html_content = """
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Qwen CLI Bridge Test</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial;
|
||||||
|
padding: 40px;
|
||||||
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
background: white;
|
||||||
|
color: #333;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 10px;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
h1 { color: #667eea; }
|
||||||
|
button {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
button:hover { opacity: 0.9; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>🚀 Qwen CLI Bridge Test Page</h1>
|
||||||
|
<p>Extension debugging environment is ready!</p>
|
||||||
|
|
||||||
|
<h2>Quick Tests</h2>
|
||||||
|
<button onclick="console.log('Test log message')">Test Console Log</button>
|
||||||
|
<button onclick="console.error('Test error message')">Test Console Error</button>
|
||||||
|
<button onclick="fetch('/api/test').catch(e => console.error(e))">Test Network Request</button>
|
||||||
|
|
||||||
|
<h2>Instructions</h2>
|
||||||
|
<ol>
|
||||||
|
<li>Click the extension icon in Chrome toolbar</li>
|
||||||
|
<li>Click "Connect to Qwen CLI"</li>
|
||||||
|
<li>Try the various features</li>
|
||||||
|
<li>Open DevTools (F12) to see console output</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h2>Sample Content</h2>
|
||||||
|
<p>This is sample text content that can be extracted by the extension.</p>
|
||||||
|
<ul>
|
||||||
|
<li>Item 1: Lorem ipsum dolor sit amet</li>
|
||||||
|
<li>Item 2: Consectetur adipiscing elit</li>
|
||||||
|
<li>Item 3: Sed do eiusmod tempor</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
console.log('Test page loaded successfully');
|
||||||
|
console.info('Ready for debugging');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
class MyHandler(http.server.SimpleHTTPRequestHandler):
|
||||||
|
def do_GET(self):
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-type', 'text/html')
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(html_content.encode())
|
||||||
|
|
||||||
|
with socketserver.TCPServer(("", PORT), MyHandler) as httpd:
|
||||||
|
print(f"Test server running at http://localhost:{PORT}")
|
||||||
|
httpd.serve_forever()
|
||||||
|
EOF
|
||||||
|
|
||||||
|
python3 /tmp/test-server.py > /tmp/test-server.log 2>&1 &
|
||||||
|
TEST_SERVER_PID=$!
|
||||||
|
echo $TEST_SERVER_PID > /tmp/test-server.pid
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
print_success "Test server started at http://localhost:3000"
|
||||||
|
|
||||||
|
# 5. 启动 Chrome
|
||||||
|
print_info "Starting Chrome with extension..."
|
||||||
|
|
||||||
|
# Chrome 参数
|
||||||
|
CHROME_ARGS=(
|
||||||
|
"--load-extension=$EXTENSION_DIR"
|
||||||
|
"--auto-open-devtools-for-tabs"
|
||||||
|
"--no-first-run"
|
||||||
|
"--no-default-browser-check"
|
||||||
|
"--disable-default-apps"
|
||||||
|
"http://localhost:3000"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 启动 Chrome
|
||||||
|
"$CHROME_PATH" "${CHROME_ARGS[@]}" &
|
||||||
|
CHROME_PID=$!
|
||||||
|
|
||||||
|
print_success "Chrome started with extension loaded"
|
||||||
|
|
||||||
|
# 6. 显示状态和清理指令
|
||||||
|
echo ""
|
||||||
|
echo "======================================"
|
||||||
|
echo " ✅ All Services Running"
|
||||||
|
echo "======================================"
|
||||||
|
echo ""
|
||||||
|
echo "📌 Chrome: Running (PID: $CHROME_PID)"
|
||||||
|
echo "📌 Test Page: http://localhost:3000"
|
||||||
|
if [[ -n "${QWEN_PID:-}" ]]; then
|
||||||
|
echo "📌 Qwen Server: http://localhost:8080 (PID: $QWEN_PID)"
|
||||||
|
fi
|
||||||
|
echo "📌 Extension: Loaded in Chrome toolbar"
|
||||||
|
echo ""
|
||||||
|
echo "📝 Debug Locations:"
|
||||||
|
echo " • Extension Logs: Chrome DevTools Console"
|
||||||
|
echo " • Background Page: chrome://extensions → Service Worker"
|
||||||
|
echo " • Native Host Log: /tmp/qwen-bridge-host.log"
|
||||||
|
if [[ -n "${QWEN_PID:-}" ]]; then
|
||||||
|
echo " • Qwen Server Log: /tmp/qwen-server.log"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
echo "🛑 To stop all services, run: $SCRIPT_DIR/stop.sh"
|
||||||
|
echo " Or press Ctrl+C to stop this script"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 创建停止脚本
|
||||||
|
cat > "$SCRIPT_DIR/stop.sh" << 'STOP_SCRIPT'
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "Stopping services..."
|
||||||
|
|
||||||
|
# 停止 Qwen server
|
||||||
|
if [[ -f /tmp/qwen-server.pid ]]; then
|
||||||
|
PID=$(cat /tmp/qwen-server.pid)
|
||||||
|
if kill -0 $PID 2>/dev/null; then
|
||||||
|
kill $PID
|
||||||
|
echo "✓ Qwen server stopped"
|
||||||
|
fi
|
||||||
|
rm /tmp/qwen-server.pid
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 停止测试服务器
|
||||||
|
if [[ -f /tmp/test-server.pid ]]; then
|
||||||
|
PID=$(cat /tmp/test-server.pid)
|
||||||
|
if kill -0 $PID 2>/dev/null; then
|
||||||
|
kill $PID
|
||||||
|
echo "✓ Test server stopped"
|
||||||
|
fi
|
||||||
|
rm /tmp/test-server.pid
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✓ All services stopped"
|
||||||
|
STOP_SCRIPT
|
||||||
|
|
||||||
|
chmod +x "$SCRIPT_DIR/stop.sh"
|
||||||
|
|
||||||
|
# 等待用户中断
|
||||||
|
trap 'echo "Stopping services..."; $SCRIPT_DIR/stop.sh; exit 0' INT TERM
|
||||||
|
|
||||||
|
# 保持脚本运行
|
||||||
|
while true; do
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
166
packages/chrome-qwen-bridge/test-connection.sh
Executable file
166
packages/chrome-qwen-bridge/test-connection.sh
Executable file
@@ -0,0 +1,166 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "🔗 Chrome Extension 连接完整测试"
|
||||||
|
echo "================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 颜色定义
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
# 扩展 ID
|
||||||
|
EXTENSION_ID="cimaabkejokbhjkdnajgfniiolfjgbhd"
|
||||||
|
|
||||||
|
# Step 1: 测试 Native Host 直接响应
|
||||||
|
echo -e "${BLUE}Step 1: 测试 Native Host 直接响应${NC}"
|
||||||
|
echo "----------------------------------------"
|
||||||
|
|
||||||
|
# 创建测试 Python 脚本
|
||||||
|
cat > /tmp/test_native.py << 'EOF'
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
import json
|
||||||
|
import struct
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
def test_native_host():
|
||||||
|
host_path = sys.argv[1] if len(sys.argv) > 1 else './native-host/run.sh'
|
||||||
|
|
||||||
|
if not os.path.exists(host_path):
|
||||||
|
print(f"❌ Host 文件不存在: {host_path}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 启动 Native Host
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
[host_path],
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE
|
||||||
|
)
|
||||||
|
|
||||||
|
# 发送握手消息
|
||||||
|
message = {"type": "handshake", "version": "1.0.0"}
|
||||||
|
encoded = json.dumps(message).encode('utf-8')
|
||||||
|
proc.stdin.write(struct.pack('<I', len(encoded)))
|
||||||
|
proc.stdin.write(encoded)
|
||||||
|
proc.stdin.flush()
|
||||||
|
|
||||||
|
# 读取响应
|
||||||
|
raw_length = proc.stdout.read(4)
|
||||||
|
if raw_length:
|
||||||
|
message_length = struct.unpack('<I', raw_length)[0]
|
||||||
|
response = proc.stdout.read(message_length)
|
||||||
|
result = json.loads(response.decode('utf-8'))
|
||||||
|
print(f"✅ Native Host 响应成功")
|
||||||
|
print(f" 响应内容: {json.dumps(result, indent=2)}")
|
||||||
|
proc.terminate()
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print("❌ Native Host 无响应")
|
||||||
|
proc.terminate()
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 测试失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_native_host()
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod +x /tmp/test_native.py
|
||||||
|
python3 /tmp/test_native.py ./native-host/run.sh
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Step 2: 检查 Native Host 配置
|
||||||
|
echo -e "${BLUE}Step 2: 检查 Native Host 配置${NC}"
|
||||||
|
echo "----------------------------------------"
|
||||||
|
CONFIG_FILE="$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts/com.qwen.cli.bridge.json"
|
||||||
|
|
||||||
|
if [ -f "$CONFIG_FILE" ]; then
|
||||||
|
echo -e "${GREEN}✓${NC} 配置文件存在"
|
||||||
|
|
||||||
|
# 检查路径
|
||||||
|
PATH_IN_CONFIG=$(grep '"path"' "$CONFIG_FILE" | sed 's/.*"path".*:.*"\(.*\)".*/\1/')
|
||||||
|
if [ -f "$PATH_IN_CONFIG" ]; then
|
||||||
|
echo -e "${GREEN}✓${NC} 配置的路径有效: $PATH_IN_CONFIG"
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗${NC} 配置的路径无效: $PATH_IN_CONFIG"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 检查扩展 ID
|
||||||
|
if grep -q "chrome-extension://$EXTENSION_ID/" "$CONFIG_FILE"; then
|
||||||
|
echo -e "${GREEN}✓${NC} 配置包含正确的扩展 ID"
|
||||||
|
elif grep -q 'chrome-extension://\*/' "$CONFIG_FILE"; then
|
||||||
|
echo -e "${YELLOW}⚠${NC} 配置使用通配符 (接受所有扩展)"
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗${NC} 配置不包含扩展 ID"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗${NC} 配置文件不存在"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Step 3: 检查 Chrome 进程
|
||||||
|
echo -e "${BLUE}Step 3: 检查 Chrome 状态${NC}"
|
||||||
|
echo "----------------------------------------"
|
||||||
|
if pgrep -x "Google Chrome" > /dev/null; then
|
||||||
|
echo -e "${GREEN}✓${NC} Chrome 正在运行"
|
||||||
|
|
||||||
|
# 获取 Chrome 版本
|
||||||
|
CHROME_VERSION=$("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --version 2>/dev/null | cut -d' ' -f3)
|
||||||
|
if [ -n "$CHROME_VERSION" ]; then
|
||||||
|
echo " 版本: $CHROME_VERSION"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}⚠${NC} Chrome 未运行"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Step 4: 提供测试指令
|
||||||
|
echo -e "${BLUE}Step 4: 手动测试步骤${NC}"
|
||||||
|
echo "----------------------------------------"
|
||||||
|
echo "请按以下步骤进行手动测试:"
|
||||||
|
echo ""
|
||||||
|
echo "1. 打开 Chrome 并访问: chrome://extensions/"
|
||||||
|
echo " 扩展 ID 应为: ${EXTENSION_ID}"
|
||||||
|
echo ""
|
||||||
|
echo "2. 重新加载扩展:"
|
||||||
|
echo " - 找到 'Qwen CLI Bridge'"
|
||||||
|
echo " - 点击重新加载按钮 🔄"
|
||||||
|
echo ""
|
||||||
|
echo "3. 查看后台日志:"
|
||||||
|
echo " - 点击 'Service Worker' 链接"
|
||||||
|
echo " - 在控制台中查看日志"
|
||||||
|
echo ""
|
||||||
|
echo "4. 测试连接:"
|
||||||
|
echo " - 点击扩展图标"
|
||||||
|
echo " - 点击 'Connect to Qwen CLI'"
|
||||||
|
echo " - 观察控制台输出"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Step 5: 提供快速命令
|
||||||
|
echo -e "${BLUE}Step 5: 快速命令${NC}"
|
||||||
|
echo "----------------------------------------"
|
||||||
|
echo "打开扩展管理页面:"
|
||||||
|
echo -e "${YELLOW} open 'chrome://extensions/'${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "查看 Service Worker:"
|
||||||
|
echo -e "${YELLOW} open 'chrome://extensions/?id=$EXTENSION_ID'${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "查看 Native Host 日志:"
|
||||||
|
echo -e "${YELLOW} tail -f /tmp/qwen-bridge-host.log${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 清理临时文件
|
||||||
|
rm -f /tmp/test_native.py
|
||||||
|
|
||||||
|
echo "================================"
|
||||||
|
echo -e "${GREEN}测试完成!${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "如果连接仍然失败,请检查 Service Worker 控制台的具体错误信息。"
|
||||||
44
packages/chrome-qwen-bridge/test-handshake.sh
Executable file
44
packages/chrome-qwen-bridge/test-handshake.sh
Executable file
@@ -0,0 +1,44 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Native Host 握手测试脚本
|
||||||
|
|
||||||
|
echo "🤝 测试 Native Host 握手..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 清理旧日志
|
||||||
|
> /tmp/qwen-bridge-host.log
|
||||||
|
|
||||||
|
# 握手消息
|
||||||
|
TEST_MSG='{"type":"handshake","version":"1.0.0"}'
|
||||||
|
|
||||||
|
# 计算消息长度
|
||||||
|
MSG_LEN=${#TEST_MSG}
|
||||||
|
|
||||||
|
printf "发送握手消息: $TEST_MSG\n"
|
||||||
|
printf "消息长度: $MSG_LEN bytes\n\n"
|
||||||
|
|
||||||
|
# 创建一个临时文件来存储二进制数据
|
||||||
|
TEMP_FILE=$(mktemp)
|
||||||
|
|
||||||
|
# 写入长度头(4字节小端序)
|
||||||
|
printf "\\x$(printf '%02x' $((MSG_LEN & 0xff)))" > "$TEMP_FILE"
|
||||||
|
printf "\\x$(printf '%02x' $(((MSG_LEN >> 8) & 0xff)))" >> "$TEMP_FILE"
|
||||||
|
printf "\\x$(printf '%02x' $(((MSG_LEN >> 16) & 0xff)))" >> "$TEMP_FILE"
|
||||||
|
printf "\\x$(printf '%02x' $(((MSG_LEN >> 24) & 0xff)))" >> "$TEMP_FILE"
|
||||||
|
# 写入消息内容
|
||||||
|
printf "$TEST_MSG" >> "$TEMP_FILE"
|
||||||
|
|
||||||
|
echo "启动 Native Host 并发送握手消息..."
|
||||||
|
cat "$TEMP_FILE" | timeout 2 ./native-host/start.sh 2>&1 | od -c | head -10
|
||||||
|
|
||||||
|
# 清理临时文件
|
||||||
|
rm -f "$TEMP_FILE"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "检查日志文件..."
|
||||||
|
if [ -f /tmp/qwen-bridge-host.log ]; then
|
||||||
|
echo "📋 日志内容:"
|
||||||
|
tail -20 /tmp/qwen-bridge-host.log
|
||||||
|
else
|
||||||
|
echo "⚠️ 未找到日志文件"
|
||||||
|
fi
|
||||||
41
packages/chrome-qwen-bridge/test-native-host.sh
Executable file
41
packages/chrome-qwen-bridge/test-native-host.sh
Executable file
@@ -0,0 +1,41 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Native Host 连接测试脚本
|
||||||
|
|
||||||
|
echo "🔍 测试 Native Host 连接..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 清理旧日志
|
||||||
|
> /tmp/qwen-bridge-host.log
|
||||||
|
|
||||||
|
# 测试消息
|
||||||
|
TEST_MSG='{"type":"PING"}'
|
||||||
|
|
||||||
|
# 计算消息长度(4字节小端序)
|
||||||
|
MSG_LEN=${#TEST_MSG}
|
||||||
|
|
||||||
|
# 将长度转换为4字节小端序
|
||||||
|
printf "发送测试消息: $TEST_MSG\n"
|
||||||
|
printf "消息长度: $MSG_LEN bytes\n\n"
|
||||||
|
|
||||||
|
# 发送消息到 Native Host
|
||||||
|
echo "启动 Native Host 并发送测试消息..."
|
||||||
|
(
|
||||||
|
# 发送长度头(4字节)
|
||||||
|
printf "\\x$(printf '%02x' $((MSG_LEN & 0xff)))"
|
||||||
|
printf "\\x$(printf '%02x' $(((MSG_LEN >> 8) & 0xff)))"
|
||||||
|
printf "\\x$(printf '%02x' $(((MSG_LEN >> 16) & 0xff)))"
|
||||||
|
printf "\\x$(printf '%02x' $(((MSG_LEN >> 24) & 0xff)))"
|
||||||
|
# 发送消息内容
|
||||||
|
printf "$TEST_MSG"
|
||||||
|
) | ./native-host/start.sh 2>&1 | head -c 100
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo ""
|
||||||
|
echo "检查日志文件..."
|
||||||
|
if [ -f /tmp/qwen-bridge-host.log ]; then
|
||||||
|
echo "📋 日志内容:"
|
||||||
|
cat /tmp/qwen-bridge-host.log
|
||||||
|
else
|
||||||
|
echo "⚠️ 未找到日志文件"
|
||||||
|
fi
|
||||||
44
packages/chrome-qwen-bridge/test.sh
Executable file
44
packages/chrome-qwen-bridge/test.sh
Executable file
@@ -0,0 +1,44 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 快速测试脚本
|
||||||
|
|
||||||
|
echo "🔍 检查 Chrome 扩展配置..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 检查目录结构
|
||||||
|
echo "📂 目录结构:"
|
||||||
|
ls -la extension/ | grep -E "background|content|icons|options|popup"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 检查 manifest.json
|
||||||
|
echo "📄 Manifest 配置:"
|
||||||
|
cat extension/manifest.json | grep -E "options_ui|version|name" | head -5
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 检查关键文件
|
||||||
|
echo "✅ 文件检查:"
|
||||||
|
FILES=(
|
||||||
|
"extension/manifest.json"
|
||||||
|
"extension/popup/popup.html"
|
||||||
|
"extension/popup/popup.js"
|
||||||
|
"extension/options/options.html"
|
||||||
|
"extension/options/options.js"
|
||||||
|
"extension/background/service-worker.js"
|
||||||
|
"extension/content/content-script.js"
|
||||||
|
)
|
||||||
|
|
||||||
|
for file in "${FILES[@]}"; do
|
||||||
|
if [[ -f "$file" ]]; then
|
||||||
|
echo " ✓ $file"
|
||||||
|
else
|
||||||
|
echo " ✗ $file - 缺失!"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "💡 提示:"
|
||||||
|
echo " 1. 在 Chrome 中重新加载扩展 (chrome://extensions/)"
|
||||||
|
echo " 2. 点击扩展图标测试功能"
|
||||||
|
echo " 3. 如有错误,查看 Chrome DevTools Console"
|
||||||
|
echo ""
|
||||||
|
echo "🚀 运行 'npm run dev' 启动完整调试环境"
|
||||||
90
packages/chrome-qwen-bridge/test_fix.py
Executable file
90
packages/chrome-qwen-bridge/test_fix.py
Executable file
@@ -0,0 +1,90 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import json
|
||||||
|
import struct
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
def send_message(proc, message):
|
||||||
|
"""Send a message to the Native Host"""
|
||||||
|
encoded = json.dumps(message).encode('utf-8')
|
||||||
|
proc.stdin.write(struct.pack('<I', len(encoded)))
|
||||||
|
proc.stdin.write(encoded)
|
||||||
|
proc.stdin.flush()
|
||||||
|
|
||||||
|
def read_message(proc):
|
||||||
|
"""Read a message from the Native Host"""
|
||||||
|
raw_length = proc.stdout.read(4)
|
||||||
|
if not raw_length:
|
||||||
|
return None
|
||||||
|
message_length = struct.unpack('<I', raw_length)[0]
|
||||||
|
message = proc.stdout.read(message_length)
|
||||||
|
return json.loads(message.decode('utf-8'))
|
||||||
|
|
||||||
|
def test_native_host():
|
||||||
|
"""Test the Native Host with various operations"""
|
||||||
|
print("🔍 Testing Native Host after fix...")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Start the Native Host
|
||||||
|
host_path = './native-host/run.sh'
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
[host_path],
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Test 1: Handshake
|
||||||
|
print("1. Testing handshake...")
|
||||||
|
send_message(proc, {"type": "handshake", "version": "1.0.0"})
|
||||||
|
response = read_message(proc)
|
||||||
|
print(f" ✓ Handshake response: {response['type']}")
|
||||||
|
print(f" Qwen installed: {response.get('qwenInstalled', False)}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Test 2: Get status (should not crash with null pid)
|
||||||
|
print("2. Testing get_status...")
|
||||||
|
send_message(proc, {"type": "get_status"})
|
||||||
|
response = read_message(proc)
|
||||||
|
if response and 'data' in response:
|
||||||
|
print(f" ✓ Status response received")
|
||||||
|
print(f" Qwen PID: {response['data'].get('qwenPid', 'None')}")
|
||||||
|
print(f" Qwen Status: {response['data'].get('qwenStatus', 'Unknown')}")
|
||||||
|
else:
|
||||||
|
print(f" Response: {response}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Test 3: Try to start Qwen (might fail if not installed)
|
||||||
|
print("3. Testing start_qwen...")
|
||||||
|
send_message(proc, {
|
||||||
|
"type": "start_qwen",
|
||||||
|
"config": {
|
||||||
|
"httpPort": 8080,
|
||||||
|
"mcpServers": []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
response = read_message(proc)
|
||||||
|
if response:
|
||||||
|
if response.get('success'):
|
||||||
|
print(f" ✓ Qwen started successfully")
|
||||||
|
if response.get('data'):
|
||||||
|
print(f" PID: {response['data'].get('pid', 'None')}")
|
||||||
|
else:
|
||||||
|
print(f" ⚠ Qwen start failed: {response.get('error', 'Unknown error')}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("✅ All tests completed without crashes!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error during testing: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
finally:
|
||||||
|
proc.terminate()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_native_host()
|
||||||
69
packages/chrome-qwen-bridge/test_native_host.py
Executable file
69
packages/chrome-qwen-bridge/test_native_host.py
Executable file
@@ -0,0 +1,69 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import json
|
||||||
|
import struct
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def send_message(proc, message):
|
||||||
|
"""Send a message to the Native Host"""
|
||||||
|
encoded = json.dumps(message).encode('utf-8')
|
||||||
|
# Write message length (4 bytes, little-endian)
|
||||||
|
proc.stdin.write(struct.pack('<I', len(encoded)))
|
||||||
|
# Write message
|
||||||
|
proc.stdin.write(encoded)
|
||||||
|
proc.stdin.flush()
|
||||||
|
|
||||||
|
def read_message(proc):
|
||||||
|
"""Read a message from the Native Host"""
|
||||||
|
# Read message length (4 bytes)
|
||||||
|
raw_length = proc.stdout.read(4)
|
||||||
|
if not raw_length:
|
||||||
|
return None
|
||||||
|
|
||||||
|
message_length = struct.unpack('<I', raw_length)[0]
|
||||||
|
# Read message
|
||||||
|
message = proc.stdout.read(message_length)
|
||||||
|
return json.loads(message.decode('utf-8'))
|
||||||
|
|
||||||
|
def test_native_host():
|
||||||
|
"""Test the Native Host connection"""
|
||||||
|
print("🔍 Testing Native Host connection...")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Start the Native Host
|
||||||
|
host_path = './native-host/start.sh'
|
||||||
|
try:
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
[host_path],
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test 1: Handshake
|
||||||
|
print("1. Sending handshake...")
|
||||||
|
send_message(proc, {"type": "handshake", "version": "1.0.0"})
|
||||||
|
response = read_message(proc)
|
||||||
|
print(f" Response: {response}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Test 2: Get status
|
||||||
|
print("2. Getting status...")
|
||||||
|
send_message(proc, {"type": "get_status"})
|
||||||
|
response = read_message(proc)
|
||||||
|
print(f" Response: {response}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Terminate
|
||||||
|
proc.terminate()
|
||||||
|
print("✅ Native Host is working correctly!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error: {e}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(test_native_host())
|
||||||
16
packages/chrome-qwen-bridge/tsconfig.json
Normal file
16
packages/chrome-qwen-bridge/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./",
|
||||||
|
"composite": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"extension/**/*",
|
||||||
|
"native-host/**/*"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"dist"
|
||||||
|
]
|
||||||
|
}
|
||||||
26
packages/chrome-qwen-bridge/update-host-config.sh
Executable file
26
packages/chrome-qwen-bridge/update-host-config.sh
Executable file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "🔧 更新 Native Host 配置..."
|
||||||
|
|
||||||
|
CONFIG_FILE="$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts/com.qwen.cli.bridge.json"
|
||||||
|
RUN_SCRIPT="$PWD/native-host/run.sh"
|
||||||
|
|
||||||
|
# 创建新的配置
|
||||||
|
cat > "$CONFIG_FILE" <<EOF
|
||||||
|
{
|
||||||
|
"name": "com.qwen.cli.bridge",
|
||||||
|
"description": "Native messaging host for Qwen CLI Bridge",
|
||||||
|
"path": "$RUN_SCRIPT",
|
||||||
|
"type": "stdio",
|
||||||
|
"allowed_origins": ["chrome-extension://*/"]
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "✅ 配置已更新"
|
||||||
|
echo ""
|
||||||
|
echo "配置内容:"
|
||||||
|
cat "$CONFIG_FILE"
|
||||||
|
echo ""
|
||||||
|
echo "现在请:"
|
||||||
|
echo "1. 重新加载 Chrome 扩展 (chrome://extensions/)"
|
||||||
|
echo "2. 点击扩展图标测试连接"
|
||||||
203
packages/chrome-qwen-bridge/verify.sh
Executable file
203
packages/chrome-qwen-bridge/verify.sh
Executable file
@@ -0,0 +1,203 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Chrome Qwen Bridge 完整性检查脚本
|
||||||
|
|
||||||
|
echo "======================================"
|
||||||
|
echo " Chrome Qwen Bridge 健康检查 🏥 "
|
||||||
|
echo "======================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 颜色定义
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# 检查函数
|
||||||
|
check_file() {
|
||||||
|
if [ -f "$1" ]; then
|
||||||
|
echo -e "${GREEN}✓${NC} $2"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗${NC} $2 - 缺失!"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_dir() {
|
||||||
|
if [ -d "$1" ]; then
|
||||||
|
echo -e "${GREEN}✓${NC} $2"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗${NC} $2 - 缺失!"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 1. 检查目录结构
|
||||||
|
echo -e "${BLUE}📂 检查目录结构${NC}"
|
||||||
|
echo "-------------------"
|
||||||
|
check_dir "extension" "扩展主目录"
|
||||||
|
check_dir "extension/background" "后台脚本目录"
|
||||||
|
check_dir "extension/content" "内容脚本目录"
|
||||||
|
check_dir "extension/popup" "弹窗目录"
|
||||||
|
check_dir "extension/options" "选项页目录"
|
||||||
|
check_dir "extension/icons" "图标目录"
|
||||||
|
check_dir "native-host" "Native Host 目录"
|
||||||
|
check_dir "docs" "文档目录"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 2. 检查核心文件
|
||||||
|
echo -e "${BLUE}📄 检查核心文件${NC}"
|
||||||
|
echo "-------------------"
|
||||||
|
ERROR_COUNT=0
|
||||||
|
|
||||||
|
# Manifest 文件
|
||||||
|
if ! check_file "extension/manifest.json" "Manifest V3 配置"; then
|
||||||
|
((ERROR_COUNT++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 后台脚本
|
||||||
|
if ! check_file "extension/background/service-worker.js" "Service Worker"; then
|
||||||
|
((ERROR_COUNT++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 内容脚本
|
||||||
|
if ! check_file "extension/content/content-script.js" "内容脚本"; then
|
||||||
|
((ERROR_COUNT++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 弹窗文件
|
||||||
|
if ! check_file "extension/popup/popup.html" "弹窗 HTML"; then
|
||||||
|
((ERROR_COUNT++))
|
||||||
|
fi
|
||||||
|
if ! check_file "extension/popup/popup.js" "弹窗脚本"; then
|
||||||
|
((ERROR_COUNT++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 选项页文件
|
||||||
|
if ! check_file "extension/options/options.html" "选项页 HTML"; then
|
||||||
|
((ERROR_COUNT++))
|
||||||
|
fi
|
||||||
|
if ! check_file "extension/options/options.js" "选项页脚本"; then
|
||||||
|
((ERROR_COUNT++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Native Host 文件
|
||||||
|
if ! check_file "native-host/host.js" "Native Host 脚本"; then
|
||||||
|
((ERROR_COUNT++))
|
||||||
|
fi
|
||||||
|
if ! check_file "native-host/com.qwen.bridge.json.template" "Native Host 配置模板"; then
|
||||||
|
((ERROR_COUNT++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 3. 检查 Manifest 配置
|
||||||
|
echo -e "${BLUE}🔧 检查 Manifest 配置${NC}"
|
||||||
|
echo "-------------------"
|
||||||
|
if [ -f "extension/manifest.json" ]; then
|
||||||
|
# 检查关键字段
|
||||||
|
if grep -q '"manifest_version": 3' extension/manifest.json; then
|
||||||
|
echo -e "${GREEN}✓${NC} Manifest V3"
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗${NC} 不是 Manifest V3"
|
||||||
|
((ERROR_COUNT++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -q '"options_ui"' extension/manifest.json; then
|
||||||
|
echo -e "${GREEN}✓${NC} options_ui 配置正确"
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗${NC} 缺少 options_ui 配置"
|
||||||
|
((ERROR_COUNT++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -q '"nativeMessaging"' extension/manifest.json; then
|
||||||
|
echo -e "${GREEN}✓${NC} Native Messaging 权限"
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}⚠${NC} 可能缺少 nativeMessaging 权限"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 4. 检查安装脚本
|
||||||
|
echo -e "${BLUE}🛠 检查安装脚本${NC}"
|
||||||
|
echo "-------------------"
|
||||||
|
check_file "first-install.sh" "首次安装脚本"
|
||||||
|
check_file "native-host/smart-install.sh" "智能安装脚本"
|
||||||
|
check_file "debug.sh" "调试脚本"
|
||||||
|
check_file "test.sh" "测试脚本"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 5. 检查 Native Host 安装状态
|
||||||
|
echo -e "${BLUE}🔌 检查 Native Host 安装状态${NC}"
|
||||||
|
echo "-------------------"
|
||||||
|
NATIVE_HOST_PATH="$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts/com.qwen.bridge.json"
|
||||||
|
if [ -f "$NATIVE_HOST_PATH" ]; then
|
||||||
|
echo -e "${GREEN}✓${NC} Native Host 已安装"
|
||||||
|
echo " 位置: $NATIVE_HOST_PATH"
|
||||||
|
|
||||||
|
# 检查配置的扩展 ID
|
||||||
|
if grep -q "chrome-extension://" "$NATIVE_HOST_PATH"; then
|
||||||
|
INSTALLED_ID=$(grep -o 'chrome-extension://[^/]*' "$NATIVE_HOST_PATH" | cut -d'/' -f3)
|
||||||
|
echo " 配置的扩展 ID: $INSTALLED_ID"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}⚠${NC} Native Host 未安装"
|
||||||
|
echo " 请运行: npm run install:host"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 6. 检查扩展 ID 记录
|
||||||
|
echo -e "${BLUE}🆔 检查扩展 ID 记录${NC}"
|
||||||
|
echo "-------------------"
|
||||||
|
if [ -f ".extension-id" ]; then
|
||||||
|
SAVED_ID=$(cat .extension-id)
|
||||||
|
echo -e "${GREEN}✓${NC} 已保存扩展 ID: $SAVED_ID"
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}⚠${NC} 未保存扩展 ID"
|
||||||
|
echo " 首次安装后会自动保存"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 7. 检查 Node.js 环境
|
||||||
|
echo -e "${BLUE}📦 检查 Node.js 环境${NC}"
|
||||||
|
echo "-------------------"
|
||||||
|
if command -v node &> /dev/null; then
|
||||||
|
NODE_VERSION=$(node --version)
|
||||||
|
echo -e "${GREEN}✓${NC} Node.js 已安装: $NODE_VERSION"
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗${NC} Node.js 未安装"
|
||||||
|
((ERROR_COUNT++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v npm &> /dev/null; then
|
||||||
|
NPM_VERSION=$(npm --version)
|
||||||
|
echo -e "${GREEN}✓${NC} npm 已安装: $NPM_VERSION"
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗${NC} npm 未安装"
|
||||||
|
((ERROR_COUNT++))
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 8. 总结
|
||||||
|
echo "======================================"
|
||||||
|
if [ $ERROR_COUNT -eq 0 ]; then
|
||||||
|
echo -e "${GREEN}✅ 所有检查通过!${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "下一步操作:"
|
||||||
|
echo "1. 运行 'npm run dev' 启动调试"
|
||||||
|
echo "2. 或运行 './reload.sh' 重新加载扩展"
|
||||||
|
else
|
||||||
|
echo -e "${RED}❌ 发现 $ERROR_COUNT 个问题${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "建议操作:"
|
||||||
|
if [ ! -f "$NATIVE_HOST_PATH" ]; then
|
||||||
|
echo "• 运行 'npm run install:host' 安装 Native Host"
|
||||||
|
fi
|
||||||
|
if [ $ERROR_COUNT -gt 0 ]; then
|
||||||
|
echo "• 检查上述错误并修复缺失的文件"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo "======================================"
|
||||||
Reference in New Issue
Block a user