mirror of
https://github.com/Tencent/WeKnora.git
synced 2026-06-04 13:30:32 +08:00
feat: implement automatic update checking and downloading functionality in the desktop app, enhancing user experience with seamless updates and corresponding UI settings
This commit is contained in:
@@ -68,3 +68,17 @@ func (a *App) GetAPILanBaseURL() string {
|
||||
func (a *App) GetDesktopListenPublicActive() bool {
|
||||
return a.listenPublic
|
||||
}
|
||||
|
||||
// CheckForUpdates manually triggers the update check from the frontend.
|
||||
func (a *App) CheckForUpdates() {
|
||||
if a.ctx != nil {
|
||||
checkUpdate(a.ctx, desktopAboutVersion(), true, false)
|
||||
}
|
||||
}
|
||||
|
||||
// AutoCheckForUpdates silently checks for updates and downloads them.
|
||||
func (a *App) AutoCheckForUpdates() {
|
||||
if a.ctx != nil {
|
||||
checkUpdate(a.ctx, desktopAboutVersion(), false, true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ import (
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/config"
|
||||
"github.com/Tencent/WeKnora/internal/container"
|
||||
"github.com/Tencent/WeKnora/internal/handler"
|
||||
"github.com/Tencent/WeKnora/internal/logger"
|
||||
"github.com/Tencent/WeKnora/internal/runtime"
|
||||
"github.com/Tencent/WeKnora/internal/tracing"
|
||||
@@ -146,27 +145,6 @@ const wailsThemeSyncJS = `(function(){try{var t=localStorage.getItem('WeKnora_th
|
||||
|
||||
const weknoraGitHubRepoURL = "https://github.com/Tencent/WeKnora"
|
||||
|
||||
// desktopAboutVersion 优先使用构建脚本注入的 handler.Version,否则尝试读取仓库根目录 VERSION(本地 wails dev 等未带 ldflags 时)。
|
||||
func desktopAboutVersion() string {
|
||||
if v := strings.TrimSpace(handler.Version); v != "" && v != "unknown" {
|
||||
return v
|
||||
}
|
||||
for _, p := range []string{
|
||||
"VERSION",
|
||||
filepath.Join("..", "..", "VERSION"),
|
||||
filepath.Join("..", "..", "..", "VERSION"),
|
||||
} {
|
||||
b, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if v := strings.TrimSpace(string(b)); v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func main() {
|
||||
// For macOS .app bundle, the working directory is usually "/" or the MacOS folder.
|
||||
// We need to change the working directory to the Resources folder where our configs are.
|
||||
@@ -300,6 +278,12 @@ func main() {
|
||||
wailsruntime.BrowserOpenURL(app.ctx, weknoraGitHubRepoURL)
|
||||
}
|
||||
})
|
||||
FileMenu.AddText("Check for Updates...", nil, func(_ *menu.CallbackData) {
|
||||
if app.ctx == nil {
|
||||
return
|
||||
}
|
||||
checkUpdate(app.ctx, desktopAboutVersion(), true, false)
|
||||
})
|
||||
FileMenu.AddSeparator()
|
||||
FileMenu.AddText("Quit", keys.CmdOrCtrl("q"), func(_ *menu.CallbackData) {
|
||||
app.shutdown(context.Background())
|
||||
|
||||
413
cmd/desktop/update.go
Normal file
413
cmd/desktop/update.go
Normal file
@@ -0,0 +1,413 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/handler"
|
||||
"github.com/Tencent/WeKnora/internal/logger"
|
||||
wailsruntime "github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
type githubAsset struct {
|
||||
Name string `json:"name"`
|
||||
BrowserDownloadURL string `json:"browser_download_url"`
|
||||
}
|
||||
|
||||
type githubRelease struct {
|
||||
TagName string `json:"tag_name"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
Assets []githubAsset `json:"assets"`
|
||||
}
|
||||
|
||||
func checkUpdate(ctx context.Context, currentVersion string, showUpToDate bool, autoDownload bool) {
|
||||
go func() {
|
||||
if currentVersion == "unknown" || currentVersion == "" {
|
||||
if showUpToDate {
|
||||
wailsruntime.MessageDialog(ctx, wailsruntime.MessageDialogOptions{
|
||||
Type: wailsruntime.InfoDialog,
|
||||
Title: "Check for Updates",
|
||||
Message: "Unable to determine the current version. Cannot check for updates.",
|
||||
Buttons: []string{"OK"},
|
||||
DefaultButton: "OK",
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(currentVersion, "v") {
|
||||
currentVersion = "v" + currentVersion
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
req, err := http.NewRequest("GET", "https://api.github.com/repos/Tencent/WeKnora/releases/latest", nil)
|
||||
if err != nil {
|
||||
logger.Warnf(context.Background(), "Check update failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Add User-Agent header which is required/recommended by GitHub API
|
||||
req.Header.Set("User-Agent", "WeKnora-Lite-Desktop-App")
|
||||
|
||||
// Add Authorization header if GITHUB_TOKEN is present to increase rate limit
|
||||
if token := os.Getenv("GITHUB_TOKEN"); token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
logger.Warnf(context.Background(), "Check update failed: %v", err)
|
||||
if showUpToDate {
|
||||
wailsruntime.MessageDialog(ctx, wailsruntime.MessageDialogOptions{
|
||||
Type: wailsruntime.ErrorDialog,
|
||||
Title: "Check Update Failed",
|
||||
Message: fmt.Sprintf("Failed to connect to GitHub to check for updates:\n%v", err),
|
||||
Buttons: []string{"OK"},
|
||||
DefaultButton: "OK",
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
logger.Warnf(context.Background(), "GitHub API returned status code: %d", resp.StatusCode)
|
||||
if showUpToDate {
|
||||
msg := fmt.Sprintf("GitHub API returned an unexpected status code: %d", resp.StatusCode)
|
||||
if resp.StatusCode == 403 {
|
||||
msg = "GitHub API rate limit exceeded. Please try again later."
|
||||
}
|
||||
wailsruntime.MessageDialog(ctx, wailsruntime.MessageDialogOptions{
|
||||
Type: wailsruntime.ErrorDialog,
|
||||
Title: "Check Update Failed",
|
||||
Message: msg,
|
||||
Buttons: []string{"OK"},
|
||||
DefaultButton: "OK",
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var release githubRelease
|
||||
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
|
||||
logger.Warnf(context.Background(), "Failed to parse release info: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
latestVersion := release.TagName
|
||||
if !strings.HasPrefix(latestVersion, "v") {
|
||||
latestVersion = "v" + latestVersion
|
||||
}
|
||||
|
||||
if semver.IsValid(latestVersion) && semver.IsValid(currentVersion) {
|
||||
if semver.Compare(latestVersion, currentVersion) > 0 {
|
||||
assetURL, assetName := findBestAsset(release.Assets, runtime.GOOS, runtime.GOARCH)
|
||||
|
||||
if autoDownload && assetURL != "" {
|
||||
// Silent download in background
|
||||
downloadAndInstall(ctx, assetURL, assetName, currentVersion, latestVersion, true)
|
||||
return
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf("A new version of WeKnora Lite is available!\n\nCurrent version: %s\nLatest version: %s\n\nWould you like to download it now?", currentVersion, latestVersion)
|
||||
choice, _ := wailsruntime.MessageDialog(ctx, wailsruntime.MessageDialogOptions{
|
||||
Type: wailsruntime.InfoDialog,
|
||||
Title: "Update Available",
|
||||
Message: msg,
|
||||
Buttons: []string{"Download", "Cancel"},
|
||||
DefaultButton: "Download",
|
||||
})
|
||||
if choice == "Download" {
|
||||
if assetURL != "" {
|
||||
downloadAndInstall(ctx, assetURL, assetName, currentVersion, latestVersion, false)
|
||||
} else {
|
||||
// Fallback to opening the release page if no specific asset is found
|
||||
wailsruntime.BrowserOpenURL(ctx, release.HTMLURL)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if showUpToDate {
|
||||
wailsruntime.MessageDialog(ctx, wailsruntime.MessageDialogOptions{
|
||||
Type: wailsruntime.InfoDialog,
|
||||
Title: "Up to Date",
|
||||
Message: fmt.Sprintf("You are using the latest version of WeKnora Lite.\n\nCurrent version: %s", currentVersion),
|
||||
Buttons: []string{"OK"},
|
||||
DefaultButton: "OK",
|
||||
})
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func findBestAsset(assets []githubAsset, goos, goarch string) (string, string) {
|
||||
osKeyword := ""
|
||||
switch goos {
|
||||
case "darwin":
|
||||
osKeyword = "mac"
|
||||
case "windows":
|
||||
osKeyword = "win"
|
||||
case "linux":
|
||||
osKeyword = "linux"
|
||||
}
|
||||
|
||||
archKeyword := ""
|
||||
switch goarch {
|
||||
case "amd64":
|
||||
archKeyword = "amd64"
|
||||
case "arm64":
|
||||
archKeyword = "arm64"
|
||||
}
|
||||
|
||||
// 1. Try to match both OS and Arch
|
||||
for _, asset := range assets {
|
||||
name := strings.ToLower(asset.Name)
|
||||
if strings.Contains(name, osKeyword) && (strings.Contains(name, archKeyword) || strings.Contains(name, "universal") || strings.Contains(name, "aarch64")) {
|
||||
return asset.BrowserDownloadURL, asset.Name
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Try to match OS only (e.g. universal binaries without arch in name)
|
||||
for _, asset := range assets {
|
||||
name := strings.ToLower(asset.Name)
|
||||
if strings.Contains(name, osKeyword) {
|
||||
return asset.BrowserDownloadURL, asset.Name
|
||||
}
|
||||
}
|
||||
|
||||
// 3. MacOS specific fallback (e.g. .dmg)
|
||||
if goos == "darwin" {
|
||||
for _, asset := range assets {
|
||||
if strings.HasSuffix(strings.ToLower(asset.Name), ".dmg") {
|
||||
return asset.BrowserDownloadURL, asset.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Windows specific fallback (e.g. .exe)
|
||||
if goos == "windows" {
|
||||
for _, asset := range assets {
|
||||
if strings.HasSuffix(strings.ToLower(asset.Name), ".exe") {
|
||||
return asset.BrowserDownloadURL, asset.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", ""
|
||||
}
|
||||
|
||||
func downloadAndInstall(ctx context.Context, url string, filename string, currentVersion string, latestVersion string, silent bool) {
|
||||
tempDir := os.TempDir()
|
||||
savePath := filepath.Join(tempDir, filename)
|
||||
|
||||
go func() {
|
||||
logger.Infof(context.Background(), "Starting download from %s to %s", url, savePath)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
logger.Warnf(context.Background(), "Download failed: %v", err)
|
||||
if !silent {
|
||||
wailsruntime.MessageDialog(ctx, wailsruntime.MessageDialogOptions{
|
||||
Type: wailsruntime.ErrorDialog,
|
||||
Title: "Download Failed",
|
||||
Message: fmt.Sprintf("Failed to download the update:\n%v", err),
|
||||
Buttons: []string{"OK"},
|
||||
DefaultButton: "OK",
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
logger.Warnf(context.Background(), "Download failed, server returned status: %d", resp.StatusCode)
|
||||
if !silent {
|
||||
wailsruntime.MessageDialog(ctx, wailsruntime.MessageDialogOptions{
|
||||
Type: wailsruntime.ErrorDialog,
|
||||
Title: "Download Failed",
|
||||
Message: fmt.Sprintf("Server returned status: %d", resp.StatusCode),
|
||||
Buttons: []string{"OK"},
|
||||
DefaultButton: "OK",
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
out, err := os.Create(savePath)
|
||||
if err != nil {
|
||||
logger.Warnf(context.Background(), "Failed to create file for download: %v", err)
|
||||
if !silent {
|
||||
wailsruntime.MessageDialog(ctx, wailsruntime.MessageDialogOptions{
|
||||
Type: wailsruntime.ErrorDialog,
|
||||
Title: "Save Failed",
|
||||
Message: fmt.Sprintf("Failed to save the update file:\n%v", err),
|
||||
Buttons: []string{"OK"},
|
||||
DefaultButton: "OK",
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
if err != nil {
|
||||
logger.Warnf(context.Background(), "Error occurred during download copying: %v", err)
|
||||
if !silent {
|
||||
wailsruntime.MessageDialog(ctx, wailsruntime.MessageDialogOptions{
|
||||
Type: wailsruntime.ErrorDialog,
|
||||
Title: "Download Error",
|
||||
Message: fmt.Sprintf("An error occurred while downloading:\n%v", err),
|
||||
Buttons: []string{"OK"},
|
||||
DefaultButton: "OK",
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
logger.Infof(context.Background(), "Download completed successfully: %s", savePath)
|
||||
|
||||
// Prompt user to restart
|
||||
choice, _ := wailsruntime.MessageDialog(ctx, wailsruntime.MessageDialogOptions{
|
||||
Type: wailsruntime.InfoDialog,
|
||||
Title: "Update Ready",
|
||||
Message: fmt.Sprintf("WeKnora Lite %s has been downloaded successfully.\n\nWould you like to restart and install the new version now?", latestVersion),
|
||||
Buttons: []string{"Restart Now", "Later"},
|
||||
DefaultButton: "Restart Now",
|
||||
})
|
||||
|
||||
if choice == "Restart Now" {
|
||||
applyUpdateAndRestart(ctx, savePath)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// desktopAboutVersion 优先使用构建脚本注入的 handler.Version,否则尝试读取仓库根目录 VERSION(本地 wails dev 等未带 ldflags 时)。
|
||||
func desktopAboutVersion() string {
|
||||
if v := strings.TrimSpace(handler.Version); v != "" && v != "unknown" {
|
||||
return v
|
||||
}
|
||||
for _, p := range []string{
|
||||
"VERSION",
|
||||
filepath.Join("..", "..", "VERSION"),
|
||||
filepath.Join("..", "..", "..", "VERSION"),
|
||||
} {
|
||||
b, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if v := strings.TrimSpace(string(b)); v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// applyUpdateAndRestart applies the downloaded update and restarts the application
|
||||
func applyUpdateAndRestart(ctx context.Context, savePath string) {
|
||||
if runtime.GOOS == "windows" {
|
||||
scriptPath := filepath.Join(os.TempDir(), "weknora_update.bat")
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
logger.Warnf(context.Background(), "Failed to get executable path: %v", err)
|
||||
return
|
||||
}
|
||||
scriptContent := fmt.Sprintf(`@echo off
|
||||
timeout /t 2 /nobreak
|
||||
start /wait "" "%s" /S
|
||||
start "" "%s"
|
||||
del "%%~f0"
|
||||
`, savePath, execPath)
|
||||
os.WriteFile(scriptPath, []byte(scriptContent), 0755)
|
||||
|
||||
cmd := exec.Command("cmd.exe", "/C", "start", "/b", scriptPath)
|
||||
cmd.Start()
|
||||
wailsruntime.Quit(ctx)
|
||||
|
||||
} else if runtime.GOOS == "darwin" {
|
||||
if strings.HasSuffix(strings.ToLower(savePath), ".dmg") {
|
||||
go func() {
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
logger.Warnf(context.Background(), "Failed to get executable path: %v", err)
|
||||
exec.Command("open", savePath).Start()
|
||||
wailsruntime.Quit(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
appBundlePath := filepath.Dir(filepath.Dir(filepath.Dir(execPath)))
|
||||
if !strings.HasSuffix(appBundlePath, ".app") {
|
||||
exec.Command("open", savePath).Start()
|
||||
wailsruntime.Quit(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
mountPoint := filepath.Join(os.TempDir(), "WeKnoraUpdateMount")
|
||||
os.MkdirAll(mountPoint, 0755)
|
||||
|
||||
cmdMount := exec.Command("hdiutil", "attach", savePath, "-mountpoint", mountPoint, "-nobrowse", "-quiet")
|
||||
if err := cmdMount.Run(); err != nil {
|
||||
logger.Warnf(context.Background(), "Failed to mount dmg: %v", err)
|
||||
exec.Command("open", savePath).Start()
|
||||
wailsruntime.Quit(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(mountPoint)
|
||||
if err != nil {
|
||||
exec.Command("hdiutil", "detach", mountPoint, "-force").Run()
|
||||
exec.Command("open", savePath).Start()
|
||||
wailsruntime.Quit(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
var newAppPath string
|
||||
for _, entry := range entries {
|
||||
if strings.HasSuffix(entry.Name(), ".app") {
|
||||
newAppPath = filepath.Join(mountPoint, entry.Name())
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if newAppPath == "" {
|
||||
exec.Command("hdiutil", "detach", mountPoint, "-force").Run()
|
||||
exec.Command("open", savePath).Start()
|
||||
wailsruntime.Quit(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
appDir := filepath.Dir(appBundlePath)
|
||||
scriptPath := filepath.Join(os.TempDir(), "weknora_update.sh")
|
||||
scriptContent := fmt.Sprintf(`#!/bin/bash
|
||||
sleep 2
|
||||
if ! (rm -rf "%s" && cp -a "%s" "%s"); then
|
||||
osascript -e "do shell script \"rm -rf \\\"%s\\\" && cp -a \\\"%s\\\" \\\"%s\\\"\" with administrator privileges"
|
||||
fi
|
||||
hdiutil detach "%s" -force
|
||||
open "%s"
|
||||
rm "$0"
|
||||
`, appBundlePath, newAppPath, appDir, appBundlePath, newAppPath, appDir, mountPoint, appBundlePath)
|
||||
|
||||
os.WriteFile(scriptPath, []byte(scriptContent), 0755)
|
||||
|
||||
exec.Command("bash", scriptPath).Start()
|
||||
wailsruntime.Quit(ctx)
|
||||
}()
|
||||
} else {
|
||||
exec.Command("open", savePath).Start()
|
||||
wailsruntime.Quit(ctx)
|
||||
}
|
||||
} else {
|
||||
exec.Command("xdg-open", savePath).Start()
|
||||
wailsruntime.Quit(ctx)
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import { MessagePlugin } from 'tdesign-vue-next'
|
||||
import ManualKnowledgeEditor from '@/components/manual-knowledge-editor.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
import { getCurrentUser } from '@/api/auth'
|
||||
|
||||
// TDesign locale configs
|
||||
@@ -16,6 +17,7 @@ import ruRUConfig from 'tdesign-vue-next/esm/locale/ru_RU'
|
||||
const { locale } = useI18n()
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
const tdLocaleMap: Record<string, object> = {
|
||||
'en-US': enUSConfig,
|
||||
@@ -132,8 +134,38 @@ const handleGlobalOIDCCallback = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
let updateCheckTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
onMounted(() => {
|
||||
handleGlobalOIDCCallback()
|
||||
|
||||
// Auto check for updates on startup
|
||||
setTimeout(() => {
|
||||
if (settingsStore.isAutoCheckUpdateEnabled) {
|
||||
// @ts-ignore
|
||||
if (window.go && window.go.main && window.go.main.App && window.go.main.App.AutoCheckForUpdates) {
|
||||
// @ts-ignore
|
||||
window.go.main.App.AutoCheckForUpdates()
|
||||
}
|
||||
}
|
||||
}, 2000)
|
||||
|
||||
// Periodically check for updates (every 4 hours)
|
||||
updateCheckTimer = setInterval(() => {
|
||||
if (settingsStore.isAutoCheckUpdateEnabled) {
|
||||
// @ts-ignore
|
||||
if (window.go && window.go.main && window.go.main.App && window.go.main.App.AutoCheckForUpdates) {
|
||||
// @ts-ignore
|
||||
window.go.main.App.AutoCheckForUpdates()
|
||||
}
|
||||
}
|
||||
}, 4 * 60 * 60 * 1000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (updateCheckTimer) {
|
||||
clearInterval(updateCheckTimer)
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
@@ -569,6 +569,8 @@ export default {
|
||||
webSearchConfig: 'Web Search',
|
||||
enableMemory: 'Enable Memory',
|
||||
enableMemoryDesc: 'When enabled, the system will record your conversation history and automatically recall relevant content in future conversations to provide more personalized answers.',
|
||||
autoCheckUpdate: 'Auto Download Updates',
|
||||
autoCheckUpdateDesc: 'When enabled, automatically check and download the latest version in the background.',
|
||||
memoryRequiresNeo4j: 'Memory feature requires Neo4j graph database. Please configure and enable Neo4j (set NEO4J_ENABLE=true) before enabling this feature.',
|
||||
memoryHowToEnable: 'View Neo4j Configuration Guide',
|
||||
parserEngine: 'Parser Engine',
|
||||
|
||||
@@ -426,6 +426,8 @@ export default {
|
||||
webSearchConfig: "웹 검색",
|
||||
enableMemory: "기억 기능 활성화",
|
||||
enableMemoryDesc: "활성화하면 시스템이 대화 기록을 저장하고 향후 대화에서 관련 내용을 자동으로 회상하여 더 개인화된 답변을 제공합니다.",
|
||||
autoCheckUpdate: '업데이트 자동 다운로드',
|
||||
autoCheckUpdateDesc: '활성화하면 시작 시 최신 버전을 자동으로 확인하고 백그라운드에서 다운로드합니다.',
|
||||
memoryRequiresNeo4j: "기억 기능은 Neo4j 그래프 데이터베이스가 필요합니다. 이 기능을 활성화하기 전에 Neo4j를 구성하고 활성화해 주세요 (NEO4J_ENABLE=true 설정).",
|
||||
memoryHowToEnable: "Neo4j 구성 가이드 보기",
|
||||
parserEngine: "파싱 엔진",
|
||||
|
||||
@@ -548,6 +548,8 @@ export default {
|
||||
webSearchConfig: 'Сетевой поиск',
|
||||
enableMemory: 'Включить память',
|
||||
enableMemoryDesc: 'При включении система будет записывать историю ваших разговоров и автоматически вспоминать соответствующий контент в будущих беседах для более персонализированных ответов.',
|
||||
autoCheckUpdate: 'Автоматическая загрузка обновлений',
|
||||
autoCheckUpdateDesc: 'При включении автоматически проверять и скачивать последнюю версию в фоновом режиме при запуске.',
|
||||
memoryRequiresNeo4j: 'Функция памяти требует графовую базу данных Neo4j. Пожалуйста, настройте и включите Neo4j (установите NEO4J_ENABLE=true) перед активацией этой функции.',
|
||||
memoryHowToEnable: 'Руководство по настройке Neo4j',
|
||||
parserEngine: 'Движок парсинга',
|
||||
|
||||
@@ -423,6 +423,8 @@ export default {
|
||||
webSearchConfig: "网络搜索",
|
||||
enableMemory: "开启记忆功能",
|
||||
enableMemoryDesc: "开启后,系统将记录您的对话历史,并在后续对话中自动回忆相关内容,提供更个性化的回答。",
|
||||
autoCheckUpdate: '自动下载更新',
|
||||
autoCheckUpdateDesc: '开启后自动检查并在后台下载最新版本安装包。',
|
||||
memoryRequiresNeo4j: "记忆功能依赖 Neo4j 图数据库,请先配置并启用 Neo4j(设置环境变量 NEO4J_ENABLE=true)后再开启此功能。",
|
||||
memoryHowToEnable: "查看 Neo4j 配置指南",
|
||||
parserEngine: "解析引擎",
|
||||
|
||||
@@ -19,6 +19,7 @@ interface Settings {
|
||||
conversationModels: ConversationModels;
|
||||
selectedAgentId: string; // 当前选中的智能体ID
|
||||
selectedAgentSourceTenantId: string | null; // 当使用共享智能体时,来源租户 ID(用于后端 model/KB/MCP 解析)
|
||||
autoCheckUpdate?: boolean; // 是否自动检查并下载更新
|
||||
}
|
||||
|
||||
// Agent 配置接口
|
||||
@@ -96,6 +97,7 @@ const defaultSettings: Settings = {
|
||||
},
|
||||
selectedAgentId: BUILTIN_QUICK_ANSWER_ID, // 默认选中快速问答模式
|
||||
selectedAgentSourceTenantId: null as string | null, // 共享智能体来源租户 ID
|
||||
autoCheckUpdate: true,
|
||||
};
|
||||
|
||||
export const useSettingsStore = defineStore("settings", {
|
||||
@@ -144,6 +146,9 @@ export const useSettingsStore = defineStore("settings", {
|
||||
// 记忆功能是否启用
|
||||
isMemoryEnabled: (state) => state.settings.enableMemory || false,
|
||||
|
||||
// 是否自动检查并下载更新
|
||||
isAutoCheckUpdateEnabled: (state) => state.settings.autoCheckUpdate ?? true,
|
||||
|
||||
// 当前选中的智能体ID
|
||||
selectedAgentId: (state) => state.settings.selectedAgentId || BUILTIN_QUICK_ANSWER_ID,
|
||||
// 共享智能体来源租户 ID(可选)
|
||||
@@ -308,6 +313,12 @@ export const useSettingsStore = defineStore("settings", {
|
||||
localStorage.setItem("WeKnora_settings", JSON.stringify(this.settings));
|
||||
},
|
||||
|
||||
// 启用/禁用自动检查更新
|
||||
toggleAutoCheckUpdate(enabled: boolean) {
|
||||
this.settings.autoCheckUpdate = enabled;
|
||||
localStorage.setItem("WeKnora_settings", JSON.stringify(this.settings));
|
||||
},
|
||||
|
||||
// File selection actions
|
||||
addFile(fileId: string) {
|
||||
if (!this.settings.selectedFiles) this.settings.selectedFiles = [];
|
||||
|
||||
@@ -73,6 +73,19 @@
|
||||
</t-link>
|
||||
</template>
|
||||
</t-alert>
|
||||
|
||||
<!-- 自动下载更新开关 (Lite edition only) -->
|
||||
<div class="setting-row" v-if="authStore.isLiteMode">
|
||||
<div class="setting-info">
|
||||
<label>{{ $t('settings.autoCheckUpdate') }}</label>
|
||||
<p class="desc">{{ $t('settings.autoCheckUpdateDesc') }}</p>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<t-switch
|
||||
v-model="isAutoCheckUpdateEnabled"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -82,11 +95,13 @@ import { ref, onMounted, computed } from 'vue'
|
||||
import { MessagePlugin } from 'tdesign-vue-next'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { getSystemInfo } from '@/api/system'
|
||||
import { useTheme, type ThemeMode } from '@/composables/useTheme'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const settingsStore = useSettingsStore()
|
||||
const authStore = useAuthStore()
|
||||
const { currentTheme, setTheme } = useTheme()
|
||||
|
||||
// 本地状态
|
||||
@@ -106,6 +121,21 @@ const isMemoryEnabled = computed({
|
||||
set: (val) => settingsStore.toggleMemory(val)
|
||||
})
|
||||
|
||||
// 自动检查更新状态
|
||||
const isAutoCheckUpdateEnabled = computed({
|
||||
get: () => settingsStore.isAutoCheckUpdateEnabled,
|
||||
set: (val) => {
|
||||
settingsStore.toggleAutoCheckUpdate(val)
|
||||
if (val) {
|
||||
// @ts-ignore
|
||||
if (window.go && window.go.main && window.go.main.App && window.go.main.App.AutoCheckForUpdates) {
|
||||
// @ts-ignore
|
||||
window.go.main.App.AutoCheckForUpdates()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 初始化加载
|
||||
onMounted(async () => {
|
||||
// 从 localStorage 加载语言设置
|
||||
|
||||
4
frontend/src/wailsjs/go/main/App.d.ts
vendored
4
frontend/src/wailsjs/go/main/App.d.ts
vendored
@@ -1,6 +1,10 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function AutoCheckForUpdates():Promise<void>;
|
||||
|
||||
export function CheckForUpdates():Promise<void>;
|
||||
|
||||
export function GetAPIBaseURL():Promise<string>;
|
||||
|
||||
export function GetAPILanBaseURL():Promise<string>;
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function AutoCheckForUpdates() {
|
||||
return window['go']['main']['App']['AutoCheckForUpdates']();
|
||||
}
|
||||
|
||||
export function CheckForUpdates() {
|
||||
return window['go']['main']['App']['CheckForUpdates']();
|
||||
}
|
||||
|
||||
export function GetAPIBaseURL() {
|
||||
return window['go']['main']['App']['GetAPIBaseURL']();
|
||||
}
|
||||
|
||||
2
go.mod
2
go.mod
@@ -63,6 +63,7 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.38.0
|
||||
go.uber.org/dig v1.18.1
|
||||
golang.org/x/crypto v0.46.0
|
||||
golang.org/x/mod v0.31.0
|
||||
golang.org/x/net v0.48.0
|
||||
golang.org/x/sync v0.19.0
|
||||
golang.org/x/time v0.14.0
|
||||
@@ -297,7 +298,6 @@ require (
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/arch v0.20.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 // indirect
|
||||
golang.org/x/mod v0.31.0 // indirect
|
||||
golang.org/x/oauth2 v0.34.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/telemetry v0.0.0-20251208220230-2638a1023523 // indirect
|
||||
|
||||
Reference in New Issue
Block a user