feat(api): 将万川平台配置迁移至后端存储
- 移除前端localStorage依赖,改用后端SQLite作为唯一数据源 - 新增getWanchuanConfig和saveWanchuanConfig函数用于配置读写 - 添加getBoundKnowledgeBase函数统一获取绑定知识库信息 - 支持桌面应用端口变化时正确读取配置 refactor(settings): 重构万川平台配置管理逻辑 - 移除localStorage配置存储,改为后端API调用 - 实现配置自动恢复和防抖保存机制 - 添加token过期自动重登功能 - 优化知识库选择和连接状态管理 fix(knowledge): 修复知识库上传异步问题 - 将getBoundKnowledgeBase调用改为await异步处理 - 统一各页面的知识库信息获取方式 - 修正上传接口datasetId使用逻辑 feat(electron): 添加chatlog.exe存在性检查 - 新增ensureChatlogExe函数验证执行文件存在 - 防止杀毒软件误删导致的ENONENT错误 - 提供用户友好的错误提示和解决方案
This commit is contained in:
@@ -105,3 +105,50 @@ export async function uploadToKnowledgeBase(baseUrl, datasetId, file, secretLeve
|
|||||||
export function clearWanchuanToken() {
|
export function clearWanchuanToken() {
|
||||||
try { localStorage.removeItem(TOKEN_KEY) } catch {}
|
try { localStorage.removeItem(TOKEN_KEY) } catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// 万川平台配置:后端为唯一数据源(/api/settings/wanchuan,同源相对路径)
|
||||||
|
//
|
||||||
|
// 配置(平台地址、账号、密码、已选知识库)存在后端 SQLite,与前端 origin 无关,
|
||||||
|
// 桌面应用(exe)端口/origin 变化也能正常读取。用时直接请求后端,不再依赖 localStorage。
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** 读取万川配置;无数据或失败返回 {} */
|
||||||
|
export async function getWanchuanConfig() {
|
||||||
|
try {
|
||||||
|
const resp = await fetch('/api/settings/wanchuan')
|
||||||
|
if (!resp.ok) return {}
|
||||||
|
const data = await resp.json()
|
||||||
|
return (data && typeof data === 'object') ? data : {}
|
||||||
|
} catch {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 保存万川配置到后端 */
|
||||||
|
export async function saveWanchuanConfig(config) {
|
||||||
|
try {
|
||||||
|
await fetch('/api/settings/wanchuan', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(config || {}),
|
||||||
|
})
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前绑定的知识库信息(供报告上传等处调用)。
|
||||||
|
* 直接从后端读取配置,返回归一化结构;未绑定返回 null。
|
||||||
|
*/
|
||||||
|
export async function getBoundKnowledgeBase() {
|
||||||
|
const cfg = await getWanchuanConfig()
|
||||||
|
if (!cfg.selectedKbId) return null
|
||||||
|
const info = cfg.selectedKbInfo || {}
|
||||||
|
return {
|
||||||
|
id: info.kbDatasetId || cfg.selectedKbId,
|
||||||
|
platformUrl: cfg.platformUrl,
|
||||||
|
kbName: info.kbName || cfg.selectedKbId,
|
||||||
|
kbDatasetId: info.kbDatasetId || cfg.selectedKbId,
|
||||||
|
kbType: info.kbType,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import { useState, useEffect, useCallback } from 'react'
|
|||||||
import { Search, RefreshCw, Edit3, Check, X, Download, UploadCloud } from 'lucide-react'
|
import { Search, RefreshCw, Edit3, Check, X, Download, UploadCloud } from 'lucide-react'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { getKnowledge, getKnowledgeDoc, patchKnowledge } from '../api'
|
import { getKnowledge, getKnowledgeDoc, patchKnowledge } from '../api'
|
||||||
import { uploadToKnowledgeBase } from '../api/wanchuan'
|
import { uploadToKnowledgeBase, getBoundKnowledgeBase } from '../api/wanchuan'
|
||||||
import { getBoundKnowledgeBase } from './SettingsPage'
|
|
||||||
import ReportDocumentView from '../components/ReportDocumentView'
|
import ReportDocumentView from '../components/ReportDocumentView'
|
||||||
import { exportWordDoc } from '../utils/wordExport'
|
import { exportWordDoc } from '../utils/wordExport'
|
||||||
|
|
||||||
@@ -87,7 +86,7 @@ export default function KnowledgePage({ onToast }) {
|
|||||||
onToast?.('暂无可上传的售后报告', 'error')
|
onToast?.('暂无可上传的售后报告', 'error')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const bound = getBoundKnowledgeBase()
|
const bound = await getBoundKnowledgeBase()
|
||||||
// 上传接口必须使用 datasetId
|
// 上传接口必须使用 datasetId
|
||||||
const datasetId = bound?.kbDatasetId || bound?.id
|
const datasetId = bound?.kbDatasetId || bound?.id
|
||||||
if (!bound?.platformUrl || !datasetId) {
|
if (!bound?.platformUrl || !datasetId) {
|
||||||
@@ -138,7 +137,7 @@ export default function KnowledgePage({ onToast }) {
|
|||||||
onToast?.('请先勾选要上传的报告', 'error')
|
onToast?.('请先勾选要上传的报告', 'error')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const bound = getBoundKnowledgeBase()
|
const bound = await getBoundKnowledgeBase()
|
||||||
const datasetId = bound?.kbDatasetId || bound?.id
|
const datasetId = bound?.kbDatasetId || bound?.id
|
||||||
if (!bound?.platformUrl || !datasetId) {
|
if (!bound?.platformUrl || !datasetId) {
|
||||||
onToast?.('未绑定知识库,请先到「设置」选择万川知识库', 'error')
|
onToast?.('未绑定知识库,请先到「设置」选择万川知识库', 'error')
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect, useRef } from 'react'
|
||||||
import { Copy, Check, Save, Link2, Loader, AlertCircle, ChevronDown, ChevronRight, RefreshCw, BookOpen, ExternalLink, FileText } from 'lucide-react'
|
import { Copy, Check, Save, Link2, Loader, AlertCircle, ChevronDown, ChevronRight, RefreshCw, BookOpen, ExternalLink, FileText } from 'lucide-react'
|
||||||
import { wanchuanLogin, getWanchuanKnowledgeBases, uploadToKnowledgeBase, clearWanchuanToken } from '../api/wanchuan'
|
import { wanchuanLogin, getWanchuanKnowledgeBases, uploadToKnowledgeBase, clearWanchuanToken, getWanchuanConfig, saveWanchuanConfig } from '../api/wanchuan'
|
||||||
|
|
||||||
const CONFIG_ITEMS = [
|
const CONFIG_ITEMS = [
|
||||||
{
|
{
|
||||||
@@ -218,7 +218,6 @@ function AISettingsForm() {
|
|||||||
* 支持输入平台地址 + 账号密码,测试连接后可展示平台模块内容(预留)
|
* 支持输入平台地址 + 账号密码,测试连接后可展示平台模块内容(预留)
|
||||||
*/
|
*/
|
||||||
function WanchuanPlatformForm() {
|
function WanchuanPlatformForm() {
|
||||||
const STORAGE_KEY = 'chatlab_wanchuan_config'
|
|
||||||
const [form, setForm] = useState({ platformUrl: '', username: '', password: '' })
|
const [form, setForm] = useState({ platformUrl: '', username: '', password: '' })
|
||||||
const [connecting, setConnecting] = useState(false)
|
const [connecting, setConnecting] = useState(false)
|
||||||
const [connected, setConnected] = useState(false)
|
const [connected, setConnected] = useState(false)
|
||||||
@@ -234,59 +233,106 @@ function WanchuanPlatformForm() {
|
|||||||
const [kbError, setKbError] = useState('')
|
const [kbError, setKbError] = useState('')
|
||||||
const [kbExpanded, setKbExpanded] = useState(false)
|
const [kbExpanded, setKbExpanded] = useState(false)
|
||||||
|
|
||||||
// 从 localStorage 加载已保存配置
|
// 后端为唯一数据源:打开设置页时拉取已保存配置并填入表单,
|
||||||
|
// 然后按已保存凭证自动恢复会话(拉取知识库列表)。
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
let cancelled = false
|
||||||
const raw = localStorage.getItem(STORAGE_KEY)
|
getWanchuanConfig().then(saved => {
|
||||||
if (raw) setForm(JSON.parse(raw))
|
if (cancelled || !saved || Object.keys(saved).length === 0) return
|
||||||
} catch {}
|
const loadedForm = {
|
||||||
}, [])
|
platformUrl: saved.platformUrl || '',
|
||||||
|
username: saved.username || '',
|
||||||
// 从 localStorage 加载已选知识库;若有缓存 token + 平台地址则自动拉取列表回显
|
password: saved.password || '',
|
||||||
useEffect(() => {
|
}
|
||||||
try {
|
setForm(loadedForm)
|
||||||
const raw = localStorage.getItem(STORAGE_KEY)
|
// 同步更新 ref,确保紧接着的 fetchKnowledgeBases → handleSave 用到的是
|
||||||
if (!raw) return
|
// 刚加载的凭证,而不是首次渲染闭包里的空表单(否则会把后端配置覆盖成空)。
|
||||||
const saved = JSON.parse(raw)
|
formRef.current = loadedForm
|
||||||
if (saved.selectedKbId) setSelectedKbId(saved.selectedKbId)
|
if (saved.selectedKbId) setSelectedKbId(saved.selectedKbId)
|
||||||
if (saved.selectedKbInfo) setSavedKbInfo(saved.selectedKbInfo)
|
if (saved.selectedKbInfo) setSavedKbInfo(saved.selectedKbInfo)
|
||||||
// 已登录过(有缓存 token)则自动拉取知识库列表,无需手动点连接
|
if (!saved.platformUrl) return
|
||||||
const token = localStorage.getItem('chatlab_wanchuan_token')
|
const token = localStorage.getItem('chatlab_wanchuan_token')
|
||||||
if (token && saved.platformUrl) {
|
if (token) {
|
||||||
fetchKnowledgeBases(saved.platformUrl)
|
fetchKnowledgeBases(saved.platformUrl, { allowRelogin: true, savedKbId: saved.selectedKbId, credsForRelogin: saved })
|
||||||
|
} else if (saved.username && saved.password) {
|
||||||
|
autoLogin(saved).then(ok => {
|
||||||
|
if (ok) fetchKnowledgeBases(saved.platformUrl, { savedKbId: saved.selectedKbId })
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} catch {}
|
})
|
||||||
|
return () => { cancelled = true }
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// 防抖保存:把最新待存配置放进 ref,停止输入 500ms 后写一次后端。
|
||||||
|
// 既避免每次按键都发 PUT(并发乱序可能让后端存到中间态),
|
||||||
|
// 也保证切到别的页(组件卸载)前把挂起的修改 flush 掉,不丢数据。
|
||||||
|
const pendingConfigRef = useRef(null)
|
||||||
|
const saveTimerRef = useRef(null)
|
||||||
|
|
||||||
|
// 始终持有最新表单值。persistConfig 不再依赖渲染闭包里的 form:
|
||||||
|
// 挂载时自动恢复会跑首次渲染闭包里的 fetchKnowledgeBases → handleSave,
|
||||||
|
// 那时闭包 form 还是空的,若直接 ...form 会把后端凭证覆盖成空。读 ref 可避免。
|
||||||
|
const formRef = useRef(form)
|
||||||
|
formRef.current = form
|
||||||
|
|
||||||
|
const flushSave = () => {
|
||||||
|
if (saveTimerRef.current) {
|
||||||
|
clearTimeout(saveTimerRef.current)
|
||||||
|
saveTimerRef.current = null
|
||||||
|
}
|
||||||
|
if (pendingConfigRef.current) {
|
||||||
|
saveWanchuanConfig(pendingConfigRef.current)
|
||||||
|
pendingConfigRef.current = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 卸载(切换标签页)时把挂起的保存立即写出
|
||||||
|
return () => flushSave()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// 把当前完整配置写入后端(平台地址/账号/密码 + 已选知识库)。
|
||||||
|
// kbIdOverride / listOverride 用于在 React 状态尚未刷新时(setState 异步)传入即时值。
|
||||||
|
// immediate=true 时跳过防抖立即保存(如选择知识库、测试连接成功后)。
|
||||||
|
const persistConfig = (nextForm, kbIdOverride, listOverride, immediate = false) => {
|
||||||
|
const effectiveKbId = kbIdOverride !== undefined ? kbIdOverride : selectedKbId
|
||||||
|
const kbs = listOverride || knowledgeBases
|
||||||
|
const selectedKb = kbs.find(k => kbKey(k) === effectiveKbId) || null
|
||||||
|
const data = {
|
||||||
|
...nextForm,
|
||||||
|
selectedKbId: effectiveKbId || '',
|
||||||
|
selectedKbInfo: selectedKb ? {
|
||||||
|
selectedKbId: kbKey(selectedKb),
|
||||||
|
kbName: selectedKb.relationName || selectedKb.id,
|
||||||
|
kbDatasetId: selectedKb.datasetId || selectedKb.id,
|
||||||
|
kbType: selectedKb.kbType,
|
||||||
|
} : (savedKbInfo || null),
|
||||||
|
}
|
||||||
|
pendingConfigRef.current = data
|
||||||
|
if (immediate) {
|
||||||
|
flushSave()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (saveTimerRef.current) clearTimeout(saveTimerRef.current)
|
||||||
|
saveTimerRef.current = setTimeout(flushSave, 500)
|
||||||
|
}
|
||||||
|
|
||||||
const handleChange = (key, value) => {
|
const handleChange = (key, value) => {
|
||||||
setForm(prev => ({ ...prev, [key]: value }))
|
const next = { ...form, [key]: value }
|
||||||
|
setForm(next)
|
||||||
if (connected) setConnected(false)
|
if (connected) setConnected(false)
|
||||||
|
// 改字段防抖存回后端,避免未点「测试连接」就离开导致凭证丢失。
|
||||||
|
persistConfig(next)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 知识库的唯一标识:上传接口需要的是 datasetId,没有则回退到 id。
|
// 知识库的唯一标识:上传接口需要的是 datasetId,没有则回退到 id。
|
||||||
// 全程以此作为 selectedKbId,保证选择、回显、持久化、上传四处一致。
|
// 全程以此作为 selectedKbId,保证选择、回显、持久化、上传四处一致。
|
||||||
const kbKey = (kb) => String(kb?.datasetId ?? kb?.id ?? '')
|
const kbKey = (kb) => String(kb?.datasetId ?? kb?.id ?? '')
|
||||||
|
|
||||||
// 持久化整个知识库配置。
|
// 知识库选择 / 测试连接后的保存走立即写,确保关键操作不被防抖延迟。
|
||||||
// kbIdOverride / listOverride 用于在 React 状态尚未刷新时(setState 异步)
|
// 用 formRef.current 而非闭包 form:挂载恢复时跑的是首次渲染闭包,form 还是空的。
|
||||||
// 传入即时值,避免读到旧的 selectedKbId 或旧的知识库列表。
|
|
||||||
const handleSave = (kbIdOverride, listOverride) => {
|
const handleSave = (kbIdOverride, listOverride) => {
|
||||||
try {
|
persistConfig(formRef.current, kbIdOverride, listOverride, true)
|
||||||
const effectiveKbId = kbIdOverride !== undefined ? kbIdOverride : selectedKbId
|
|
||||||
const kbs = listOverride || knowledgeBases
|
|
||||||
const selectedKb = kbs.find(k => kbKey(k) === effectiveKbId) || null
|
|
||||||
const data = {
|
|
||||||
...form,
|
|
||||||
selectedKbId: effectiveKbId,
|
|
||||||
selectedKbInfo: selectedKb ? {
|
|
||||||
selectedKbId: kbKey(selectedKb),
|
|
||||||
kbName: selectedKb.relationName || selectedKb.id,
|
|
||||||
kbDatasetId: selectedKb.datasetId || selectedKb.id,
|
|
||||||
kbType: selectedKb.kbType,
|
|
||||||
} : null,
|
|
||||||
}
|
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(data))
|
|
||||||
} catch {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSelectKb = (key) => {
|
const handleSelectKb = (key) => {
|
||||||
@@ -318,11 +364,6 @@ function WanchuanPlatformForm() {
|
|||||||
return uploadToKnowledgeBase(form.platformUrl, targetDatasetId, file)
|
return uploadToKnowledgeBase(form.platformUrl, targetDatasetId, file)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取当前绑定的知识库信息(供外部调用)
|
|
||||||
const getBoundKnowledgeBase = () => {
|
|
||||||
return selectedKb || null
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleTestConnection = async () => {
|
const handleTestConnection = async () => {
|
||||||
if (!form.platformUrl) {
|
if (!form.platformUrl) {
|
||||||
setConnectError('请输入平台地址')
|
setConnectError('请输入平台地址')
|
||||||
@@ -357,20 +398,53 @@ function WanchuanPlatformForm() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchKnowledgeBases = async (baseUrl) => {
|
// 用已保存的账号密码静默重新登录,刷新缓存 token。
|
||||||
|
// saved 缺省用当前表单(重登时表单已填好);首次加载时由调用方传入后端配置。
|
||||||
|
// 成功返回 true;失败返回 false(不抛出,由调用方决定后续)。
|
||||||
|
const autoLogin = async (saved = form) => {
|
||||||
|
try {
|
||||||
|
if (!saved.platformUrl || !saved.username || !saved.password) return false
|
||||||
|
const { token, data } = await wanchuanLogin(saved.platformUrl, saved.username, saved.password)
|
||||||
|
if (!token) return false
|
||||||
|
setModules(data.modules || [])
|
||||||
|
setConnected(true)
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchKnowledgeBases = async (baseUrl, { allowRelogin = false, savedKbId, credsForRelogin } = {}) => {
|
||||||
|
// 首次加载时 selectedKbId 状态可能还没刷新到,用传入的 savedKbId 兜底
|
||||||
|
const targetKbId = savedKbId !== undefined ? savedKbId : selectedKbId
|
||||||
setKbLoading(true)
|
setKbLoading(true)
|
||||||
setKbError('')
|
setKbError('')
|
||||||
try {
|
try {
|
||||||
const { list } = await getWanchuanKnowledgeBases(baseUrl, { relationType: 'position', page: 1, pageSize: 100 })
|
let list
|
||||||
|
try {
|
||||||
|
({ list } = await getWanchuanKnowledgeBases(baseUrl, { relationType: 'position', page: 1, pageSize: 100 }))
|
||||||
|
} catch (e) {
|
||||||
|
// token 可能已过期:用已保存凭证自动重登一次后重试。
|
||||||
|
// credsForRelogin 用于首次加载场景——此时 form 状态尚未刷新,
|
||||||
|
// 必须显式传入后端读到的凭证,否则 autoLogin 拿到的是空表单。
|
||||||
|
const isAuthErr = /\b401\b|\b403\b/.test(e.message || '')
|
||||||
|
if (allowRelogin && isAuthErr && await autoLogin(credsForRelogin)) {
|
||||||
|
({ list } = await getWanchuanKnowledgeBases(baseUrl, { relationType: 'position', page: 1, pageSize: 100 }))
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
setKnowledgeBases(list)
|
setKnowledgeBases(list)
|
||||||
|
setConnected(true)
|
||||||
// 登录后恢复之前保存的已选知识库(按统一的 kbKey 匹配)
|
// 登录后恢复之前保存的已选知识库(按统一的 kbKey 匹配)
|
||||||
const keys = new Set(list.map(kb => kbKey(kb)))
|
const keys = new Set(list.map(kb => kbKey(kb)))
|
||||||
if (selectedKbId && keys.has(selectedKbId)) {
|
if (targetKbId && keys.has(targetKbId)) {
|
||||||
// 已选项仍在列表中:用完整信息重新持久化(登录时曾因列表为空被写成 null)
|
// 已选项仍在列表中:用完整信息重新持久化
|
||||||
handleSave(selectedKbId, list)
|
setSelectedKbId(targetKbId)
|
||||||
} else if (selectedKbId) {
|
handleSave(targetKbId, list)
|
||||||
|
} else if (targetKbId) {
|
||||||
// 兼容旧数据:之前可能保存的是 kb.id,尝试映射到 datasetId
|
// 兼容旧数据:之前可能保存的是 kb.id,尝试映射到 datasetId
|
||||||
const matchById = list.find(kb => String(kb.id) === selectedKbId)
|
const matchById = list.find(kb => String(kb.id) === targetKbId)
|
||||||
if (matchById) {
|
if (matchById) {
|
||||||
const newKey = kbKey(matchById)
|
const newKey = kbKey(matchById)
|
||||||
setSelectedKbId(newKey)
|
setSelectedKbId(newKey)
|
||||||
@@ -389,7 +463,8 @@ function WanchuanPlatformForm() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleRefreshKB = () => {
|
const handleRefreshKB = () => {
|
||||||
if (form.platformUrl) fetchKnowledgeBases(form.platformUrl)
|
// 手动刷新也允许 token 过期时自动重登,避免必须重新点「测试连接」
|
||||||
|
if (form.platformUrl) fetchKnowledgeBases(form.platformUrl, { allowRelogin: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
const kbTypeLabel = (type) => ({
|
const kbTypeLabel = (type) => ({
|
||||||
@@ -638,27 +713,6 @@ function WanchuanPlatformForm() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导出供外部调用获取已绑定知识库信息
|
|
||||||
export function getBoundKnowledgeBase() {
|
|
||||||
try {
|
|
||||||
const STORAGE_KEY = 'chatlab_wanchuan_config'
|
|
||||||
const raw = localStorage.getItem(STORAGE_KEY)
|
|
||||||
if (!raw) return null
|
|
||||||
const saved = JSON.parse(raw)
|
|
||||||
// 返回完整的知识库信息,包括保存的详细信息
|
|
||||||
if (!saved.selectedKbId) return null
|
|
||||||
return {
|
|
||||||
id: saved.selectedKbInfo?.kbDatasetId || saved.selectedKbId,
|
|
||||||
platformUrl: saved.platformUrl,
|
|
||||||
kbName: saved.selectedKbInfo?.kbName || saved.selectedKbId,
|
|
||||||
kbDatasetId: saved.selectedKbInfo?.kbDatasetId || saved.selectedKbId,
|
|
||||||
kbType: saved.selectedKbInfo?.kbType,
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
return (
|
return (
|
||||||
<div style={{ flex: 1, overflowY: 'auto', padding: '28px 36px' }}>
|
<div style={{ flex: 1, overflowY: 'auto', padding: '28px 36px' }}>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
import MessageBubble from '../components/MessageBubble'
|
import MessageBubble from '../components/MessageBubble'
|
||||||
import ReportDocumentView from '../components/ReportDocumentView'
|
import ReportDocumentView from '../components/ReportDocumentView'
|
||||||
import { exportWordDoc } from '../utils/wordExport'
|
import { exportWordDoc } from '../utils/wordExport'
|
||||||
import { uploadToKnowledgeBase, getWanchuanKnowledgeBases } from '../api/wanchuan'
|
import { uploadToKnowledgeBase, getWanchuanKnowledgeBases, getBoundKnowledgeBase } from '../api/wanchuan'
|
||||||
|
|
||||||
const TYPE_MAP = {
|
const TYPE_MAP = {
|
||||||
1: 0,
|
1: 0,
|
||||||
@@ -648,19 +648,13 @@ export default function TopicsPage({ sessions = [], onToast }) {
|
|||||||
if (uploadingToKb) return
|
if (uploadingToKb) return
|
||||||
setUploadingToKb(true)
|
setUploadingToKb(true)
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem('chatlab_wanchuan_config')
|
const bound = await getBoundKnowledgeBase()
|
||||||
if (!raw) return
|
if (!bound) return
|
||||||
const bound = JSON.parse(raw)
|
// 上传接口必须使用 datasetId
|
||||||
const kbInfo = bound?.selectedKbInfo
|
const datasetId = bound.kbDatasetId || bound.id
|
||||||
// 上传接口必须使用 datasetId。selectedKbId 现已统一存为 datasetId,
|
if (!bound.platformUrl || !datasetId) return
|
||||||
// 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)
|
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' })
|
const file = new File([blob], `${safeTitle}.md`, { type: 'text/markdown' })
|
||||||
|
|
||||||
// 上传接口使用 datasetId
|
// 上传接口使用 datasetId
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import Optional
|
from typing import Optional, Any
|
||||||
|
import json
|
||||||
import aiosqlite
|
import aiosqlite
|
||||||
from database import get_db
|
from database import get_db
|
||||||
|
|
||||||
@@ -11,6 +12,11 @@ EDITABLE_KEYS = [
|
|||||||
"vision_model", "voice_model", "topic_analysis_prompt",
|
"vision_model", "voice_model", "topic_analysis_prompt",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# 万川 AI 平台对接配置整体作为一条 JSON 存储,独立于上面的 AI 模型配置。
|
||||||
|
# 存到后端 SQLite(app_settings 表)后,配置不再依赖前端 localStorage 的 origin,
|
||||||
|
# 桌面应用(exe)即便后端端口/origin 变化也能跨次启动恢复平台地址、账号、密码与已选知识库。
|
||||||
|
WANCHUAN_KEY = "wanchuan_config"
|
||||||
|
|
||||||
|
|
||||||
def _mask_key(value: str) -> str:
|
def _mask_key(value: str) -> str:
|
||||||
if not value or len(value) <= 8:
|
if not value or len(value) <= 8:
|
||||||
@@ -62,3 +68,32 @@ async def update_settings(body: SettingsUpdate, db: aiosqlite.Connection = Depen
|
|||||||
from services.runtime_settings import invalidate_cache
|
from services.runtime_settings import invalidate_cache
|
||||||
invalidate_cache()
|
invalidate_cache()
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
# ── 万川 AI 平台对接配置(整体 JSON 持久化) ──────────────────
|
||||||
|
# 前端把 { platformUrl, username, password, selectedKbId, selectedKbInfo } 整体存这里。
|
||||||
|
# 桌面应用 origin 变化不影响该配置(不依赖 localStorage)。
|
||||||
|
|
||||||
|
@router.get("/wanchuan")
|
||||||
|
async def get_wanchuan_config(db: aiosqlite.Connection = Depends(get_db)):
|
||||||
|
async with db.execute(
|
||||||
|
"SELECT value FROM app_settings WHERE key = ?", (WANCHUAN_KEY,)
|
||||||
|
) as cur:
|
||||||
|
row = await cur.fetchone()
|
||||||
|
if not row or not row["value"]:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return json.loads(row["value"])
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/wanchuan")
|
||||||
|
async def update_wanchuan_config(body: dict[str, Any], db: aiosqlite.Connection = Depends(get_db)):
|
||||||
|
value = json.dumps(body, ensure_ascii=False)
|
||||||
|
await db.execute(
|
||||||
|
"INSERT INTO app_settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = ?",
|
||||||
|
(WANCHUAN_KEY, value, value),
|
||||||
|
)
|
||||||
|
await db.commit()
|
||||||
|
return {"status": "ok"}
|
||||||
|
|||||||
@@ -44,6 +44,22 @@ function chatlogExePath() {
|
|||||||
return resourcePath('chatlog.exe');
|
return resourcePath('chatlog.exe');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 启动 chatlog.exe 前确认文件确实存在。
|
||||||
|
// 该文件常被杀毒软件(尤其 Windows Defender)误判为风险程序而静默隔离/删除,
|
||||||
|
// 导致安装后“有时候”运行就报 spawn ... ENOENT。这里把底层的 ENOENT
|
||||||
|
// 提前转成一句用户能看懂、能自救的中文提示。
|
||||||
|
function ensureChatlogExe() {
|
||||||
|
const exePath = chatlogExePath();
|
||||||
|
if (!fs.existsSync(exePath)) {
|
||||||
|
throw new Error(
|
||||||
|
`未找到 chatlog.exe(应在:${exePath})。` +
|
||||||
|
`该文件可能被杀毒软件误删或隔离。请将安装目录加入杀毒软件白名单,` +
|
||||||
|
`从隔离区恢复 chatlog.exe,或重新安装本应用后再试。`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return exePath;
|
||||||
|
}
|
||||||
|
|
||||||
function backendExePath() {
|
function backendExePath() {
|
||||||
return resourcePath('backend', 'ChatLabBackend.exe');
|
return resourcePath('backend', 'ChatLabBackend.exe');
|
||||||
}
|
}
|
||||||
@@ -688,7 +704,13 @@ function selectMainWeChatProcess(processes) {
|
|||||||
|
|
||||||
function runChatlogKey(args, label) {
|
function runChatlogKey(args, label) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const exePath = chatlogExePath();
|
let exePath;
|
||||||
|
try {
|
||||||
|
exePath = ensureChatlogExe();
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const keyProcess = spawn(exePath, args, {
|
const keyProcess = spawn(exePath, args, {
|
||||||
cwd: projectRoot(),
|
cwd: projectRoot(),
|
||||||
windowsHide: true,
|
windowsHide: true,
|
||||||
|
|||||||
26525
release/manifest.txt
26525
release/manifest.txt
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,8 @@
|
|||||||
[string]$PublisherName,
|
[string]$PublisherName,
|
||||||
[string]$TimestampServer = "http://timestamp.digicert.com",
|
[string]$TimestampServer = "http://timestamp.digicert.com",
|
||||||
[string[]]$VoiceSmokeKeys = @(),
|
[string[]]$VoiceSmokeKeys = @(),
|
||||||
[switch]$ForceSign
|
[switch]$ForceSign,
|
||||||
|
[switch]$CleanBackend
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
@@ -160,7 +161,16 @@ if (-not $SkipFrontend) {
|
|||||||
|
|
||||||
if (-not $SkipBackend) {
|
if (-not $SkipBackend) {
|
||||||
Push-Location $Backend
|
Push-Location $Backend
|
||||||
Invoke-Python312 @("-m", "PyInstaller", "ChatLabBackend.spec", "--noconfirm", "--clean")
|
# 默认复用 PyInstaller 的分析缓存(build\ 目录),代码未大改时增量打包可从 ~9 分钟降到 1-2 分钟。
|
||||||
|
# 仅在升级依赖或缓存异常时用 -CleanBackend 做一次全量重建。
|
||||||
|
$pyArgs = @("-m", "PyInstaller", "ChatLabBackend.spec", "--noconfirm")
|
||||||
|
if ($CleanBackend) {
|
||||||
|
$pyArgs += "--clean"
|
||||||
|
Write-Host "Backend: full clean rebuild (--clean)."
|
||||||
|
} else {
|
||||||
|
Write-Host "Backend: incremental build (reusing cache). Use -CleanBackend for a full rebuild."
|
||||||
|
}
|
||||||
|
Invoke-Python312 $pyArgs
|
||||||
Pop-Location
|
Pop-Location
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user