3232
This commit is contained in:
parent
9c22a1dc07
commit
aa4c7a09a9
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
|
|
@ -0,0 +1,78 @@
|
|||
package collect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"geo/internal/collect"
|
||||
"geo/internal/config"
|
||||
"testing"
|
||||
|
||||
"github.com/gofiber/fiber/v2/log"
|
||||
)
|
||||
|
||||
var (
|
||||
deepseekCfg, _ = config.LoadConfig()
|
||||
|
||||
deepseekManager = collect.NewCollectManager(context.Background(), deepseekCfg, log.DefaultLogger())
|
||||
)
|
||||
|
||||
// TestDeepseekCollector_WaitLogin 测试DeepSeek登录功能
|
||||
func TestDeepseekCollector_WaitLogin(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("跳过需要浏览器交互的测试")
|
||||
}
|
||||
|
||||
params := &collect.CollectParams{
|
||||
Headless: false, // 显示浏览器窗口以便扫码登录
|
||||
RequestID: "test_deepseek_login_001",
|
||||
Platform: "deepseek",
|
||||
}
|
||||
|
||||
t.Log("开始测试DeepSeek登录...")
|
||||
t.Log("请在打开的浏览器窗口中完成DeepSeek账号登录(扫码或输入账号密码)")
|
||||
|
||||
success, msg := deepseekManager.WaitLogin("deepseek", params)
|
||||
|
||||
if !success {
|
||||
t.Errorf("DeepSeek登录失败: %s", msg)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("DeepSeek登录成功: %s", msg)
|
||||
t.Log("Cookie已保存,后续测试可以使用已登录状态")
|
||||
}
|
||||
|
||||
// TestDeepseekCollector_AskQuestion 测试DeepSeek提问功能
|
||||
// 注意:此测试需要有效的登录状态
|
||||
func TestDeepseekCollector_AskQuestion(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("跳过需要浏览器交互的测试")
|
||||
}
|
||||
|
||||
// 设置收集参数
|
||||
params := &collect.CollectParams{
|
||||
Headless: false, // 显示浏览器以便调试
|
||||
RequestID: "test_deepseek_001",
|
||||
Platform: "deepseek",
|
||||
KeyWords: []string{"AI", "人工智能"}, // 测试关键词高亮
|
||||
}
|
||||
|
||||
// 定义提问内容
|
||||
question := "什么是人工智能?"
|
||||
t.Logf("向DeepSeek提问: %s", question)
|
||||
|
||||
// 调用管理器提问并获取答案
|
||||
result, err := deepseekManager.AskQuestion("deepseek", params, question)
|
||||
if err != nil {
|
||||
t.Errorf("提问失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("获取到答案:\n%s", result.Answer)
|
||||
t.Logf("分享链接: %s", result.ShareLink)
|
||||
t.Logf("是否包含关键词: %v", result.IsExposure)
|
||||
|
||||
// 验证答案非空
|
||||
if len(result.Answer) == 0 {
|
||||
t.Error("答案为空")
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 105 KiB |
|
|
@ -31,14 +31,8 @@ func NewCollectBiz(ctx context.Context, cfg *config.Config, logger log.AllLogger
|
|||
// requestID: 请求ID
|
||||
// question: 问题内容
|
||||
// headless: 是否无头模式
|
||||
func (b *CollectBiz) AskAIQuestion(platform string, requestID, question string, headless bool) (*collect.CollectResult, error) {
|
||||
params := &collect.CollectParams{
|
||||
Headless: headless,
|
||||
RequestID: requestID,
|
||||
Platform: platform,
|
||||
}
|
||||
|
||||
result, err := b.manager.AskQuestion(platform, params, question)
|
||||
func (b *CollectBiz) AskAIQuestion(platform string, requestID, question string, keywords []string, headless bool) (*collect.CollectResult, error) {
|
||||
result, err := b.manager.AskQuestion(platform, question)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("向%s提问失败: %w", platform, err)
|
||||
}
|
||||
|
|
@ -46,41 +40,11 @@ func (b *CollectBiz) AskAIQuestion(platform string, requestID, question string,
|
|||
return result, nil
|
||||
}
|
||||
|
||||
// WaitAILogin 等待AI平台登录
|
||||
func (b *CollectBiz) WaitAILogin(platform string, requestID string, headless bool) (bool, string) {
|
||||
params := &collect.CollectParams{
|
||||
Headless: headless,
|
||||
RequestID: requestID,
|
||||
Platform: platform,
|
||||
}
|
||||
|
||||
return b.manager.WaitLogin(platform, params)
|
||||
}
|
||||
// WaitAILogin 等待AI平台登录 - 已废弃
|
||||
// 注意:新架构中不再需要单独的登录方法,登录状态通过 Cookie 自动维持
|
||||
|
||||
// ListAIPlatforms 列出所有支持的AI平台
|
||||
func (b *CollectBiz) ListAIPlatforms() []string {
|
||||
return b.manager.ListPlatforms()
|
||||
}
|
||||
|
||||
// AskMultipleAI 向多个AI平台提问并收集答案
|
||||
func (b *CollectBiz) AskMultipleAI(platforms []string, requestID, question string, headless bool) map[string]*collect.CollectResult {
|
||||
results := make(map[string]*collect.CollectResult)
|
||||
|
||||
for _, platform := range platforms {
|
||||
// 为每个平台生成唯一的 requestID
|
||||
platformRequestID := requestID + "_" + platform
|
||||
result, err := b.AskAIQuestion(platform, platformRequestID, question, headless)
|
||||
if err != nil {
|
||||
b.logger.Errorf("向%s提问失败: %v", platform, err)
|
||||
// 创建一个包含错误信息的结果
|
||||
results[platform] = &collect.CollectResult{
|
||||
Answer: fmt.Sprintf("错误: %v", err),
|
||||
ShareLink: "",
|
||||
}
|
||||
} else {
|
||||
results[platform] = result
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
platforms := []string{"wenxin", "deepseek", "doubao", "qianwen"}
|
||||
return platforms
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
### 1. 基本使用
|
||||
|
||||
```go
|
||||
``go
|
||||
package main
|
||||
|
||||
import (
|
||||
|
|
@ -89,7 +89,7 @@ func main() {
|
|||
|
||||
### 2. 多平台对比
|
||||
|
||||
```go
|
||||
``go
|
||||
// 向多个AI平台提问同一个问题
|
||||
platforms := []string{"wenxin", "deepseek", "doubao", "qianwen"}
|
||||
question := "什么是人工智能?"
|
||||
|
|
@ -115,7 +115,7 @@ for _, platform := range platforms {
|
|||
|
||||
### 3. 登录管理
|
||||
|
||||
```go
|
||||
``go
|
||||
// 首次使用时需要登录
|
||||
params := &collect.CollectParams{
|
||||
Headless: false, // 显示浏览器窗口以便扫码登录
|
||||
|
|
@ -140,7 +140,7 @@ answer, _ := manager.AskQuestion("wenxin", params, "你好")
|
|||
|
||||
### 4. 列出支持的平台
|
||||
|
||||
```go
|
||||
``go
|
||||
platforms := manager.ListPlatforms()
|
||||
fmt.Printf("支持的平台: %v\n", platforms)
|
||||
// 输出: 支持的平台: [wenxin deepseek doubao qianwen]
|
||||
|
|
@ -150,7 +150,7 @@ fmt.Printf("支持的平台: %v\n", platforms)
|
|||
|
||||
### 必需的配置项
|
||||
|
||||
```go
|
||||
``go
|
||||
type SysConfig struct {
|
||||
ChromePath string // Chrome浏览器可执行文件路径
|
||||
ChromeDataDir string // Chrome用户数据目录
|
||||
|
|
@ -161,7 +161,7 @@ type SysConfig struct {
|
|||
|
||||
### 示例配置
|
||||
|
||||
```go
|
||||
``go
|
||||
cfg := &config.Config{
|
||||
Sys: config.SysConfig{
|
||||
ChromePath: "/usr/bin/google-chrome", // Linux
|
||||
|
|
@ -229,7 +229,7 @@ cfg := &config.Config{
|
|||
|
||||
示例:
|
||||
|
||||
```go
|
||||
``go
|
||||
package collect
|
||||
|
||||
import (
|
||||
|
|
@ -257,7 +257,7 @@ func NewNewPlatformCollector(ctx context.Context, params *CollectParams, cfg *co
|
|||
|
||||
然后在 `interface.go` 中注册:
|
||||
|
||||
```go
|
||||
``go
|
||||
var CollectorMap = map[string]*CollectorValue{
|
||||
// ... 其他平台
|
||||
"newplatform": {
|
||||
|
|
@ -304,3 +304,316 @@ var CollectorMap = map[string]*CollectorValue{
|
|||
## 许可证
|
||||
|
||||
与项目主许可证保持一致。
|
||||
|
||||
# Collect 模块 - 单浏览器多Page架构
|
||||
|
||||
## 架构说明
|
||||
|
||||
本模块采用**单浏览器多Page模式**,服务启动时创建一个全局浏览器实例,并为每个平台打开一个常驻 Page。
|
||||
|
||||
### 核心特性
|
||||
|
||||
1. **单一浏览器**:所有平台共用同一个浏览器实例
|
||||
2. **启动时预创建**:服务启动时立即创建浏览器并打开所有平台的页面
|
||||
3. **Page 常驻**:每个平台的 Page 在整个服务生命周期内保持活跃
|
||||
4. **强制有头模式**:便于调试和人工干预(如扫码登录)
|
||||
5. **统一管理**:Browser 和 Page 都由 Manager 统一管理和关闭
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 基本用法
|
||||
|
||||
``go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"geo/internal/collect"
|
||||
"geo/internal/config"
|
||||
"github.com/gofiber/fiber/v2/log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg, _ := config.LoadConfig()
|
||||
ctx := context.Background()
|
||||
|
||||
// 创建管理器(会自动:启动浏览器 + 打开所有平台页面)
|
||||
manager := collect.NewCollectManager(ctx, cfg, log.DefaultLogger())
|
||||
|
||||
// ⚠️ 重要:确保程序退出时关闭所有资源
|
||||
defer manager.Close()
|
||||
|
||||
// 每次调用都使用对应的常驻 Page
|
||||
params := &collect.CollectParams{
|
||||
RequestID: "req_001",
|
||||
Platform: "wenxin",
|
||||
KeyWords: []string{"AI", "人工智能"},
|
||||
}
|
||||
|
||||
// 提问(使用 wenxin 的常驻 Page)
|
||||
result, err := manager.AskQuestion("wenxin", params, "什么是人工智能?")
|
||||
if err != nil {
|
||||
log.Errorf("提问失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("答案长度: %d", len(result.Answer))
|
||||
log.Infof("分享链接: %s", result.ShareLink)
|
||||
|
||||
// 可以切换到其他平台(使用对应的常驻 Page)
|
||||
result2, _ := manager.AskQuestion("deepseek", params, "第二个问题")
|
||||
}
|
||||
```
|
||||
|
||||
### 登录测试示例
|
||||
|
||||
``go
|
||||
func loginTest(manager *collect.CollectManager) {
|
||||
params := &collect.CollectParams{
|
||||
RequestID: "login_test_001",
|
||||
Platform: "deepseek",
|
||||
}
|
||||
|
||||
// 等待登录(使用 deepseek 的常驻 Page)
|
||||
// 浏览器窗口已经打开,可以直接看到页面并进行扫码登录
|
||||
success, msg := manager.WaitLogin("deepseek", params)
|
||||
if !success {
|
||||
log.Errorf("登录失败: %s", msg)
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("登录成功: %s", msg)
|
||||
// Cookie 已自动保存,下次可以直接使用
|
||||
}
|
||||
```
|
||||
|
||||
### 并发操作示例
|
||||
|
||||
``go
|
||||
func concurrentExample(manager *collect.CollectManager) {
|
||||
// 可以安全地并发调用不同平台
|
||||
go func() {
|
||||
params := &collect.CollectParams{
|
||||
RequestID: "req_001",
|
||||
Platform: "wenxin",
|
||||
}
|
||||
result, _ := manager.AskQuestion("wenxin", params, "问题1")
|
||||
log.Infof("文心一言回答: %s", result.Answer)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
params := &collect.CollectParams{
|
||||
RequestID: "req_002",
|
||||
Platform: "deepseek",
|
||||
}
|
||||
result, _ := manager.AskQuestion("deepseek", params, "问题2")
|
||||
log.Infof("DeepSeek回答: %s", result.Answer)
|
||||
}()
|
||||
}
|
||||
```
|
||||
|
||||
## 架构细节
|
||||
|
||||
### 浏览器管理
|
||||
|
||||
- **数量**:全局只有一个浏览器实例
|
||||
- **创建时机**:`NewCollectManager()` 调用时立即创建
|
||||
- **存储方式**:`manager.browser` 字段
|
||||
- **线程安全**:browser 只读访问,无竞态条件
|
||||
- **生命周期**:从服务启动到 `manager.Close()` 调用
|
||||
- **关闭方式**:调用 `manager.Close()` 关闭浏览器和所有 Page
|
||||
|
||||
### Page 管理
|
||||
|
||||
- **数量**:每个平台一个 Page(共 4 个:wenxin, deepseek, doubao, qianwen)
|
||||
- **创建时机**:`NewCollectManager()` 调用时为所有平台打开页面
|
||||
- **存储方式**:`map[string]*rod.Page`,key 为平台名称
|
||||
- **线程安全**:使用 `sync.RWMutex` 保护并发访问
|
||||
- **生命周期**:从服务启动到 `manager.Close()` 调用
|
||||
- **特点**:Page 常驻,不会在操作后关闭
|
||||
|
||||
### 数据流
|
||||
|
||||
```
|
||||
服务启动
|
||||
↓
|
||||
NewCollectManager()
|
||||
↓
|
||||
创建全局浏览器(1个)
|
||||
↓
|
||||
为每个平台打开 Page(4个)
|
||||
├─ wenxin Page → https://yiyan.baidu.com/
|
||||
├─ deepseek Page → https://chat.deepseek.com/
|
||||
├─ doubao Page → https://www.doubao.com/chat/
|
||||
└─ qianwen Page → https://tongyi.aliyun.com/qianwen/
|
||||
↓
|
||||
所有 Page 保持活跃(可看到浏览器窗口)
|
||||
|
||||
每次请求:
|
||||
↓
|
||||
AskQuestion(platform, ...)
|
||||
↓
|
||||
获取对应平台的常驻 Page
|
||||
↓
|
||||
执行操作(输入、点击等)
|
||||
↓
|
||||
返回结果(Page 保持活跃,不关闭)
|
||||
|
||||
服务关闭:
|
||||
↓
|
||||
manager.Close()
|
||||
↓
|
||||
关闭所有 Page
|
||||
↓
|
||||
关闭浏览器
|
||||
```
|
||||
|
||||
### 数据存储
|
||||
|
||||
```
|
||||
ChromeDataDir/
|
||||
└── global/
|
||||
└── main/ # 全局浏览器用户数据(所有平台共用)
|
||||
|
||||
CookiesDir/
|
||||
├── wenxin/
|
||||
│ └── wenxin.json # Cookie 文件(按平台隔离)
|
||||
├── deepseek/
|
||||
│ └── deepseek.json
|
||||
└── doubao/
|
||||
└── doubao.json
|
||||
```
|
||||
|
||||
## 关键优势
|
||||
|
||||
### 1. 资源占用最小化
|
||||
- ✅ 只启动一个浏览器进程
|
||||
- ✅ 内存占用最低
|
||||
- ✅ 系统资源消耗最少
|
||||
|
||||
### 2. 启动速度快
|
||||
- ✅ 浏览器在服务启动时已就绪
|
||||
- ✅ 所有平台页面已打开
|
||||
- ✅ 无需等待页面加载
|
||||
|
||||
### 3. 调试友好
|
||||
- ✅ 有头模式,可实时观察所有平台
|
||||
- ✅ 支持人工干预(扫码、验证码等)
|
||||
- ✅ 便于问题排查
|
||||
|
||||
### 4. 会话保持
|
||||
- ✅ Page 常驻,登录状态持续有效
|
||||
- ✅ Cookie 自动保存和加载
|
||||
- ✅ 无需重复登录
|
||||
|
||||
## 注意事项
|
||||
|
||||
### ⚠️ 必须调用 Close()
|
||||
|
||||
``go
|
||||
manager := collect.NewCollectManager(ctx, cfg, logger)
|
||||
defer manager.Close() // 确保程序退出时关闭所有资源
|
||||
```
|
||||
|
||||
如果不调用 `Close()`,浏览器进程会残留。
|
||||
|
||||
### ⚠️ 有头模式限制
|
||||
|
||||
- 所有浏览器都以有头模式运行
|
||||
- 无法切换到无头模式
|
||||
- 适合开发和调试环境
|
||||
|
||||
### ⚠️ 并发访问注意
|
||||
|
||||
虽然使用了 `sync.RWMutex` 保护 pages map,但 rod 的 Page 本身不是线程安全的。建议:
|
||||
- 同一平台的请求串行执行
|
||||
- 不同平台可以并发执行
|
||||
- 避免在同一 Page 上同时执行多个操作
|
||||
|
||||
### ⚠️ Cookie 隔离
|
||||
|
||||
虽然浏览器是共用的,但 Cookie 文件按平台隔离存储,确保各平台的会话独立。
|
||||
|
||||
## 迁移指南
|
||||
|
||||
### 变化点
|
||||
|
||||
1. ✅ `NewCollectManager()` 会立即启动浏览器并打开所有平台页面
|
||||
2. ✅ Page 是常驻的,不会在操作后关闭
|
||||
3. ✅ `Collector.Close()` 不再关闭 Page(空实现)
|
||||
4. ✅ `Headless` 参数被忽略,强制为 `false`
|
||||
|
||||
### 无需修改
|
||||
|
||||
- ❌ Manager 的创建方式不变
|
||||
- ❌ 业务代码调用方式不变
|
||||
- ❌ Collector 的业务逻辑无需修改
|
||||
|
||||
## 性能对比
|
||||
|
||||
| 指标 | 旧架构 | 新架构 |
|
||||
|------|--------|--------|
|
||||
| 浏览器进程数 | 4个(每平台1个) | 1个(全局共用) |
|
||||
| 首次启动 | ~10秒 | ~5秒 |
|
||||
| 后续请求 | ~0.1秒 | ~0.1秒 |
|
||||
| 内存占用 | 高 | 低 |
|
||||
| 并发能力 | 中 | 中(需注意Page线程安全) |
|
||||
| 资源泄漏风险 | 低 | 低 |
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **服务启动时初始化**
|
||||
```go
|
||||
func initService() {
|
||||
manager = collect.NewCollectManager(ctx, cfg, logger)
|
||||
}
|
||||
```
|
||||
|
||||
2. **服务关闭时清理**
|
||||
```go
|
||||
func shutdownService() {
|
||||
manager.Close()
|
||||
}
|
||||
```
|
||||
|
||||
3. **异常处理**
|
||||
```go
|
||||
result, err := manager.AskQuestion(platform, params, question)
|
||||
if err != nil {
|
||||
log.Errorf("操作失败: %v", err)
|
||||
// Page 仍然可用,可以继续尝试
|
||||
}
|
||||
```
|
||||
|
||||
4. **监控建议**
|
||||
- 监控浏览器进程数量(应该只有1个)
|
||||
- 监控内存使用情况
|
||||
- 记录每次操作的耗时
|
||||
- 监控各平台 Page 的健康状态
|
||||
|
||||
## 架构图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ CollectManager │
|
||||
│ │
|
||||
│ ┌───────────────────────────────┐ │
|
||||
│ │ Global Browser (1个) │ │
|
||||
│ │ Headless: false │ │
|
||||
│ │ Window: 1920x1080 │ │
|
||||
│ └───────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ├──────────────────┐ │
|
||||
│ │ │ │
|
||||
│ ┌───────▼───────┐ ┌──────▼──┐│
|
||||
│ │ Wenxin Page │ │Deepseek ││
|
||||
│ │ (常驻) │ │Page ││
|
||||
│ └───────────────┘ └─────────┘│
|
||||
│ │ │ │
|
||||
│ ┌───────▼───────┐ ┌──────▼──┐│
|
||||
│ │ Doubao Page │ │Qianwen ││
|
||||
│ │ (常驻) │ │Page ││
|
||||
│ └───────────────┘ └─────────┘│
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/go-rod/rod"
|
||||
"github.com/go-rod/rod/lib/launcher"
|
||||
"github.com/go-rod/rod/lib/proto"
|
||||
"github.com/gofiber/fiber/v2/log"
|
||||
)
|
||||
|
|
@ -38,8 +37,8 @@ type BaseCollector struct {
|
|||
RetryDelay int
|
||||
}
|
||||
|
||||
// NewBaseCollector 构造函数
|
||||
func NewBaseCollector(ctx context.Context, params *CollectParams, config *config.Config, logger log.AllLogger) *BaseCollector {
|
||||
// NewBaseCollector 构造函数 - 接收已有的 browser 和 page
|
||||
func NewBaseCollector(ctx context.Context, params *CollectParams, config *config.Config, logger log.AllLogger, browser *rod.Browser, page *rod.Page) *BaseCollector {
|
||||
var baseLogger log.AllLogger
|
||||
|
||||
if logger != nil {
|
||||
|
|
@ -50,12 +49,14 @@ func NewBaseCollector(ctx context.Context, params *CollectParams, config *config
|
|||
|
||||
base := &BaseCollector{
|
||||
ctx: ctx,
|
||||
Headless: params.Headless,
|
||||
Headless: false, // 强制有头模式
|
||||
RequestID: params.RequestID,
|
||||
Platform: params.Platform,
|
||||
KeyWords: params.KeyWords,
|
||||
Logger: baseLogger,
|
||||
config: config,
|
||||
Browser: browser,
|
||||
Page: page,
|
||||
MaxRetries: 3,
|
||||
RetryDelay: 200,
|
||||
}
|
||||
|
|
@ -65,52 +66,22 @@ func NewBaseCollector(ctx context.Context, params *CollectParams, config *config
|
|||
return base
|
||||
}
|
||||
|
||||
// SetupDriver 初始化浏览器驱动
|
||||
// SetupDriver 初始化浏览器驱动 - 在常驻浏览器模式下,此方法不再需要创建浏览器
|
||||
// 保留此方法仅为兼容性,实际不做任何操作
|
||||
func (b *BaseCollector) SetupDriver() error {
|
||||
userDataDir := filepath.Join(b.config.Sys.ChromeDataDir, b.Platform, b.RequestID+fmt.Sprintf("___%d", time.Now().UnixNano()))
|
||||
os.MkdirAll(userDataDir, 0755)
|
||||
|
||||
l := launcher.New().
|
||||
Bin(b.config.Sys.ChromePath).
|
||||
UserDataDir(userDataDir).
|
||||
Headless(b.Headless).
|
||||
Leakless(false).
|
||||
Set("disable-blink-features", "AutomationControlled")
|
||||
|
||||
if b.Headless {
|
||||
l.Set("headless", "new")
|
||||
l.Set("disable-gpu")
|
||||
l.Set("no-sandbox")
|
||||
l.Set("disable-dev-shm-usage")
|
||||
} else {
|
||||
l.Set("window-size", "1920,1080")
|
||||
l.Set("start-maximized")
|
||||
l.Delete("headless")
|
||||
// 在常驻浏览器模式下,Browser 和 Page 已由 Manager 创建
|
||||
if b.Browser == nil || b.Page == nil {
|
||||
return fmt.Errorf("Browser 或 Page 未初始化")
|
||||
}
|
||||
|
||||
l.Set("lang", "zh-CN")
|
||||
l.Set("accept-lang", "zh-CN,zh;q=0.9,en;q=0.8")
|
||||
l.Set("force-device-scale-factor", "1")
|
||||
l.Set("timezone", "Asia/Shanghai")
|
||||
|
||||
url, err := l.Launch()
|
||||
if err != nil {
|
||||
return fmt.Errorf("启动浏览器失败: %v", err)
|
||||
}
|
||||
|
||||
b.Browser = rod.New().Context(b.ctx).ControlURL(url).MustConnect()
|
||||
b.Page = b.Browser.MustPage()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close 关闭浏览器
|
||||
// Close 关闭页面 - 如果是临时 Page,则关闭它
|
||||
func (b *BaseCollector) Close() {
|
||||
if b.Page != nil {
|
||||
b.Page.Close()
|
||||
}
|
||||
if b.Browser != nil {
|
||||
b.Browser.Close()
|
||||
b.Page.MustClose()
|
||||
b.Page = nil
|
||||
b.LogInfo("🔒 临时 Page 已关闭")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -273,13 +244,24 @@ func (b *BaseCollector) AskQuestion(question string) (*CollectResult, error) {
|
|||
return nil, fmt.Errorf("需要实现")
|
||||
}
|
||||
|
||||
// InitPage 初始化页面
|
||||
// InitPage 初始化页面 - 如果 Page 为 nil,则创建临时 Page
|
||||
func (b *BaseCollector) InitPage() error {
|
||||
// 如果 Page 为 nil,创建临时 Page
|
||||
if b.Page == nil {
|
||||
b.Page = b.Browser.MustPage()
|
||||
b.LogInfo("✅ 创建临时 Page")
|
||||
}
|
||||
|
||||
// 尝试加载cookies
|
||||
if err := b.LoadCookies(); err == nil {
|
||||
b.Page.MustNavigate(b.ChatURL)
|
||||
b.WaitForPageReady(5)
|
||||
} else {
|
||||
// 如果没有 cookies,直接导航到聊天页面
|
||||
b.Page.MustNavigate(b.ChatURL)
|
||||
b.WaitForPageReady(5)
|
||||
}
|
||||
|
||||
b.SaveCookies()
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,11 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"geo/internal/config"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/atotto/clipboard"
|
||||
"github.com/go-rod/rod"
|
||||
"github.com/go-rod/rod/lib/proto"
|
||||
"github.com/gofiber/fiber/v2/log"
|
||||
|
|
@ -18,9 +20,9 @@ type DeepseekCollector struct {
|
|||
}
|
||||
|
||||
// NewDeepseekCollector 创建DeepSeek收集器
|
||||
func NewDeepseekCollector(ctx context.Context, params *CollectParams, cfg *config.Config, logger log.AllLogger) CollectorInterface {
|
||||
func NewDeepseekCollector(ctx context.Context, params *CollectParams, cfg *config.Config, logger log.AllLogger, browser *rod.Browser, page *rod.Page) CollectorInterface {
|
||||
collector := &DeepseekCollector{
|
||||
BaseCollector: NewBaseCollector(ctx, params, cfg, logger),
|
||||
BaseCollector: NewBaseCollector(ctx, params, cfg, logger, browser, page),
|
||||
}
|
||||
|
||||
// 设置DeepSeek的URL
|
||||
|
|
@ -33,23 +35,12 @@ func NewDeepseekCollector(ctx context.Context, params *CollectParams, cfg *confi
|
|||
// CheckLoginStatus 检查登录状态
|
||||
func (c *DeepseekCollector) CheckLoginStatus() bool {
|
||||
currentURL := c.GetCurrentURL()
|
||||
|
||||
// 如果在首页或登录页面,可能未登录
|
||||
if strings.Contains(currentURL, "chat.deepseek.com") {
|
||||
// 检查是否有用户头像或登录标识
|
||||
userAvatar, err := c.SafeElement(".user-avatar, [class*='avatar'], [class*='profile']")
|
||||
if err == nil && userAvatar != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查是否有聊天输入框(登录后才有)
|
||||
inputBox, err := c.SafeElement("textarea, [contenteditable='true']")
|
||||
if err == nil && inputBox != nil {
|
||||
return true
|
||||
}
|
||||
c.LogInfo(fmt.Sprintf("当前URL: %s", currentURL))
|
||||
if currentURL == c.LoginURL {
|
||||
return false
|
||||
}
|
||||
|
||||
return false
|
||||
return true
|
||||
}
|
||||
|
||||
// WaitLogin 等待登录
|
||||
|
|
@ -67,6 +58,9 @@ func (c *DeepseekCollector) WaitLogin() (bool, string) {
|
|||
return true, "already_logged_in"
|
||||
}
|
||||
|
||||
c.LogInfo("未检测到登录状态,等待用户登录...")
|
||||
|
||||
// 最多等待300秒
|
||||
for i := 0; i < 300; i++ {
|
||||
if c.CheckLoginStatus() {
|
||||
c.Sleep(2)
|
||||
|
|
@ -81,10 +75,7 @@ func (c *DeepseekCollector) WaitLogin() (bool, string) {
|
|||
|
||||
// AskQuestion 提问并获取答案
|
||||
func (c *DeepseekCollector) AskQuestion(question string) (*CollectResult, error) {
|
||||
if err := c.SetupDriver(); err != nil {
|
||||
return nil, fmt.Errorf("浏览器启动失败: %v", err)
|
||||
}
|
||||
defer c.Close()
|
||||
// 注意:SetupDriver 和 Close 已由 Manager 管理,这里不再调用
|
||||
|
||||
if err := c.InitPage(); err != nil {
|
||||
return nil, fmt.Errorf("页面初始化失败: %v", err)
|
||||
|
|
@ -105,14 +96,27 @@ func (c *DeepseekCollector) AskQuestion(question string) (*CollectResult, error)
|
|||
return nil, fmt.Errorf("获取答案失败: %v", err)
|
||||
}
|
||||
|
||||
// 关键词高亮处理
|
||||
answerStr, isExposure := HighlightKeywordsInHTML(answer, c.KeyWords)
|
||||
|
||||
// 获取分享链接
|
||||
shareLink := ""
|
||||
link, _ := c.getShareLink()
|
||||
if link != "" {
|
||||
shareLink = link
|
||||
}
|
||||
|
||||
return &CollectResult{
|
||||
Answer: answer,
|
||||
ShareLink: "",
|
||||
Answer: answerStr,
|
||||
ShareLink: shareLink,
|
||||
IsExposure: isExposure,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// inputQuestion 输入问题
|
||||
func (c *DeepseekCollector) inputQuestion(question string) error {
|
||||
c.LogInfo("输入问题...")
|
||||
|
||||
// DeepSeek的输入框选择器
|
||||
inputSelectors := []string{
|
||||
"textarea[placeholder*='输入']",
|
||||
|
|
@ -129,6 +133,7 @@ func (c *DeepseekCollector) inputQuestion(question string) error {
|
|||
for _, selector := range inputSelectors {
|
||||
inputBox, err = c.WaitForElementVisible(selector, 10)
|
||||
if err == nil && inputBox != nil {
|
||||
c.LogInfo(fmt.Sprintf("找到输入框: %s", selector))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
@ -143,17 +148,12 @@ func (c *DeepseekCollector) inputQuestion(question string) error {
|
|||
}
|
||||
c.SleepMs(500)
|
||||
|
||||
// 清空输入框
|
||||
if err := c.ClearInput(inputBox); err != nil {
|
||||
// Ignore clear error
|
||||
}
|
||||
c.SleepMs(300)
|
||||
|
||||
// 输入问题
|
||||
if err := c.SetInputValue(inputBox, question); err != nil {
|
||||
inputBox.Input(question)
|
||||
}
|
||||
// fallback: 使用Focus + Input
|
||||
inputBox.Focus()
|
||||
c.SleepMs(200)
|
||||
inputBox.Input(question)
|
||||
|
||||
c.LogInfo(fmt.Sprintf("问题已输入: %s", question))
|
||||
c.SleepMs(1000)
|
||||
|
||||
return nil
|
||||
|
|
@ -161,96 +161,349 @@ func (c *DeepseekCollector) inputQuestion(question string) error {
|
|||
|
||||
// clickSendButton 点击发送按钮
|
||||
func (c *DeepseekCollector) clickSendButton() error {
|
||||
// 发送按钮选择器
|
||||
sendSelectors := []string{
|
||||
"button[class*='send']",
|
||||
"button[class*='submit']",
|
||||
".send-button",
|
||||
".submit-button",
|
||||
"button svg[path*='send']",
|
||||
"[aria-label*='发送']",
|
||||
"[aria-label*='Send']",
|
||||
c.LogInfo("点击发送按钮...")
|
||||
|
||||
// 使用正则匹配包含"send"或"提交"的class
|
||||
allElements, err := c.Page.Elements("*")
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取页面元素失败: %v", err)
|
||||
}
|
||||
|
||||
var sendBtn *rod.Element
|
||||
var err error
|
||||
|
||||
for _, selector := range sendSelectors {
|
||||
sendBtn, err = c.WaitForElementClickable(selector, 5)
|
||||
if err == nil && sendBtn != nil {
|
||||
break
|
||||
for _, elem := range allElements {
|
||||
classAttr, _ := elem.Attribute("class")
|
||||
if classAttr != nil {
|
||||
classLower := strings.ToLower(*classAttr)
|
||||
if strings.Contains(classLower, "send") || strings.Contains(classLower, "submit") {
|
||||
// 检查是否是可点击的元素(button、div等)
|
||||
tagName, _ := elem.Property("tagName")
|
||||
if tagName.Str() == "BUTTON" || tagName.Str() == "DIV" || tagName.Str() == "SVG" {
|
||||
sendBtn = elem
|
||||
c.LogInfo(fmt.Sprintf("通过正则找到发送按钮: class=%s, tag=%s", *classAttr, tagName.Str()))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if sendBtn == nil {
|
||||
// 尝试查找发送图标
|
||||
sendBtn, err = c.Page.Element("button svg")
|
||||
if err != nil {
|
||||
return fmt.Errorf("未找到发送按钮")
|
||||
// fallback: 尝试查找发送图标或最后一个button
|
||||
buttons, _ := c.Page.Elements("button")
|
||||
if len(buttons) > 0 {
|
||||
sendBtn = buttons[len(buttons)-1]
|
||||
c.LogInfo("使用最后一个button作为发送按钮")
|
||||
}
|
||||
}
|
||||
|
||||
if sendBtn == nil {
|
||||
// 尝试查找SVG图标
|
||||
svgs, _ := c.Page.Elements("svg")
|
||||
for _, svg := range svgs {
|
||||
parent, _ := svg.Parent()
|
||||
if parent != nil {
|
||||
tagName, _ := parent.Property("tagName")
|
||||
if tagName.Str() == "BUTTON" {
|
||||
sendBtn = parent
|
||||
c.LogInfo("使用包含SVG的button作为发送按钮")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if sendBtn == nil {
|
||||
return fmt.Errorf("未找到发送按钮")
|
||||
}
|
||||
|
||||
c.SleepMs(500)
|
||||
|
||||
// 滚动到可见区域
|
||||
if err := sendBtn.ScrollIntoView(); err != nil {
|
||||
c.LogInfo(fmt.Sprintf("滚动失败: %v", err))
|
||||
}
|
||||
c.SleepMs(300)
|
||||
|
||||
// 点击发送按钮
|
||||
if err := c.JSClick(sendBtn); err != nil {
|
||||
c.LogInfo("执行点击...")
|
||||
if err := sendBtn.Click(proto.InputMouseButtonLeft, 1); err != nil {
|
||||
return fmt.Errorf("点击发送按钮失败: %v", err)
|
||||
}
|
||||
|
||||
c.LogInfo("已点击发送按钮")
|
||||
c.SleepMs(2000)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// waitForAnswer 等待并获取答案
|
||||
// waitForAnswer 等待并获取答案(处理流式输出)
|
||||
func (c *DeepseekCollector) waitForAnswer() (string, error) {
|
||||
timeout := 120 // 最大等待时间(秒)
|
||||
c.LogInfo("等待AI回答...")
|
||||
|
||||
timeout := 180 // 最大等待时间(秒)
|
||||
startTime := time.Now()
|
||||
lastAnswerLength := 0
|
||||
|
||||
var lastAnswer string
|
||||
var stableCount int
|
||||
const requiredStableCount = 5 // 需要连续5次内容不变才认为完成
|
||||
isAnswering := false
|
||||
|
||||
for time.Since(startTime).Seconds() < float64(timeout) {
|
||||
// 查找答案区域
|
||||
// 查找答案区域 - 尝试多种选择器
|
||||
answerSelectors := []string{
|
||||
".message-content",
|
||||
".response-content",
|
||||
"[class*='assistant'] [class*='content']",
|
||||
"[class*='ai'] [class*='message']",
|
||||
".chat-message.ai",
|
||||
"[class*='answer']",
|
||||
"[class*='response']",
|
||||
}
|
||||
|
||||
var answerText string
|
||||
var foundAnswer bool
|
||||
|
||||
for _, selector := range answerSelectors {
|
||||
answerElements, err := c.Page.Elements(selector)
|
||||
if err == nil && len(answerElements) > 0 {
|
||||
// 获取最后一个答案元素
|
||||
// 获取最后一个答案元素(最新的回答)
|
||||
lastAnswer := answerElements[len(answerElements)-1]
|
||||
|
||||
visible, _ := lastAnswer.Visible()
|
||||
if visible {
|
||||
text, err := lastAnswer.Text()
|
||||
if err == nil && len(strings.TrimSpace(text)) > 0 {
|
||||
// 检查是否正在生成
|
||||
isGenerating := strings.Contains(text, "正在") ||
|
||||
strings.Contains(text, "思考") ||
|
||||
strings.Contains(text, "generating")
|
||||
// 尝试获取HTML内容
|
||||
htmlContent, err := lastAnswer.HTML()
|
||||
if err == nil && len(strings.TrimSpace(htmlContent)) > 30 {
|
||||
answerText = CleanDivTags(htmlContent)
|
||||
foundAnswer = true
|
||||
c.LogInfo(fmt.Sprintf("找到答案(%s),清理后文本长度: %d", selector, len(answerText)))
|
||||
break
|
||||
}
|
||||
|
||||
if !isGenerating {
|
||||
// 检查答案是否还在增长
|
||||
currentLength := len(text)
|
||||
if currentLength == lastAnswerLength && currentLength > 10 {
|
||||
// 答案不再增长,认为已完成
|
||||
return strings.TrimSpace(text), nil
|
||||
}
|
||||
lastAnswerLength = currentLength
|
||||
}
|
||||
// 如果HTML获取失败,尝试获取文本
|
||||
textContent, _ := lastAnswer.Text()
|
||||
if len(strings.TrimSpace(textContent)) > 30 {
|
||||
answerText = strings.TrimSpace(textContent)
|
||||
foundAnswer = true
|
||||
c.LogInfo(fmt.Sprintf("找到答案(%s),文本长度: %d", selector, len(answerText)))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.SleepMs(1500)
|
||||
if !foundAnswer {
|
||||
c.LogInfo("未找到答案元素,继续等待...")
|
||||
}
|
||||
|
||||
// 检查是否获取到答案
|
||||
if answerText != "" && len(answerText) > 30 {
|
||||
if !isAnswering {
|
||||
c.LogInfo("检测到AI开始回答...")
|
||||
isAnswering = true
|
||||
}
|
||||
|
||||
// 检查内容是否稳定(流式输出完成)
|
||||
if answerText == lastAnswer {
|
||||
stableCount++
|
||||
c.LogInfo(fmt.Sprintf("答案稳定中... (%d/%d), 长度: %d", stableCount, requiredStableCount, len(answerText)))
|
||||
|
||||
// 如果内容稳定,说明回答完成
|
||||
if stableCount >= requiredStableCount {
|
||||
c.LogInfo(fmt.Sprintf("✓ AI回答完成,最终长度: %d 字符", len(answerText)))
|
||||
return answerText, nil
|
||||
}
|
||||
} else {
|
||||
// 内容还在变化,重置计数器
|
||||
stableCount = 0
|
||||
lastAnswer = answerText
|
||||
c.LogInfo(fmt.Sprintf("检测到流式输出,当前长度: %d 字符", len(answerText)))
|
||||
}
|
||||
}
|
||||
|
||||
c.SleepMs(1500) // 每1.5秒检查一次
|
||||
|
||||
// 每10秒输出一次等待状态
|
||||
elapsed := int(time.Since(startTime).Seconds())
|
||||
if elapsed > 0 && elapsed%10 == 0 {
|
||||
c.LogInfo(fmt.Sprintf("等待AI回答中... 已等待 %d 秒", elapsed))
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("等待答案超时")
|
||||
return "", fmt.Errorf("等待答案超时(%d秒)", timeout)
|
||||
}
|
||||
|
||||
// getShareLink 获取分享链接
|
||||
func (c *DeepseekCollector) getShareLink() (string, error) {
|
||||
c.LogInfo("=== 开始获取分享链接 ===")
|
||||
|
||||
// 步骤1: 查找分享按钮(需要根据DeepSeek实际页面结构调整)
|
||||
c.LogInfo("步骤1: 查找分享按钮...")
|
||||
|
||||
var shareBtn *rod.Element
|
||||
|
||||
// 尝试多种方式查找分享按钮
|
||||
shareSelectors := []string{
|
||||
"[class*='share']",
|
||||
"[aria-label*='分享']",
|
||||
"[aria-label*='Share']",
|
||||
"button svg[path*='share']",
|
||||
".share-button",
|
||||
".share-icon",
|
||||
}
|
||||
|
||||
for _, selector := range shareSelectors {
|
||||
btns, err := c.Page.Elements(selector)
|
||||
if err == nil && len(btns) > 0 {
|
||||
shareBtn = btns[0]
|
||||
c.LogInfo(fmt.Sprintf("✓ 找到分享按钮: %s", selector))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if shareBtn == nil {
|
||||
// fallback: 遍历所有元素查找包含share的class
|
||||
allElements, _ := c.Page.Elements("*")
|
||||
for _, elem := range allElements {
|
||||
classAttr, _ := elem.Attribute("class")
|
||||
if classAttr != nil && strings.Contains(strings.ToLower(*classAttr), "share") {
|
||||
tagName, _ := elem.Property("tagName")
|
||||
if tagName.Str() == "BUTTON" || tagName.Str() == "DIV" || tagName.Str() == "SVG" {
|
||||
shareBtn = elem
|
||||
c.LogInfo(fmt.Sprintf("✓ 通过正则找到分享按钮: tag=%s, class=%s", tagName.Str(), *classAttr))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if shareBtn == nil {
|
||||
c.LogInfo("未找到分享按钮,跳过获取分享链接")
|
||||
return "", fmt.Errorf("未找到分享按钮")
|
||||
}
|
||||
|
||||
// 滚动到元素位置
|
||||
c.LogInfo("滚动到分享按钮位置...")
|
||||
if scrollErr := shareBtn.ScrollIntoView(); scrollErr != nil {
|
||||
c.LogInfo(fmt.Sprintf("滚动失败: %v", scrollErr))
|
||||
}
|
||||
c.SleepMs(800)
|
||||
|
||||
// 点击分享按钮
|
||||
c.LogInfo("执行点击分享按钮...")
|
||||
if clickErr := shareBtn.Click(proto.InputMouseButtonLeft, 1); clickErr != nil {
|
||||
return "", fmt.Errorf("点击分享按钮失败: %v", clickErr)
|
||||
}
|
||||
|
||||
c.LogInfo("✓ 点击成功")
|
||||
c.SleepMs(3000) // 等待弹窗出现
|
||||
c.Screenshot("after_share_click")
|
||||
|
||||
// 步骤2: 在弹窗中查找复制链接按钮(带重试机制)
|
||||
c.LogInfo("步骤2: 查找复制链接按钮...")
|
||||
|
||||
var copyLinkBtn *rod.Element
|
||||
maxRetries := 5
|
||||
retryDelay := 1000
|
||||
|
||||
for attempt := 1; attempt <= maxRetries; attempt++ {
|
||||
c.LogInfo(fmt.Sprintf("第 %d/%d 次尝试查找复制链接按钮...", attempt, maxRetries))
|
||||
|
||||
// 尝试多种方式查找复制按钮
|
||||
copySelectors := []string{
|
||||
"[class*='copy']",
|
||||
"[class*='Copy']",
|
||||
"[aria-label*='复制']",
|
||||
"[aria-label*='Copy']",
|
||||
"button[class*='link']",
|
||||
}
|
||||
|
||||
for _, selector := range copySelectors {
|
||||
btns, err := c.Page.Elements(selector)
|
||||
if err == nil && len(btns) > 0 {
|
||||
copyLinkBtn = btns[0]
|
||||
c.LogInfo(fmt.Sprintf("✓ 找到复制链接按钮: %s", selector))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if copyLinkBtn != nil {
|
||||
break
|
||||
}
|
||||
|
||||
// fallback: 遍历所有元素
|
||||
allElements, _ := c.Page.Elements("*")
|
||||
for _, elem := range allElements {
|
||||
classAttr, _ := elem.Attribute("class")
|
||||
if classAttr != nil {
|
||||
classLower := strings.ToLower(*classAttr)
|
||||
if strings.Contains(classLower, "copy") || strings.Contains(classLower, "link") {
|
||||
tagName, _ := elem.Property("tagName")
|
||||
if tagName.Str() == "BUTTON" || tagName.Str() == "DIV" {
|
||||
copyLinkBtn = elem
|
||||
c.LogInfo(fmt.Sprintf("✓ 通过正则找到复制按钮: tag=%s, class=%s", tagName.Str(), *classAttr))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if copyLinkBtn != nil {
|
||||
break
|
||||
}
|
||||
|
||||
// 没找到,等待后重试
|
||||
if attempt < maxRetries {
|
||||
c.LogInfo(fmt.Sprintf("未找到复制链接按钮,%d毫秒后重试...", retryDelay))
|
||||
c.SleepMs(retryDelay)
|
||||
}
|
||||
}
|
||||
|
||||
if copyLinkBtn == nil {
|
||||
c.Screenshot("copy_button_not_found")
|
||||
return "", fmt.Errorf("经过 %d 次重试仍未找到复制链接按钮", maxRetries)
|
||||
}
|
||||
|
||||
// 滚动到按钮位置
|
||||
c.LogInfo("滚动到复制链接按钮位置...")
|
||||
if scrollErr := copyLinkBtn.ScrollIntoView(); scrollErr != nil {
|
||||
c.LogInfo(fmt.Sprintf("滚动失败: %v", scrollErr))
|
||||
}
|
||||
c.SleepMs(500)
|
||||
|
||||
// 点击复制链接按钮
|
||||
c.LogInfo("点击复制链接按钮...")
|
||||
if clickErr := copyLinkBtn.Click(proto.InputMouseButtonLeft, 1); clickErr != nil {
|
||||
return "", fmt.Errorf("点击复制链接按钮失败: %v", clickErr)
|
||||
}
|
||||
|
||||
c.LogInfo("✓ 复制链接按钮点击成功")
|
||||
c.SleepMs(1500) // 等待复制链接完成
|
||||
|
||||
// 步骤3: 从剪贴板读取分享链接
|
||||
c.LogInfo("步骤3: 从系统剪贴板读取分享链接...")
|
||||
|
||||
clipboardText, err := clipboard.ReadAll()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("读取剪贴板失败: %v", err)
|
||||
}
|
||||
|
||||
if clipboardText == "" {
|
||||
return "", fmt.Errorf("剪贴板内容为空")
|
||||
}
|
||||
|
||||
c.LogInfo(fmt.Sprintf("剪贴板原始内容: %s", clipboardText))
|
||||
|
||||
// 使用正则表达式提取URL
|
||||
re := regexp.MustCompile(`https?://[^\s]+`)
|
||||
matches := re.FindStringSubmatch(clipboardText)
|
||||
|
||||
if len(matches) == 0 {
|
||||
return "", fmt.Errorf("未能从剪贴板内容中提取URL")
|
||||
}
|
||||
|
||||
url := matches[0]
|
||||
c.LogInfo(fmt.Sprintf("✓✓✓ 成功获取分享链接: %s", url))
|
||||
return url, nil
|
||||
}
|
||||
|
||||
// SafeElement 安全地获取元素
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@ type DoubaoCollector struct {
|
|||
}
|
||||
|
||||
// NewDoubaoCollector 创建豆包收集器
|
||||
func NewDoubaoCollector(ctx context.Context, params *CollectParams, cfg *config.Config, logger log.AllLogger) CollectorInterface {
|
||||
func NewDoubaoCollector(ctx context.Context, params *CollectParams, cfg *config.Config, logger log.AllLogger, browser *rod.Browser, page *rod.Page) CollectorInterface {
|
||||
collector := &DoubaoCollector{
|
||||
BaseCollector: NewBaseCollector(ctx, params, cfg, logger),
|
||||
BaseCollector: NewBaseCollector(ctx, params, cfg, logger, browser, page),
|
||||
}
|
||||
|
||||
// 设置豆包的URL
|
||||
|
|
@ -98,20 +98,10 @@ func (c *DoubaoCollector) WaitLogin() (bool, string) {
|
|||
|
||||
// AskQuestion 提问并获取答案
|
||||
func (c *DoubaoCollector) AskQuestion(question string) (*CollectResult, error) {
|
||||
if err := c.SetupDriver(); err != nil {
|
||||
return nil, fmt.Errorf("浏览器启动失败: %v", err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
if err := c.InitPage(); err != nil {
|
||||
return nil, fmt.Errorf("页面初始化失败: %v", err)
|
||||
}
|
||||
|
||||
// 检查是否登录
|
||||
if !c.CheckLoginStatus() {
|
||||
return nil, fmt.Errorf("未登录,请先调用WaitLogin进行登录")
|
||||
}
|
||||
|
||||
c.LogInfo(fmt.Sprintf("开始提问: %s", question))
|
||||
|
||||
if err := c.inputQuestion(question); err != nil {
|
||||
|
|
|
|||
|
|
@ -4,15 +4,16 @@ import (
|
|||
"context"
|
||||
"geo/internal/config"
|
||||
|
||||
"github.com/go-rod/rod"
|
||||
"github.com/gofiber/fiber/v2/log"
|
||||
)
|
||||
|
||||
// CollectorInterface AI平台收集器接口
|
||||
type CollectorInterface interface {
|
||||
// WaitLogin 等待登录
|
||||
WaitLogin() (bool, string)
|
||||
// AskQuestion 提问并获取答案
|
||||
AskQuestion(question string) (*CollectResult, error)
|
||||
// Close 关闭页面(释放资源)
|
||||
Close()
|
||||
}
|
||||
|
||||
// CollectResult 收集结果
|
||||
|
|
@ -27,7 +28,9 @@ type NewCollector func(
|
|||
ctx context.Context,
|
||||
param *CollectParams,
|
||||
cfg *config.Config,
|
||||
logger log.AllLogger) CollectorInterface
|
||||
logger log.AllLogger,
|
||||
browser *rod.Browser,
|
||||
page *rod.Page) CollectorInterface
|
||||
|
||||
// CollectorValue 收集器配置信息
|
||||
type CollectorValue struct {
|
||||
|
|
|
|||
|
|
@ -4,66 +4,129 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"geo/internal/config"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/go-rod/rod"
|
||||
"github.com/go-rod/rod/lib/launcher"
|
||||
"github.com/gofiber/fiber/v2/log"
|
||||
)
|
||||
|
||||
// CollectManager 收集管理器
|
||||
// CollectManager 收集管理器 - 单浏览器临时Page模式
|
||||
type CollectManager struct {
|
||||
ctx context.Context
|
||||
config *config.Config
|
||||
logger log.AllLogger
|
||||
|
||||
browser *rod.Browser // 全局唯一的浏览器实例
|
||||
mu sync.RWMutex // 保护浏览器访问的互斥锁
|
||||
|
||||
collectors map[string]CollectorInterface // 平台名称 -> Collector 实例
|
||||
}
|
||||
|
||||
// NewCollectManager 创建收集管理器
|
||||
// NewCollectManager 创建收集管理器(服务启动时调用)
|
||||
func NewCollectManager(ctx context.Context, cfg *config.Config, logger log.AllLogger) *CollectManager {
|
||||
return &CollectManager{
|
||||
ctx: ctx,
|
||||
config: cfg,
|
||||
logger: logger,
|
||||
manager := &CollectManager{
|
||||
ctx: ctx,
|
||||
config: cfg,
|
||||
logger: logger,
|
||||
collectors: make(map[string]CollectorInterface),
|
||||
}
|
||||
|
||||
// 注册所有平台的 Collector
|
||||
manager.registerCollectors()
|
||||
|
||||
// 服务启动时立即创建全局浏览器
|
||||
manager.initBrowser()
|
||||
|
||||
return manager
|
||||
}
|
||||
|
||||
// GetCollector 获取指定平台的收集器
|
||||
func (m *CollectManager) GetCollector(platform string, params *CollectParams) (CollectorInterface, error) {
|
||||
collectorValue, ok := CollectorMap[platform]
|
||||
// initBrowser 初始化全局浏览器实例
|
||||
func (m *CollectManager) initBrowser() {
|
||||
m.logger.Info("🚀 正在初始化浏览器...")
|
||||
|
||||
// 创建用户数据目录(所有平台共用)
|
||||
userDataDir := filepath.Join(m.config.Sys.ChromeDataDir, "global", "main")
|
||||
os.MkdirAll(userDataDir, 0755)
|
||||
|
||||
// 使用 launcher 启动 Chrome
|
||||
l := launcher.New().
|
||||
Bin(m.config.Sys.ChromePath).
|
||||
UserDataDir(userDataDir).
|
||||
Headless(false). // 有头模式,显示窗口
|
||||
Leakless(true). // 让 Chrome 进程独立运行
|
||||
Set("disable-blink-features", "AutomationControlled").
|
||||
Set("window-size", "1920,1080").
|
||||
Set("start-maximized", "").
|
||||
Set("lang", "zh-CN")
|
||||
|
||||
url, err := l.Launch()
|
||||
if err != nil {
|
||||
m.logger.Errorf("❌ 启动浏览器失败: %v", err)
|
||||
panic(fmt.Sprintf("启动浏览器失败: %v", err))
|
||||
}
|
||||
|
||||
m.logger.Infof("✅ 浏览器启动成功: %s", url)
|
||||
|
||||
// 连接到浏览器
|
||||
m.browser = rod.New().Context(m.ctx).ControlURL(url).MustConnect()
|
||||
|
||||
m.logger.Info("✅ 浏览器连接成功")
|
||||
}
|
||||
|
||||
// Close 关闭管理器(注意:不关闭浏览器,让浏览器进程继续运行)
|
||||
func (m *CollectManager) Close() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.logger.Info("🔒 CollectManager 关闭(浏览器保持运行)...")
|
||||
|
||||
// 注意:这里不调用 m.browser.Close()
|
||||
// 让浏览器进程继续运行,下次服务启动时可以复用
|
||||
|
||||
m.logger.Info("✅ Manager 已关闭,浏览器仍在后台运行")
|
||||
}
|
||||
|
||||
// registerCollectors 注册所有平台的 Collector
|
||||
func (m *CollectManager) registerCollectors() {
|
||||
// 文心一言
|
||||
params := &CollectParams{Platform: "wenxin"}
|
||||
m.collectors["wenxin"] = NewWenxinCollector(m.ctx, params, m.config, m.logger, m.browser, nil)
|
||||
|
||||
// DeepSeek
|
||||
params = &CollectParams{Platform: "deepseek"}
|
||||
m.collectors["deepseek"] = NewDeepseekCollector(m.ctx, params, m.config, m.logger, m.browser, nil)
|
||||
|
||||
// 豆包
|
||||
params = &CollectParams{Platform: "doubao"}
|
||||
m.collectors["doubao"] = NewDoubaoCollector(m.ctx, params, m.config, m.logger, m.browser, nil)
|
||||
|
||||
// 通义千问
|
||||
params = &CollectParams{Platform: "qianwen"}
|
||||
m.collectors["qianwen"] = NewQianwenCollector(m.ctx, params, m.config, m.logger, m.browser, nil)
|
||||
}
|
||||
|
||||
// GetCollector 获取指定平台的 Collector
|
||||
func (m *CollectManager) GetCollector(platform string) (CollectorInterface, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
collector, ok := m.collectors[platform]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("不支持的平台: %s", platform)
|
||||
}
|
||||
|
||||
collector := collectorValue.InitMethod(m.ctx, params, m.config, m.logger)
|
||||
if collector == nil {
|
||||
return nil, fmt.Errorf("创建收集器失败: %s", platform)
|
||||
}
|
||||
|
||||
return collector, nil
|
||||
}
|
||||
|
||||
// AskQuestion 向指定AI平台提问
|
||||
func (m *CollectManager) AskQuestion(platform string, params *CollectParams, question string) (*CollectResult, error) {
|
||||
collector, err := m.GetCollector(platform, params)
|
||||
// AskQuestion 向指定平台提问(每次创建临时 Page)
|
||||
func (m *CollectManager) AskQuestion(platform string, question string) (*CollectResult, error) {
|
||||
collector, err := m.GetCollector(platform)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return collector.AskQuestion(question)
|
||||
}
|
||||
|
||||
// WaitLogin 等待指定平台登录
|
||||
func (m *CollectManager) WaitLogin(platform string, params *CollectParams) (bool, string) {
|
||||
collector, err := m.GetCollector(platform, params)
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
|
||||
return collector.WaitLogin()
|
||||
}
|
||||
|
||||
// ListPlatforms 列出所有支持的平台
|
||||
func (m *CollectManager) ListPlatforms() []string {
|
||||
platforms := make([]string, 0, len(CollectorMap))
|
||||
for platform := range CollectorMap {
|
||||
platforms = append(platforms, platform)
|
||||
}
|
||||
return platforms
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,9 +18,9 @@ type QianwenCollector struct {
|
|||
}
|
||||
|
||||
// NewQianwenCollector 创建通义千问收集器
|
||||
func NewQianwenCollector(ctx context.Context, params *CollectParams, cfg *config.Config, logger log.AllLogger) CollectorInterface {
|
||||
func NewQianwenCollector(ctx context.Context, params *CollectParams, cfg *config.Config, logger log.AllLogger, browser *rod.Browser, page *rod.Page) CollectorInterface {
|
||||
collector := &QianwenCollector{
|
||||
BaseCollector: NewBaseCollector(ctx, params, cfg, logger),
|
||||
BaseCollector: NewBaseCollector(ctx, params, cfg, logger, browser, page),
|
||||
}
|
||||
|
||||
// 设置通义千问的URL
|
||||
|
|
@ -81,10 +81,7 @@ func (c *QianwenCollector) WaitLogin() (bool, string) {
|
|||
|
||||
// AskQuestion 提问并获取答案
|
||||
func (c *QianwenCollector) AskQuestion(question string) (*CollectResult, error) {
|
||||
if err := c.SetupDriver(); err != nil {
|
||||
return nil, fmt.Errorf("浏览器启动失败: %v", err)
|
||||
}
|
||||
defer c.Close()
|
||||
// 注意:SetupDriver 和 Close 已由 Manager 管理,这里不再调用
|
||||
|
||||
if err := c.InitPage(); err != nil {
|
||||
return nil, fmt.Errorf("页面初始化失败: %v", err)
|
||||
|
|
|
|||
|
|
@ -5,12 +5,11 @@ import (
|
|||
"fmt"
|
||||
"geo/internal/config"
|
||||
|
||||
"github.com/gofiber/fiber/v2/log"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2/log"
|
||||
|
||||
"github.com/atotto/clipboard"
|
||||
"github.com/go-rod/rod"
|
||||
"github.com/go-rod/rod/lib/proto"
|
||||
|
|
@ -30,9 +29,9 @@ type WenxinCollector struct {
|
|||
}
|
||||
|
||||
// NewWenxinCollector 创建文心一言收集器
|
||||
func NewWenxinCollector(ctx context.Context, params *CollectParams, cfg *config.Config, logger log.AllLogger) CollectorInterface {
|
||||
func NewWenxinCollector(ctx context.Context, params *CollectParams, cfg *config.Config, logger log.AllLogger, browser *rod.Browser, page *rod.Page) CollectorInterface {
|
||||
collector := &WenxinCollector{
|
||||
BaseCollector: NewBaseCollector(ctx, params, cfg, logger),
|
||||
BaseCollector: NewBaseCollector(ctx, params, cfg, logger, browser, page),
|
||||
}
|
||||
|
||||
// 设置文心一言的URL
|
||||
|
|
@ -101,15 +100,14 @@ func (c *WenxinCollector) WaitLogin() (bool, string) {
|
|||
|
||||
// AskQuestion 提问并获取答案
|
||||
func (c *WenxinCollector) AskQuestion(question string) (*CollectResult, error) {
|
||||
if err := c.SetupDriver(); err != nil {
|
||||
return nil, fmt.Errorf("浏览器启动失败: %v", err)
|
||||
}
|
||||
defer c.Close()
|
||||
// 注意:SetupDriver 和 Close 已由 Manager 管理,这里不再调用
|
||||
|
||||
if err := c.InitPage(); err != nil {
|
||||
return nil, fmt.Errorf("页面初始化失败: %v", err)
|
||||
}
|
||||
|
||||
c.Sleep(3)
|
||||
|
||||
if err := c.inputQuestion(question); err != nil {
|
||||
return nil, fmt.Errorf("输入问题失败: %v", err)
|
||||
}
|
||||
|
|
@ -122,7 +120,7 @@ func (c *WenxinCollector) AskQuestion(question string) (*CollectResult, error) {
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("获取答案失败: %v", err)
|
||||
}
|
||||
answerStr, isExposure := HighlightKeywordsInHTML(answer, c.KeyWords)
|
||||
|
||||
// 获取分享链接
|
||||
shareLink := ""
|
||||
link, _ := c.getShareLink()
|
||||
|
|
@ -131,9 +129,8 @@ func (c *WenxinCollector) AskQuestion(question string) (*CollectResult, error) {
|
|||
}
|
||||
|
||||
return &CollectResult{
|
||||
Answer: answerStr,
|
||||
ShareLink: shareLink,
|
||||
IsExposure: isExposure,
|
||||
Answer: answer,
|
||||
ShareLink: shareLink,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
@ -145,6 +142,7 @@ func (c *WenxinCollector) inputQuestion(question string) error {
|
|||
inputSelectors := []string{
|
||||
"[contenteditable='true']",
|
||||
"div[contenteditable]",
|
||||
".editable__T7WAW4uW",
|
||||
"[class*='editable']",
|
||||
}
|
||||
|
||||
|
|
@ -483,105 +481,61 @@ func (c *WenxinCollector) getShareLink() (string, error) {
|
|||
}
|
||||
|
||||
c.LogInfo("✓ 点击成功")
|
||||
c.SleepMs(3000) // 等待弹窗出现
|
||||
c.SleepMs(2000) // 等待弹窗出现
|
||||
c.Screenshot("after_share_icon_click")
|
||||
|
||||
// 步骤3: 在弹窗中查找shareContainer的div(带重试机制)
|
||||
// 步骤3: 在弹窗中查找shareContainer的div
|
||||
c.LogInfo("步骤3: 查找包含'shareContainer'的div元素...")
|
||||
|
||||
var shareContainerDiv *rod.Element
|
||||
maxRetries := 5
|
||||
retryDelay := 1000 // 每次重试间隔1秒
|
||||
|
||||
for attempt := 1; attempt <= maxRetries; attempt++ {
|
||||
c.LogInfo(fmt.Sprintf("第 %d/%d 次尝试查找shareContainer...", attempt, maxRetries))
|
||||
// 重新获取所有div元素
|
||||
allDivs, err = c.Page.Elements("div")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("获取页面div元素失败: %v", err)
|
||||
}
|
||||
|
||||
// 重新获取所有div元素
|
||||
allDivs, err = c.Page.Elements("div")
|
||||
if err != nil {
|
||||
c.LogInfo(fmt.Sprintf("获取页面div元素失败: %v", err))
|
||||
if attempt < maxRetries {
|
||||
c.SleepMs(retryDelay)
|
||||
continue
|
||||
}
|
||||
return "", fmt.Errorf("获取页面div元素失败: %v", err)
|
||||
}
|
||||
c.LogInfo(fmt.Sprintf("在 %d 个div元素中查找包含'shareContainer'的class", len(allDivs)))
|
||||
|
||||
c.LogInfo(fmt.Sprintf("在 %d 个div元素中查找包含'shareContainer'的class", len(allDivs)))
|
||||
|
||||
for _, elem := range allDivs {
|
||||
classAttr, _ := elem.Attribute("class")
|
||||
if classAttr != nil && strings.Contains(strings.ToLower(*classAttr), "sharecontainer") {
|
||||
tagName, _ := elem.Property("tagName")
|
||||
c.LogInfo(fmt.Sprintf("✓ 找到shareContainer容器: tag=%s, class=%s", tagName.Str(), *classAttr))
|
||||
shareContainerDiv = elem
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if shareContainerDiv != nil {
|
||||
break // 找到了,退出重试循环
|
||||
}
|
||||
|
||||
// 没找到,等待后重试
|
||||
if attempt < maxRetries {
|
||||
c.LogInfo(fmt.Sprintf("未找到shareContainer,%d毫秒后重试...", retryDelay))
|
||||
c.SleepMs(retryDelay)
|
||||
for _, elem := range allDivs {
|
||||
classAttr, _ := elem.Attribute("class")
|
||||
if classAttr != nil && strings.Contains(strings.ToLower(*classAttr), "sharecontainer") {
|
||||
tagName, _ := elem.Property("tagName")
|
||||
c.LogInfo(fmt.Sprintf("✓ 找到shareContainer容器: tag=%s, class=%s", tagName.Str(), *classAttr))
|
||||
shareContainerDiv = elem
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if shareContainerDiv == nil {
|
||||
c.Screenshot("share_container_not_found")
|
||||
return "", fmt.Errorf("经过 %d 次重试仍未找到包含'shareContainer' class的div元素", maxRetries)
|
||||
return "", fmt.Errorf("未找到包含'shareContainer' class的div元素")
|
||||
}
|
||||
|
||||
// 步骤4: 在shareContainer内查找genLink的button(带重试机制)
|
||||
// 步骤4: 在shareContainer内查找genLink的button
|
||||
c.LogInfo("步骤4: 在shareContainer容器内查找包含'genLink'的button...")
|
||||
|
||||
var genLinkBtn *rod.Element
|
||||
maxRetries = 3
|
||||
retryDelay = 800
|
||||
|
||||
for attempt := 1; attempt <= maxRetries; attempt++ {
|
||||
c.LogInfo(fmt.Sprintf("第 %d/%d 次尝试查找genLink按钮...", attempt, maxRetries))
|
||||
buttons, err := shareContainerDiv.Elements("button")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("获取button元素失败: %v", err)
|
||||
}
|
||||
|
||||
buttons, err := shareContainerDiv.Elements("button")
|
||||
if err != nil {
|
||||
c.LogInfo(fmt.Sprintf("获取button元素失败: %v", err))
|
||||
if attempt < maxRetries {
|
||||
c.SleepMs(retryDelay)
|
||||
continue
|
||||
}
|
||||
return "", fmt.Errorf("获取button元素失败: %v", err)
|
||||
}
|
||||
c.LogInfo(fmt.Sprintf("在 %d 个button元素中查找包含'genLink'的class", len(buttons)))
|
||||
|
||||
c.LogInfo(fmt.Sprintf("在 %d 个button元素中查找包含'genLink'的class", len(buttons)))
|
||||
|
||||
for _, elem := range buttons {
|
||||
classAttr, _ := elem.Attribute("class")
|
||||
if classAttr != nil && strings.Contains(strings.ToLower(*classAttr), "genlink") {
|
||||
tagName, _ := elem.Property("tagName")
|
||||
text, _ := elem.Text()
|
||||
c.LogInfo(fmt.Sprintf("✓ 找到genLink按钮: tag=%s, class=%s, text=%s", tagName.Str(), *classAttr, strings.TrimSpace(text)))
|
||||
genLinkBtn = elem
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if genLinkBtn != nil {
|
||||
break // 找到了,退出重试循环
|
||||
}
|
||||
|
||||
// 没找到,等待后重试
|
||||
if attempt < maxRetries {
|
||||
c.LogInfo(fmt.Sprintf("未找到genLink按钮,%d毫秒后重试...", retryDelay))
|
||||
c.SleepMs(retryDelay)
|
||||
for _, elem := range buttons {
|
||||
classAttr, _ := elem.Attribute("class")
|
||||
if classAttr != nil && strings.Contains(strings.ToLower(*classAttr), "genlink") {
|
||||
tagName, _ := elem.Property("tagName")
|
||||
text, _ := elem.Text()
|
||||
c.LogInfo(fmt.Sprintf("✓ 找到genLink按钮: tag=%s, class=%s, text=%s", tagName.Str(), *classAttr, strings.TrimSpace(text)))
|
||||
genLinkBtn = elem
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if genLinkBtn == nil {
|
||||
c.Screenshot("genlink_button_not_found")
|
||||
return "", fmt.Errorf("经过 %d 次重试仍未在shareContainer容器内找到包含'genLink' class的button", maxRetries)
|
||||
return "", fmt.Errorf("在shareContainer容器内未找到包含'genLink' class的button")
|
||||
}
|
||||
|
||||
// 滚动到按钮位置
|
||||
|
|
|
|||
|
|
@ -153,13 +153,13 @@ func (c *CollectService) Collect(ctx *fiber.Ctx, req *entitys.ProductCollectRequ
|
|||
return err
|
||||
}
|
||||
|
||||
go c.doCollectAsync(collectCode, req.PlatformIndex, req.Question)
|
||||
go c.doCollectAsync(collectCode, req.PlatformIndex, req.Question, req.Keywords)
|
||||
|
||||
return ctx.JSON(fiber.Map{"message": "收录生成中"})
|
||||
}
|
||||
|
||||
// doCollectAsync 异步执行收集任务
|
||||
func (c *CollectService) doCollectAsync(collectCode string, platforms []string, question string) {
|
||||
func (c *CollectService) doCollectAsync(collectCode string, platforms []string, question string, keywords []string) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*240)
|
||||
defer cancel()
|
||||
defer func() {
|
||||
|
|
@ -177,14 +177,14 @@ func (c *CollectService) doCollectAsync(collectCode string, platforms []string,
|
|||
|
||||
platformName, exist := collect.CollectorMap[platIndex]
|
||||
if !exist {
|
||||
log.Printf("未知的平台索引: %d", platIndex)
|
||||
log.Printf("未知的平台索引: %s", platIndex)
|
||||
return
|
||||
}
|
||||
|
||||
requestID := fmt.Sprintf("%s_%s", collectCode, platIndex)
|
||||
result, err := c.collectBiz.AskAIQuestion(platIndex, requestID, question, true)
|
||||
result, err := c.collectBiz.AskAIQuestion(platIndex, requestID, question, keywords, true)
|
||||
if err != nil {
|
||||
log.Printf("平台 %s 收集失败: %v", platformName, err)
|
||||
log.Printf("平台 %s 收集失败: %v", platformName.Name, err)
|
||||
return
|
||||
}
|
||||
ise := 1
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ func TestWenxinCollector_AskQuestion(t *testing.T) {
|
|||
|
||||
// 设置收集参数
|
||||
params := &collect.CollectParams{
|
||||
Headless: false, // 显示浏览器以便调试
|
||||
Headless: true, // 显示浏览器以便调试
|
||||
RequestID: "test_wenxin_001",
|
||||
Platform: "wenxin",
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue