新增通用的浏览器自动化扩展插件
This commit is contained in:
@@ -90,3 +90,15 @@ func (*Twitter) Register1(ctx *gin.Context) {
|
|||||||
"data": res.Data,
|
"data": res.Data,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (*Twitter) Test(ctx *gin.Context) {
|
||||||
|
// 打开一个浏览器并导入扩展
|
||||||
|
var Browser libs.Browser
|
||||||
|
obj := Browser.GetNewBrowser(&libs.Fingerprint{})
|
||||||
|
obj.Page.MustNavigate("https://baidu.com").MustWaitLoad()
|
||||||
|
ctx.JSON(http.StatusOK, gin.H{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "成功",
|
||||||
|
"data": "",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
93
expand/general-auto/INSTALL.md
Normal file
93
expand/general-auto/INSTALL.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# General Auto 扩展安装说明
|
||||||
|
|
||||||
|
## 当前状态
|
||||||
|
扩展已简化,移除了图标依赖,应该可以正常加载。
|
||||||
|
|
||||||
|
## 手动安装测试步骤
|
||||||
|
|
||||||
|
1. **打开 Chrome 扩展页面**
|
||||||
|
- 在浏览器地址栏输入:`chrome://extensions/`
|
||||||
|
- 或者:菜单 → 更多工具 → 扩展程序
|
||||||
|
|
||||||
|
2. **启用开发者模式**
|
||||||
|
- 在右上角打开"开发者模式"开关
|
||||||
|
|
||||||
|
3. **加载扩展**
|
||||||
|
- 点击"加载已解压的扩展程序"
|
||||||
|
- 选择目录:`d:\Project\go-account-register\expand\general-auto`
|
||||||
|
|
||||||
|
4. **验证扩展**
|
||||||
|
- 应该看到扩展名称:**General Auto**
|
||||||
|
- 版本:1.0.0
|
||||||
|
- 状态:已启用
|
||||||
|
|
||||||
|
5. **测试扩展是否工作**
|
||||||
|
- 打开任意网页(如 https://www.google.com)
|
||||||
|
- 按 F12 打开控制台
|
||||||
|
- 输入:`console.log(window.GeneralAuto)`
|
||||||
|
- 应该看到扩展对象而不是 undefined
|
||||||
|
|
||||||
|
## 程序化加载问题排查
|
||||||
|
|
||||||
|
如果通过 Rod 加载仍然失败,可能的原因:
|
||||||
|
|
||||||
|
### 1. 路径问题
|
||||||
|
检查日志输出的"扩展目录"是否正确:
|
||||||
|
```
|
||||||
|
扩展目录: d:\Project\go-account-register\expand\general-auto
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. user-data-dir 冲突
|
||||||
|
临时用户数据目录可能会影响扩展加载。尝试使用固定目录。
|
||||||
|
|
||||||
|
### 3. Chrome 版本兼容性
|
||||||
|
Manifest V3 需要 Chrome 88+ 版本。
|
||||||
|
|
||||||
|
## 替代方案
|
||||||
|
|
||||||
|
如果自动加载仍有问题,可以使用以下方法:
|
||||||
|
|
||||||
|
### 方案 1: 使用固定的 user-data-dir
|
||||||
|
```go
|
||||||
|
userDataDir := "user-data/with-extension"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方案 2: 手动复制扩展到 user-data 目录
|
||||||
|
程序启动前,将扩展复制到用户数据目录的 Extensions 文件夹。
|
||||||
|
|
||||||
|
### 方案 3: 使用打包的扩展
|
||||||
|
```bash
|
||||||
|
# 在 general-auto 目录打包
|
||||||
|
chrome --pack-extension=d:\Project\go-account-register\expand\general-auto
|
||||||
|
```
|
||||||
|
|
||||||
|
然后使用 .crx 文件加载。
|
||||||
|
|
||||||
|
## 调试建议
|
||||||
|
|
||||||
|
在 chrome.go 中添加更多日志:
|
||||||
|
```go
|
||||||
|
log.Println("扩展目录:", expandPath)
|
||||||
|
log.Println("用户数据目录:", absPath)
|
||||||
|
log.Println("Chrome 路径:", path)
|
||||||
|
|
||||||
|
// 验证扩展目录是否存在
|
||||||
|
if _, err := os.Stat(expandPath); os.IsNotExist(err) {
|
||||||
|
log.Fatal("扩展目录不存在:", expandPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证 manifest.json 是否存在
|
||||||
|
manifestPath := filepath.Join(expandPath, "manifest.json")
|
||||||
|
if _, err := os.Stat(manifestPath); os.IsNotExist(err) {
|
||||||
|
log.Fatal("manifest.json 不存在:", manifestPath)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 当前 manifest.json 配置
|
||||||
|
|
||||||
|
已移除图标依赖,使用最小化配置:
|
||||||
|
- ✅ manifest_version: 3
|
||||||
|
- ✅ 基础权限:tabs, cookies, storage, scripting
|
||||||
|
- ✅ background service worker
|
||||||
|
- ✅ content scripts
|
||||||
|
- ❌ 图标(暂时移除,不影响功能)
|
||||||
293
expand/general-auto/README.md
Normal file
293
expand/general-auto/README.md
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
# General Auto - 浏览器自动化控制扩展
|
||||||
|
|
||||||
|
## 简介
|
||||||
|
|
||||||
|
General Auto 是一个强大的 Chrome 浏览器自动化控制扩展,专为自动化测试和浏览器控制设计。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
### 核心功能
|
||||||
|
|
||||||
|
- ✅ 完整的页面元素操作(点击、输入、滚动等)
|
||||||
|
- ✅ Cookie 管理(获取、设置、清除)
|
||||||
|
- ✅ 请求拦截和日志记录(XHR、Fetch)
|
||||||
|
- ✅ 模拟人类行为(随机延迟、自然输入)
|
||||||
|
- ✅ 元素等待和检测
|
||||||
|
- ✅ 自定义脚本执行
|
||||||
|
- ✅ 任务队列管理
|
||||||
|
|
||||||
|
### API 功能
|
||||||
|
|
||||||
|
#### Content Script API (window.GeneralAuto)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 执行任务
|
||||||
|
await GeneralAuto.executeTask({
|
||||||
|
type: 'click',
|
||||||
|
selector: 'button#submit',
|
||||||
|
timeout: 5000
|
||||||
|
});
|
||||||
|
|
||||||
|
// 执行自定义脚本
|
||||||
|
await GeneralAuto.executeScript('document.title');
|
||||||
|
|
||||||
|
// Cookie 管理
|
||||||
|
const cookies = await GeneralAuto.getCookies();
|
||||||
|
await GeneralAuto.setCookies([...]);
|
||||||
|
await GeneralAuto.clearCookies();
|
||||||
|
|
||||||
|
// 导航
|
||||||
|
await GeneralAuto.navigateTo('https://example.com');
|
||||||
|
|
||||||
|
// 获取请求日志
|
||||||
|
const logs = await GeneralAuto.getRequestLogs();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Injected Script API (window.AutomationUtils)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 模拟人类输入
|
||||||
|
await AutomationUtils.humanInput(element, 'Hello World');
|
||||||
|
|
||||||
|
// 模拟人类点击
|
||||||
|
await AutomationUtils.humanClick(element);
|
||||||
|
|
||||||
|
// 等待元素
|
||||||
|
const el = await AutomationUtils.waitForElement('.selector', 5000);
|
||||||
|
|
||||||
|
// 等待元素消失
|
||||||
|
await AutomationUtils.waitForElementRemoved('.loading');
|
||||||
|
|
||||||
|
// 随机延迟
|
||||||
|
await AutomationUtils.randomDelay(100, 500);
|
||||||
|
|
||||||
|
// 监听请求
|
||||||
|
AutomationUtils.onRequest((data) => {
|
||||||
|
console.log('Request:', data);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听响应
|
||||||
|
AutomationUtils.onResponse((data) => {
|
||||||
|
console.log('Response:', data);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 任务类型
|
||||||
|
|
||||||
|
### 支持的任务类型
|
||||||
|
|
||||||
|
| 类型 | 说明 | 参数 |
|
||||||
|
|------|------|------|
|
||||||
|
| `click` | 点击元素 | `selector` |
|
||||||
|
| `input` | 输入文本 | `selector`, `value` |
|
||||||
|
| `humanInput` | 模拟人类输入 | `selector`, `value` |
|
||||||
|
| `getText` | 获取文本 | `selector` |
|
||||||
|
| `getAttribute` | 获取属性 | `selector`, `value`(属性名) |
|
||||||
|
| `scroll` | 滚动页面 | `value: {x, y}` |
|
||||||
|
| `scrollToElement` | 滚动到元素 | `selector` |
|
||||||
|
| `wait` | 等待指定时间 | `value`(毫秒) |
|
||||||
|
| `waitForElement` | 等待元素出现 | `selector` |
|
||||||
|
| `evaluate` | 执行 JavaScript | `value`(代码) |
|
||||||
|
|
||||||
|
### 任务示例
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 点击按钮
|
||||||
|
await GeneralAuto.executeTask({
|
||||||
|
type: 'click',
|
||||||
|
selector: 'button[data-testid="submit"]',
|
||||||
|
timeout: 5000,
|
||||||
|
waitVisible: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// 输入文本
|
||||||
|
await GeneralAuto.executeTask({
|
||||||
|
type: 'input',
|
||||||
|
selector: 'input[name="username"]',
|
||||||
|
value: 'myusername',
|
||||||
|
timeout: 5000
|
||||||
|
});
|
||||||
|
|
||||||
|
// 模拟人类输入
|
||||||
|
await GeneralAuto.executeTask({
|
||||||
|
type: 'humanInput',
|
||||||
|
selector: 'textarea',
|
||||||
|
value: 'This is a human-like typing',
|
||||||
|
timeout: 10000
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取文本
|
||||||
|
const result = await GeneralAuto.executeTask({
|
||||||
|
type: 'getText',
|
||||||
|
selector: '.title'
|
||||||
|
});
|
||||||
|
console.log(result.text);
|
||||||
|
|
||||||
|
// 滚动
|
||||||
|
await GeneralAuto.executeTask({
|
||||||
|
type: 'scroll',
|
||||||
|
value: { x: 0, y: 500 }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 等待元素
|
||||||
|
await GeneralAuto.executeTask({
|
||||||
|
type: 'waitForElement',
|
||||||
|
selector: '.dynamic-content',
|
||||||
|
timeout: 10000
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 安装说明
|
||||||
|
|
||||||
|
### 1. 开发者模式安装
|
||||||
|
|
||||||
|
1. 打开 Chrome 浏览器
|
||||||
|
2. 访问 `chrome://extensions/`
|
||||||
|
3. 开启右上角的"开发者模式"
|
||||||
|
4. 点击"加载已解压的扩展程序"
|
||||||
|
5. 选择 `expand/general-auto` 文件夹
|
||||||
|
|
||||||
|
### 2. 在 Rod/Chromedp 中使用
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 使用 Rod
|
||||||
|
browser := rod.New().
|
||||||
|
ControlURL(launcher.New().
|
||||||
|
Headless(false).
|
||||||
|
Set("load-extension", "d:\\Project\\go-account-register\\expand\\general-auto").
|
||||||
|
MustLaunch()).
|
||||||
|
MustConnect()
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
### Go 代码中使用
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 在浏览器启动时加载扩展
|
||||||
|
func OpenBrowserWithExtension() *rod.Browser {
|
||||||
|
extensionPath := "d:\\Project\\go-account-register\\expand\\general-auto"
|
||||||
|
|
||||||
|
browser := rod.New().
|
||||||
|
ControlURL(launcher.New().
|
||||||
|
Headless(false).
|
||||||
|
Set("load-extension", extensionPath).
|
||||||
|
MustLaunch()).
|
||||||
|
MustConnect()
|
||||||
|
|
||||||
|
return browser
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在页面中执行自动化任务
|
||||||
|
func AutomateTwitterPost(page *rod.Page, content string) error {
|
||||||
|
// 等待扩展加载
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
|
// 执行任务
|
||||||
|
_, err := page.Eval(`
|
||||||
|
(async () => {
|
||||||
|
// 等待输入框
|
||||||
|
await GeneralAuto.executeTask({
|
||||||
|
type: 'waitForElement',
|
||||||
|
selector: '[data-testid="tweetTextarea_0"]',
|
||||||
|
timeout: 10000
|
||||||
|
});
|
||||||
|
|
||||||
|
// 输入内容
|
||||||
|
await GeneralAuto.executeTask({
|
||||||
|
type: 'humanInput',
|
||||||
|
selector: '[data-testid="tweetTextarea_0"]',
|
||||||
|
value: ` + "`" + content + "`" + `,
|
||||||
|
timeout: 15000
|
||||||
|
});
|
||||||
|
|
||||||
|
// 点击发布按钮
|
||||||
|
await GeneralAuto.executeTask({
|
||||||
|
type: 'click',
|
||||||
|
selector: '[data-testid="tweetButtonInline"]',
|
||||||
|
timeout: 5000
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
})()
|
||||||
|
`)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 页面控制台中使用
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 1. 点击登录按钮
|
||||||
|
await GeneralAuto.executeTask({
|
||||||
|
type: 'click',
|
||||||
|
selector: 'button.login-btn'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. 等待登录框出现
|
||||||
|
await GeneralAuto.executeTask({
|
||||||
|
type: 'waitForElement',
|
||||||
|
selector: '#login-modal'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. 输入用户名
|
||||||
|
await GeneralAuto.executeTask({
|
||||||
|
type: 'humanInput',
|
||||||
|
selector: 'input[name="username"]',
|
||||||
|
value: 'myusername'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. 输入密码
|
||||||
|
await GeneralAuto.executeTask({
|
||||||
|
type: 'humanInput',
|
||||||
|
selector: 'input[name="password"]',
|
||||||
|
value: 'mypassword'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5. 点击提交
|
||||||
|
await GeneralAuto.executeTask({
|
||||||
|
type: 'click',
|
||||||
|
selector: 'button[type="submit"]'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置选项
|
||||||
|
|
||||||
|
### 请求拦截配置
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 控制请求拦截
|
||||||
|
window.GeneralAutoConfig = {
|
||||||
|
interceptXHR: true, // 拦截 XHR 请求
|
||||||
|
interceptFetch: true, // 拦截 Fetch 请求
|
||||||
|
logRequests: true, // 记录请求日志
|
||||||
|
requestCallbacks: [], // 请求回调
|
||||||
|
responseCallbacks: [] // 响应回调
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 扩展需要在浏览器启动时加载
|
||||||
|
2. 某些网站可能检测自动化行为,建议使用人类模拟功能
|
||||||
|
3. 请求日志会占用内存,定期清理
|
||||||
|
4. 开发模式下扩展可能需要手动刷新
|
||||||
|
|
||||||
|
## 更新日志
|
||||||
|
|
||||||
|
### v1.0.0 (2025-01-21)
|
||||||
|
|
||||||
|
- ✨ 初始版本发布
|
||||||
|
- ✅ 实现基础自动化功能
|
||||||
|
- ✅ 添加 Cookie 管理
|
||||||
|
- ✅ 添加请求拦截
|
||||||
|
- ✅ 添加人类行为模拟
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
## 支持
|
||||||
|
|
||||||
|
如有问题,请联系开发团队。
|
||||||
124
expand/general-auto/TROUBLESHOOT.md
Normal file
124
expand/general-auto/TROUBLESHOOT.md
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
# Chrome 扩展加载诊断
|
||||||
|
|
||||||
|
## 问题:扩展目录正确但未加载
|
||||||
|
|
||||||
|
### 已确认正确的信息:
|
||||||
|
- ✅ 扩展目录: D:\Project\go-account-register\expand\general-auto
|
||||||
|
- ✅ manifest.json 存在
|
||||||
|
- ✅ Chrome 路径正确
|
||||||
|
|
||||||
|
### 可能的原因和解决方案
|
||||||
|
|
||||||
|
#### 1. 用户数据目录冲突
|
||||||
|
**问题**: 每次使用随机临时目录可能导致扩展状态丢失
|
||||||
|
|
||||||
|
**解决方案**: 使用固定的用户数据目录测试
|
||||||
|
```go
|
||||||
|
userDataDir := "user-data/test-with-extension" // 固定目录
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Chrome 启动参数问题
|
||||||
|
某些参数组合可能导致扩展加载失败。
|
||||||
|
|
||||||
|
**测试步骤**:
|
||||||
|
1. 手动在命令行启动 Chrome 测试扩展加载:
|
||||||
|
```powershell
|
||||||
|
cd "C:\Program Files\Google\Chrome\Application"
|
||||||
|
.\chrome.exe --user-data-dir="D:\Project\go-account-register\user-data\test" --load-extension="D:\Project\go-account-register\expand\general-auto" --no-sandbox
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 如果手动启动成功,说明是参数组合问题
|
||||||
|
|
||||||
|
#### 3. manifest.json 兼容性
|
||||||
|
**当前 manifest.json 可能的问题**:
|
||||||
|
- Service Worker 可能需要额外配置
|
||||||
|
- 权限设置可能有冲突
|
||||||
|
|
||||||
|
**测试方案**: 临时使用更简单的 manifest
|
||||||
|
```bash
|
||||||
|
# 重命名当前 manifest
|
||||||
|
cd D:\Project\go-account-register\expand\general-auto
|
||||||
|
rename manifest.json manifest-full.json
|
||||||
|
rename manifest-simple.json manifest.json
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. 扩展 ID 和路径大小写
|
||||||
|
Windows 系统路径大小写敏感性问题
|
||||||
|
|
||||||
|
**验证**: 确保路径没有大小写混用
|
||||||
|
|
||||||
|
#### 5. Chrome 策略限制
|
||||||
|
企业版 Chrome 可能有扩展策略限制
|
||||||
|
|
||||||
|
**检查**:
|
||||||
|
1. 访问 `chrome://policy/`
|
||||||
|
2. 查看是否有 ExtensionInstallBlacklist 或类似策略
|
||||||
|
|
||||||
|
### 推荐的调试步骤
|
||||||
|
|
||||||
|
**步骤 1: 手动加载测试**
|
||||||
|
1. 打开 Chrome (不通过程序)
|
||||||
|
2. 访问 `chrome://extensions/`
|
||||||
|
3. 启用"开发者模式"
|
||||||
|
4. 点击"加载已解压的扩展程序"
|
||||||
|
5. 选择 `D:\Project\go-account-register\expand\general-auto`
|
||||||
|
6. 看是否有错误提示
|
||||||
|
|
||||||
|
**步骤 2: 使用简化版 manifest**
|
||||||
|
```powershell
|
||||||
|
cd D:\Project\go-account-register\expand\general-auto
|
||||||
|
Copy-Item manifest.json manifest-backup.json
|
||||||
|
Copy-Item manifest-simple.json manifest.json
|
||||||
|
```
|
||||||
|
然后重新运行程序
|
||||||
|
|
||||||
|
**步骤 3: 查看 Chrome 日志**
|
||||||
|
启动时添加日志参数:
|
||||||
|
```go
|
||||||
|
Set("enable-logging", "stderr").
|
||||||
|
Set("v", "1")
|
||||||
|
```
|
||||||
|
|
||||||
|
**步骤 4: 检查扩展文件权限**
|
||||||
|
确保所有文件可读:
|
||||||
|
```powershell
|
||||||
|
icacls "D:\Project\go-account-register\expand\general-auto" /t
|
||||||
|
```
|
||||||
|
|
||||||
|
### 临时解决方案
|
||||||
|
|
||||||
|
如果自动加载持续失败,可以:
|
||||||
|
|
||||||
|
1. **手动预加载扩展到用户数据目录**
|
||||||
|
```go
|
||||||
|
// 在程序启动前,将扩展复制到用户数据目录
|
||||||
|
extensionDir := filepath.Join(absPath, "Default", "Extensions", "general-auto")
|
||||||
|
os.MkdirAll(extensionDir, 0755)
|
||||||
|
// 复制扩展文件...
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **使用打包的 .crx 文件**
|
||||||
|
```powershell
|
||||||
|
# 打包扩展
|
||||||
|
chrome --pack-extension="D:\Project\go-account-register\expand\general-auto"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **不使用扩展,直接注入脚本**
|
||||||
|
```go
|
||||||
|
page.MustEvalOnNewDocument(`
|
||||||
|
// 直接注入你的自动化代码
|
||||||
|
window.GeneralAuto = { ... };
|
||||||
|
`)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 下一步行动
|
||||||
|
|
||||||
|
请尝试:
|
||||||
|
1. 运行程序后,手动访问 `chrome://extensions/` 截图
|
||||||
|
2. 尝试手动加载扩展,看是否有错误信息
|
||||||
|
3. 使用简化版 manifest 测试
|
||||||
|
|
||||||
|
如果还是不行,请提供:
|
||||||
|
- `chrome://extensions/` 的截图
|
||||||
|
- 控制台是否有任何错误信息
|
||||||
|
- 手动加载时的错误提示(如果有)
|
||||||
388
expand/general-auto/background.js
Normal file
388
expand/general-auto/background.js
Normal file
@@ -0,0 +1,388 @@
|
|||||||
|
// General Auto - Background Service Worker
|
||||||
|
console.log('General Auto 后台服务已启动');
|
||||||
|
|
||||||
|
// 任务队列和状态管理
|
||||||
|
const state = {
|
||||||
|
taskQueue: [],
|
||||||
|
isProcessing: false,
|
||||||
|
requestLogs: [],
|
||||||
|
maxLogs: 100
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听来自 content script 的消息
|
||||||
|
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||||
|
console.log('收到消息:', request);
|
||||||
|
|
||||||
|
switch (request.action) {
|
||||||
|
case 'executeTask':
|
||||||
|
executeTask(request.task, sender.tab.id)
|
||||||
|
.then(result => sendResponse({ success: true, data: result }))
|
||||||
|
.catch(error => sendResponse({ success: false, error: error.message }));
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case 'executeScript':
|
||||||
|
executeScript(request.code, sender.tab.id)
|
||||||
|
.then(result => sendResponse({ success: true, data: result }))
|
||||||
|
.catch(error => sendResponse({ success: false, error: error.message }));
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case 'getCookies':
|
||||||
|
getCookies(request.url)
|
||||||
|
.then(cookies => sendResponse({ success: true, cookies }))
|
||||||
|
.catch(error => sendResponse({ success: false, error: error.message }));
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case 'setCookies':
|
||||||
|
setCookies(request.cookies)
|
||||||
|
.then(() => sendResponse({ success: true }))
|
||||||
|
.catch(error => sendResponse({ success: false, error: error.message }));
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case 'clearCookies':
|
||||||
|
clearCookies(request.url)
|
||||||
|
.then(() => sendResponse({ success: true }))
|
||||||
|
.catch(error => sendResponse({ success: false, error: error.message }));
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case 'navigateTo':
|
||||||
|
navigateTo(request.url, sender.tab.id)
|
||||||
|
.then(() => sendResponse({ success: true }))
|
||||||
|
.catch(error => sendResponse({ success: false, error: error.message }));
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case 'getRequestLogs':
|
||||||
|
sendResponse({ success: true, logs: state.requestLogs });
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'clearRequestLogs':
|
||||||
|
state.requestLogs = [];
|
||||||
|
sendResponse({ success: true });
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'getStatus':
|
||||||
|
sendResponse({
|
||||||
|
success: true,
|
||||||
|
status: {
|
||||||
|
taskCount: state.taskQueue.length,
|
||||||
|
isProcessing: state.isProcessing,
|
||||||
|
requestLogCount: state.requestLogs.length
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'logRequest':
|
||||||
|
logRequest(request.data);
|
||||||
|
sendResponse({ success: true });
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
sendResponse({ success: false, error: '未知操作' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 执行自动化任务
|
||||||
|
async function executeTask(task, tabId) {
|
||||||
|
try {
|
||||||
|
const result = await chrome.scripting.executeScript({
|
||||||
|
target: { tabId: tabId },
|
||||||
|
func: executeTaskInPage,
|
||||||
|
args: [task]
|
||||||
|
});
|
||||||
|
return result[0].result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('执行任务失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在页面中执行的任务函数
|
||||||
|
function executeTaskInPage(task) {
|
||||||
|
const { type, selector, action, value, timeout = 10000, waitVisible = true } = task;
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
reject(new Error(`任务执行超时: ${timeout}ms`));
|
||||||
|
}, timeout);
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (type) {
|
||||||
|
case 'click':
|
||||||
|
waitForElement(selector, timeout, waitVisible).then(el => {
|
||||||
|
el.click();
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolve({ success: true, message: '点击成功' });
|
||||||
|
}).catch(err => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'input':
|
||||||
|
waitForElement(selector, timeout, waitVisible).then(el => {
|
||||||
|
el.value = value;
|
||||||
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolve({ success: true, message: '输入成功' });
|
||||||
|
}).catch(err => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'humanInput':
|
||||||
|
waitForElement(selector, timeout, waitVisible).then(el => {
|
||||||
|
humanTypeInput(el, value).then(() => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolve({ success: true, message: '人类输入成功' });
|
||||||
|
});
|
||||||
|
}).catch(err => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'getText':
|
||||||
|
waitForElement(selector, timeout, waitVisible).then(el => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolve({ success: true, text: el.textContent.trim() });
|
||||||
|
}).catch(err => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'getAttribute':
|
||||||
|
waitForElement(selector, timeout, waitVisible).then(el => {
|
||||||
|
const attrValue = el.getAttribute(value);
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolve({ success: true, value: attrValue });
|
||||||
|
}).catch(err => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'scroll':
|
||||||
|
window.scrollBy({
|
||||||
|
top: value.y || 0,
|
||||||
|
left: value.x || 0,
|
||||||
|
behavior: 'smooth'
|
||||||
|
});
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolve({ success: true, message: '滚动成功' });
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'scrollToElement':
|
||||||
|
waitForElement(selector, timeout, false).then(el => {
|
||||||
|
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolve({ success: true, message: '滚动到元素成功' });
|
||||||
|
}).catch(err => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'wait':
|
||||||
|
setTimeout(() => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolve({ success: true, message: `等待 ${value}ms 完成` });
|
||||||
|
}, value);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'waitForElement':
|
||||||
|
waitForElement(selector, timeout, waitVisible).then(el => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolve({ success: true, message: '元素已出现' });
|
||||||
|
}).catch(err => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'evaluate':
|
||||||
|
try {
|
||||||
|
const result = eval(value);
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolve({ success: true, result: result });
|
||||||
|
} catch (err) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
clearTimeout(timer);
|
||||||
|
reject(new Error(`未知任务类型: ${type}`));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 等待元素出现
|
||||||
|
function waitForElement(selector, timeout, checkVisible) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const check = () => {
|
||||||
|
const element = document.querySelector(selector);
|
||||||
|
if (element) {
|
||||||
|
if (!checkVisible) {
|
||||||
|
resolve(element);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
const isVisible = rect.width > 0 && rect.height > 0 &&
|
||||||
|
window.getComputedStyle(element).visibility !== 'hidden' &&
|
||||||
|
window.getComputedStyle(element).display !== 'none';
|
||||||
|
if (isVisible) {
|
||||||
|
resolve(element);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (check()) return;
|
||||||
|
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
if (check()) {
|
||||||
|
observer.disconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['style', 'class']
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
observer.disconnect();
|
||||||
|
reject(new Error(`等待元素超时: ${selector}`));
|
||||||
|
}, timeout);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟人类输入
|
||||||
|
function humanTypeInput(element, text, minDelay = 50, maxDelay = 150) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let index = 0;
|
||||||
|
const type = () => {
|
||||||
|
if (index < text.length) {
|
||||||
|
element.value += text[index];
|
||||||
|
element.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
index++;
|
||||||
|
const delay = minDelay + Math.random() * (maxDelay - minDelay);
|
||||||
|
setTimeout(type, delay);
|
||||||
|
} else {
|
||||||
|
element.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
type();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行自定义脚本
|
||||||
|
async function executeScript(code, tabId) {
|
||||||
|
try {
|
||||||
|
const result = await chrome.scripting.executeScript({
|
||||||
|
target: { tabId: tabId },
|
||||||
|
func: (code) => eval(code),
|
||||||
|
args: [code]
|
||||||
|
});
|
||||||
|
return result[0].result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('执行脚本失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cookie 管理
|
||||||
|
async function getCookies(url) {
|
||||||
|
if (url) {
|
||||||
|
return await chrome.cookies.getAll({ url });
|
||||||
|
} else {
|
||||||
|
return await chrome.cookies.getAll({});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setCookies(cookies) {
|
||||||
|
for (const cookie of cookies) {
|
||||||
|
try {
|
||||||
|
await chrome.cookies.set(cookie);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('设置 Cookie 失败:', cookie, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearCookies(url) {
|
||||||
|
let cookies;
|
||||||
|
if (url) {
|
||||||
|
cookies = await chrome.cookies.getAll({ url });
|
||||||
|
} else {
|
||||||
|
cookies = await chrome.cookies.getAll({});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const cookie of cookies) {
|
||||||
|
const protocol = cookie.secure ? 'https:' : 'http:';
|
||||||
|
const cookieUrl = `${protocol}//${cookie.domain}${cookie.path}`;
|
||||||
|
await chrome.cookies.remove({
|
||||||
|
url: cookieUrl,
|
||||||
|
name: cookie.name
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导航到指定 URL
|
||||||
|
async function navigateTo(url, tabId) {
|
||||||
|
await chrome.tabs.update(tabId, { url: url });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录请求
|
||||||
|
function logRequest(data) {
|
||||||
|
state.requestLogs.push({
|
||||||
|
...data,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
// 限制日志数量
|
||||||
|
if (state.requestLogs.length > state.maxLogs) {
|
||||||
|
state.requestLogs.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听标签页更新
|
||||||
|
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
||||||
|
if (changeInfo.status === 'complete') {
|
||||||
|
console.log('页面加载完成:', tab.url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听网络请求
|
||||||
|
chrome.webRequest.onBeforeRequest.addListener(
|
||||||
|
(details) => {
|
||||||
|
// 可以在这里拦截和修改请求
|
||||||
|
if (details.type === 'xmlhttprequest' || details.type === 'fetch') {
|
||||||
|
console.log('API 请求:', details.url);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ urls: ["<all_urls>"] },
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 监听响应头
|
||||||
|
chrome.webRequest.onCompleted.addListener(
|
||||||
|
(details) => {
|
||||||
|
if (details.type === 'xmlhttprequest' || details.type === 'fetch') {
|
||||||
|
console.log('API 响应:', details.url, details.statusCode);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ urls: ["<all_urls>"] },
|
||||||
|
[]
|
||||||
|
);
|
||||||
186
expand/general-auto/content.js
Normal file
186
expand/general-auto/content.js
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
// General Auto - Content Script
|
||||||
|
console.log('General Auto 内容脚本已加载');
|
||||||
|
|
||||||
|
// 注入自定义脚本到页面
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = chrome.runtime.getURL('injected.js');
|
||||||
|
script.onload = function() {
|
||||||
|
this.remove();
|
||||||
|
};
|
||||||
|
(document.head || document.documentElement).appendChild(script);
|
||||||
|
|
||||||
|
// 监听页面消息
|
||||||
|
window.addEventListener('message', function(event) {
|
||||||
|
if (event.source !== window) return;
|
||||||
|
|
||||||
|
if (event.data.type && event.data.type === 'FROM_PAGE') {
|
||||||
|
console.log('Content script 收到页面消息:', event.data);
|
||||||
|
|
||||||
|
// 转发到 background script
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
action: event.data.action,
|
||||||
|
data: event.data.data
|
||||||
|
}, response => {
|
||||||
|
if (response) {
|
||||||
|
window.postMessage({
|
||||||
|
type: 'FROM_EXTENSION',
|
||||||
|
action: event.data.action,
|
||||||
|
requestId: event.data.requestId,
|
||||||
|
data: response
|
||||||
|
}, '*');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 提供 API 供外部调用
|
||||||
|
window.GeneralAuto = {
|
||||||
|
version: '1.0.0',
|
||||||
|
|
||||||
|
// 执行任务
|
||||||
|
executeTask: async function(task) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
action: 'executeTask',
|
||||||
|
task: task
|
||||||
|
}, response => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
reject(new Error(chrome.runtime.lastError.message));
|
||||||
|
} else if (response && response.success) {
|
||||||
|
resolve(response.data);
|
||||||
|
} else {
|
||||||
|
reject(new Error(response ? response.error : '未知错误'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 执行自定义脚本
|
||||||
|
executeScript: async function(code) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
action: 'executeScript',
|
||||||
|
code: code
|
||||||
|
}, response => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
reject(new Error(chrome.runtime.lastError.message));
|
||||||
|
} else if (response && response.success) {
|
||||||
|
resolve(response.data);
|
||||||
|
} else {
|
||||||
|
reject(new Error(response ? response.error : '未知错误'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取 Cookies
|
||||||
|
getCookies: async function(url) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
action: 'getCookies',
|
||||||
|
url: url || window.location.href
|
||||||
|
}, response => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
reject(new Error(chrome.runtime.lastError.message));
|
||||||
|
} else if (response && response.success) {
|
||||||
|
resolve(response.cookies);
|
||||||
|
} else {
|
||||||
|
reject(new Error(response ? response.error : '未知错误'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 设置 Cookies
|
||||||
|
setCookies: async function(cookies) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
action: 'setCookies',
|
||||||
|
cookies: cookies
|
||||||
|
}, response => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
reject(new Error(chrome.runtime.lastError.message));
|
||||||
|
} else if (response && response.success) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject(new Error(response ? response.error : '未知错误'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 清除 Cookies
|
||||||
|
clearCookies: async function(url) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
action: 'clearCookies',
|
||||||
|
url: url || window.location.href
|
||||||
|
}, response => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
reject(new Error(chrome.runtime.lastError.message));
|
||||||
|
} else if (response && response.success) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject(new Error(response ? response.error : '未知错误'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 导航到指定 URL
|
||||||
|
navigateTo: async function(url) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
action: 'navigateTo',
|
||||||
|
url: url
|
||||||
|
}, response => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
reject(new Error(chrome.runtime.lastError.message));
|
||||||
|
} else if (response && response.success) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject(new Error(response ? response.error : '未知错误'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取请求日志
|
||||||
|
getRequestLogs: async function() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
action: 'getRequestLogs'
|
||||||
|
}, response => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
reject(new Error(chrome.runtime.lastError.message));
|
||||||
|
} else if (response && response.success) {
|
||||||
|
resolve(response.logs);
|
||||||
|
} else {
|
||||||
|
reject(new Error(response ? response.error : '未知错误'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 清除请求日志
|
||||||
|
clearRequestLogs: async function() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
action: 'clearRequestLogs'
|
||||||
|
}, response => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
reject(new Error(chrome.runtime.lastError.message));
|
||||||
|
} else if (response && response.success) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject(new Error(response ? response.error : '未知错误'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 导出到全局
|
||||||
|
window.GA = window.GeneralAuto;
|
||||||
|
|
||||||
|
console.log('GeneralAuto API 已就绪');
|
||||||
85
expand/general-auto/generate-icons.html
Normal file
85
expand/general-auto/generate-icons.html
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>生成图标</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<canvas id="canvas16" width="16" height="16"></canvas>
|
||||||
|
<canvas id="canvas48" width="48" height="48"></canvas>
|
||||||
|
<canvas id="canvas128" width="128" height="128"></canvas>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 生成 16x16 图标
|
||||||
|
const canvas16 = document.getElementById('canvas16');
|
||||||
|
const ctx16 = canvas16.getContext('2d');
|
||||||
|
const gradient16 = ctx16.createLinearGradient(0, 0, 16, 16);
|
||||||
|
gradient16.addColorStop(0, '#667eea');
|
||||||
|
gradient16.addColorStop(1, '#764ba2');
|
||||||
|
ctx16.fillStyle = gradient16;
|
||||||
|
ctx16.fillRect(0, 0, 16, 16);
|
||||||
|
ctx16.fillStyle = 'white';
|
||||||
|
ctx16.font = 'bold 10px Arial';
|
||||||
|
ctx16.textAlign = 'center';
|
||||||
|
ctx16.textBaseline = 'middle';
|
||||||
|
ctx16.fillText('GA', 8, 8);
|
||||||
|
|
||||||
|
// 生成 48x48 图标
|
||||||
|
const canvas48 = document.getElementById('canvas48');
|
||||||
|
const ctx48 = canvas48.getContext('2d');
|
||||||
|
const gradient48 = ctx48.createLinearGradient(0, 0, 48, 48);
|
||||||
|
gradient48.addColorStop(0, '#667eea');
|
||||||
|
gradient48.addColorStop(1, '#764ba2');
|
||||||
|
ctx48.fillStyle = gradient48;
|
||||||
|
ctx48.fillRect(0, 0, 48, 48);
|
||||||
|
ctx48.fillStyle = 'white';
|
||||||
|
ctx48.font = 'bold 24px Arial';
|
||||||
|
ctx48.textAlign = 'center';
|
||||||
|
ctx48.textBaseline = 'middle';
|
||||||
|
ctx48.fillText('GA', 24, 24);
|
||||||
|
|
||||||
|
// 生成 128x128 图标
|
||||||
|
const canvas128 = document.getElementById('canvas128');
|
||||||
|
const ctx128 = canvas128.getContext('2d');
|
||||||
|
const gradient128 = ctx128.createLinearGradient(0, 0, 128, 128);
|
||||||
|
gradient128.addColorStop(0, '#667eea');
|
||||||
|
gradient128.addColorStop(1, '#764ba2');
|
||||||
|
ctx128.fillStyle = gradient128;
|
||||||
|
ctx128.fillRect(0, 0, 128, 128);
|
||||||
|
ctx128.fillStyle = 'white';
|
||||||
|
ctx128.font = 'bold 56px Arial';
|
||||||
|
ctx128.textAlign = 'center';
|
||||||
|
ctx128.textBaseline = 'middle';
|
||||||
|
ctx128.fillText('GA', 64, 64);
|
||||||
|
|
||||||
|
// 自动下载
|
||||||
|
setTimeout(() => {
|
||||||
|
['16', '48', '128'].forEach(size => {
|
||||||
|
const canvas = document.getElementById('canvas' + size);
|
||||||
|
canvas.toBlob(blob => {
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'icon' + size + '.png';
|
||||||
|
a.click();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<p>图标生成中,请保存下载的 PNG 文件到 icons 目录...</p>
|
||||||
|
<p>或者手动右键点击下面的图标保存:</p>
|
||||||
|
<div>
|
||||||
|
<h3>16x16:</h3>
|
||||||
|
<canvas id="preview16" width="16" height="16"></canvas>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3>48x48:</h3>
|
||||||
|
<canvas id="preview48" width="48" height="48"></canvas>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3>128x128:</h3>
|
||||||
|
<canvas id="preview128" width="128" height="128"></canvas>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
33
expand/general-auto/icons/converter.html
Normal file
33
expand/general-auto/icons/converter.html
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>SVG to PNG Converter</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>SVG 转 PNG 工具</h2>
|
||||||
|
<p>请按照以下步骤操作:</p>
|
||||||
|
<ol>
|
||||||
|
<li>在浏览器中打开 icon16.svg, icon48.svg, icon128.svg</li>
|
||||||
|
<li>右键点击 → 另存为</li>
|
||||||
|
<li>保存为 icon16.png, icon48.png, icon128.png</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>或者使用在线工具:</h3>
|
||||||
|
<ul>
|
||||||
|
<li><a href="https://cloudconvert.com/svg-to-png" target="_blank">CloudConvert</a></li>
|
||||||
|
<li><a href="https://www.aconvert.com/image/svg-to-png/" target="_blank">Aconvert</a></li>
|
||||||
|
<li><a href="https://svgtopng.com/" target="_blank">SVG to PNG</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>临时解决方案:</h3>
|
||||||
|
<p>Chrome 扩展也可以直接使用 SVG 文件,修改 manifest.json:</p>
|
||||||
|
<pre>
|
||||||
|
"icons": {
|
||||||
|
"16": "icons/icon16.svg",
|
||||||
|
"48": "icons/icon48.svg",
|
||||||
|
"128": "icons/icon128.svg"
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
13
expand/general-auto/icons/icon128.svg
Normal file
13
expand/general-auto/icons/icon128.svg
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="grad1" 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>
|
||||||
|
<rect width="128" height="128" rx="20" fill="url(#grad1)"/>
|
||||||
|
<text x="64" y="90" font-family="Arial, sans-serif" font-size="56" font-weight="bold" fill="white" text-anchor="middle">GA</text>
|
||||||
|
<circle cx="64" cy="40" r="3" fill="white" opacity="0.8"/>
|
||||||
|
<circle cx="90" cy="30" r="2" fill="white" opacity="0.6"/>
|
||||||
|
<circle cx="40" cy="35" r="2" fill="white" opacity="0.6"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 699 B |
10
expand/general-auto/icons/icon16.svg
Normal file
10
expand/general-auto/icons/icon16.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="grad1" 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>
|
||||||
|
<rect width="16" height="16" rx="3" fill="url(#grad1)"/>
|
||||||
|
<text x="8" y="12" font-family="Arial" font-size="10" font-weight="bold" fill="white" text-anchor="middle">GA</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 498 B |
10
expand/general-auto/icons/icon48.svg
Normal file
10
expand/general-auto/icons/icon48.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg width="48" height="48" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="grad1" 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>
|
||||||
|
<rect width="48" height="48" rx="8" fill="url(#grad1)"/>
|
||||||
|
<text x="24" y="34" font-family="Arial, sans-serif" font-size="24" font-weight="bold" fill="white" text-anchor="middle">GA</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 511 B |
422
expand/general-auto/injected.js
Normal file
422
expand/general-auto/injected.js
Normal file
@@ -0,0 +1,422 @@
|
|||||||
|
// General Auto - Injected Script
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
console.log('General Auto Injected script 已加载');
|
||||||
|
|
||||||
|
// 存储原始方法
|
||||||
|
const originalXHR = window.XMLHttpRequest;
|
||||||
|
const originalFetch = window.fetch;
|
||||||
|
const originalOpen = window.open;
|
||||||
|
|
||||||
|
// 请求拦截器配置
|
||||||
|
window.GeneralAutoConfig = {
|
||||||
|
interceptXHR: true,
|
||||||
|
interceptFetch: true,
|
||||||
|
logRequests: true,
|
||||||
|
requestCallbacks: [],
|
||||||
|
responseCallbacks: []
|
||||||
|
};
|
||||||
|
|
||||||
|
// 拦截 XMLHttpRequest
|
||||||
|
window.XMLHttpRequest = function() {
|
||||||
|
const xhr = new originalXHR();
|
||||||
|
const config = window.GeneralAutoConfig;
|
||||||
|
|
||||||
|
if (!config.interceptXHR) return xhr;
|
||||||
|
|
||||||
|
const originalOpen = xhr.open;
|
||||||
|
const originalSend = xhr.send;
|
||||||
|
const originalSetRequestHeader = xhr.setRequestHeader;
|
||||||
|
|
||||||
|
let requestData = {
|
||||||
|
method: '',
|
||||||
|
url: '',
|
||||||
|
headers: {},
|
||||||
|
body: null,
|
||||||
|
timestamp: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.open = function(method, url) {
|
||||||
|
requestData.method = method;
|
||||||
|
requestData.url = url;
|
||||||
|
return originalOpen.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.setRequestHeader = function(name, value) {
|
||||||
|
requestData.headers[name] = value;
|
||||||
|
return originalSetRequestHeader.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.send = function(body) {
|
||||||
|
requestData.body = body;
|
||||||
|
|
||||||
|
// 请求前回调
|
||||||
|
config.requestCallbacks.forEach(callback => {
|
||||||
|
try {
|
||||||
|
callback(requestData);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Request callback error:', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听响应
|
||||||
|
this.addEventListener('load', function() {
|
||||||
|
const responseData = {
|
||||||
|
...requestData,
|
||||||
|
status: this.status,
|
||||||
|
statusText: this.statusText,
|
||||||
|
responseHeaders: this.getAllResponseHeaders(),
|
||||||
|
response: this.response,
|
||||||
|
responseText: this.responseText,
|
||||||
|
responseType: this.responseType,
|
||||||
|
responseURL: this.responseURL
|
||||||
|
};
|
||||||
|
|
||||||
|
// 响应后回调
|
||||||
|
config.responseCallbacks.forEach(callback => {
|
||||||
|
try {
|
||||||
|
callback(responseData);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Response callback error:', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 发送到 background
|
||||||
|
if (config.logRequests) {
|
||||||
|
window.postMessage({
|
||||||
|
type: 'FROM_PAGE',
|
||||||
|
action: 'logRequest',
|
||||||
|
data: {
|
||||||
|
type: 'xhr',
|
||||||
|
method: responseData.method,
|
||||||
|
url: responseData.url,
|
||||||
|
status: responseData.status,
|
||||||
|
requestHeaders: responseData.headers,
|
||||||
|
requestBody: responseData.body,
|
||||||
|
responseHeaders: responseData.responseHeaders,
|
||||||
|
response: responseData.responseText
|
||||||
|
}
|
||||||
|
}, '*');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addEventListener('error', function() {
|
||||||
|
console.error('XHR Error:', requestData.url);
|
||||||
|
});
|
||||||
|
|
||||||
|
return originalSend.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
return xhr;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 拦截 Fetch
|
||||||
|
window.fetch = function() {
|
||||||
|
const config = window.GeneralAutoConfig;
|
||||||
|
|
||||||
|
if (!config.interceptFetch) {
|
||||||
|
return originalFetch.apply(this, arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = arguments[0];
|
||||||
|
const options = arguments[1] || {};
|
||||||
|
|
||||||
|
const requestData = {
|
||||||
|
method: options.method || 'GET',
|
||||||
|
url: typeof url === 'string' ? url : url.url,
|
||||||
|
headers: options.headers || {},
|
||||||
|
body: options.body,
|
||||||
|
timestamp: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
// 请求前回调
|
||||||
|
config.requestCallbacks.forEach(callback => {
|
||||||
|
try {
|
||||||
|
callback(requestData);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Request callback error:', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return originalFetch.apply(this, arguments).then(response => {
|
||||||
|
const clonedResponse = response.clone();
|
||||||
|
|
||||||
|
clonedResponse.text().then(body => {
|
||||||
|
const responseData = {
|
||||||
|
...requestData,
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
headers: Object.fromEntries(response.headers.entries()),
|
||||||
|
response: body,
|
||||||
|
responseURL: response.url
|
||||||
|
};
|
||||||
|
|
||||||
|
// 响应后回调
|
||||||
|
config.responseCallbacks.forEach(callback => {
|
||||||
|
try {
|
||||||
|
callback(responseData);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Response callback error:', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 发送到 background
|
||||||
|
if (config.logRequests) {
|
||||||
|
window.postMessage({
|
||||||
|
type: 'FROM_PAGE',
|
||||||
|
action: 'logRequest',
|
||||||
|
data: {
|
||||||
|
type: 'fetch',
|
||||||
|
method: responseData.method,
|
||||||
|
url: responseData.url,
|
||||||
|
status: responseData.status,
|
||||||
|
requestHeaders: responseData.headers,
|
||||||
|
requestBody: responseData.body,
|
||||||
|
responseHeaders: responseData.headers,
|
||||||
|
response: body
|
||||||
|
}
|
||||||
|
}, '*');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提供全局自动化工具 API
|
||||||
|
window.AutomationUtils = {
|
||||||
|
// 模拟人类输入
|
||||||
|
humanInput: function(element, text, minDelay = 50, maxDelay = 150) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
element.focus();
|
||||||
|
let index = 0;
|
||||||
|
const type = () => {
|
||||||
|
if (index < text.length) {
|
||||||
|
const char = text[index];
|
||||||
|
|
||||||
|
// 触发键盘事件
|
||||||
|
const keydownEvent = new KeyboardEvent('keydown', {
|
||||||
|
key: char,
|
||||||
|
code: `Key${char.toUpperCase()}`,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true
|
||||||
|
});
|
||||||
|
const keypressEvent = new KeyboardEvent('keypress', {
|
||||||
|
key: char,
|
||||||
|
code: `Key${char.toUpperCase()}`,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true
|
||||||
|
});
|
||||||
|
const keyupEvent = new KeyboardEvent('keyup', {
|
||||||
|
key: char,
|
||||||
|
code: `Key${char.toUpperCase()}`,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true
|
||||||
|
});
|
||||||
|
|
||||||
|
element.dispatchEvent(keydownEvent);
|
||||||
|
element.dispatchEvent(keypressEvent);
|
||||||
|
|
||||||
|
element.value += char;
|
||||||
|
element.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
|
||||||
|
element.dispatchEvent(keyupEvent);
|
||||||
|
|
||||||
|
index++;
|
||||||
|
const delay = minDelay + Math.random() * (maxDelay - minDelay);
|
||||||
|
setTimeout(type, delay);
|
||||||
|
} else {
|
||||||
|
element.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
element.blur();
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
setTimeout(type, 100);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 模拟人类点击
|
||||||
|
humanClick: function(element, options = {}) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
const x = rect.left + (options.offsetX || rect.width * (0.3 + Math.random() * 0.4));
|
||||||
|
const y = rect.top + (options.offsetY || rect.height * (0.3 + Math.random() * 0.4));
|
||||||
|
|
||||||
|
// 滚动到元素
|
||||||
|
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
// 鼠标移动
|
||||||
|
const mousemoveEvent = new MouseEvent('mousemove', {
|
||||||
|
view: window,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
clientX: x,
|
||||||
|
clientY: y
|
||||||
|
});
|
||||||
|
element.dispatchEvent(mousemoveEvent);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
// 鼠标按下
|
||||||
|
const mousedownEvent = new MouseEvent('mousedown', {
|
||||||
|
view: window,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
clientX: x,
|
||||||
|
clientY: y,
|
||||||
|
button: 0
|
||||||
|
});
|
||||||
|
element.dispatchEvent(mousedownEvent);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
// 鼠标抬起
|
||||||
|
const mouseupEvent = new MouseEvent('mouseup', {
|
||||||
|
view: window,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
clientX: x,
|
||||||
|
clientY: y,
|
||||||
|
button: 0
|
||||||
|
});
|
||||||
|
element.dispatchEvent(mouseupEvent);
|
||||||
|
|
||||||
|
// 点击
|
||||||
|
const clickEvent = new MouseEvent('click', {
|
||||||
|
view: window,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
clientX: x,
|
||||||
|
clientY: y,
|
||||||
|
button: 0
|
||||||
|
});
|
||||||
|
element.dispatchEvent(clickEvent);
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
}, 50 + Math.random() * 50);
|
||||||
|
}, 50 + Math.random() * 50);
|
||||||
|
}, 200 + Math.random() * 300);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 等待元素出现
|
||||||
|
waitForElement: function(selector, timeout = 10000, checkVisible = true) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const check = () => {
|
||||||
|
const element = document.querySelector(selector);
|
||||||
|
if (element) {
|
||||||
|
if (!checkVisible) {
|
||||||
|
resolve(element);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
const isVisible = rect.width > 0 && rect.height > 0 &&
|
||||||
|
window.getComputedStyle(element).visibility !== 'hidden' &&
|
||||||
|
window.getComputedStyle(element).display !== 'none';
|
||||||
|
if (isVisible) {
|
||||||
|
resolve(element);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (check()) return;
|
||||||
|
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
if (check()) {
|
||||||
|
observer.disconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['style', 'class']
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
observer.disconnect();
|
||||||
|
reject(new Error(`等待元素超时: ${selector}`));
|
||||||
|
}, timeout);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 等待元素消失
|
||||||
|
waitForElementRemoved: function(selector, timeout = 10000) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const check = () => {
|
||||||
|
const element = document.querySelector(selector);
|
||||||
|
if (!element) {
|
||||||
|
resolve();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
const isHidden = rect.width === 0 || rect.height === 0 ||
|
||||||
|
window.getComputedStyle(element).visibility === 'hidden' ||
|
||||||
|
window.getComputedStyle(element).display === 'none';
|
||||||
|
if (isHidden) {
|
||||||
|
resolve();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (check()) return;
|
||||||
|
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
if (check()) {
|
||||||
|
observer.disconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['style', 'class']
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
observer.disconnect();
|
||||||
|
reject(new Error(`等待元素消失超时: ${selector}`));
|
||||||
|
}, timeout);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 随机延迟
|
||||||
|
randomDelay: function(min = 100, max = 500) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
setTimeout(resolve, min + Math.random() * (max - min));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取所有元素
|
||||||
|
getElements: function(selector) {
|
||||||
|
return Array.from(document.querySelectorAll(selector));
|
||||||
|
},
|
||||||
|
|
||||||
|
// 监听请求
|
||||||
|
onRequest: function(callback) {
|
||||||
|
window.GeneralAutoConfig.requestCallbacks.push(callback);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 监听响应
|
||||||
|
onResponse: function(callback) {
|
||||||
|
window.GeneralAutoConfig.responseCallbacks.push(callback);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 清除所有回调
|
||||||
|
clearCallbacks: function() {
|
||||||
|
window.GeneralAutoConfig.requestCallbacks = [];
|
||||||
|
window.GeneralAutoConfig.responseCallbacks = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 简写
|
||||||
|
window.AU = window.AutomationUtils;
|
||||||
|
|
||||||
|
console.log('AutomationUtils API 已就绪');
|
||||||
|
|
||||||
|
})();
|
||||||
39
expand/general-auto/manifest.json
Normal file
39
expand/general-auto/manifest.json
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"manifest_version": 3,
|
||||||
|
"name": "General Auto - Browser Automation Controller",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "通用浏览器自动化控制扩展",
|
||||||
|
"permissions": [
|
||||||
|
"tabs",
|
||||||
|
"webRequest",
|
||||||
|
"webNavigation",
|
||||||
|
"cookies",
|
||||||
|
"storage",
|
||||||
|
"debugger",
|
||||||
|
"scripting"
|
||||||
|
],
|
||||||
|
"host_permissions": [
|
||||||
|
"http://*/*",
|
||||||
|
"https://*/*"
|
||||||
|
],
|
||||||
|
"background": {
|
||||||
|
"service_worker": "background.js"
|
||||||
|
},
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"matches": ["<all_urls>"],
|
||||||
|
"js": ["content.js"],
|
||||||
|
"run_at": "document_start",
|
||||||
|
"all_frames": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"web_accessible_resources": [
|
||||||
|
{
|
||||||
|
"resources": ["injected.js"],
|
||||||
|
"matches": ["<all_urls>"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"action": {
|
||||||
|
"default_popup": "popup.html"
|
||||||
|
}
|
||||||
|
}
|
||||||
280
expand/general-auto/popup.html
Normal file
280
expand/general-auto/popup.html
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>General Auto - 浏览器自动化控制器</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
width: 350px;
|
||||||
|
min-height: 400px;
|
||||||
|
padding: 15px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h2 {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .version {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card {
|
||||||
|
background: white;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card h3 {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card h3::before {
|
||||||
|
content: '';
|
||||||
|
width: 3px;
|
||||||
|
height: 14px;
|
||||||
|
background: #667eea;
|
||||||
|
margin-right: 8px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 6px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-label {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-value {
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-value.active {
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-value.inactive {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 10px 15px;
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s;
|
||||||
|
box-shadow: 0 2px 4px rgba(102, 126, 234, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: #5568d3;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 8px rgba(102, 126, 234, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.secondary {
|
||||||
|
background: #95de64;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.secondary:hover {
|
||||||
|
background: #7ec050;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.danger {
|
||||||
|
background: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.danger:hover {
|
||||||
|
background: #ff1f21;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.full-width {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-header h3 {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-log {
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
background: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-content {
|
||||||
|
max-height: 150px;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-size: 12px;
|
||||||
|
background: #fafafa;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry {
|
||||||
|
padding: 4px 0;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-time {
|
||||||
|
color: #999;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-message {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-content::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-content::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-content::-webkit-scrollbar-thumb {
|
||||||
|
background: #888;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-content::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-log {
|
||||||
|
text-align: center;
|
||||||
|
color: #999;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-top: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<h2>General Auto</h2>
|
||||||
|
<div class="version">浏览器自动化控制器 v1.0.0</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-card">
|
||||||
|
<h3>运行状态</h3>
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">状态:</span>
|
||||||
|
<span class="status-value" id="status">就绪</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">任务队列:</span>
|
||||||
|
<span class="status-value" id="taskCount">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">请求日志:</span>
|
||||||
|
<span class="status-value" id="requestLogCount">0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="button-group">
|
||||||
|
<button id="refreshStatus">刷新状态</button>
|
||||||
|
<button id="getCookies" class="secondary">获取Cookie</button>
|
||||||
|
<button id="clearCookies" class="danger">清除Cookie</button>
|
||||||
|
<button id="viewLogs" class="secondary">查看日志</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="log-container">
|
||||||
|
<div class="log-header">
|
||||||
|
<h3>操作日志</h3>
|
||||||
|
<button class="clear-log" id="clearLog">清空</button>
|
||||||
|
</div>
|
||||||
|
<div class="log-content" id="logContent">
|
||||||
|
<div class="empty-log">暂无日志</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
© 2025 General Auto | 浏览器自动化工具
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="popup.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
153
expand/general-auto/popup.js
Normal file
153
expand/general-auto/popup.js
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
// Popup Script
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const statusEl = document.getElementById('status');
|
||||||
|
const taskCountEl = document.getElementById('taskCount');
|
||||||
|
const requestLogCountEl = document.getElementById('requestLogCount');
|
||||||
|
const logContentEl = document.getElementById('logContent');
|
||||||
|
|
||||||
|
let logs = [];
|
||||||
|
|
||||||
|
// 添加日志
|
||||||
|
function addLog(message, type = 'info') {
|
||||||
|
const now = new Date();
|
||||||
|
const timeStr = now.toLocaleTimeString('zh-CN', {
|
||||||
|
hour12: false,
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit'
|
||||||
|
});
|
||||||
|
|
||||||
|
logs.push({
|
||||||
|
time: timeStr,
|
||||||
|
message: message,
|
||||||
|
type: type
|
||||||
|
});
|
||||||
|
|
||||||
|
// 限制日志数量
|
||||||
|
if (logs.length > 50) {
|
||||||
|
logs.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderLogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染日志
|
||||||
|
function renderLogs() {
|
||||||
|
if (logs.length === 0) {
|
||||||
|
logContentEl.innerHTML = '<div class="empty-log">暂无日志</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logContentEl.innerHTML = logs.map(log => `
|
||||||
|
<div class="log-entry">
|
||||||
|
<span class="log-time">[${log.time}]</span>
|
||||||
|
<span class="log-message">${log.message}</span>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
logContentEl.scrollTop = logContentEl.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新状态
|
||||||
|
function updateStatus() {
|
||||||
|
chrome.runtime.sendMessage({ action: 'getStatus' }, response => {
|
||||||
|
if (response && response.success) {
|
||||||
|
const status = response.status;
|
||||||
|
statusEl.textContent = status.isProcessing ? '运行中' : '就绪';
|
||||||
|
statusEl.className = 'status-value ' + (status.isProcessing ? 'active' : 'inactive');
|
||||||
|
taskCountEl.textContent = status.taskCount;
|
||||||
|
requestLogCountEl.textContent = status.requestLogCount;
|
||||||
|
addLog('状态已更新');
|
||||||
|
} else {
|
||||||
|
addLog('获取状态失败', 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新状态按钮
|
||||||
|
document.getElementById('refreshStatus').addEventListener('click', function() {
|
||||||
|
updateStatus();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取 Cookies 按钮
|
||||||
|
document.getElementById('getCookies').addEventListener('click', function() {
|
||||||
|
chrome.tabs.query({ active: true, currentWindow: true }, function(tabs) {
|
||||||
|
if (tabs[0]) {
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
action: 'getCookies',
|
||||||
|
url: tabs[0].url
|
||||||
|
}, response => {
|
||||||
|
if (response && response.success) {
|
||||||
|
const cookieCount = response.cookies.length;
|
||||||
|
addLog(`获取到 ${cookieCount} 个 Cookie`);
|
||||||
|
console.log('Cookies:', response.cookies);
|
||||||
|
|
||||||
|
// 复制到剪贴板
|
||||||
|
const cookieText = JSON.stringify(response.cookies, null, 2);
|
||||||
|
navigator.clipboard.writeText(cookieText).then(() => {
|
||||||
|
addLog('Cookie 已复制到剪贴板');
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('复制失败:', err);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
addLog('获取 Cookie 失败', 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 清除 Cookies 按钮
|
||||||
|
document.getElementById('clearCookies').addEventListener('click', function() {
|
||||||
|
if (confirm('确定要清除当前页面的所有 Cookies 吗?')) {
|
||||||
|
chrome.tabs.query({ active: true, currentWindow: true }, function(tabs) {
|
||||||
|
if (tabs[0]) {
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
action: 'clearCookies',
|
||||||
|
url: tabs[0].url
|
||||||
|
}, response => {
|
||||||
|
if (response && response.success) {
|
||||||
|
addLog('Cookies 已清除');
|
||||||
|
} else {
|
||||||
|
addLog('清除 Cookies 失败', 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 查看日志按钮
|
||||||
|
document.getElementById('viewLogs').addEventListener('click', function() {
|
||||||
|
chrome.runtime.sendMessage({ action: 'getRequestLogs' }, response => {
|
||||||
|
if (response && response.success) {
|
||||||
|
const logCount = response.logs.length;
|
||||||
|
addLog(`获取到 ${logCount} 条请求日志`);
|
||||||
|
console.log('请求日志:', response.logs);
|
||||||
|
|
||||||
|
if (logCount > 0) {
|
||||||
|
// 显示最近 5 条
|
||||||
|
response.logs.slice(-5).forEach(log => {
|
||||||
|
addLog(`${log.method} ${log.url} - ${log.status}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addLog('获取请求日志失败', 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 清空日志按钮
|
||||||
|
document.getElementById('clearLog').addEventListener('click', function() {
|
||||||
|
logs = [];
|
||||||
|
renderLogs();
|
||||||
|
addLog('日志已清空');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
addLog('扩展已就绪');
|
||||||
|
updateStatus();
|
||||||
|
|
||||||
|
// 定时更新状态
|
||||||
|
setInterval(updateStatus, 5000);
|
||||||
|
});
|
||||||
@@ -144,15 +144,27 @@ func (c *Chrome) Create(accountId int, params *Fingerprint) (*rod.Browser, strin
|
|||||||
func (c *Chrome) TemporaryCreate(params *Fingerprint) (*rod.Browser, string, *rod.Page) {
|
func (c *Chrome) TemporaryCreate(params *Fingerprint) (*rod.Browser, string, *rod.Page) {
|
||||||
userDataDir := "user-data/" + utils.RandString(5)
|
userDataDir := "user-data/" + utils.RandString(5)
|
||||||
absPath, _ := filepath.Abs(userDataDir)
|
absPath, _ := filepath.Abs(userDataDir)
|
||||||
path, _ := launcher.LookPath() // 获取当前系统的浏览器目录
|
// path, _ := launcher.LookPath() // 获取当前系统的浏览器目录
|
||||||
// 判断是否传递代理
|
// 判断是否传递代理
|
||||||
// var proxyHandle utils.ProxyHandle
|
// var proxyHandle utils.ProxyHandle
|
||||||
// protocol, username, password, ip, port, errProxy := proxyHandle.ParseProxy(params.Proxy)
|
// protocol, username, password, ip, port, errProxy := proxyHandle.ParseProxy(params.Proxy)
|
||||||
|
// 加载扩展
|
||||||
|
expandPath, _ := filepath.Abs("expand/general-auto")
|
||||||
|
|
||||||
|
// 验证扩展目录
|
||||||
|
if _, err := os.Stat(expandPath); os.IsNotExist(err) {
|
||||||
|
log.Fatal("扩展目录不存在:", expandPath)
|
||||||
|
}
|
||||||
|
manifestPath := filepath.Join(expandPath, "manifest.json")
|
||||||
|
if _, err := os.Stat(manifestPath); os.IsNotExist(err) {
|
||||||
|
log.Fatal("manifest.json 不存在:", manifestPath)
|
||||||
|
}
|
||||||
|
|
||||||
ipInfo := external.GetIpInfo(strings.TrimSpace(string(params.Proxy)))
|
ipInfo := external.GetIpInfo(strings.TrimSpace(string(params.Proxy)))
|
||||||
|
|
||||||
l := launcher.New().
|
l := launcher.New().
|
||||||
Bin(path).
|
// Bin(path).
|
||||||
|
Set("load-extension", expandPath).
|
||||||
// Proxy("210.51.27.121:50003"). // 直接传入完整认证URL
|
// Proxy("210.51.27.121:50003"). // 直接传入完整认证URL
|
||||||
Delete("use-mock-keychain"). // delete flag "--use-mock-keychain"
|
Delete("use-mock-keychain"). // delete flag "--use-mock-keychain"
|
||||||
Set("disable-blink-features", "AutomationControlled"). // 绕过自动化检测
|
Set("disable-blink-features", "AutomationControlled"). // 绕过自动化检测
|
||||||
|
|||||||
@@ -16,4 +16,5 @@ func (*Twitter) TwitterRouterInit(app *gin.Engine) {
|
|||||||
twitter.GET("/login", twitterApi.Login) // 登录账号
|
twitter.GET("/login", twitterApi.Login) // 登录账号
|
||||||
twitter.GET("/logout", twitterApi.Logout) // 退出登录
|
twitter.GET("/logout", twitterApi.Logout) // 退出登录
|
||||||
twitter.POST("/register1", twitterApi.Register1)
|
twitter.POST("/register1", twitterApi.Register1)
|
||||||
|
twitter.GET("/test", twitterApi.Test)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user