Skip to content

Commit 4f83612

Browse files
macosmacos
authored andcommitted
feat: 桌面端一键安装+升级功能 & 数据一致性修复
- #227: 问题跟踪页 UNION 查询支持全部状态(含归档) - #228: connection.go 防御性建表(新用户不再报错) - #229: CheckEnvironment/InstallPackage/CheckUpgrade 环境检测与一键安装 - ProjectSelect.vue 提示条 UI(无Python/未安装/升级/桌面端更新)
1 parent 6e8de91 commit 4f83612

15 files changed

Lines changed: 566 additions & 101 deletions

File tree

desktop/app.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ import (
99
"sort"
1010
"strings"
1111

12+
"io"
13+
"net/http"
1214
"os/exec"
15+
"time"
1316

1417
"desktop/internal/auth"
1518
"desktop/internal/backup"
@@ -21,6 +24,8 @@ import (
2124
wailsRuntime "github.com/wailsapp/wails/v2/pkg/runtime"
2225
)
2326

27+
const AppVersion = "0.1.0"
28+
2429
type App struct {
2530
ctx context.Context
2631
database *db.DB
@@ -473,6 +478,156 @@ func (a *App) GetCurrentUser(token string) (map[string]string, error) {
473478
return map[string]string{"username": username}, nil
474479
}
475480

481+
// ============== Environment & Install ==============
482+
483+
func (a *App) GetAppVersion() string {
484+
return AppVersion
485+
}
486+
487+
func (a *App) CheckEnvironment() map[string]interface{} {
488+
result := map[string]interface{}{
489+
"python_found": false,
490+
"python_path": "",
491+
"avm_installed": false,
492+
"avm_version": "",
493+
}
494+
495+
// Find Python (reuse candidate logic from embedding.DetectPython)
496+
pythonPath := a.findPython()
497+
if pythonPath == "" {
498+
return result
499+
}
500+
result["python_found"] = true
501+
result["python_path"] = pythonPath
502+
503+
// Check aivectormemory installed + version
504+
out, err := exec.Command(pythonPath, "-c",
505+
"import aivectormemory; print(aivectormemory.__version__)").Output()
506+
if err != nil {
507+
return result
508+
}
509+
version := strings.TrimSpace(string(out))
510+
if version != "" {
511+
result["avm_installed"] = true
512+
result["avm_version"] = version
513+
}
514+
return result
515+
}
516+
517+
func (a *App) CheckUpgrade(currentAvmVersion string) map[string]interface{} {
518+
result := map[string]interface{}{
519+
"avm_latest": "",
520+
"avm_update_available": false,
521+
"app_latest": "",
522+
"app_update_available": false,
523+
"app_download_url": "",
524+
}
525+
526+
// 1. Check PyPI latest version
527+
pythonPath := a.findPython()
528+
if pythonPath != "" {
529+
// pip install aivectormemory== triggers error with available versions
530+
out, _ := exec.Command(pythonPath, "-m", "pip", "install", "aivectormemory==___").CombinedOutput()
531+
outStr := string(out)
532+
// Parse "from versions: 0.2.1, 0.2.2, ..., 0.2.6)"
533+
if idx := strings.LastIndex(outStr, "from versions:"); idx >= 0 {
534+
tail := outStr[idx+len("from versions:"):]
535+
if end := strings.Index(tail, ")"); end >= 0 {
536+
versions := strings.TrimSpace(tail[:end])
537+
parts := strings.Split(versions, ",")
538+
if len(parts) > 0 {
539+
latest := strings.TrimSpace(parts[len(parts)-1])
540+
result["avm_latest"] = latest
541+
if latest != "" && latest != currentAvmVersion {
542+
result["avm_update_available"] = true
543+
}
544+
}
545+
}
546+
}
547+
}
548+
549+
// 2. Check GitHub Releases latest version
550+
client := &http.Client{Timeout: 10 * time.Second}
551+
resp, err := client.Get("https://api.github.com/repos/Edlineas/aivectormemory/releases/latest")
552+
if err == nil {
553+
defer resp.Body.Close()
554+
if resp.StatusCode == 200 {
555+
body, _ := io.ReadAll(resp.Body)
556+
var release struct {
557+
TagName string `json:"tag_name"`
558+
HTMLURL string `json:"html_url"`
559+
}
560+
if json.Unmarshal(body, &release) == nil && release.TagName != "" {
561+
appLatest := strings.TrimPrefix(release.TagName, "v")
562+
result["app_latest"] = appLatest
563+
result["app_download_url"] = release.HTMLURL
564+
if appLatest != AppVersion {
565+
result["app_update_available"] = true
566+
}
567+
}
568+
}
569+
}
570+
571+
return result
572+
}
573+
574+
func (a *App) InstallPackage(upgrade bool) (string, error) {
575+
pythonPath := a.findPython()
576+
if pythonPath == "" {
577+
return "", fmt.Errorf("Python not found")
578+
}
579+
580+
args := []string{"-m", "pip", "install"}
581+
if upgrade {
582+
args = append(args, "--upgrade")
583+
}
584+
args = append(args, "aivectormemory")
585+
586+
cmd := exec.Command(pythonPath, args...)
587+
output, err := cmd.CombinedOutput()
588+
if err != nil {
589+
return string(output), fmt.Errorf("install failed: %w\n%s", err, string(output))
590+
}
591+
return string(output), nil
592+
}
593+
594+
func (a *App) findPython() string {
595+
// If settings has a custom python path, try it first
596+
if a.settings != nil && a.settings.PythonPath != "" {
597+
if _, err := os.Stat(a.settings.PythonPath); err == nil {
598+
return a.settings.PythonPath
599+
}
600+
}
601+
// If engine already detected one, use it
602+
if a.engine != nil && a.engine.PythonPath != "" {
603+
return a.engine.PythonPath
604+
}
605+
606+
// Scan candidates (find Python, not necessarily with aivectormemory)
607+
home, _ := os.UserHomeDir()
608+
candidates := []string{
609+
filepath.Join(home, "item", "run-memory-mcp-server", ".venv", "bin", "python3"),
610+
"python3", "python",
611+
"/usr/local/bin/python3",
612+
"/usr/bin/python3",
613+
"/opt/homebrew/bin/python3",
614+
}
615+
for _, py := range candidates {
616+
path := py
617+
if !filepath.IsAbs(path) {
618+
found, err := exec.LookPath(path)
619+
if err != nil {
620+
continue
621+
}
622+
path = found
623+
}
624+
if _, err := os.Stat(path); err == nil {
625+
return path
626+
}
627+
}
628+
return ""
629+
}
630+
476631
func expandHome(path string) string {
477632
if strings.HasPrefix(path, "~") {
478633
home, _ := os.UserHomeDir()

desktop/frontend/src/components/layout/Sidebar.vue

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,13 @@ import { computed, ref, onMounted } from 'vue'
33
import { useRoute, useRouter } from 'vue-router'
44
import { useI18n } from 'vue-i18n'
55
import { useProjectStore } from '../../stores/project'
6-
import { useThemeStore } from '../../stores/theme'
76
import { useAuthStore } from '../../stores/auth'
87
import { LaunchWebDashboard, StopWebDashboard, IsWebDashboardRunning } from '../../../wailsjs/go/main/App'
98
109
const { t } = useI18n()
1110
const route = useRoute()
1211
const router = useRouter()
1312
const projectStore = useProjectStore()
14-
const themeStore = useThemeStore()
1513
const authStore = useAuthStore()
1614
const webRunning = ref(false)
1715
@@ -37,7 +35,7 @@ const memoryNav: { name: string; icon: string; key?: string }[] = [
3735
]
3836
3937
const systemNav: { name: string; icon: string; key?: string }[] = [
40-
{ name: 'settings', icon: 'settings' },
38+
{ name: 'settings', key: 'systemSettings', icon: 'settings' },
4139
]
4240
4341
function isActive(name: string) { return route.name === name }
@@ -48,9 +46,9 @@ function goBack() {
4846
router.push('/')
4947
}
5048
51-
function toggleTheme() {
52-
const next = themeStore.resolvedTheme() === 'dark' ? 'light' : 'dark'
53-
themeStore.setMode(next)
49+
async function doLogout() {
50+
await authStore.logout()
51+
window.location.reload()
5452
}
5553
5654
async function toggleWebDashboard() {
@@ -113,12 +111,9 @@ async function toggleWebDashboard() {
113111
</div>
114112
<span v-if="webRunning" class="web-dot" title="Web Dashboard Running"></span>
115113
</div>
116-
</div>
117-
118-
<div class="theme-toggle" @click="toggleTheme">
119-
<svg v-if="themeStore.resolvedTheme() === 'dark'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
120-
<svg v-else viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
121-
<span>{{ themeStore.resolvedTheme() === 'dark' ? 'Light Mode' : 'Dark Mode' }}</span>
114+
<button class="sidebar-logout" @click.stop="doLogout" :title="t('auth.logout')">
115+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
116+
</button>
122117
</div>
123118

124119
<div class="sidebar-decor">
@@ -189,7 +184,6 @@ async function toggleWebDashboard() {
189184
font-weight: 500;
190185
}
191186
192-
.sidebar-bottom { border-top: 1px solid var(--bg-surface); }
193187
.sidebar-user {
194188
display: flex;
195189
align-items: center;
@@ -236,20 +230,23 @@ async function toggleWebDashboard() {
236230
flex-shrink: 0;
237231
animation: pulse 2s infinite;
238232
}
239-
240-
.theme-toggle {
233+
.sidebar-bottom {
241234
display: flex;
242235
align-items: center;
243-
gap: 8px;
244-
padding: 8px 20px;
245-
cursor: pointer;
246-
transition: all 0.15s;
247-
font-size: 12px;
248-
color: var(--text-secondary);
249236
border-top: 1px solid var(--bg-surface);
250237
}
251-
.theme-toggle:hover { color: var(--text-primary); background: hsl(0 0% 50% / 0.06); }
252-
.theme-toggle svg { width: 16px; height: 16px; flex-shrink: 0; }
238+
.sidebar-logout {
239+
background: none;
240+
border: none;
241+
cursor: pointer;
242+
padding: 8px;
243+
margin-right: 8px;
244+
color: var(--text-muted);
245+
transition: color 0.15s;
246+
flex-shrink: 0;
247+
}
248+
.sidebar-logout:hover { color: var(--color-danger); }
249+
.sidebar-logout svg { width: 18px; height: 18px; }
253250
254251
.sidebar-decor {
255252
padding: 10px 16px 14px;

desktop/frontend/src/composables/useIssues.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export function useIssues() {
1212
const issues = ref<any[]>([])
1313
const total = ref(0)
1414
const page = ref(1)
15-
const statusFilter = ref('')
15+
const statusFilter = ref('all')
1616
const dateFilter = ref('')
1717
const query = ref('')
1818
const loading = ref(false)

desktop/frontend/src/composables/useMemories.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export function useMemories(defaultScope: string) {
2020
try {
2121
const dir = projectStore.current
2222
const offset = (page.value - 1) * PAGE_SIZE
23-
const data = await GetMemories(dir, defaultScope, query.value, '', '', PAGE_SIZE, offset)
23+
const data = await GetMemories(defaultScope, dir, query.value, '', '', PAGE_SIZE, offset)
2424
memories.value = data?.memories || []
2525
total.value = data?.total || 0
2626
} catch {

desktop/frontend/src/composables/useTags.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ref } from 'vue'
2-
import { GetTags, RenameTag, MergeTags, DeleteTags, SearchMemories } from '../../wailsjs/go/main/App'
2+
import { GetTags, RenameTag, MergeTags, DeleteTags, GetMemories } from '../../wailsjs/go/main/App'
33
import { useProjectStore } from '../stores/project'
44

55
export function useTags() {
@@ -34,7 +34,8 @@ export function useTags() {
3434
}
3535

3636
async function getMemoriesByTag(tag: string, topK = 50) {
37-
return await SearchMemories(projectStore.current, tag, 'all', [tag], topK)
37+
const result = await GetMemories('all', projectStore.current, '', tag, '', topK, 0)
38+
return result?.memories || []
3839
}
3940

4041
function toggleSelect(name: string) {

desktop/frontend/src/i18n/en.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export default {
99
taskManagement: 'Tasks',
1010
maintenance: 'Maintenance',
1111
settings: 'Settings',
12+
systemSettings: 'System Settings',
1213
searchProject: 'Search project memories...',
1314
searchGlobal: 'Search global memories...',
1415
searchTag: 'Search tags...',
@@ -118,6 +119,17 @@ export default {
118119
welcomeTitle: 'Welcome to AIVectorMemory',
119120
welcomeDesc: 'A persistent cross-session memory system for AI.\nRun the following command in your project to get started:',
120121
welcomeCmd: 'aivectormemory install',
122+
envNoPython: 'Python not detected. Please install Python 3.10+',
123+
envNoPythonLink: 'Download Python',
124+
envNoPackage: 'Python detected, but AIVectorMemory is not installed',
125+
envInstallBtn: 'Install Now',
126+
envInstalling: 'Installing...',
127+
envInstallSuccess: 'AIVectorMemory installed successfully!',
128+
envInstallError: 'Installation failed. Please run manually: pip install aivectormemory',
129+
envAvmUpdate: 'AIVectorMemory {version} is available',
130+
envUpgradeBtn: 'Upgrade Now',
131+
envAppUpdate: 'Desktop app {version} is available',
132+
envDownloadBtn: 'Download',
121133
addProjectSuccess: 'Project added',
122134
addProjectInstallHint: 'Run aivectormemory install in the project directory to complete IDE integration',
123135
emptyStatsHint: 'Data is generated automatically from AI conversations. Start a conversation to see stats here',

desktop/frontend/src/i18n/zh-CN.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export default {
99
taskManagement: '任务管理',
1010
maintenance: '数据维护',
1111
settings: '设置',
12+
systemSettings: '系统设置',
1213
searchProject: '搜索项目记忆...',
1314
searchGlobal: '搜索全局记忆...',
1415
searchTag: '搜索标签...',
@@ -118,6 +119,17 @@ export default {
118119
welcomeTitle: '欢迎使用 AIVectorMemory',
119120
welcomeDesc: '这是一个 AI 跨会话持久记忆系统。\n在你的项目中运行以下命令开始使用:',
120121
welcomeCmd: 'aivectormemory install',
122+
envNoPython: '未检测到 Python,请安装 Python 3.10+',
123+
envNoPythonLink: '下载 Python',
124+
envNoPackage: '检测到 Python,但未安装 AIVectorMemory',
125+
envInstallBtn: '一键安装',
126+
envInstalling: '安装中...',
127+
envInstallSuccess: 'AIVectorMemory 安装成功!',
128+
envInstallError: '安装失败,请手动执行:pip install aivectormemory',
129+
envAvmUpdate: 'AIVectorMemory 有新版本 {version}',
130+
envUpgradeBtn: '一键升级',
131+
envAppUpdate: '桌面端有新版本 {version}',
132+
envDownloadBtn: '前往下载',
121133
addProjectSuccess: '项目已添加',
122134
addProjectInstallHint: '请在项目目录运行 aivectormemory install 完成 IDE 集成',
123135
emptyStatsHint: '数据由 AI 对话自动产生,开始对话后这里会显示统计信息',

desktop/frontend/src/views/IssuesView.vue

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,15 @@ onMounted(() => {
4040
4141
function onSearch(q: string) { setQuery(q) }
4242
43-
function quickFilter(s: string) { setStatus(s) }
43+
function quickFilter(s: string) {
44+
if (s === 'today') {
45+
const d = new Date()
46+
setDate(`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`)
47+
} else {
48+
setDate('')
49+
setStatus(s)
50+
}
51+
}
4452
4553
function onStatusChange(e: Event) { setStatus((e.target as HTMLSelectElement).value) }
4654
@@ -134,7 +142,7 @@ async function openView(issue: any) {
134142
<div class="toolbar toolbar--wrap">
135143
<input type="date" class="filter-input" @change="onDateChange" />
136144
<select class="filter-select" :value="statusFilter" @change="onStatusChange">
137-
<option value="">{{ t('allStatus') }}</option>
145+
<option value="all">{{ t('allIncludeArchived') }}</option>
138146
<option value="active">{{ t('activeOnly') }}</option>
139147
<option value="pending">{{ t('status.pending') }}</option>
140148
<option value="in_progress">{{ t('status.in_progress') }}</option>
@@ -143,7 +151,7 @@ async function openView(issue: any) {
143151
</select>
144152
<SearchBox :placeholder="t('searchIssue')" @search="onSearch" />
145153
<div class="toolbar-right">
146-
<button class="btn btn--outline btn--sm" @click="quickFilter('')">{{ t('viewAll') }}</button>
154+
<button class="btn btn--outline btn--sm" @click="quickFilter('all')">{{ t('viewAll') }}</button>
147155
<button class="btn btn--outline btn--sm" @click="quickFilter('today')">{{ t('today') }}</button>
148156
<button class="btn btn--primary btn--sm" @click="openCreate">{{ t('addIssue') }}</button>
149157
</div>

0 commit comments

Comments
 (0)