diff --git a/api/twitter.go b/api/twitter.go index 976e7ca..86f4811 100644 --- a/api/twitter.go +++ b/api/twitter.go @@ -90,3 +90,15 @@ func (*Twitter) Register1(ctx *gin.Context) { "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": "", + }) +} diff --git a/expand/general-auto/INSTALL.md b/expand/general-auto/INSTALL.md new file mode 100644 index 0000000..376166e --- /dev/null +++ b/expand/general-auto/INSTALL.md @@ -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 +- ❌ 图标(暂时移除,不影响功能) diff --git a/expand/general-auto/README.md b/expand/general-auto/README.md new file mode 100644 index 0000000..8bc9692 --- /dev/null +++ b/expand/general-auto/README.md @@ -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 + +## 支持 + +如有问题,请联系开发团队。 diff --git a/expand/general-auto/TROUBLESHOOT.md b/expand/general-auto/TROUBLESHOOT.md new file mode 100644 index 0000000..5a9bb1a --- /dev/null +++ b/expand/general-auto/TROUBLESHOOT.md @@ -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/` 的截图 +- 控制台是否有任何错误信息 +- 手动加载时的错误提示(如果有) diff --git a/expand/general-auto/background.js b/expand/general-auto/background.js new file mode 100644 index 0000000..75f2f41 --- /dev/null +++ b/expand/general-auto/background.js @@ -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: [""] }, + [] +); + +// 监听响应头 +chrome.webRequest.onCompleted.addListener( + (details) => { + if (details.type === 'xmlhttprequest' || details.type === 'fetch') { + console.log('API 响应:', details.url, details.statusCode); + } + }, + { urls: [""] }, + [] +); diff --git a/expand/general-auto/content.js b/expand/general-auto/content.js new file mode 100644 index 0000000..2676485 --- /dev/null +++ b/expand/general-auto/content.js @@ -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 已就绪'); diff --git a/expand/general-auto/generate-icons.html b/expand/general-auto/generate-icons.html new file mode 100644 index 0000000..2dab348 --- /dev/null +++ b/expand/general-auto/generate-icons.html @@ -0,0 +1,85 @@ + + + + + 生成图标 + + + + + + + + +

图标生成中,请保存下载的 PNG 文件到 icons 目录...

+

或者手动右键点击下面的图标保存:

+
+

16x16:

+ +
+
+

48x48:

+ +
+
+

128x128:

+ +
+ + diff --git a/expand/general-auto/icons/converter.html b/expand/general-auto/icons/converter.html new file mode 100644 index 0000000..74b1e79 --- /dev/null +++ b/expand/general-auto/icons/converter.html @@ -0,0 +1,33 @@ + + + + + SVG to PNG Converter + + +

SVG 转 PNG 工具

+

请按照以下步骤操作:

+
    +
  1. 在浏览器中打开 icon16.svg, icon48.svg, icon128.svg
  2. +
  3. 右键点击 → 另存为
  4. +
  5. 保存为 icon16.png, icon48.png, icon128.png
  6. +
+ +

或者使用在线工具:

+ + +

临时解决方案:

+

Chrome 扩展也可以直接使用 SVG 文件,修改 manifest.json:

+
+"icons": {
+  "16": "icons/icon16.svg",
+  "48": "icons/icon48.svg",
+  "128": "icons/icon128.svg"
+}
+    
+ + diff --git a/expand/general-auto/icons/icon128.svg b/expand/general-auto/icons/icon128.svg new file mode 100644 index 0000000..5dc79df --- /dev/null +++ b/expand/general-auto/icons/icon128.svg @@ -0,0 +1,13 @@ + + + + + + + + + GA + + + + diff --git a/expand/general-auto/icons/icon16.svg b/expand/general-auto/icons/icon16.svg new file mode 100644 index 0000000..e5433f7 --- /dev/null +++ b/expand/general-auto/icons/icon16.svg @@ -0,0 +1,10 @@ + + + + + + + + + GA + diff --git a/expand/general-auto/icons/icon48.svg b/expand/general-auto/icons/icon48.svg new file mode 100644 index 0000000..3f7f4a8 --- /dev/null +++ b/expand/general-auto/icons/icon48.svg @@ -0,0 +1,10 @@ + + + + + + + + + GA + diff --git a/expand/general-auto/injected.js b/expand/general-auto/injected.js new file mode 100644 index 0000000..a7c404d --- /dev/null +++ b/expand/general-auto/injected.js @@ -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 已就绪'); + +})(); diff --git a/expand/general-auto/manifest.json b/expand/general-auto/manifest.json new file mode 100644 index 0000000..8aa26a0 --- /dev/null +++ b/expand/general-auto/manifest.json @@ -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": [""], + "js": ["content.js"], + "run_at": "document_start", + "all_frames": true + } + ], + "web_accessible_resources": [ + { + "resources": ["injected.js"], + "matches": [""] + } + ], + "action": { + "default_popup": "popup.html" + } +} diff --git a/expand/general-auto/popup.html b/expand/general-auto/popup.html new file mode 100644 index 0000000..265e5d2 --- /dev/null +++ b/expand/general-auto/popup.html @@ -0,0 +1,280 @@ + + + + + + General Auto - 浏览器自动化控制器 + + + +
+

General Auto

+
浏览器自动化控制器 v1.0.0
+
+ +
+

运行状态

+
+ 状态: + 就绪 +
+
+ 任务队列: + 0 +
+
+ 请求日志: + 0 +
+
+ +
+ + + + +
+ +
+
+

操作日志

+ +
+
+
暂无日志
+
+
+ + + + + + diff --git a/expand/general-auto/popup.js b/expand/general-auto/popup.js new file mode 100644 index 0000000..e200657 --- /dev/null +++ b/expand/general-auto/popup.js @@ -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 = '
暂无日志
'; + return; + } + + logContentEl.innerHTML = logs.map(log => ` +
+ [${log.time}] + ${log.message} +
+ `).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); +}); diff --git a/libs/chrome.go b/libs/chrome.go index 1195ccb..c679003 100644 --- a/libs/chrome.go +++ b/libs/chrome.go @@ -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) { userDataDir := "user-data/" + utils.RandString(5) absPath, _ := filepath.Abs(userDataDir) - path, _ := launcher.LookPath() // 获取当前系统的浏览器目录 + // path, _ := launcher.LookPath() // 获取当前系统的浏览器目录 // 判断是否传递代理 // var proxyHandle utils.ProxyHandle // 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))) l := launcher.New(). - Bin(path). + // Bin(path). + Set("load-extension", expandPath). // Proxy("210.51.27.121:50003"). // 直接传入完整认证URL Delete("use-mock-keychain"). // delete flag "--use-mock-keychain" Set("disable-blink-features", "AutomationControlled"). // 绕过自动化检测 diff --git a/router/twitter.go b/router/twitter.go index ff52318..472d9cc 100644 --- a/router/twitter.go +++ b/router/twitter.go @@ -16,4 +16,5 @@ func (*Twitter) TwitterRouterInit(app *gin.Engine) { twitter.GET("/login", twitterApi.Login) // 登录账号 twitter.GET("/logout", twitterApi.Logout) // 退出登录 twitter.POST("/register1", twitterApi.Register1) + twitter.GET("/test", twitterApi.Test) }