feat: 添加万川AI平台对接功能

- 新增万川AI平台API接口层,支持登录、获取知识库列表、上传文件等操作
- 实现万川平台配置界面,支持平台地址、账号密码配置和知识库绑定
- 在知识库页面添加上传功能,支持单个或批量上传售后报告到万川知识库
- 提供检查配置的工具脚本便于调试
This commit is contained in:
2026-06-11 16:14:38 +08:00
parent d332844459
commit b137fd7915
5 changed files with 796 additions and 14 deletions

View File

@@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Plus, Trash2, Sparkles, RefreshCw, ChevronRight, Loader, Settings2, X, Download } from 'lucide-react'
import { Plus, Trash2, Sparkles, RefreshCw, ChevronRight, Loader, Settings2, X, Download, Upload } from 'lucide-react'
import dayjs from 'dayjs'
import {
getGroups,
@@ -22,6 +22,7 @@ import {
import MessageBubble from '../components/MessageBubble'
import ReportDocumentView from '../components/ReportDocumentView'
import { exportWordDoc } from '../utils/wordExport'
import { uploadToKnowledgeBase, getWanchuanKnowledgeBases } from '../api/wanchuan'
const TYPE_MAP = {
1: 0,
@@ -139,6 +140,8 @@ export default function TopicsPage({ sessions = [], onToast }) {
const [groupPromptDraft, setGroupPromptDraft] = useState('')
const [savingGroupPrompt, setSavingGroupPrompt] = useState(false)
const [uploadingToKb, setUploadingToKb] = useState(false)
const loadGroups = useCallback(async () => {
try {
const data = await getGroups()
@@ -550,6 +553,7 @@ export default function TopicsPage({ sessions = [], onToast }) {
const handleSummarize = async () => {
if (!selectedTopic) return
if (uploadingToKb) return
setSummarizing(true)
setSummarizeProgress(null)
try {
@@ -566,7 +570,9 @@ export default function TopicsPage({ sessions = [], onToast }) {
}
let tries = 0
let handled = false
const poll = setInterval(async () => {
if (handled) return
tries += 1
try {
const task = await getTask(taskId)
@@ -578,14 +584,26 @@ export default function TopicsPage({ sessions = [], onToast }) {
// Ignore malformed progress payloads.
}
if (taskData.status === 'done') {
handled = true
clearInterval(poll)
setSummarizeProgress(null)
setSummarizing(false)
handleSelectTopic(selectedTopic)
onToast?.('AI 报告生成完成')
// 报告生成成功后,如果已绑定知识库则自动上传
try {
const freshDetail = await getTopic(selectedTopic.id)
const reportContent = freshDetail?.knowledge_doc?.content
if (reportContent) {
await uploadToKnowledgeBaseIfBound(selectedTopic.title, reportContent)
}
} catch {
// 获取报告详情失败不影响主流程
}
setSummarizeProgress(null)
setSummarizing(false)
return
}
if (taskData.status === 'error') {
handled = true
clearInterval(poll)
setSummarizeProgress(null)
setSummarizing(false)
@@ -594,6 +612,7 @@ export default function TopicsPage({ sessions = [], onToast }) {
return
}
if (tries > 60) {
handled = true
clearInterval(poll)
setSummarizeProgress(null)
setSummarizing(false)
@@ -602,6 +621,7 @@ export default function TopicsPage({ sessions = [], onToast }) {
}
} catch (e) {
if (tries > 60) {
handled = true
clearInterval(poll)
setSummarizeProgress(null)
setSummarizing(false)
@@ -631,6 +651,37 @@ export default function TopicsPage({ sessions = [], onToast }) {
}
}
// 上传报告到已绑定的万川知识库
const uploadToKnowledgeBaseIfBound = async (reportTitle, content) => {
if (uploadingToKb) return
setUploadingToKb(true)
try {
const raw = localStorage.getItem('chatlab_wanchuan_config')
if (!raw) return
const bound = JSON.parse(raw)
const kbInfo = bound?.selectedKbInfo
// 上传接口必须使用 datasetId。selectedKbId 现已统一存为 datasetId
// selectedKbInfo.kbDatasetId 为冗余备份,两者任一可用。
const datasetId = kbInfo?.kbDatasetId || bound?.selectedKbId
if (!bound?.platformUrl || !datasetId) return
const safeTitle = (reportTitle || 'report').replace(/[^a-zA-Z0-9\u4e00-\u9fa5_-]/g, '_').slice(0, 50)
// 上传接口必须使用 datasetId而不是知识库对象的 id
console.log('[上传KB] datasetId:', datasetId, 'kbName:', kbInfo?.kbName || 'N/A')
const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' })
const file = new File([blob], `${safeTitle}.md`, { type: 'text/markdown' })
// 上传接口使用 datasetId
await uploadToKnowledgeBase(bound.platformUrl, datasetId, file)
onToast?.('报告已同步上传到知识库')
} catch (e) {
console.error('[上传知识库失败]', e?.message || e)
onToast?.(`报告生成成功,但上传到知识库失败: ${e.message}`, 'error')
} finally {
setUploadingToKb(false)
}
}
const selectedInitState = selectedGroup ? initStateByGroup[selectedGroup.id] : null
const analyzing = selectedInitState?.status === 'running'