From b137fd791523e252be621d41a7bda929d78ec027 Mon Sep 17 00:00:00 2001 From: yuanzhipeng <2501363769@qq.com> Date: Thu, 11 Jun 2026 16:14:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E4=B8=87=E5=B7=9DAI?= =?UTF-8?q?=E5=B9=B3=E5=8F=B0=E5=AF=B9=E6=8E=A5=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增万川AI平台API接口层,支持登录、获取知识库列表、上传文件等操作 - 实现万川平台配置界面,支持平台地址、账号密码配置和知识库绑定 - 在知识库页面添加上传功能,支持单个或批量上传售后报告到万川知识库 - 提供检查配置的工具脚本便于调试 --- chatlab-web/frontend/check-wanchuan-config.js | 16 + chatlab-web/frontend/src/api/wanchuan.js | 107 +++++ .../frontend/src/pages/KnowledgePage.jsx | 176 ++++++- .../frontend/src/pages/SettingsPage.jsx | 454 +++++++++++++++++- chatlab-web/frontend/src/pages/TopicsPage.jsx | 57 ++- 5 files changed, 796 insertions(+), 14 deletions(-) create mode 100644 chatlab-web/frontend/check-wanchuan-config.js create mode 100644 chatlab-web/frontend/src/api/wanchuan.js diff --git a/chatlab-web/frontend/check-wanchuan-config.js b/chatlab-web/frontend/check-wanchuan-config.js new file mode 100644 index 0000000..939a49c --- /dev/null +++ b/chatlab-web/frontend/check-wanchuan-config.js @@ -0,0 +1,16 @@ +// 在浏览器控制台(F12)粘贴运行以下代码: +console.log('=== 检查万川知识库配置 ==='); +const raw = localStorage.getItem('chatlab_wanchuan_config'); +console.log('原始配置:', raw); +if (raw) { + try { + const config = JSON.parse(raw); + console.log('解析后配置:', config); + console.log('platformUrl:', config.platformUrl); + console.log('selectedKbId:', config.selectedKbId); + } catch (e) { + console.error('配置解析失败:', e); + } +} else { + console.warn('未找到 chatlab_wanchuan_config,需要先在设置页面绑定知识库'); +} diff --git a/chatlab-web/frontend/src/api/wanchuan.js b/chatlab-web/frontend/src/api/wanchuan.js new file mode 100644 index 0000000..5af72ab --- /dev/null +++ b/chatlab-web/frontend/src/api/wanchuan.js @@ -0,0 +1,107 @@ +/** + * 万川 AI 平台 API 接口层 + * + * 接口说明: + * POST /api/login 登录获取 token + * GET /api/system/kb/relation/query 获取岗位知识库列表 + * POST /api/system/kb/file/upload/async/{kbId} 上传文件到知识库(异步) + * + * 所有接口使用 fetch 直接调用第三方平台(不走 vite proxy), + * 平台地址由用户配置(localStorage),token 登录后自动缓存。 + */ + +const TOKEN_KEY = 'chatlab_wanchuan_token' + +function getToken() { + try { return localStorage.getItem(TOKEN_KEY) || '' } catch { return '' } +} + +function setToken(token) { + try { localStorage.setItem(TOKEN_KEY, token) } catch {} +} + +function authHeaders(baseUrl) { + const token = getToken() + return { + 'Content-Type': 'application/json;charset=UTF-8', + 'Authorization': `Bearer ${token}`, + 'token': `Bearer ${token}`, + } +} + +/** + * 登录万川 AI 平台 + * @param {string} baseUrl - 平台地址(如 http://192.168.2.236:8082) + * @param {string} username + * @param {string} password + * @returns {Promise<{token: string, data: object}>} + */ +export async function wanchuanLogin(baseUrl, username, password) { + const resp = await fetch(`${baseUrl}/api/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json;charset=UTF-8' }, + body: JSON.stringify({ username, password, loginType: 'user' }), + }) + if (!resp.ok) throw new Error(`HTTP ${resp.status}`) + const result = await resp.json() + + // 尝试从不同位置提取 token + const token = result.token || result.data?.token || result.data?.access_token || result.access_token || '' + if (token) setToken(token) + return { token, data: result.data || result } +} + +/** + * 获取岗位知识库列表 + * @param {string} baseUrl + * @param {object} params - { relationType, page, pageSize } + * @returns {Promise<{list: Array, total: number}>} + */ +export async function getWanchuanKnowledgeBases(baseUrl, params = {}) { + const { relationType = 'position', page = 1, pageSize = 100, ...rest } = params + const qs = new URLSearchParams({ relationType, page: String(page), pageSize: String(pageSize), ...rest }) + const resp = await fetch(`${baseUrl}/api/system/kb/relation/query?${qs}`, { + method: 'GET', + headers: authHeaders(baseUrl), + }) + if (!resp.ok) throw new Error(`HTTP ${resp.status}`) + const result = await resp.json() + + // 兼容不同返回格式 + const list = result.rows || result.data?.rows || result.data?.list || result.data || result.list || result || [] + const total = result.total ?? result.data?.total ?? list.length + return { list: Array.isArray(list) ? list : [], total } +} + +/** + * 上传文件到知识库(异步) + * @param {string} baseUrl + * @param {string} datasetId - 知识库 datasetId + * @param {File} file - 要上传的文件 + * @param {number} secretLevel - 密级(默认 1) + * @returns {Promise<{taskId: string}>} + */ +export async function uploadToKnowledgeBase(baseUrl, datasetId, file, secretLevel = 1) { + const token = getToken() + const formData = new FormData() + formData.append('file', file) + formData.append('secretLevel', String(secretLevel)) + + const resp = await fetch(`${baseUrl}/api/system/kb/file/upload/async/${encodeURIComponent(datasetId)}`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'token': `Bearer ${token}`, + }, + body: formData, + }) + if (!resp.ok) throw new Error(`HTTP ${resp.status}`) + return resp.json() +} + +/** + * 清除缓存的 token + */ +export function clearWanchuanToken() { + try { localStorage.removeItem(TOKEN_KEY) } catch {} +} diff --git a/chatlab-web/frontend/src/pages/KnowledgePage.jsx b/chatlab-web/frontend/src/pages/KnowledgePage.jsx index 44de653..7139797 100644 --- a/chatlab-web/frontend/src/pages/KnowledgePage.jsx +++ b/chatlab-web/frontend/src/pages/KnowledgePage.jsx @@ -1,7 +1,9 @@ import { useState, useEffect, useCallback } from 'react' -import { Search, RefreshCw, Edit3, Check, X, Download } from 'lucide-react' +import { Search, RefreshCw, Edit3, Check, X, Download, UploadCloud } from 'lucide-react' import dayjs from 'dayjs' import { getKnowledge, getKnowledgeDoc, patchKnowledge } from '../api' +import { uploadToKnowledgeBase } from '../api/wanchuan' +import { getBoundKnowledgeBase } from './SettingsPage' import ReportDocumentView from '../components/ReportDocumentView' import { exportWordDoc } from '../utils/wordExport' @@ -14,6 +16,12 @@ export default function KnowledgePage({ onToast }) { const [editing, setEditing] = useState(false) const [editContent, setEditContent] = useState('') const [saving, setSaving] = useState(false) + const [uploadingKb, setUploadingKb] = useState(false) + // 批量选择上传 + const [selectMode, setSelectMode] = useState(false) + const [checkedIds, setCheckedIds] = useState(() => new Set()) + const [batchUploading, setBatchUploading] = useState(false) + const [batchProgress, setBatchProgress] = useState(null) const loadDocs = useCallback(async (kw) => { setLoading(true) @@ -72,13 +80,122 @@ export default function KnowledgePage({ onToast }) { } } + // 上传当前报告到已绑定的万川知识库 + const handleUploadToKb = async () => { + if (uploadingKb) return + if (!selectedDoc || !docDetail?.content) { + onToast?.('暂无可上传的售后报告', 'error') + return + } + const bound = getBoundKnowledgeBase() + // 上传接口必须使用 datasetId + const datasetId = bound?.kbDatasetId || bound?.id + if (!bound?.platformUrl || !datasetId) { + onToast?.('未绑定知识库,请先到「设置」选择万川知识库', 'error') + return + } + setUploadingKb(true) + try { + const safeTitle = (selectedDoc.title || `售后报告_${selectedDoc.id}`) + .replace(/[^a-zA-Z0-9一-龥_-]/g, '_') + .slice(0, 50) + const blob = new Blob([docDetail.content], { type: 'text/markdown;charset=utf-8' }) + const file = new File([blob], `${safeTitle}.md`, { type: 'text/markdown' }) + await uploadToKnowledgeBase(bound.platformUrl, datasetId, file) + onToast?.(`已上传到知识库:${bound.kbName || datasetId}`) + } catch (e) { + onToast?.(`上传知识库失败:${e?.message || e}`, 'error') + } finally { + setUploadingKb(false) + } + } + + // 切换批量选择模式 + const toggleSelectMode = () => { + setSelectMode((m) => !m) + setCheckedIds(new Set()) + } + + const toggleChecked = (id) => { + setCheckedIds((prev) => { + const next = new Set(prev) + if (next.has(id)) next.delete(id) + else next.add(id) + return next + }) + } + + const toggleCheckAll = () => { + setCheckedIds((prev) => ( + prev.size === docs.length ? new Set() : new Set(docs.map((d) => d.id)) + )) + } + + // 批量上传选中的报告到已绑定知识库 + const handleBatchUpload = async () => { + if (batchUploading) return + if (checkedIds.size === 0) { + onToast?.('请先勾选要上传的报告', 'error') + return + } + const bound = getBoundKnowledgeBase() + const datasetId = bound?.kbDatasetId || bound?.id + if (!bound?.platformUrl || !datasetId) { + onToast?.('未绑定知识库,请先到「设置」选择万川知识库', 'error') + return + } + const targets = docs.filter((d) => checkedIds.has(d.id)) + setBatchUploading(true) + setBatchProgress({ done: 0, total: targets.length }) + let ok = 0 + const failed = [] + for (const doc of targets) { + try { + // 列表项不带正文,需要逐个拉取内容 + const detail = await getKnowledgeDoc(doc.id) + const content = detail?.content + if (!content) { failed.push(doc.title || `#${doc.id}`); continue } + const safeTitle = (doc.title || `售后报告_${doc.id}`) + .replace(/[^a-zA-Z0-9一-龥_-]/g, '_') + .slice(0, 50) + const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' }) + const file = new File([blob], `${safeTitle}.md`, { type: 'text/markdown' }) + await uploadToKnowledgeBase(bound.platformUrl, datasetId, file) + ok += 1 + } catch (e) { + failed.push(doc.title || `#${doc.id}`) + } finally { + setBatchProgress((p) => ({ done: (p?.done || 0) + 1, total: targets.length })) + } + } + setBatchUploading(false) + setBatchProgress(null) + if (failed.length === 0) { + onToast?.(`已上传 ${ok} 篇报告到知识库:${bound.kbName || datasetId}`) + setSelectMode(false) + setCheckedIds(new Set()) + } else { + onToast?.(`上传完成:成功 ${ok} 篇,失败 ${failed.length} 篇(${failed.slice(0, 3).join('、')}${failed.length > 3 ? '…' : ''})`, 'error') + } + } + return (