mirror of
https://github.com/YFGaia/dify-plus.git
synced 2026-06-04 10:14:00 +08:00
fix: 修改管理端的请求api端的CSRF逻辑:
需要 x-csrf-token header 需要 csrf_token cookie 两者必须一致,且是有效的JWT(包含 exp 和 sub=user_id)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
+1
-1
@@ -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'
|
||||
|
||||
@@ -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<IMainProps> = ({
|
||||
doSetAllTaskList(taskList)
|
||||
allTaskListRef.current = taskList
|
||||
}
|
||||
|
||||
// Extend: Start Batch import - 批量处理相关状态
|
||||
const [batchJobs, setBatchJobs] = useState<Array<{
|
||||
id: string
|
||||
fileName: string
|
||||
createdAt: string
|
||||
status: string
|
||||
totalRows: number
|
||||
processedRows: number
|
||||
error?: string
|
||||
}>>([])
|
||||
|
||||
// 分页状态
|
||||
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<IMainProps> = ({
|
||||
// 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<string, string> = {}
|
||||
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<IMainProps> = ({
|
||||
: 'bg-chatbot-bg',
|
||||
)}
|
||||
>
|
||||
{isCallBatchAPI && (
|
||||
{/* Extend: Start Batch import */}
|
||||
{(isCallBatchAPI || (isInBatchTab && batchJobs.length > 0)) && (
|
||||
<div className={cn(
|
||||
'flex shrink-0 items-center justify-between px-14 pb-2 pt-9',
|
||||
!isPC && 'px-4 pb-1 pt-3',
|
||||
)}
|
||||
>
|
||||
<div className="system-md-semibold-uppercase text-text-primary">{t('generation.executions', { ns: 'share', num: allTaskList.length })}</div>
|
||||
<div className="system-md-semibold-uppercase text-text-primary">
|
||||
{isCallBatchAPI ? t('generation.executions', { ns: 'share', num: allTaskList.length }) : t('batchWorkflow.batchJobs', { ns: 'extend', num: batchJobs.length })}
|
||||
</div>
|
||||
{allSuccessTaskList.length > 0 && (
|
||||
<ResDownload
|
||||
isMobile={!isPC}
|
||||
@@ -482,17 +629,73 @@ const TextGeneration: FC<IMainProps> = ({
|
||||
<div className={cn(
|
||||
'flex h-0 grow flex-col overflow-y-auto',
|
||||
isPC && 'px-14 py-8',
|
||||
isPC && isCallBatchAPI && 'pt-0',
|
||||
isPC && (isCallBatchAPI || (isInBatchTab && batchJobs.length > 0)) && 'pt-0',
|
||||
!isPC && 'p-0 pb-2',
|
||||
)}
|
||||
>
|
||||
{!isCallBatchAPI ? renderRes() : renderBatchRes()}
|
||||
{!noPendingTask && (
|
||||
{!isCallBatchAPI && !(isInBatchTab && batchJobs.length > 0) ? renderRes() : (
|
||||
<>
|
||||
{isCallBatchAPI && renderBatchRes()}
|
||||
{isInBatchTab && batchJobs.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
{/* 数据保留提示 */}
|
||||
<div className="rounded-lg border border-yellow-200 bg-yellow-50 p-3">
|
||||
<div className="text-sm text-yellow-800">
|
||||
<strong>{t('batchWorkflow.dataRetentionNotice', { ns: 'extend' })}:</strong> {t('batchWorkflow.dataRetentionDescription', { ns: 'extend' })}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 批量任务列表 */}
|
||||
<div className="space-y-4">
|
||||
{isLoadingBatchJobs ? (
|
||||
<div className="flex justify-center py-8">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
) : paginatedBatchJobs.length > 0 ? (
|
||||
paginatedBatchJobs.map(job => (
|
||||
<BatchProgress
|
||||
key={job.id}
|
||||
fileName={job.fileName}
|
||||
batchId={job.id}
|
||||
workflowId={appId}
|
||||
jobData={job}
|
||||
onDownload={() => handleBatchDownload(job.id)}
|
||||
onRetrySuccess={handleRetrySuccess}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="py-8 text-center text-gray-500">
|
||||
{t('batchWorkflow.noBatchTasks', { ns: 'extend' })}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 分页控件 */}
|
||||
{totalBatchJobs > batchJobsLimit && (
|
||||
<div className="mt-6 flex justify-center">
|
||||
<Pagination
|
||||
current={currentPage}
|
||||
onChange={(page) => {
|
||||
setCurrentPage(page)
|
||||
// 页面变化时会自动触发useEffect重新加载数据
|
||||
}}
|
||||
total={totalBatchJobs}
|
||||
limit={batchJobsLimit}
|
||||
className="w-auto"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!noPendingTask && isCallBatchAPI && (
|
||||
<div className="mt-4">
|
||||
<Loading type="area" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Extend: Stop Batch import */}
|
||||
{isCallBatchAPI && allFailedTaskList.length > 0 && (
|
||||
<div className="absolute bottom-6 left-1/2 z-10 flex -translate-x-1/2 items-center gap-2 rounded-xl border border-components-panel-border bg-components-panel-bg-blur p-3 shadow-lg backdrop-blur-sm">
|
||||
<RiErrorWarningFill className="h-4 w-4 text-text-destructive" />
|
||||
@@ -590,7 +793,10 @@ const TextGeneration: FC<IMainProps> = ({
|
||||
<RunBatch
|
||||
vars={promptConfig.prompt_variables}
|
||||
onSend={handleRunBatch}
|
||||
onBatchSend={handleBatchUpload} // Extend: Batch import
|
||||
isAllFinished={allTasksRun}
|
||||
isInstalledApp={isInstalledApp} // Extend: Batch import
|
||||
installedAppInfo={installedAppInfo} // Extend: Batch import
|
||||
/>
|
||||
</div>
|
||||
{currentTab === 'saved' && (
|
||||
|
||||
@@ -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<BatchProgressProps> = ({
|
||||
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<BatchProgressProps> = ({
|
||||
{/* 文件信息 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900">{t('extend.batchWorkflow.uploadedFileName')}</div>
|
||||
<div className="mt-1 text-xs text-gray-500">{t('extend.batchWorkflow.uploadTime')}</div>
|
||||
<div className="text-sm font-medium text-gray-900">{t('batchWorkflow.uploadedFileName', { ns: 'extend' })}</div>
|
||||
<div className="mt-1 text-xs text-gray-500">{t('batchWorkflow.uploadTime', { ns: 'extend' })}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-right">
|
||||
@@ -234,9 +234,10 @@ const BatchProgress: FC<BatchProgressProps> = ({
|
||||
{/* 详细进度信息 */}
|
||||
{jobData.totalRows > 0 && (
|
||||
<div className="mt-2 text-xs text-gray-500">
|
||||
{t('extend.batchWorkflow.processed', {
|
||||
{t('batchWorkflow.processed', {
|
||||
processed: jobData.processedRows || 0,
|
||||
total: jobData.totalRows || 0,
|
||||
ns: 'extend',
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
@@ -248,7 +249,7 @@ const BatchProgress: FC<BatchProgressProps> = ({
|
||||
<RiErrorWarningLine className="h-4 w-4 text-red-500 mt-0.5 flex-shrink-0" />
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-medium text-red-800 mb-1">
|
||||
{t('extend.batchWorkflow.errorOccurred')}
|
||||
{t('batchWorkflow.errorOccurred', { ns: 'extend'} )}
|
||||
</div>
|
||||
<div className="text-xs text-red-700 break-words">
|
||||
{jobData.error}
|
||||
@@ -271,7 +272,7 @@ const BatchProgress: FC<BatchProgressProps> = ({
|
||||
) : (
|
||||
<RiStopLine className="h-4 w-4" />
|
||||
)}
|
||||
<span className="ml-1">{t('extend.batchWorkflow.stop')}</span>
|
||||
<span className="ml-1">{t('batchWorkflow.stop', { ns: 'extend'})}</span>
|
||||
</ActionButton>
|
||||
)}
|
||||
{status === 'stopped' && (
|
||||
@@ -281,7 +282,7 @@ const BatchProgress: FC<BatchProgressProps> = ({
|
||||
) : (
|
||||
<RiPlayLargeLine className="h-4 w-4" />
|
||||
)}
|
||||
<span className="ml-1">{t('extend.batchWorkflow.resume')}</span>
|
||||
<span className="ml-1">{t('batchWorkflow.resume', { ns: 'extend'})}</span>
|
||||
</ActionButton>
|
||||
)}
|
||||
{(status === 'failed') && (
|
||||
@@ -291,7 +292,7 @@ const BatchProgress: FC<BatchProgressProps> = ({
|
||||
) : (
|
||||
<RiRefreshLine className="h-4 w-4" />
|
||||
)}
|
||||
<span className="ml-1">{t('extend.batchWorkflow.retry')}</span>
|
||||
<span className="ml-1">{t('batchWorkflow.retry', { ns: 'extend'})}</span>
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
@@ -300,7 +301,7 @@ const BatchProgress: FC<BatchProgressProps> = ({
|
||||
{/* 下载按钮 */}
|
||||
{(status === 'failed' || status === 'completed' || (status === 'processing' && progress >= 100)) && (
|
||||
<ActionButton onClick={onDownload} size="sm">
|
||||
<span>{t('extend.batchWorkflow.download')}</span>
|
||||
<span>{t('batchWorkflow.download', { ns: 'extend'})}</span>
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -50,23 +50,30 @@ const CSVDownload: FC<ICSVDownloadProps> = ({
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<CSVDownloader
|
||||
className="mt-2 block cursor-pointer"
|
||||
type={Type.Link}
|
||||
filename="template"
|
||||
bom={true}
|
||||
config={{
|
||||
// delimiter: ';',
|
||||
}}
|
||||
data={[
|
||||
template,
|
||||
]}
|
||||
>
|
||||
<div className="system-xs-medium flex h-[18px] items-center space-x-1 text-text-accent">
|
||||
<DownloadIcon className="h-3 w-3" />
|
||||
<span>{t('generation.downloadTemplate', { ns: 'share' })}</span>
|
||||
</div>
|
||||
</CSVDownloader>
|
||||
{/* Extend: start 聊天批量处理 */}
|
||||
<div className="mt-2 flex items-center justify-between">
|
||||
<CSVDownloader
|
||||
className="cursor-pointer"
|
||||
type={Type.Link}
|
||||
filename="template"
|
||||
bom={true}
|
||||
config={{
|
||||
// delimiter: ';',
|
||||
}}
|
||||
data={[
|
||||
template,
|
||||
]}
|
||||
>
|
||||
<div className="system-xs-medium flex h-[18px] items-center space-x-1 text-text-accent">
|
||||
<DownloadIcon className="h-3 w-3" />
|
||||
<span>{t('generation.downloadTemplate', { ns: 'share' } )}</span>
|
||||
</div>
|
||||
</CSVDownloader>
|
||||
<span className="system-xs-medium text-text-tertiary">
|
||||
{t('batchWorkflow.willUseBatchProcessing', { ns: 'extend' } )}
|
||||
</span>
|
||||
</div>
|
||||
{/* Extend: stop 聊天批量处理 */}
|
||||
</div>
|
||||
|
||||
)
|
||||
|
||||
@@ -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<CCProps> = ({
|
||||
onUploadAccepted, onDragOver, onDragLeave, children,
|
||||
}) => {
|
||||
const [zoneHover, setZoneHover] = useState(false)
|
||||
const [acceptedFile, setAcceptedFile] = useState<File | null>(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<ReactNode>(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 (
|
||||
<>
|
||||
<input
|
||||
accept="text/csv, .csv, application/vnd.ms-excel"
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
style={{ display: 'none' }}
|
||||
required={false}
|
||||
multiple={false}
|
||||
onChange={async (event) => {
|
||||
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<Props> = ({
|
||||
onParsed,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { CSVReader } = useCSVReader()
|
||||
const [zoneHover, setZoneHover] = useState(false)
|
||||
return (
|
||||
<CSVReader
|
||||
onUploadAccepted={(results: any) => {
|
||||
onParsed(results.data)
|
||||
<CustomCSVReader
|
||||
onUploadAccepted={(results: any, file: File) => {
|
||||
onParsed(results.data, file)
|
||||
setZoneHover(false)
|
||||
}}
|
||||
onDragOver={(event: DragEvent) => {
|
||||
@@ -50,28 +164,28 @@ const CSVReader: FC<Props> = ({
|
||||
{
|
||||
acceptedFile
|
||||
? (
|
||||
<div className="flex w-full items-center space-x-2">
|
||||
<CSVIcon className="shrink-0" />
|
||||
<div className="flex w-0 grow">
|
||||
<span className="max-w-[calc(100%_-_30px)] truncate text-text-secondary">{acceptedFile.name.replace(/.csv$/, '')}</span>
|
||||
<span className="shrink-0 text-text-tertiary">.csv</span>
|
||||
</div>
|
||||
<div className="flex w-full items-center space-x-2">
|
||||
<CSVIcon className="shrink-0" />
|
||||
<div className="flex w-0 grow">
|
||||
<span className="max-w-[calc(100%_-_30px)] truncate text-text-secondary">{acceptedFile.name.replace(/.csv$/, '')}</span>
|
||||
<span className="shrink-0 text-text-tertiary">.csv</span>
|
||||
</div>
|
||||
)
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className="flex w-full items-center justify-center space-x-2">
|
||||
<CSVIcon className="shrink-0" />
|
||||
<div className="text-text-tertiary">
|
||||
{t('generation.csvUploadTitle', { ns: 'share' })}
|
||||
<span className="cursor-pointer text-text-accent">{t('generation.browse', { ns: 'share' })}</span>
|
||||
</div>
|
||||
<div className="flex w-full items-center justify-center space-x-2">
|
||||
<CSVIcon className="shrink-0" />
|
||||
<div className="text-text-tertiary">
|
||||
{t('generation.csvUploadTitle', {ns: 'share'})}
|
||||
<span className="cursor-pointer text-text-accent">{t('generation.browse', {ns: 'share'})}</span>
|
||||
</div>
|
||||
)
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CSVReader>
|
||||
</CustomCSVReader>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -40,34 +40,17 @@ const RunBatch: FC<IRunBatchProps> = ({
|
||||
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<IRunBatchProps> = ({
|
||||
|
||||
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<IRunBatchProps> = ({
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.log('❌ 使用旧的前端处理逻辑')
|
||||
// 10行以内,使用原有的在线处理
|
||||
onSend(csvData)
|
||||
}
|
||||
}
|
||||
@@ -111,16 +90,37 @@ const RunBatch: FC<IRunBatchProps> = ({
|
||||
<div className="pt-4">
|
||||
<CSVReader onParsed={handleParsed} />
|
||||
<CSVDownload vars={vars} />
|
||||
|
||||
{/* 显示行数信息 Extend: Start Batch import */}
|
||||
{isParsed && csvData.length > 1 && (
|
||||
<div className="mt-2 text-sm text-gray-500">
|
||||
{t(
|
||||
'batchWorkflow.rowCount',
|
||||
{ ns: 'extend', count: csvData.slice(1).filter(row => !row.every(
|
||||
cell => cell === '')).length
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* Extend: Stop Batch import */}
|
||||
|
||||
<div className="flex justify-end">
|
||||
{/* Extend: Start Batch import */}
|
||||
<Button
|
||||
variant="primary"
|
||||
className={cn('mt-4 pl-3 pr-4', !isPC && 'grow')}
|
||||
onClick={handleSend}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
<Icon className={cn(!isAllFinished && 'animate-spin', 'mr-1 h-4 w-4 shrink-0')} aria-hidden="true" />
|
||||
<span className="text-[13px] uppercase">{t('generation.run', { ns: 'share' })}</span>
|
||||
<Icon
|
||||
className={cn((!isAllFinished || isUploading) && 'animate-spin', 'mr-1 h-4 w-4 shrink-0')}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="text-[13px] uppercase">
|
||||
{isUploading ? t('batchWorkflow.uploading', { ns: 'extend' } ) : t('generation.run', { ns: 'share' })}
|
||||
</span>
|
||||
</Button>
|
||||
{/* Extend: Stop Batch import */}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -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 = () => {
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger>
|
||||
<TipPopup title={t('workflow.common.exportImage')}>
|
||||
<TipPopup title={t('common.exportImage', { sn: 'workflow' })}>
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg hover:bg-state-base-hover hover:text-text-secondary',
|
||||
@@ -202,49 +202,49 @@ const ExportImage: FC = () => {
|
||||
<div className='min-w-[180px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur text-text-secondary shadow-lg'>
|
||||
<div className='p-1'>
|
||||
<div className='px-2 py-1 text-xs font-medium text-text-tertiary'>
|
||||
{t('workflow.common.currentView')}
|
||||
{t('common.currentView', { sn: 'workflow' })}
|
||||
</div>
|
||||
<div
|
||||
className='system-md-regular flex h-8 cursor-pointer items-center rounded-lg px-2 hover:bg-state-base-hover'
|
||||
onClick={() => handleExportImage('png')}
|
||||
>
|
||||
{t('workflow.common.exportPNG')}
|
||||
{t('common.exportPNG', { sn: 'workflow' })}
|
||||
</div>
|
||||
<div
|
||||
className='system-md-regular flex h-8 cursor-pointer items-center rounded-lg px-2 hover:bg-state-base-hover'
|
||||
onClick={() => handleExportImage('jpeg')}
|
||||
>
|
||||
{t('workflow.common.exportJPEG')}
|
||||
{t('common.exportJPEG', { sn: 'workflow' })}
|
||||
</div>
|
||||
<div
|
||||
className='system-md-regular flex h-8 cursor-pointer items-center rounded-lg px-2 hover:bg-state-base-hover'
|
||||
onClick={() => handleExportImage('svg')}
|
||||
>
|
||||
{t('workflow.common.exportSVG')}
|
||||
{t('common.exportSVG', { sn: 'workflow' })}
|
||||
</div>
|
||||
|
||||
<div className='border-border-divider mx-2 my-1 border-t' />
|
||||
|
||||
<div className='px-2 py-1 text-xs font-medium text-text-tertiary'>
|
||||
{t('workflow.common.currentWorkflow')}
|
||||
{t('common.currentWorkflow', { sn: 'workflow' })}
|
||||
</div>
|
||||
<div
|
||||
className='system-md-regular flex h-8 cursor-pointer items-center rounded-lg px-2 hover:bg-state-base-hover'
|
||||
onClick={() => handleExportImage('png', true)}
|
||||
>
|
||||
{t('workflow.common.exportPNG')}
|
||||
{t('common.exportPNG', { sn: 'workflow' })}
|
||||
</div>
|
||||
<div
|
||||
className='system-md-regular flex h-8 cursor-pointer items-center rounded-lg px-2 hover:bg-state-base-hover'
|
||||
onClick={() => handleExportImage('jpeg', true)}
|
||||
>
|
||||
{t('workflow.common.exportJPEG')}
|
||||
{t('common.exportJPEG', { sn: 'workflow' })}
|
||||
</div>
|
||||
<div
|
||||
className='system-md-regular flex h-8 cursor-pointer items-center rounded-lg px-2 hover:bg-state-base-hover'
|
||||
onClick={() => handleExportImage('svg', true)}
|
||||
>
|
||||
{t('workflow.common.exportSVG')}
|
||||
{t('common.exportSVG', { sn: 'workflow' })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"batchWorkflow.errorOccurred": "发生错误",
|
||||
"batchWorkflow.failed": "失败",
|
||||
"batchWorkflow.initializing": "正在初始化批量任务...",
|
||||
"batchWorkflow.noBatchTasks": "暂无批量处理任务",
|
||||
"batchWorkflow.pending": "待处理",
|
||||
"batchWorkflow.pleaseWait": "请稍候",
|
||||
"batchWorkflow.processed": "已处理 {{processed}}/{{total}} 行",
|
||||
|
||||
+9
-6
@@ -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",
|
||||
|
||||
Generated
+19
-4
@@ -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):
|
||||
|
||||
+138
-90
@@ -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<Blob | null> => {
|
||||
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}`)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user