diff --git a/admin/server/api/v1/gaia/workflow.go b/admin/server/api/v1/gaia/workflow.go index 4b3a27e38..a1c11d0dc 100644 --- a/admin/server/api/v1/gaia/workflow.go +++ b/admin/server/api/v1/gaia/workflow.go @@ -514,7 +514,15 @@ func generateCSVFromTasks(flow *gaia.BatchWorkflow, tasks []gaia.BatchWorkflowTa if key == "task_id" { continue } - text += fmt.Sprintf("%s\r", v) + switch vv := v.(type) { + case string: + text += fmt.Sprintf("%s\r", vv) + case float64: + text += fmt.Sprintf("%s\r", strconv.FormatFloat(vv, 'f', -1, 64)) + case int64: + text += fmt.Sprintf("%d\r", vv) + } + } } row = append(row, text) diff --git a/admin/server/config.yaml b/admin/server/config.yaml index 38396a5d5..aa811b014 100644 --- a/admin/server/config.yaml +++ b/admin/server/config.yaml @@ -1,74 +1,221 @@ -gaia: - url: http://127.0.0.1:5001 - login_max_error_limit: 5 - SUPER_ADMIN_ACCOUNT_ID: - SUPER_ADMIN_TENANT_ID: +aliyun-oss: + endpoint: "" + access-key-id: "" + access-key-secret: "" + bucket-name: "" + bucket-url: "" + base-path: "" +autocode: + web: "" + root: /Users/tasia/Desktop/code/python/dify-plus/admin + server: "" + module: github.com/flipped-aurora/gin-vue-admin/server + ai-path: "" +aws-s3: + bucket: "" + region: "" + endpoint: "" + secret-id: "" + secret-key: "" + base-url: "" + path-prefix: "" + s3-force-path-style: false + disable-ssl: false captcha: - key-long: 6 - img-width: 240 - img-height: 80 - open-captcha: 0 - open-captcha-timeout: 3600 -jwt: - signing-key: sk-9f73s3ljTXVcMT3Blb3ljTqtsKiGHXVcMT3BlbkFJLK7U - expires-time: 1d - buffer-time: 1d - issuer: CLOUD -local: - path: uploads/file - store-path: uploads/file -oa-login: - url: - oauth2-client-id: - oauth2-client-secret: - get-user-info-api-path: - get-token-by-code-api-path: -pgsql: - prefix: "" - port: "5432" - config: - db-name: - username: - password: - path: - engine: "" - log-mode: error - max-idle-conns: 10 - max-open-conns: 100 - singular: false - log-zap: false -redis: - name: "" - addr: 127.0.0.1:6379 - password: difyai123456 - db: 8 - useCluster: false + key-long: 6 + img-width: 240 + img-height: 80 + open-captcha: 0 + open-captcha-timeout: 3600 +cloudflare-r2: + bucket: "" + base-url: "" + path: "" + account-id: "" + access-key-id: "" + secret-access-key: "" +cors: + mode: "" + whitelist: [] +db-list: [] dify-redis: - name: "" - addr: 127.0.0.1:6379 - password: difyai123456 - db: 0 - useCluster: false + name: "" + addr: 127.0.0.1:6379 + password: difyai123456 + db: 0 + useCluster: false + clusterAddrs: [] +disk-list: [] +email: + to: "" + from: "" + host: "" + secret: "" + nickname: "" + port: 0 + is-ssl: false +excel: + dir: "" +gaia: + url: http://127.0.0.1:5001 + login_max_error_limit: 5 + SUPER_ADMIN_ACCOUNT_ID: + SUPER_ADMIN_TENANT_ID: +hua-wei-obs: + path: "" + bucket: "" + endpoint: "" + access-key: "" + secret-key: "" +jwt: + signing-key: J27pJFVthP4YF76Zej6RinXzZ3piqfYFFJStmCnNWyZtGFi1GV3LvOiT + expires-time: 1d + buffer-time: 1d + issuer: CLOUD +local: + path: uploads/file + store-path: uploads/file +minio: + endpoint: "" + access-key-id: "" + access-key-secret: "" + bucket-name: "" + use-ssl: false + base-path: "" + bucket-url: "" +mongo: + coll: "" + options: "" + database: "" + username: "" + password: "" + auth-source: "" + min-pool-size: 0 + max-pool-size: 0 + socket-timeout-ms: 0 + connect-timeout-ms: 0 + is-zap: false + hosts: [] +mssql: + prefix: "" + port: "" + config: "" + db-name: "" + username: "" + password: "" + path: "" + engine: "" + log-mode: "" + max-idle-conns: 0 + max-open-conns: 0 + singular: false + log-zap: false +mysql: + prefix: "" + port: "" + config: "" + db-name: "" + username: "" + password: "" + path: "" + engine: "" + log-mode: "" + max-idle-conns: 0 + max-open-conns: 0 + singular: false + log-zap: false +oa-login: + url: "" + oauth2-client-id: "" + oauth2-client-secret: "" + get-user-info-api-path: "" + get-token-by-code-api-path: "" +oracle: + prefix: "" + port: "" + config: "" + db-name: "" + username: "" + password: "" + path: "" + engine: "" + log-mode: "" + max-idle-conns: 0 + max-open-conns: 0 + singular: false + log-zap: false +pgsql: + prefix: "" + port: "5432" + config: sslmode=disable TimeZone=Asia/Shanghai + db-name: dify + username: postgres + password: difyai123456 + path: 127.0.0.1 + engine: "" + log-mode: error + max-idle-conns: 10 + max-open-conns: 100 + singular: false + log-zap: false +qiniu: + zone: "" + bucket: "" + img-path: "" + access-key: "" + secret-key: "" + use-https: false + use-cdn-domains: false +redis: + name: "" + addr: 127.0.0.1:6379 + password: difyai123456 + db: 8 + useCluster: false + clusterAddrs: [] +redis-list: [] +sqlite: + prefix: "" + port: "" + config: "" + db-name: "" + username: "" + password: "" + path: "" + engine: "" + log-mode: "" + max-idle-conns: 0 + max-open-conns: 0 + singular: false + log-zap: false system: - db-type: pgsql - oss-type: local - router-prefix: "" - addr: 8888 - iplimit-count: 15000 - iplimit-time: 3600 - use-multipoint: false - use-redis: true - use-mongo: false - use-strict-auth: false - user_default-group-id: "888" - work_flow_number: 100 + db-type: pgsql + oss-type: local + router-prefix: "" + addr: 8888 + iplimit-count: 15000 + iplimit-time: 3600 + use-multipoint: false + use-redis: true + use-mongo: false + use-strict-auth: false + work_flow_number: 100 + user_default-group-id: "888" + dockerrun: false +tencent-cos: + bucket: "" + region: "" + secret-id: "" + secret-key: "" + base-url: "" + path-prefix: "" zap: - level: info - prefix: '[gaia/server]' - format: json - director: log - encode-level: LowercaseColorLevelEncoder - stacktrace-key: stacktrace - show-line: true - log-in-console: true - retention-day: -1 + level: info + prefix: '[gaia/server]' + format: json + director: log + encode-level: LowercaseColorLevelEncoder + stacktrace-key: stacktrace + show-line: true + log-in-console: true + retention-day: -1 diff --git a/admin/server/go.mod b/admin/server/go.mod index f9903e434..d5cceaf3b 100644 --- a/admin/server/go.mod +++ b/admin/server/go.mod @@ -9,6 +9,8 @@ require ( github.com/aws/aws-sdk-go v1.55.5 github.com/casbin/casbin/v2 v2.100.0 github.com/casbin/gorm-adapter/v3 v3.28.0 + github.com/faabiosr/cachego v0.15.0 + github.com/fastwego/dingding v1.0.0-beta.4 github.com/fsnotify/fsnotify v1.7.0 github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6 github.com/gin-gonic/gin v1.10.0 @@ -79,8 +81,6 @@ require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/elastic/go-sysinfo v1.14.2 // indirect github.com/elastic/go-windows v1.0.2 // indirect - github.com/faabiosr/cachego v0.15.0 // indirect - github.com/fastwego/dingding v1.0.0-beta.4 // indirect github.com/gabriel-vasile/mimetype v1.4.6 // indirect github.com/gammazero/toposort v0.1.1 // indirect github.com/gin-contrib/sse v0.1.0 // indirect diff --git a/admin/server/model/gaia/request/workflow.go b/admin/server/model/gaia/request/workflow.go index 2250f423e..c85df08d3 100644 --- a/admin/server/model/gaia/request/workflow.go +++ b/admin/server/model/gaia/request/workflow.go @@ -1,7 +1,7 @@ package request type WorkflowBatchProcessing struct { - Outputs map[string]string `json:"outputs" gorm:"comment:从任务生成CSV内容"` // 从任务生成CSV内容 + Outputs map[string]interface{} `json:"outputs" gorm:"comment:从任务生成CSV内容"` // 从任务生成CSV内容 } // SSEEvent 表示一个SSE事件 diff --git a/admin/server/model/system/request/jwt.go b/admin/server/model/system/request/jwt.go index f420112e2..b13351dda 100644 --- a/admin/server/model/system/request/jwt.go +++ b/admin/server/model/system/request/jwt.go @@ -31,3 +31,9 @@ type BaseClaims struct { Sub string `json:"sub,omitempty"` // Extend Start: add gaia token } + +// CSRFClaims CSRF token claims (与Dify API兼容) +type CSRFClaims struct { + jwt.RegisteredClaims + Sub string `json:"sub"` +} diff --git a/admin/server/service/gaia/batch_workflow.go b/admin/server/service/gaia/batch_workflow.go index 341e5f05d..112297f5d 100644 --- a/admin/server/service/gaia/batch_workflow.go +++ b/admin/server/service/gaia/batch_workflow.go @@ -115,9 +115,6 @@ func (s *BatchWorkflowService) CreateBatchWorkflow( global.GVA_LOG.Error(fmt.Sprintf("更新总行数失败: %v", err)) } - global.GVA_LOG.Info(fmt.Sprintf("批量工作流 %s 创建完成,原始行数: %d,有效行数: %d", - batchWorkflow.ID, len(fileContent)-1, validRowCount)) - // 任务已创建,工作池会自动处理 // 确保工作池在运行 if pool := GetWorkerPool(); pool == nil || !pool.IsRunning() { @@ -325,7 +322,7 @@ func (s *BatchWorkflowService) parseSSEStream(body []byte) (*request.WorkflowRes // callDifyAPI 调用Dify API func (s *BatchWorkflowService) callDifyAPI( - installedID, userToken string, inputs map[string]string) (string, error) { + installedID, userToken, csrfToken string, inputs map[string]string) (string, error) { var err error var requestBodyJSON []byte @@ -358,7 +355,13 @@ func (s *BatchWorkflowService) callDifyAPI( // 设置请求头 req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+userToken) - req.Header.Set("Accept", "text/event-stream") // 接受SSE流 + req.Header.Set("Accept", "text/event-stream") + // Extend Start: 添加CSRF token支持 + if csrfToken != "" { + req.Header.Set("x-csrf-token", csrfToken) + req.Header.Set("Cookie", fmt.Sprintf("csrf_token=%s", csrfToken)) + } + // Extend End: 添加CSRF token支持 // 发送请求 client := &http.Client{} @@ -400,20 +403,23 @@ func (s *BatchWorkflowService) GetBatchWorkflow(id string) (*gaia.BatchWorkflow, } var batchWorkflow gaia.BatchWorkflow - if err := global.GVA_DB.Where("id = ?", id).First(&batchWorkflow).Error; err != nil { + if err := global.GVA_DB.Where("id = ?", id).First( + &batchWorkflow).Error; err != nil { return nil, err } return &batchWorkflow, nil } // GetBatchWorkflowTasks 获取批量处理的任务列表 -func (s *BatchWorkflowService) GetBatchWorkflowTasks(batchWorkflowID string) ([]gaia.BatchWorkflowTask, error) { +func (s *BatchWorkflowService) GetBatchWorkflowTasks( + batchWorkflowID string) ([]gaia.BatchWorkflowTask, error) { if global.GVA_DB == nil { return nil, fmt.Errorf("数据库连接未初始化") } var tasks []gaia.BatchWorkflowTask - if err := global.GVA_DB.Where("batch_workflow_id = ?", batchWorkflowID).Order("row_index").Find(&tasks).Error; err != nil { + if err := global.GVA_DB.Where("batch_workflow_id = ?", batchWorkflowID).Order( + "row_index").Find(&tasks).Error; err != nil { return nil, err } return tasks, nil @@ -425,7 +431,8 @@ func (s *BatchWorkflowService) StopBatchWorkflow(id string) error { return fmt.Errorf("数据库连接未初始化") } - return global.GVA_DB.Model(&gaia.BatchWorkflow{}).Where("id = ?", id).Update("status", "stopped").Error + return global.GVA_DB.Model(&gaia.BatchWorkflow{}).Where("id = ?", id).Update( + "status", "stopped").Error } // RetryFailedTasks 仅重试失败的任务 @@ -436,14 +443,14 @@ func (s *BatchWorkflowService) RetryFailedTasks(id string) error { // 只重置失败的任务为待处理状态,保留已完成的任务 errorCount := 0 + taskList := []string{"failed", "queued", "running"} if err := global.GVA_DB.Model(&gaia.BatchWorkflowTask{}).Where( - "batch_workflow_id = ? AND status IN ?", id, []string{"failed", "queued", "running"}).Updates( - map[string]interface{}{ - "status": "pending", - "error": "", - "error_count": &errorCount, - "updated_at": time.Now(), - }).Error; err != nil { + "batch_workflow_id = ? AND status IN ?", id, taskList).Updates(map[string]interface{}{ + "status": "pending", + "error": "", + "error_count": &errorCount, + "updated_at": time.Now(), + }).Error; err != nil { return err } @@ -453,7 +460,8 @@ func (s *BatchWorkflowService) RetryFailedTasks(id string) error { "batch_workflow_id = ? AND status = ?", id, "completed").Count(&completedCount) // 重置批量处理状态 - if err := global.GVA_DB.Model(&gaia.BatchWorkflow{}).Where("id = ?", id).Updates(map[string]interface{}{ + if err := global.GVA_DB.Model(&gaia.BatchWorkflow{}).Where( + "id = ?", id).Updates(map[string]interface{}{ "status": "pending", "processed_rows": completedCount, "error": "", @@ -473,7 +481,8 @@ func (s *BatchWorkflowService) RetryBatchWorkflow(id string) error { } // 重置所有失败的任务为待处理状态 - if err := global.GVA_DB.Model(&gaia.BatchWorkflowTask{}).Where("batch_workflow_id = ? AND status IN ?", id, []string{"failed", "queued", "running"}).Updates(map[string]interface{}{ + if err := global.GVA_DB.Model(&gaia.BatchWorkflowTask{}).Where( + "batch_workflow_id = ? AND status IN ?", id, []string{"failed", "queued", "running"}).Updates(map[string]interface{}{ "status": "pending", "error": "", "updated_at": time.Now(), @@ -483,10 +492,12 @@ func (s *BatchWorkflowService) RetryBatchWorkflow(id string) error { // 重新计算已处理行数 var completedCount int64 - global.GVA_DB.Model(&gaia.BatchWorkflowTask{}).Where("batch_workflow_id = ? AND status = ?", id, "completed").Count(&completedCount) + global.GVA_DB.Model(&gaia.BatchWorkflowTask{}).Where( + "batch_workflow_id = ? AND status = ?", id, "completed").Count(&completedCount) // 重置批量处理状态 - if err := global.GVA_DB.Model(&gaia.BatchWorkflow{}).Where("id = ?", id).Updates(map[string]interface{}{ + if err := global.GVA_DB.Model(&gaia.BatchWorkflow{}).Where( + "id = ?", id).Updates(map[string]interface{}{ "status": "processing", "processed_rows": completedCount, "error": "", @@ -522,17 +533,59 @@ func (s *BatchWorkflowService) ResumeBatchWorkflow(id string) error { return fmt.Errorf("只能恢复已停止的批量处理") } + // 重新计算已处理行数(基于实际完成的任务数) + var completedCount int64 + global.GVA_DB.Model(&gaia.BatchWorkflowTask{}).Where( + "batch_workflow_id = ? AND status = ?", id, "completed").Count(&completedCount) + + // 同步 processed_rows 字段 + if err := global.GVA_DB.Model(&gaia.BatchWorkflow{}).Where( + "id = ?", id).Update("processed_rows", completedCount).Error; err != nil { + global.GVA_LOG.Error(fmt.Sprintf("同步已处理行数失败: %v", err)) + } + + // 检查是否所有任务都已完成 + if completedCount == int64(batchWorkflow.TotalRows) { + // 所有任务都已完成,更新状态为 completed + if err := global.GVA_DB.Model(&gaia.BatchWorkflow{}).Where( + "id = ?", id).Updates(map[string]interface{}{ + "status": "completed", + "processed_rows": completedCount, + "error": "", + "error_count": 0, + "updated_at": time.Now(), + }).Error; err != nil { + return fmt.Errorf("更新批量工作流完成状态失败: %v", err) + } + return nil + } + // 检查是否有可恢复的任务(pending 或 cancelled 状态) var resumableTasks int64 - global.GVA_DB.Model(&gaia.BatchWorkflowTask{}).Where("batch_workflow_id = ? AND status IN (?)", id, []string{"pending", "cancelled"}).Count(&resumableTasks) + global.GVA_DB.Model(&gaia.BatchWorkflowTask{}).Where( + "batch_workflow_id = ? AND status IN (?)", id, []string{"pending", "cancelled"}).Count(&resumableTasks) if resumableTasks == 0 { + // 没有可恢复的任务,但也没有全部完成,可能是数据不一致 + // 检查是否有其他状态的任务 + var otherTasks int64 + global.GVA_DB.Model(&gaia.BatchWorkflowTask{}).Where( + "batch_workflow_id = ? AND status NOT IN (?)", id, []string{"completed", "failed"}).Count(&otherTasks) + + if otherTasks == 0 { + // 所有任务都是 completed 或 failed,但 completedCount 不等于 TotalRows + // 可能是数据不一致,尝试调用 checkBatchWorkflowCompletion 来修复 + if pool := GetWorkerPool(); pool != nil { + pool.checkBatchWorkflowCompletion(id) + } + return fmt.Errorf("没有可恢复的任务,但任务状态可能不一致,已尝试修复") + } return fmt.Errorf("没有可恢复的任务") } // 将cancelled状态的任务恢复为pending状态 if err := global.GVA_DB.Model(&gaia.BatchWorkflowTask{}). - Where("batch_workflow_id = ? AND status = ?", id, "cancelled"). + Where("batch_workflow_id = ? AND status IN (?)", id, []string{"pending", "cancelled"}). Updates(map[string]interface{}{ "status": "pending", "updated_at": time.Now(), @@ -541,9 +594,11 @@ func (s *BatchWorkflowService) ResumeBatchWorkflow(id string) error { } // 更新批量工作流状态为处理中 - if err := global.GVA_DB.Model(&gaia.BatchWorkflow{}).Where("id = ?", id).Updates(map[string]interface{}{ - "status": "processing", - "updated_at": time.Now(), + if err := global.GVA_DB.Model(&gaia.BatchWorkflow{}).Where( + "id = ?", id).Updates(map[string]interface{}{ + "status": "processing", + "processed_rows": completedCount, + "updated_at": time.Now(), }).Error; err != nil { return err } @@ -554,7 +609,6 @@ func (s *BatchWorkflowService) ResumeBatchWorkflow(id string) error { InitWorkerPool(global.GVA_CONFIG.System.WorkFlowNumber) } - global.GVA_LOG.Info(fmt.Sprintf("批量工作流 %s 恢复已启动,工作池将自动处理待处理任务", id)) return nil } diff --git a/admin/server/service/gaia/worker_pool.go b/admin/server/service/gaia/worker_pool.go index caa4bc21b..1df0b8e4e 100644 --- a/admin/server/service/gaia/worker_pool.go +++ b/admin/server/service/gaia/worker_pool.go @@ -22,6 +22,9 @@ type UserWorkerAllocation struct { MaxLimit int `json:"max_limit"` } +// 全局工作池实例 +var globalWorkerPool *WorkerPool + // WorkerPool 工作池管理器 type WorkerPool struct { ctx context.Context @@ -764,14 +767,13 @@ func (wp *WorkerPool) processTask(task *gaia.BatchWorkflowTask) { global.GVA_LOG.Info(fmt.Sprintf("任务 %s 包含全空值输入,跳过处理并标记为完成", task.ID)) // 创建空结果并标记为完成 - emptyResult := map[string]interface{}{ + emptyResultJSON, _ := json.Marshal(map[string]interface{}{ "status": gaia.BatchTaskStatusCompleted, "message": "跳过空值输入任务", "outputs": map[string]interface{}{ "text": "输入为空,已跳过处理", }, - } - emptyResultJSON, _ := json.Marshal(emptyResult) + }) // 更新任务状态为完成 if err := global.GVA_DB.Model(task).Updates(map[string]interface{}{ @@ -792,28 +794,30 @@ func (wp *WorkerPool) processTask(task *gaia.BatchWorkflowTask) { return } - // 快速生成即时token + // 快速生成即时token和CSRF token var err error var token string + var csrfToken string var user system.SysUser if err = global.GVA_DB.Where( "id = ? AND enable = ?", batchWorkflow.UserID, system.UserActive).First(&user).Error; err != nil { wp.updateTaskError(task, "用户不存在: "+err.Error()) return } - // 生成这个用户的token - if token, _, err = utils.LoginToken(&user); err != nil { + // 生成这个用户的token和CSRF token + if token, csrfToken, _, err = utils.LoginTokenWithCSRF(&user); err != nil { wp.updateTaskError(task, "用户token生成失败: "+err.Error()) return } // 调用Dify API - result, err := wp.batchService.callDifyAPI(batchWorkflow.InstalledID, token, inputs) + result, err := wp.batchService.callDifyAPI(batchWorkflow.InstalledID, token, csrfToken, inputs) if err != nil { // 检查是否是余额不足错误(403状态码) - if strings.Contains(err.Error(), "状态码: 403") && strings.Contains(err.Error(), "Insufficient balance") { - global.GVA_LOG.Warn(fmt.Sprintf("用户 %d 余额不足,将其所有pending和processing状态的批量工作流和任务设置为失败", - batchWorkflow.UserID)) + if strings.Contains(err.Error(), "状态码: 403") && strings.Contains( + err.Error(), "Insufficient balance") { + global.GVA_LOG.Warn(fmt.Sprintf( + "用户 %d 余额不足,将其所有pending和processing状态的批量工作流和任务设置为失败", batchWorkflow.UserID)) wp.handleInsufficientBalance(batchWorkflow.UserID, task.BatchWorkflowID) wp.updateTaskError(task, gaia.ErrorInsufficientBalance) return @@ -838,9 +842,10 @@ func (wp *WorkerPool) processTask(task *gaia.BatchWorkflowTask) { errorMsg = apiError } // 检查是否是余额不足错误 - if strings.Contains(result, "call failed") || strings.Contains(apiError, "Insufficient balance") { - global.GVA_LOG.Warn(fmt.Sprintf("用户 %d 余额不足,将其所有pending和processing状态的批量工作流和任务设置为失败", - batchWorkflow.UserID)) + if strings.Contains(result, "call failed") || strings.Contains( + apiError, "Insufficient balance") { + global.GVA_LOG.Warn(fmt.Sprintf( + "用户 %d 余额不足,将其所有pending和processing状态的批量工作流和任务设置为失败", batchWorkflow.UserID)) wp.handleInsufficientBalance(batchWorkflow.UserID, task.BatchWorkflowID) wp.updateTaskError(task, gaia.ErrorInsufficientBalance) return @@ -861,7 +866,8 @@ func (wp *WorkerPool) processTask(task *gaia.BatchWorkflowTask) { } // 更新批量处理的已处理行数 - global.GVA_DB.Exec("UPDATE batch_workflows_extend SET processed_rows = processed_rows + 1, updated_at = ? WHERE id = ?", + global.GVA_DB.Exec( + "UPDATE batch_workflows_extend SET processed_rows = processed_rows + 1, updated_at = ? WHERE id = ?", time.Now(), batchWorkflow.ID) // 检查批量工作流是否完成 @@ -916,11 +922,10 @@ func (wp *WorkerPool) updateTaskError(task *gaia.BatchWorkflowTask, errorMsg str newErrorCount := task.ErrorCount + 1 // 更新批量工作流的错误次数和错误信息 - if err := global.GVA_DB.Exec("UPDATE batch_workflows_extend SET error_count = error_count + 1, error = ?, updated_at = ? WHERE id = ?", + if err := global.GVA_DB.Exec( + "UPDATE batch_workflows_extend SET error_count = error_count + 1, error = ?, updated_at = ? WHERE id = ?", decodedErrorMsg, time.Now(), task.BatchWorkflowID).Error; err != nil { global.GVA_LOG.Error(fmt.Sprintf("更新批量工作流错误次数和错误信息失败: %s", err.Error())) - } else { - global.GVA_LOG.Debug(fmt.Sprintf("批量工作流 %s 错误次数已递增,错误信息已更新", task.BatchWorkflowID)) } // 检查是否超过最大重试次数 @@ -955,7 +960,8 @@ func (wp *WorkerPool) updateTaskError(task *gaia.BatchWorkflowTask, errorMsg str // checkBatchWorkflowCompletion 检查批量工作流是否完成 func (wp *WorkerPool) checkBatchWorkflowCompletion(batchWorkflowID string) { var batchWorkflow gaia.BatchWorkflow - if err := global.GVA_DB.Where("id = ?", batchWorkflowID).First(&batchWorkflow).Error; err != nil { + if err := global.GVA_DB.Where("id = ?", batchWorkflowID).First( + &batchWorkflow).Error; err != nil { return } @@ -974,12 +980,13 @@ func (wp *WorkerPool) checkBatchWorkflowCompletion(batchWorkflowID string) { // 如果所有任务都已完成 if completedCount == int64(batchWorkflow.TotalRows) { - // 重置错误计数并更新状态 + // 重置错误计数并更新状态,同时同步 processed_rows 确保数据一致性 if err := global.GVA_DB.Model(&gaia.BatchWorkflow{}).Where("id = ?", batchWorkflowID).Updates(map[string]interface{}{ - "status": gaia.BatchWorkflowStatusCompleted, - "error": "", // 清空错误信息 - "error_count": 0, // 重置错误计数,恢复用户并发位 - "updated_at": time.Now(), + "status": gaia.BatchWorkflowStatusCompleted, + "processed_rows": completedCount, // 同步已处理行数,确保数据一致性 + "error": "", // 清空错误信息 + "error_count": 0, // 重置错误计数,恢复用户并发位 + "updated_at": time.Now(), }).Error; err != nil { global.GVA_LOG.Error(fmt.Sprintf("更新批量工作流完成状态失败: %s", err.Error())) } else { @@ -989,20 +996,18 @@ func (wp *WorkerPool) checkBatchWorkflowCompletion(batchWorkflowID string) { // 如果没有待处理、排队或运行中的任务,但有失败的任务 // 获取第一个失败任务的错误信息作为代表 var failedTask gaia.BatchWorkflowTask - var errorInfo string - if err := global.GVA_DB.Where("batch_workflow_id = ? AND status = ?", batchWorkflowID, gaia.BatchTaskStatusFailed). - First(&failedTask).Error; err == nil && failedTask.Error != "" { + var errorInfo = gaia.ErrorWorkflowFailed + if err := global.GVA_DB.Where("batch_workflow_id = ? AND status = ?", batchWorkflowID, + gaia.BatchTaskStatusFailed).First(&failedTask).Error; err == nil && failedTask.Error != "" { errorInfo = failedTask.Error - } else { - errorInfo = gaia.ErrorWorkflowFailed } - global.GVA_DB.Model(&gaia.BatchWorkflow{}).Where("id = ?", batchWorkflowID).Updates(map[string]interface{}{ + global.GVA_DB.Model(&gaia.BatchWorkflow{}).Where( + "id = ?", batchWorkflowID).Updates(map[string]interface{}{ "status": gaia.BatchWorkflowStatusFailed, "error": errorInfo, "updated_at": time.Now(), }) - global.GVA_LOG.Info(fmt.Sprintf("批量工作流 %s 处理失败,错误信息: %s", batchWorkflowID, errorInfo)) } } @@ -1052,22 +1057,17 @@ func resetAbnormalTasks() { for _, bw := range batchWorkflows { var runningCount int64 global.GVA_DB.Model(&gaia.BatchWorkflowTask{}). - Where("batch_workflow_id = ? AND status IN (?)", bw.ID, []string{gaia.BatchTaskStatusRunning, gaia.BatchTaskStatusQueued}). - Count(&runningCount) + Where("batch_workflow_id = ? AND status IN (?)", bw.ID, []string{ + gaia.BatchTaskStatusRunning, gaia.BatchTaskStatusQueued}).Count(&runningCount) // 如果没有正在运行或排队的任务,将批量工作流状态重置为 pending if runningCount == 0 { - if err = global.GVA_DB.Model(&gaia.BatchWorkflow{}). - Where("id = ?", bw.ID). - Update("status", gaia.BatchWorkflowStatusPending).Error; err != nil { + if err = global.GVA_DB.Model(&gaia.BatchWorkflow{}).Where("id = ?", bw.ID).Update( + "status", gaia.BatchWorkflowStatusPending).Error; err != nil { global.GVA_LOG.Error(fmt.Sprintf("重置批量工作流 %s 状态失败: %s", bw.ID, err.Error())) - } else { - global.GVA_LOG.Info(fmt.Sprintf("批量工作流 %s 状态从processing重置为pending", bw.ID)) } } } - - global.GVA_LOG.Info("异常状态任务重置完成") } // cleanupStoppedBatchWorkflowTasks 清理已停止的批量工作流中的待处理和排队任务 @@ -1075,25 +1075,18 @@ func cleanupStoppedBatchWorkflowTasks() { // 将已停止的批量工作流中的pending和queued任务标记为cancelled // 使用子查询方式避免JOIN在UPDATE中的别名问题 - result := global.GVA_DB.Table("batch_workflow_tasks_extend"). - Where("batch_workflow_id IN (?) AND status IN (?)", - global.GVA_DB.Table("batch_workflows_extend").Select("id").Where("status = ?", gaia.BatchWorkflowStatusStopped), - []string{gaia.BatchTaskStatusPending, gaia.BatchTaskStatusQueued}). - Update("status", gaia.BatchTaskStatusCancelled) + result := global.GVA_DB.Table("batch_workflow_tasks_extend").Where( + "batch_workflow_id IN (?) AND status IN (?)", global.GVA_DB.Table("batch_workflows_extend").Select( + "id").Where("status = ?", gaia.BatchWorkflowStatusStopped), []string{ + gaia.BatchTaskStatusPending, gaia.BatchTaskStatusQueued}).Update( + "status", gaia.BatchTaskStatusCancelled) if result.Error != nil { global.GVA_LOG.Error("清理已停止的批量工作流任务失败: " + result.Error.Error()) return } - - if result.RowsAffected > 0 { - global.GVA_LOG.Info(fmt.Sprintf("已清理 %d 个已停止批量工作流中的待处理和排队任务", result.RowsAffected)) - } } -// 全局工作池实例 -var globalWorkerPool *WorkerPool - // InitWorkerPool 初始化全局工作池 func InitWorkerPool(workers int) { if globalWorkerPool != nil { diff --git a/admin/server/utils/claims.go b/admin/server/utils/claims.go index ddda2487e..2c3294f9d 100644 --- a/admin/server/utils/claims.go +++ b/admin/server/utils/claims.go @@ -7,6 +7,7 @@ import ( systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" "github.com/gin-gonic/gin" "github.com/gofrs/uuid/v5" + jwt "github.com/golang-jwt/jwt/v4" "net" "time" ) @@ -187,8 +188,57 @@ func LoginToken(user system.Login) (token string, claims systemReq.CustomClaims, // Extend Start: add gaia token }) token, err = j.CreateToken(claims) - if err != nil { - return - } return } + +// LoginTokenWithCSRF 生成登录token和CSRF token (用于批量处理API调用) +func LoginTokenWithCSRF(user system.Login) ( + token string, csrfToken string, claims systemReq.CustomClaims, err error) { + var account gaia.Account + dr, err := ParseDuration(global.GVA_CONFIG.JWT.BufferTime) + if err != nil { + return token, csrfToken, claims, err + } + j := &JWT{SigningKey: []byte(global.GVA_CONFIG.JWT.SigningKey)} // 唯一签名 + if err = global.GVA_DB.Where("email=?", user.GetUserEmail()).First(&account).Error; err != nil { + return token, csrfToken, claims, err + } + claims = j.CreateClaims(systemReq.BaseClaims{ + UUID: user.GetUUID(), + ID: user.GetUserId(), + NickName: user.GetNickname(), + Username: user.GetUsername(), + AuthorityId: user.GetAuthorityId(), + // Extend Start: add gaia token + UserId: account.ID.String(), + Exp: time.Now().Add(dr).Unix(), + Sub: "Console API Passport", + Email: account.Email, + // Extend Start: add gaia token + }) + token, err = j.CreateToken(claims) + if err != nil { + return token, csrfToken, claims, err + } + + // 生成CSRF token + csrfToken, err = GenerateCSRFToken(account.ID.String()) + return +} + +// GenerateCSRFToken 生成CSRF token (与Dify API兼容) +func GenerateCSRFToken(userID string) (string, error) { + ep, err := ParseDuration(global.GVA_CONFIG.JWT.ExpiresTime) + if err != nil { + return "", err + } + j := &JWT{SigningKey: []byte(global.GVA_CONFIG.JWT.SigningKey)} + + // CSRF token只需要exp和sub字段,使用RegisteredClaims的ExpiresAt + return j.CreateCSRFToken(systemReq.CSRFClaims{ + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(ep)), + }, + Sub: userID, + }) +} diff --git a/admin/server/utils/jwt.go b/admin/server/utils/jwt.go index 998443eae..2aaa55657 100644 --- a/admin/server/utils/jwt.go +++ b/admin/server/utils/jwt.go @@ -54,6 +54,12 @@ func (j *JWT) CreateToken(claims request.CustomClaims) (string, error) { return token.SignedString(j.SigningKey) } +// CreateCSRFToken 创建CSRF token (与Dify API兼容) +func (j *JWT) CreateCSRFToken(claims request.CSRFClaims) (string, error) { + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(j.SigningKey) +} + // CreateTokenByOldToken 旧token 换新token 使用归并回源避免并发问题 func (j *JWT) CreateTokenByOldToken(oldToken string, claims request.CustomClaims) (string, error) { v, err, _ := global.GVA_Concurrency_Control.Do("JWT:"+oldToken, func() (interface{}, error) { diff --git a/web/app/(commonLayout)/datasets/doc.tsx b/web/app/(commonLayout)/datasets/doc.tsx index c31dad3c0..ddf8f57e5 100644 --- a/web/app/(commonLayout)/datasets/doc.tsx +++ b/web/app/(commonLayout)/datasets/doc.tsx @@ -11,7 +11,7 @@ import I18n from '@/context/i18n' import { LanguagesSupported } from '@/i18n-config/language' import useTheme from '@/hooks/use-theme' import { Theme } from '@/types/app' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' type DocProps = { apiBaseUrl: string diff --git a/web/app/components/base/chat/chat/index.tsx b/web/app/components/base/chat/chat/index.tsx index 4511bb6a6..10291a5c2 100644 --- a/web/app/components/base/chat/chat/index.tsx +++ b/web/app/components/base/chat/chat/index.tsx @@ -14,7 +14,9 @@ import type { InputForm } from './type' import type { Emoji } from '@/app/components/tools/types' import type { AppData } from '@/models/share' import { debounce } from 'es-toolkit/compat' +// extend: start messages context handling import { + Fragment, memo, useCallback, useEffect, diff --git a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/parameter-item-extend.tsx b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/parameter-item-extend.tsx index 57728fca0..fc0e7fec6 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/parameter-item-extend.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/parameter-item-extend.tsx @@ -3,7 +3,7 @@ import { useState } from 'react' import type { ModelParameterRule } from '../declarations' import { useLanguage } from '../hooks' import { isNullOrUndefined } from '../utils' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import Switch from '@/app/components/base/switch' import Tooltip from '@/app/components/base/tooltip' import Slider from '@/app/components/base/slider' diff --git a/web/app/components/share/text-generation/index.tsx b/web/app/components/share/text-generation/index.tsx index b793a03ce..425667543 100644 --- a/web/app/components/share/text-generation/index.tsx +++ b/web/app/components/share/text-generation/index.tsx @@ -42,6 +42,11 @@ import TabHeader from '../../base/tab-header' import MenuDropdown from './menu-dropdown' import RunBatch from './run-batch' import ResDownload from './run-batch/res-download' +import BatchProgress from './run-batch/batch-progress' // Extend: Batch import +import Pagination from '@/app/components/base/pagination' // Extend: Batch import +// Extend: Start Batch import +import { downloadBatchApi, fetchBatchWorkflowListApi, processExcelUploadApi } from '@/service/web-extend' +// Extend: Stop Batch import const GROUP_SIZE = BATCH_CONCURRENCY // to avoid RPM(Request per minute) limit. The group task finished then the next group. enum TaskStatus { @@ -155,6 +160,85 @@ const TextGeneration: FC = ({ doSetAllTaskList(taskList) allTaskListRef.current = taskList } + + // Extend: Start Batch import - 批量处理相关状态 + const [batchJobs, setBatchJobs] = useState>([]) + + // 分页状态 + const [currentPage, setCurrentPage] = useState(1) + const batchJobsLimit = 5 // 每页5个任务 + const [totalBatchJobs, setTotalBatchJobs] = useState(0) + const [isLoadingBatchJobs, setIsLoadingBatchJobs] = useState(false) + + // 从后端获取批量工作流列表 + const loadBatchWorkflows = useCallback(async () => { + if (!appId || currentTab !== 'batch') + return + + setIsLoadingBatchJobs(true) + try { + const result = await fetchBatchWorkflowListApi(installedAppInfo?.id, currentPage, batchJobsLimit) + if (result) { + // 转换数据格式以兼容现有组件 + const convertedJobs = result.items.map(item => ({ + id: item.id, + error: item.error, + error_count: item.error_count, + fileName: item.file_name, + createdAt: item.created_at, + status: item.status, + totalRows: item.total_rows, + processedRows: item.processed_rows, + })) + setBatchJobs(convertedJobs) + setTotalBatchJobs(result.total) + } + } + catch (error) { + console.error('Failed to load batch workflows:', error) + } + finally { + setIsLoadingBatchJobs(false) + } + }, [appId, currentTab, currentPage, installedAppInfo?.id, batchJobsLimit]) + + // 加载批量工作流列表 + useEffect(() => { + loadBatchWorkflows() + }, [loadBatchWorkflows]) + + // 自动刷新批量工作流列表(每3秒) + useEffect(() => { + if (currentTab !== 'batch' || batchJobs.length === 0) + return + + // 检查是否有进行中的任务 + const hasActiveJobs = batchJobs.some(job => + job.status === 'pending' || job.status === 'processing', + ) + + if (!hasActiveJobs) + return + + const refreshInterval = setInterval(() => { + loadBatchWorkflows() + }, 3000) // 每3秒刷新一次 + + return () => clearInterval(refreshInterval) + }, [currentTab, batchJobs, loadBatchWorkflows]) + + // 计算分页数据 - 现在数据已经是从后端分页获取的,不需要再切片 + const paginatedBatchJobs = batchJobs + // Extend: Stop Batch import + const pendingTaskList = allTaskList.filter(task => task.status === TaskStatus.pending) const noPendingTask = pendingTaskList.length === 0 const showTaskList = allTaskList.filter(task => task.status !== TaskStatus.pending) @@ -328,6 +412,66 @@ const TextGeneration: FC = ({ // eslint-disable-next-line ts/no-use-before-define showResultPanel() } + + // Extend: Start Batch import - 处理批量上传 + const handleBatchUpload = async (originalFile: File, data: string[][], originalFileName?: string) => { + if (!checkBatchInputs(data)) + return + + try { + // 创建key-name映射 + const keyNameMapping: Record = {} + promptConfig?.prompt_variables.forEach((variable) => { + keyNameMapping[variable.name] = variable.key + }) + + // 直接使用原始文件 + const result = await processExcelUploadApi(originalFile, installedAppInfo?.id || '', appId, keyNameMapping) + if (result === null) { + // API调用失败,错误信息已经在processExcelUploadApi中显示 + return + } + // 上传成功后,重新加载批量任务列表 + await loadBatchWorkflows() + // 显示结果面板 + // eslint-disable-next-line ts/no-use-before-define + showResultPanel() + notify({ type: 'success', message: t('batchWorkflow.batchUploadSuccess', { ns: 'extend' }) }) + } + catch (error) { + console.error('批量上传失败:', error) + notify({ type: 'error', message: t('batchWorkflow.batchUploadFailed', { ns: 'extend' }) }) + } + } + // 下载批量处理结果 + const handleBatchDownload = async (batchId: string) => { + try { + const blob = await downloadBatchApi(batchId) + if (blob) { + const url = window.URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `batch_results_${batchId}.csv` + document.body.appendChild(a) + a.click() + window.URL.revokeObjectURL(url) + document.body.removeChild(a) + } + } + catch (error) { + console.error('下载失败:', error) + notify({ type: 'error', message: t('batchWorkflow.downloadFailed', { ns: 'extend' }) }) + } + } + + // 处理重试成功回调 + const handleRetrySuccess = () => { + // 重试成功后,重新加载批量工作流列表 + loadBatchWorkflows() + console.log('批量任务重试成功,已刷新列表') + } + // Extend: Stop Batch import + const handleCompleted = (completionRes: string, taskId?: number, isSuccess?: boolean) => { const allTaskListLatest = getLatestTaskList() const batchCompletionResLatest = getBatchCompletionRes() @@ -464,13 +608,16 @@ const TextGeneration: FC = ({ : 'bg-chatbot-bg', )} > - {isCallBatchAPI && ( + {/* Extend: Start Batch import */} + {(isCallBatchAPI || (isInBatchTab && batchJobs.length > 0)) && (
-
{t('generation.executions', { ns: 'share', num: allTaskList.length })}
+
+ {isCallBatchAPI ? t('generation.executions', { ns: 'share', num: allTaskList.length }) : t('batchWorkflow.batchJobs', { ns: 'extend', num: batchJobs.length })} +
{allSuccessTaskList.length > 0 && ( = ({
0)) && 'pt-0', !isPC && 'p-0 pb-2', )} > - {!isCallBatchAPI ? renderRes() : renderBatchRes()} - {!noPendingTask && ( + {!isCallBatchAPI && !(isInBatchTab && batchJobs.length > 0) ? renderRes() : ( + <> + {isCallBatchAPI && renderBatchRes()} + {isInBatchTab && batchJobs.length > 0 && ( +
+ {/* 数据保留提示 */} +
+
+ {t('batchWorkflow.dataRetentionNotice', { ns: 'extend' })}: {t('batchWorkflow.dataRetentionDescription', { ns: 'extend' })} +
+
+ + {/* 批量任务列表 */} +
+ {isLoadingBatchJobs ? ( +
+
+
+ ) : paginatedBatchJobs.length > 0 ? ( + paginatedBatchJobs.map(job => ( + handleBatchDownload(job.id)} + onRetrySuccess={handleRetrySuccess} + /> + )) + ) : ( +
+ {t('batchWorkflow.noBatchTasks', { ns: 'extend' })} +
+ )} +
+ + {/* 分页控件 */} + {totalBatchJobs > batchJobsLimit && ( +
+ { + setCurrentPage(page) + // 页面变化时会自动触发useEffect重新加载数据 + }} + total={totalBatchJobs} + limit={batchJobsLimit} + className="w-auto" + /> +
+ )} +
+ )} + + )} + {!noPendingTask && isCallBatchAPI && (
)}
+ {/* Extend: Stop Batch import */} {isCallBatchAPI && allFailedTaskList.length > 0 && (
@@ -590,7 +793,10 @@ const TextGeneration: FC = ({
{currentTab === 'saved' && ( diff --git a/web/app/components/share/text-generation/run-batch/batch-progress/index.tsx b/web/app/components/share/text-generation/run-batch/batch-progress/index.tsx index 7ecfbe3c3..6e53cb6c7 100644 --- a/web/app/components/share/text-generation/run-batch/batch-progress/index.tsx +++ b/web/app/components/share/text-generation/run-batch/batch-progress/index.tsx @@ -15,7 +15,7 @@ import { resumeBatchApi, retryFailedTasksApi, stopBatchApi } from '@/service/web import type { BatchStatus } from '@/utils/batch-progress-manager' // extend: 批量运行工单 import ActionButton from '@/app/components/base/action-button' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' export type BatchProgressProps = { batchId: string @@ -103,17 +103,17 @@ const BatchProgress: FC = ({ const getStatusText = (status: BatchStatus) => { switch (status) { case 'pending': - return t('extend.batchWorkflow.pending') + return t('batchWorkflow.pending', { ns: 'extend'}) case 'processing': - return t('extend.batchWorkflow.processing') + return t('batchWorkflow.processing', { ns: 'extend'}) case 'completed': - return t('extend.batchWorkflow.completed') + return t('batchWorkflow.completed', { ns: 'extend'}) case 'failed': - return t('extend.batchWorkflow.failed') + return t('batchWorkflow.failed', { ns: 'extend'}) case 'stopped': - return t('extend.batchWorkflow.stopped') + return t('batchWorkflow.stopped', { ns: 'extend'}) default: - return t('extend.batchWorkflow.pending') + return t('batchWorkflow.pending', { ns: 'extend'}) } } @@ -188,8 +188,8 @@ const BatchProgress: FC = ({ {/* 文件信息 */}
-
{t('extend.batchWorkflow.uploadedFileName')}
-
{t('extend.batchWorkflow.uploadTime')}
+
{t('batchWorkflow.uploadedFileName', { ns: 'extend' })}
+
{t('batchWorkflow.uploadTime', { ns: 'extend' })}
@@ -234,9 +234,10 @@ const BatchProgress: FC = ({ {/* 详细进度信息 */} {jobData.totalRows > 0 && (
- {t('extend.batchWorkflow.processed', { + {t('batchWorkflow.processed', { processed: jobData.processedRows || 0, total: jobData.totalRows || 0, + ns: 'extend', })}
)} @@ -248,7 +249,7 @@ const BatchProgress: FC = ({
- {t('extend.batchWorkflow.errorOccurred')} + {t('batchWorkflow.errorOccurred', { ns: 'extend'} )}
{jobData.error} @@ -271,7 +272,7 @@ const BatchProgress: FC = ({ ) : ( )} - {t('extend.batchWorkflow.stop')} + {t('batchWorkflow.stop', { ns: 'extend'})} )} {status === 'stopped' && ( @@ -281,7 +282,7 @@ const BatchProgress: FC = ({ ) : ( )} - {t('extend.batchWorkflow.resume')} + {t('batchWorkflow.resume', { ns: 'extend'})} )} {(status === 'failed') && ( @@ -291,7 +292,7 @@ const BatchProgress: FC = ({ ) : ( )} - {t('extend.batchWorkflow.retry')} + {t('batchWorkflow.retry', { ns: 'extend'})} )}
@@ -300,7 +301,7 @@ const BatchProgress: FC = ({ {/* 下载按钮 */} {(status === 'failed' || status === 'completed' || (status === 'processing' && progress >= 100)) && ( - {t('extend.batchWorkflow.download')} + {t('batchWorkflow.download', { ns: 'extend'})} )}
diff --git a/web/app/components/share/text-generation/run-batch/csv-download/index.tsx b/web/app/components/share/text-generation/run-batch/csv-download/index.tsx index cd36627e2..e08941e56 100644 --- a/web/app/components/share/text-generation/run-batch/csv-download/index.tsx +++ b/web/app/components/share/text-generation/run-batch/csv-download/index.tsx @@ -50,23 +50,30 @@ const CSVDownload: FC = ({
- -
- - {t('generation.downloadTemplate', { ns: 'share' })} -
-
+ {/* Extend: start 聊天批量处理 */} +
+ +
+ + {t('generation.downloadTemplate', { ns: 'share' } )} +
+
+ + {t('batchWorkflow.willUseBatchProcessing', { ns: 'extend' } )} + +
+ {/* Extend: stop 聊天批量处理 */}
) diff --git a/web/app/components/share/text-generation/run-batch/csv-reader/index.tsx b/web/app/components/share/text-generation/run-batch/csv-reader/index.tsx index 80c679b1f..a2a415e4c 100644 --- a/web/app/components/share/text-generation/run-batch/csv-reader/index.tsx +++ b/web/app/components/share/text-generation/run-batch/csv-reader/index.tsx @@ -1,28 +1,142 @@ 'use client' -import type { FC } from 'react' -import * as React from 'react' -import { useState } from 'react' +import type { DragEvent, FC, ReactNode } from 'react' +import React, { useRef, useState } from 'react' +import Papa from 'papaparse' +import jschardet from 'jschardet' import { useTranslation } from 'react-i18next' -import { - useCSVReader, -} from 'react-papaparse' import { Csv as CSVIcon } from '@/app/components/base/icons/src/public/files' import { cn } from '@/utils/classnames' export type Props = { - onParsed: (data: string[][]) => void + onParsed: (data: string[][], originalFile?: File) => void // Extend: Batch import } +// 二开部分 - Begin 自定义CSVReader +type CCProps = { + onUploadAccepted: (results: any, file: File) => void // Extend: Batch import + onDragOver: (event: DragEvent) => void + onDragLeave: (event: DragEvent) => void + children: (props: any) => React.ReactElement +} + +const CustomCSVReader: React.FC = ({ + onUploadAccepted, onDragOver, onDragLeave, children, +}) => { + const [zoneHover, setZoneHover] = useState(false) + const [acceptedFile, setAcceptedFile] = useState(null) + + const readFile = (file: File) => { + const reader = new FileReader() + + reader.onload = (event) => { + const result = event.target?.result as string + + // 检测文本编码 + const encodingResult = jschardet.detect(result) + let encoding = encodingResult.encoding || 'utf-8' + // 处理可能的误判,将 ISO-8859-2 视为 GBK + if (encoding === 'ISO-8859-2') + encoding = 'gbk' + else if (encodingResult.encoding == null) { + // 判断是否windows + const language = (navigator as any).language || (navigator as any).userLanguage + const isWindows = (navigator.platform && navigator.platform.includes('Win')) || navigator.userAgent.includes('Win') + const isChineseLanguage = /^zh/i.test(language) || (navigator.languages && navigator.languages.some(lang => /^zh/i.test(lang))) + if (isWindows && isChineseLanguage) + encoding = 'gbk' + } + + // 处理可能的误判 + if (encoding === 'ISO-8859-2' || encoding === 'TIS-620' || !encoding.indexOf('windows')) + encoding = 'gbk' + + // 重新用检测到的编码读取文件内容 + const correctReader = new FileReader() + + correctReader.onload = (e) => { + const text = e.target?.result as string + + // 使用 PapaParse 解析 CSV 文件 + Papa.parse(text, { + complete: (results: any) => { + onUploadAccepted(results, file) + }, + }) + } + + correctReader.readAsText(file, encoding) + } + + reader.readAsBinaryString(file) + } + + const handleDrop = (event: DragEvent) => { + event.preventDefault() + setZoneHover(false) + + const files = event.dataTransfer.files + if (files.length > 0) { + const file = files[0] + setAcceptedFile(file) + readFile(file) + } + } + + const inputRef: any = useRef(null) + + const handleClick = () => { + inputRef.current.click() + } + + const getRootProps = () => ({ + onClick: handleClick, + onDrop: handleDrop, + onDragOver: (event: DragEvent) => { + event.preventDefault() + setZoneHover(true) + }, + onDragLeave: (event: DragEvent) => { + event.preventDefault() + setZoneHover(false) + }, + }) + + const renderChildren = () => { + return children({ getRootProps, acceptedFile }) + } + + return ( + <> + { + if (event.target.files && event.target.files.length > 0) { + const file = event.target.files[0] + setAcceptedFile(file) + readFile(file) + } + }} + /> + {renderChildren()} + + ) +} +// 二开部分 - End 自定义CSVReader + const CSVReader: FC = ({ onParsed, }) => { const { t } = useTranslation() - const { CSVReader } = useCSVReader() const [zoneHover, setZoneHover] = useState(false) return ( - { - onParsed(results.data) + { + onParsed(results.data, file) setZoneHover(false) }} onDragOver={(event: DragEvent) => { @@ -50,28 +164,28 @@ const CSVReader: FC = ({ { acceptedFile ? ( -
- -
- {acceptedFile.name.replace(/.csv$/, '')} - .csv -
+
+ +
+ {acceptedFile.name.replace(/.csv$/, '')} + .csv
- ) +
+ ) : ( -
- -
- {t('generation.csvUploadTitle', { ns: 'share' })} - {t('generation.browse', { ns: 'share' })} -
+
+ +
+ {t('generation.csvUploadTitle', {ns: 'share'})} + {t('generation.browse', {ns: 'share'})}
- ) +
+ ) }
)} - + ) } diff --git a/web/app/components/share/text-generation/run-batch/index.tsx b/web/app/components/share/text-generation/run-batch/index.tsx index d7a9813e0..eb0ff07b7 100644 --- a/web/app/components/share/text-generation/run-batch/index.tsx +++ b/web/app/components/share/text-generation/run-batch/index.tsx @@ -40,34 +40,17 @@ const RunBatch: FC = ({ const [isRecentlyClicked, setIsRecentlyClicked] = React.useState(false) const handleParsed = (data: string[][], originalFile?: File) => { - console.log('handleParsed 被调用, originalFile:', originalFile ? originalFile.name : 'undefined') setCsvData(data) setIsParsed(true) if (originalFile) { setFileName(originalFile.name) setOriginalFile(originalFile) - console.log('originalFile 已设置:', originalFile.name) - } - else { - console.warn('⚠️ originalFile 未传递!') } } const handleSend = async () => { - console.log('=== 批量运行调试信息 ===') - console.log('csvData:', csvData ? csvData.length : 'null') - console.log('originalFile:', originalFile ? originalFile.name : 'null') - console.log('onBatchSend:', onBatchSend ? '已定义' : '未定义') - console.log('isRecentlyClicked:', isRecentlyClicked) - - if (!csvData || csvData.length === 0 || !originalFile || isRecentlyClicked) { - console.log('提前返回,原因:', { - noCsvData: !csvData || csvData.length === 0, - noOriginalFile: !originalFile, - isRecentlyClicked, - }) + if (!csvData || csvData.length === 0 || !originalFile || isRecentlyClicked) return - } // 设置防重复点击状态 setIsRecentlyClicked(true) @@ -79,13 +62,9 @@ const RunBatch: FC = ({ const dataRows = csvData.slice(1).filter(row => !row.every(cell => cell === '')) const rowCount = dataRows.length - - console.log('有效数据行数:', rowCount) - console.log('判断条件: rowCount > 10 && onBatchSend =', rowCount > 10, '&&', !!onBatchSend, '=', rowCount > 10 && !!onBatchSend) // 如果超过10行,使用批量处理 if (rowCount > 10 && onBatchSend) { - console.log('✅ 使用admin后台批量处理') setIsUploading(true) try { await onBatchSend(originalFile, csvData, fileName) @@ -98,7 +77,7 @@ const RunBatch: FC = ({ } } else { - console.log('❌ 使用旧的前端处理逻辑') + // 10行以内,使用原有的在线处理 onSend(csvData) } } @@ -111,16 +90,37 @@ const RunBatch: FC = ({
+ + {/* 显示行数信息 Extend: Start Batch import */} + {isParsed && csvData.length > 1 && ( +
+ {t( + 'batchWorkflow.rowCount', + { ns: 'extend', count: csvData.slice(1).filter(row => !row.every( + cell => cell === '')).length + } + )} +
+ )} + {/* Extend: Stop Batch import */} +
+ {/* Extend: Start Batch import */} + {/* Extend: Stop Batch import */}
) diff --git a/web/app/components/workflow/operator/export-image.tsx b/web/app/components/workflow/operator/export-image.tsx index d14014ed1..54efa9299 100644 --- a/web/app/components/workflow/operator/export-image.tsx +++ b/web/app/components/workflow/operator/export-image.tsx @@ -9,7 +9,7 @@ import { toJpeg, toPng, toSvg } from 'html-to-image' import { useNodesReadOnly } from '../hooks' import TipPopup from './tip-popup' import { RiExportLine } from '@remixicon/react' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import { useStore as useAppStore } from '@/app/components/app/store' import { PortalToFollowElem, @@ -186,7 +186,7 @@ const ExportImage: FC = () => { }} > - +
{
- {t('workflow.common.currentView')} + {t('common.currentView', { sn: 'workflow' })}
handleExportImage('png')} > - {t('workflow.common.exportPNG')} + {t('common.exportPNG', { sn: 'workflow' })}
handleExportImage('jpeg')} > - {t('workflow.common.exportJPEG')} + {t('common.exportJPEG', { sn: 'workflow' })}
handleExportImage('svg')} > - {t('workflow.common.exportSVG')} + {t('common.exportSVG', { sn: 'workflow' })}
- {t('workflow.common.currentWorkflow')} + {t('common.currentWorkflow', { sn: 'workflow' })}
handleExportImage('png', true)} > - {t('workflow.common.exportPNG')} + {t('common.exportPNG', { sn: 'workflow' })}
handleExportImage('jpeg', true)} > - {t('workflow.common.exportJPEG')} + {t('common.exportJPEG', { sn: 'workflow' })}
handleExportImage('svg', true)} > - {t('workflow.common.exportSVG')} + {t('common.exportSVG', { sn: 'workflow' })}
diff --git a/web/i18n/en-US/extend.json b/web/i18n/en-US/extend.json index 80706a286..beeb94b1f 100644 --- a/web/i18n/en-US/extend.json +++ b/web/i18n/en-US/extend.json @@ -37,6 +37,7 @@ "batchWorkflow.errorOccurred": "Error occurred", "batchWorkflow.failed": "Failed", "batchWorkflow.initializing": "Initializing batch task...", + "batchWorkflow.noBatchTasks": "No batch processing tasks", "batchWorkflow.pending": "Pending", "batchWorkflow.pleaseWait": "Please wait", "batchWorkflow.processed": "Processed {{processed}}/{{total}} rows", diff --git a/web/i18n/zh-Hans/extend.json b/web/i18n/zh-Hans/extend.json index 770f81d4e..ed8fd7377 100644 --- a/web/i18n/zh-Hans/extend.json +++ b/web/i18n/zh-Hans/extend.json @@ -39,6 +39,7 @@ "batchWorkflow.errorOccurred": "发生错误", "batchWorkflow.failed": "失败", "batchWorkflow.initializing": "正在初始化批量任务...", + "batchWorkflow.noBatchTasks": "暂无批量处理任务", "batchWorkflow.pending": "待处理", "batchWorkflow.pleaseWait": "请稍候", "batchWorkflow.processed": "已处理 {{processed}}/{{total}} 行", diff --git a/web/package.json b/web/package.json index acfe1aa56..585ecdfae 100644 --- a/web/package.json +++ b/web/package.json @@ -71,12 +71,13 @@ "@sentry/react": "^8.55.0", "@svgdotjs/svg.js": "^3.2.5", "@tailwindcss/typography": "^0.5.19", - "@tanstack/react-form": "^1.23.7", - "@tanstack/react-query": "^5.90.5", - "@tanstack/react-query-devtools": "^5.90.2", "@tanstack/eslint-plugin-query": "^5.91.2", "@tanstack/react-devtools": "^0.9.0", + "@tanstack/react-form": "^1.23.7", "@tanstack/react-form-devtools": "^0.2.9", + "@tanstack/react-query": "^5.90.5", + "@tanstack/react-query-devtools": "^5.90.2", + "@types/papaparse": "^5.5.2", "abcjs": "^6.5.2", "ahooks": "^3.9.5", "class-variance-authority": "^0.7.1", @@ -86,6 +87,7 @@ "cron-parser": "^5.4.0", "dayjs": "^1.11.19", "decimal.js": "^10.6.0", + "dingtalk-jsapi": "^3.2.0", "dompurify": "^3.3.0", "echarts": "^5.6.0", "echarts-for-react": "^3.0.5", @@ -102,6 +104,7 @@ "js-audio-recorder": "^1.0.7", "js-cookie": "^3.0.5", "js-yaml": "^4.1.0", + "jschardet": "^3.1.4", "jsonschema": "^1.5.0", "katex": "^0.16.25", "ky": "^1.12.0", @@ -116,6 +119,7 @@ "next-pwa": "^5.6.0", "next-themes": "^0.4.6", "nuqs": "^2.8.6", + "papaparse": "^5.5.3", "pinyin-pro": "^3.27.0", "qrcode.react": "^4.2.0", "qs": "^6.14.0", @@ -145,16 +149,15 @@ "semver": "^7.7.3", "sharp": "^0.33.5", "sortablejs": "^1.15.6", - "swr": "^2.3.6", "string-ts": "^2.3.1", + "swr": "^2.3.6", "tailwind-merge": "^2.6.0", "tldts": "^7.0.17", "use-context-selector": "^2.0.0", "uuid": "^10.0.0", "zod": "^3.25.76", "zundo": "^2.3.0", - "zustand": "^5.0.9", - "dingtalk-jsapi": "^3.2.0" + "zustand": "^5.0.9" }, "devDependencies": { "@antfu/eslint-config": "^6.7.3", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 5e1d2ee09..82c8f2215 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -142,6 +142,9 @@ importers: '@tanstack/react-query-devtools': specifier: ^5.90.2 version: 5.91.1(@tanstack/react-query@5.90.12(react@19.2.3))(react@19.2.3) + '@types/papaparse': + specifier: ^5.5.2 + version: 5.5.2 abcjs: specifier: ^6.5.2 version: 6.5.2 @@ -220,6 +223,9 @@ importers: js-yaml: specifier: ^4.1.0 version: 4.1.1 + jschardet: + specifier: ^3.1.4 + version: 3.1.4 jsonschema: specifier: ^1.5.0 version: 1.5.0 @@ -262,6 +268,9 @@ importers: nuqs: specifier: ^2.8.6 version: 2.8.6(next@15.5.9(@babel/core@7.28.5)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.95.0))(react@19.2.3) + papaparse: + specifier: ^5.5.3 + version: 5.5.3 pinyin-pro: specifier: ^3.27.0 version: 3.27.0 @@ -3632,8 +3641,8 @@ packages: '@types/node@20.19.26': resolution: {integrity: sha512-0l6cjgF0XnihUpndDhk+nyD3exio3iKaYROSgvh/qSevPXax3L8p5DBRFjbvalnwatGgHEQn2R88y2fA3g4irg==} - '@types/papaparse@5.5.1': - resolution: {integrity: sha512-esEO+VISsLIyE+JZBmb89NzsYYbpwV8lmv2rPo6oX5y9KhBaIP7hhHgjuTut54qjdKVMufTEcrh5fUl9+58huw==} + '@types/papaparse@5.5.2': + resolution: {integrity: sha512-gFnFp/JMzLHCwRf7tQHrNnfhN4eYBVYYI897CGX4MY1tzY9l2aLkVyx2IlKZ/SAqDbB3I1AOZW5gTMGGsqWliA==} '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} @@ -6135,6 +6144,10 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + jschardet@3.1.4: + resolution: {integrity: sha512-/kmVISmrwVwtyYU40iQUOp3SUPk2dhNCMsZBQX0R1/jZ8maaXJ/oZIzUOiyOqcgtLnETFKYChbJ5iDC/eWmFHg==} + engines: {node: '>=0.1.90'} + jsdoc-type-pratt-parser@4.8.0: resolution: {integrity: sha512-iZ8Bdb84lWRuGHamRXFyML07r21pcwBrLkHEuHgEY5UbCouBwv7ECknDRKzsQIXMiqpPymqtIf8TC/shYKB5rw==} engines: {node: '>=12.0.0'} @@ -12253,7 +12266,7 @@ snapshots: dependencies: undici-types: 6.21.0 - '@types/papaparse@5.5.1': + '@types/papaparse@5.5.2': dependencies: '@types/node': 18.15.0 @@ -15138,6 +15151,8 @@ snapshots: dependencies: argparse: 2.0.1 + jschardet@3.1.4: {} + jsdoc-type-pratt-parser@4.8.0: {} jsdoc-type-pratt-parser@6.10.0: {} @@ -16724,7 +16739,7 @@ snapshots: react-papaparse@4.4.0: dependencies: - '@types/papaparse': 5.5.1 + '@types/papaparse': 5.5.2 papaparse: 5.5.3 react-pdf-highlighter@8.0.0-rc.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): diff --git a/web/service/web-extend.ts b/web/service/web-extend.ts index 9a8e504ed..12b1c1e47 100644 --- a/web/service/web-extend.ts +++ b/web/service/web-extend.ts @@ -1,5 +1,11 @@ -import { request } from './base' -import { API_ADMIN } from '@/config' +import Toast from '@/app/components/base/toast' + +// Admin server 使用独立的 JWT 认证,需要从 admin_token 获取 +const getAdminToken = () => { + // 优先使用 admin_token,如果没有则尝试使用 console_token + return localStorage.getItem('admin_token') || localStorage.getItem('console_token') +} + type batchProcessing = { id: string } @@ -14,26 +20,33 @@ export const processExcelUploadApi = async ( if (keyNameMapping) formData.append('key_name_mapping', JSON.stringify(keyNameMapping)) - const token = localStorage.getItem('console_token') + const token = getAdminToken() if (!token) return null try { - const s = await request<{ code?: number, data?: batchProcessing, msg?: string }>( - '/gaia/workflow/batch/processing', { - method: 'POST', - body: formData, - headers: new Headers({}), - credentials: 'omit', - }, { - isAdminAPI: true, - bodyStringify: false, - deleteContentType: true, + const response = await fetch(`/admin/gaia/workflow/batch/processing`, { + method: 'POST', + body: formData, + headers: { + 'Authorization': `Bearer ${token}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + Toast.notify({ + type: 'error', + message: errorData.msg || errorData.message || '批量处理上传失败', + duration: 6000, }) + return null + } + + const s = await response.json() as { code?: number, data?: batchProcessing, msg?: string } // 检查返回的错误码 if (s?.code && s.code !== 0) { - const Toast = await import('@/app/components/base/toast') - Toast.default.notify({ + Toast.notify({ type: 'error', message: s.msg || '批量处理上传失败', duration: 6000, @@ -48,16 +61,7 @@ export const processExcelUploadApi = async ( // 提取错误消息 let errorMessage = '工作流批处理上传excel失败,请重新下载或检查现有模板' - if (error?.response?.json) { - try { - const errorData = await error.response.json() - errorMessage = errorData.msg || errorData.message || errorMessage - } - catch { - // 忽略JSON解析错误 - } - } - else if (error?.message) { + if (error?.message) { errorMessage = error.message } else if (typeof error === 'string') { @@ -65,8 +69,7 @@ export const processExcelUploadApi = async ( } // 显示错误通知 - const Toast = await import('@/app/components/base/toast') - Toast.default.notify({ + Toast.notify({ type: 'error', message: errorMessage, duration: 6000, @@ -84,11 +87,12 @@ export const fetchBatchWorkflowListApi = async ( ): Promise<{ items: Array<{ id: string + error: string + error_count: number file_name: string status: string total_rows: number processed_rows: number - error?: string // 添加错误信息字段 created_at: string updated_at: string }> @@ -98,7 +102,7 @@ export const fetchBatchWorkflowListApi = async ( total_pages: number has_more: boolean } | null> => { - const token = localStorage.getItem('console_token') + const token = getAdminToken() if (!token) return null @@ -111,16 +115,29 @@ export const fetchBatchWorkflowListApi = async ( if (installedId) params.append('installed_id', installedId) - const response = await request<{ + const response = await fetch(`/admin/gaia/workflow/batch/list?${params.toString()}`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + }, + }) + + if (!response.ok) { + console.error('获取批量工作流列表失败:', response.statusText) + return null + } + + const responseData = await response.json() as { code?: number data?: { items: Array<{ id: string file_name: string status: string + error: string + error_count: number total_rows: number processed_rows: number - error?: string // 添加错误信息字段 created_at: string updated_at: string }> @@ -131,22 +148,14 @@ export const fetchBatchWorkflowListApi = async ( has_more: boolean } msg?: string - }>(`/gaia/workflow/batch/list?${params.toString()}`, { - method: 'GET', - headers: new Headers({}), - credentials: 'omit', - }, { - isAdminAPI: true, - bodyStringify: false, - deleteContentType: true, - }) + } - if (response?.code && response.code !== 0) { - console.error('获取批量工作流列表失败:', response.msg) + if (responseData?.code && responseData.code !== 0) { + console.error('获取批量工作流列表失败:', responseData.msg) return null } - return response?.data || null + return responseData?.data || null } catch (error: any) { console.error('获取批量工作流列表失败:', error) @@ -156,20 +165,23 @@ export const fetchBatchWorkflowListApi = async ( // 获取批量处理进度 export const fetchProgressApi = async (batchId: string) => { - const token = localStorage.getItem('console_token') + const token = getAdminToken() if (!token) return null try { - const s = await request<{ code?: number, data?: any, msg?: string }>( - `/gaia/workflow/batch/${batchId}/progress`, { - method: 'GET', - headers: new Headers({}), - credentials: 'omit', - }, { - isAdminAPI: true, - bodyStringify: false, - deleteContentType: true, - }) + const response = await fetch(`/admin/gaia/workflow/batch/${batchId}/progress`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + }, + }) + + if (!response.ok) { + console.error('获取批量处理进度失败:', response.statusText) + return null + } + + const s = await response.json() as { code?: number, data?: any, msg?: string } // 检查返回的错误码 if (s?.code && s.code !== 0) { @@ -189,19 +201,22 @@ export const fetchProgressApi = async (batchId: string) => { // 停止批量处理 export const stopBatchApi = async (batchId: string) => { - const token = localStorage.getItem('console_token') + const token = getAdminToken() if (!token) return false try { - const s = await request<{ code: number, msg: string }>(`/gaia/workflow/batch/${batchId}/stop`, { + const response = await fetch(`/admin/gaia/workflow/batch/${batchId}/stop`, { method: 'POST', - headers: new Headers({}), - credentials: 'omit', - }, { - isAdminAPI: true, - bodyStringify: false, - deleteContentType: true, + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, }) + + if (!response.ok) + return false + + const s = await response.json() as { code: number, msg: string } return s?.code === 0 } catch (error) { @@ -212,19 +227,22 @@ export const stopBatchApi = async (batchId: string) => { // 恢复批量处理 export const resumeBatchApi = async (batchId: string) => { - const token = localStorage.getItem('console_token') + const token = getAdminToken() if (!token) return false try { - const s = await request<{ code: number, msg: string }>(`/gaia/workflow/batch/${batchId}/resume`, { + const response = await fetch(`/admin/gaia/workflow/batch/${batchId}/resume`, { method: 'POST', - headers: new Headers({}), - credentials: 'omit', - }, { - isAdminAPI: true, - bodyStringify: false, - deleteContentType: true, + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, }) + + if (!response.ok) + return false + + const s = await response.json() as { code: number, msg: string } return s?.code === 0 } catch (error) { @@ -235,19 +253,22 @@ export const resumeBatchApi = async (batchId: string) => { // 重试批量处理(完全重新开始所有任务) export const retryBatchApi = async (batchId: string) => { - const token = localStorage.getItem('console_token') + const token = getAdminToken() if (!token) return false try { - const s = await request<{ code: number, msg: string }>(`/gaia/workflow/batch/${batchId}/retry`, { + const response = await fetch(`/admin/gaia/workflow/batch/${batchId}/retry`, { method: 'POST', - headers: new Headers({}), - credentials: 'omit', - }, { - isAdminAPI: true, - bodyStringify: false, - deleteContentType: true, + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, }) + + if (!response.ok) + return false + + const s = await response.json() as { code: number, msg: string } return s?.code === 0 } catch (error) { @@ -258,19 +279,22 @@ export const retryBatchApi = async (batchId: string) => { // 仅重试失败的任务 export const retryFailedTasksApi = async (batchId: string) => { - const token = localStorage.getItem('console_token') + const token = getAdminToken() if (!token) return false try { - const s = await request<{ code: number, msg: string }>(`/gaia/workflow/batch/${batchId}/retry-failed`, { + const response = await fetch(`/admin/gaia/workflow/batch/${batchId}/retry-failed`, { method: 'POST', - headers: new Headers({}), - credentials: 'omit', - }, { - isAdminAPI: true, - bodyStringify: false, - deleteContentType: true, + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, }) + + if (!response.ok) + return false + + const s = await response.json() as { code: number, msg: string } return s?.code === 0 } catch (error) { @@ -281,18 +305,42 @@ export const retryFailedTasksApi = async (batchId: string) => { // 下载批量处理结果 export const downloadBatchApi = async (batchId: string): Promise => { - const token = localStorage.getItem('console_token') + const token = getAdminToken() if (!token) return null try { - const response = await fetch(`${API_ADMIN}/gaia/workflow/batch/${batchId}/download`, { + const response = await fetch(`/admin/gaia/workflow/batch/${batchId}/download`, { method: 'GET', headers: { - Authorization: `Bearer ${token}`, + 'Authorization': `Bearer ${token}`, }, - credentials: 'same-origin', }) + // 检查是否返回JSON错误响应 + const contentType = response.headers.get('content-type') + console.log('contentType', contentType) + if (contentType && contentType.includes('application/json')) { + // 显示错误消息 + Toast.notify({ + type: 'error', + message: '您的登录已失效,请重新登陆后再试', + duration: 6000, + }) + + // 清除本地存储的token + localStorage.removeItem('setup_status') + localStorage.removeItem('console_token') + localStorage.removeItem('refresh_token') + + // 清除对话记录 + if (localStorage?.getItem('conversationIdInfo')) + localStorage.removeItem('conversationIdInfo') + + window.location.href = '/signin' + console.log('signin') + return null + } + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)