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:
2026-06-24 10:13:20 +08:00
parent f9f8a7b13d
commit eecbe4172e
8 changed files with 13596 additions and 13278 deletions

View File

@@ -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,
}
}

View File

@@ -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')

View File

@@ -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' }}>

View File

@@ -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

View File

@@ -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 模型配置。
# 存到后端 SQLiteapp_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"}

View File

@@ -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,

File diff suppressed because it is too large Load Diff

View File

@@ -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
} }