{{ kind === 'todo' ? d.content : d.description }}
+diff --git a/.task/checksum/build-frontend--DEV--RUNNER-npm- b/.task/checksum/build-frontend--DEV--RUNNER-npm-
index 1d3fc0d..d6b3dc8 100644
--- a/.task/checksum/build-frontend--DEV--RUNNER-npm-
+++ b/.task/checksum/build-frontend--DEV--RUNNER-npm-
@@ -1 +1 @@
-cab2c324f232a555dd35c31202652cfc
+8ef156c82c13dcfb55a861fc1e1b9483
diff --git a/.task/checksum/windows-common-generate-bindings b/.task/checksum/windows-common-generate-bindings
index 1d3f1d3..fdd825b 100644
--- a/.task/checksum/windows-common-generate-bindings
+++ b/.task/checksum/windows-common-generate-bindings
@@ -1 +1 @@
-48639c2c47b996725d8955fb73394c29
+a59624793eae992b65534b38e0fef064
diff --git a/admin.go b/admin.go
index 3b358f3..bf0906c 100644
--- a/admin.go
+++ b/admin.go
@@ -163,6 +163,15 @@ func (a *App) AdminListUsers() ([]map[string]any, error) {
return resp.Items, nil
}
+// AdminGetUser 用户详情(含待办/工单/笔记/团队数量)。
+func (a *App) AdminGetUser(id int64) (map[string]any, error) {
+ var out map[string]any
+ if e := a.adminDecode(http.MethodGet, "/api/v1/admin/users/"+strconv.FormatInt(id, 10), nil, &out, false); e != nil {
+ return nil, e
+ }
+ return out, nil
+}
+
// AdminPatchUser 更新用户禁 AI / 禁用。field: aiBanned | disabled。
func (a *App) AdminPatchUser(id int64, field string, value int) error {
body := map[string]any{}
diff --git a/ai_tasks.go b/ai_tasks.go
new file mode 100644
index 0000000..9007e76
--- /dev/null
+++ b/ai_tasks.go
@@ -0,0 +1,173 @@
+package main
+
+// ai_tasks.go 一句话生成多条待办/工单:调用 AI 把自然语言拆解成结构化草稿,
+// 前端预览确认后再逐条走 SaveTodo / SaveTicket 正常落库。
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "strings"
+ "time"
+
+ "view/service/ai"
+)
+
+// AITaskDraft 是 AI 生成的一条草稿(todo 与 ticket 共用,前端按 kind 取字段)。
+type AITaskDraft struct {
+ Kind string `json:"kind"` // todo | ticket
+ Title string `json:"title"`
+ Content string `json:"content"` // todo 备注
+ Description string `json:"description"` // ticket 描述
+ Type string `json:"type"` // ticket: feature | bug | task | improvement
+ Priority string `json:"priority"` // low | medium | high
+ StartAt string `json:"startAt"` // ticket: YYYY-MM-DD
+ DueAt string `json:"dueAt"` // todo: YYYY-MM-DDTHH:MM;ticket: YYYY-MM-DD
+}
+
+// AIGenerateTasks 把一句话拆解成多条待办或工单草稿(不落库,由前端确认后保存)。
+func (a *App) AIGenerateTasks(kind, text string) ([]AITaskDraft, error) {
+ if e := a.ready(); e != nil {
+ return nil, e
+ }
+ if kind != "todo" && kind != "ticket" {
+ return nil, errors.New("BAD_REQUEST")
+ }
+ text = strings.TrimSpace(text)
+ if text == "" {
+ return nil, errors.New("AI_EMPTY_MESSAGE")
+ }
+ provider, e := a.aiProvider()
+ if e != nil {
+ return nil, e
+ }
+ if e := a.checkAIPolicy(); e != nil {
+ return nil, e
+ }
+ msgs := []ai.Message{
+ {Role: "system", Content: aiTaskPrompt(kind)},
+ {Role: "user", Content: text},
+ }
+ ctx, cancel := context.WithTimeout(a.ctx, 2*time.Minute)
+ defer cancel()
+ stream, e := provider.ChatStream(ctx, msgs)
+ if e != nil {
+ a.store.Log("error", "AI", "AI 任务生成请求失败", e.Error())
+ return nil, e
+ }
+ var sb strings.Builder
+ var usage *ai.Usage
+ for chunk := range stream {
+ if chunk.Usage != nil {
+ usage = chunk.Usage
+ }
+ if chunk.Err != nil {
+ a.store.Log("error", "AI", "AI 任务生成失败", chunk.Err.Error())
+ return nil, chunk.Err
+ }
+ sb.WriteString(chunk.Content)
+ }
+ if usage != nil {
+ go a.reportAIUsage(provider.Name(), usage.PromptTokens, usage.CompletionTokens, usage.Estimated)
+ }
+ drafts, pe := parseAITaskDrafts(kind, sb.String())
+ if pe != nil {
+ a.store.Log("warning", "AI", "AI 任务生成解析失败", sb.String())
+ return nil, pe
+ }
+ a.store.Log("info", "AI", "AI 任务生成完成", fmt.Sprintf("kind=%s count=%d", kind, len(drafts)))
+ return drafts, nil
+}
+
+func aiTaskPrompt(kind string) string {
+ now := time.Now()
+ weekdays := []string{"日", "一", "二", "三", "四", "五", "六"}
+ head := fmt.Sprintf("今天是 %s(星期%s),当前时间 %s。\n", now.Format("2006-01-02"), weekdays[int(now.Weekday())], now.Format("15:04"))
+ if kind == "ticket" {
+ return head + `你是研发工单拆解助手。把用户的一句话拆成 1~10 条工单。
+只输出 JSON 数组,禁止任何解释、markdown 围栏或多余文字。每个元素:
+{"title":"简短标题(<=40字)","description":"补充细节,可为空","type":"feature|bug|task|improvement","priority":"low|medium|high","startAt":"YYYY-MM-DD","dueAt":"YYYY-MM-DD"}
+规则:标题用与用户输入相同的语言;startAt 默认今天;dueAt 不得早于 startAt,用户未提及工期时按任务量合理估算(1~7 天);能从用户话中推断出的日期(如“周五前”“下周”)必须转换为具体日期。`
+ }
+ return head + `你是待办事项拆解助手。把用户的一句话拆成 1~10 条待办。
+只输出 JSON 数组,禁止任何解释、markdown 围栏或多余文字。每个元素:
+{"title":"简短标题(<=40字)","content":"补充细节,可为空","priority":"low|medium|high","dueAt":"YYYY-MM-DDTHH:MM 或空字符串"}
+规则:标题用与用户输入相同的语言;只有用户明确或可合理推断截止时间时才填 dueAt(如“明天下午”→ 明天 18:00),否则留空字符串。`
+}
+
+// parseAITaskDrafts 从模型输出中提取 JSON 数组并规范化字段。
+func parseAITaskDrafts(kind, raw string) ([]AITaskDraft, error) {
+ raw = stripFence(strings.TrimSpace(raw))
+ start, end := strings.Index(raw, "["), strings.LastIndex(raw, "]")
+ if start < 0 || end <= start {
+ return nil, errors.New("AI_BAD_RESPONSE")
+ }
+ var list []AITaskDraft
+ if json.Unmarshal([]byte(raw[start:end+1]), &list) != nil {
+ return nil, errors.New("AI_BAD_RESPONSE")
+ }
+ today := time.Now().Format("2006-01-02")
+ out := make([]AITaskDraft, 0, len(list))
+ for _, d := range list {
+ d.Kind = kind
+ d.Title = strings.TrimSpace(d.Title)
+ if d.Title == "" {
+ continue
+ }
+ if r := []rune(d.Title); len(r) > 120 {
+ d.Title = string(r[:120])
+ }
+ if d.Priority != "low" && d.Priority != "medium" && d.Priority != "high" {
+ d.Priority = "medium"
+ }
+ if kind == "ticket" {
+ switch d.Type {
+ case "feature", "bug", "task", "improvement":
+ default:
+ d.Type = "task"
+ }
+ d.Content = ""
+ if !validDateStr(d.StartAt) {
+ d.StartAt = today
+ }
+ if !validDateStr(d.DueAt) {
+ d.DueAt = time.Now().AddDate(0, 0, 3).Format("2006-01-02")
+ }
+ if d.DueAt < d.StartAt {
+ d.DueAt = d.StartAt
+ }
+ } else {
+ d.Type, d.Description, d.StartAt = "", "", ""
+ d.DueAt = normalizeTodoDue(d.DueAt)
+ }
+ out = append(out, d)
+ if len(out) >= 20 {
+ break
+ }
+ }
+ if len(out) == 0 {
+ return nil, errors.New("AI_EMPTY_RESULT")
+ }
+ return out, nil
+}
+
+func validDateStr(s string) bool {
+ _, e := time.Parse("2006-01-02", strings.TrimSpace(s))
+ return e == nil
+}
+
+// normalizeTodoDue 接受 YYYY-MM-DDTHH:MM 或纯日期(补 18:00),其余返回空。
+func normalizeTodoDue(s string) string {
+ s = strings.TrimSpace(s)
+ if s == "" {
+ return ""
+ }
+ if _, e := time.Parse("2006-01-02T15:04", s); e == nil {
+ return s
+ }
+ if validDateStr(s) {
+ return s + "T18:00"
+ }
+ return ""
+}
diff --git a/app.go b/app.go
index bfbfa59..1bd324a 100644
--- a/app.go
+++ b/app.go
@@ -437,6 +437,25 @@ func (a *App) DeleteRule(id int64) error {
}
return a.store.DeleteRule(id)
}
+
+// GetProjectRules 项目专属排除规则(不含全局)。
+func (a *App) GetProjectRules(projectID int64) ([]ExclusionRule, error) {
+ if e := a.ready(); e != nil {
+ return nil, e
+ }
+ return a.store.ProjectRules(projectID)
+}
+
+// AddProjectRule 给某项目添加专属排除规则。
+func (a *App) AddProjectRule(projectID int64, pattern, category string) (ExclusionRule, error) {
+ if e := a.ready(); e != nil {
+ return ExclusionRule{}, e
+ }
+ if projectID <= 0 {
+ return ExclusionRule{}, errors.New("PROJECT_REQUIRED")
+ }
+ return a.store.AddProjectRule(projectID, pattern, category)
+}
func (a *App) GetLogs(level string) ([]LogEntry, error) {
if e := a.ready(); e != nil {
return nil, e
@@ -657,7 +676,8 @@ func (a *App) StartAnalysis(projectID int64, kind string) (string, error) {
a.store.Log("info", "代码分析", "开始分析项目", p.Name)
var err error
if kind == "all" || kind == "code" {
- rules, _ := a.store.Rules()
+ // 全局规则 + 项目专属规则叠加
+ rules, _ := a.store.RulesForProject(projectID)
var ls []LanguageStat
var fs []FileEntry
ls, fs, err = (Scanner{}).Analyze(ctx, p.Path, rules, func(n int, s string) { emit(s, n, s) })
diff --git a/cloudbind.go b/cloudbind.go
index 75478e0..fcffb50 100644
--- a/cloudbind.go
+++ b/cloudbind.go
@@ -111,7 +111,75 @@ func (a *App) rebuildCloudPending(entries []projectDocEntry) {
_ = a.store.SetMeta("cloud_projects_pending", string(b))
}
-// ListCloudPendingProjects 返回云端存在但本机尚未绑定路径的项目。
+// cloudIgnoredSet 已被用户忽略的云端项目名集合(认领弹窗里点了「忽略」)。
+func (a *App) cloudIgnoredSet() map[string]bool {
+ out := map[string]bool{}
+ raw := a.store.Meta("cloud_projects_ignored")
+ if raw == "" {
+ return out
+ }
+ var names []string
+ if json.Unmarshal([]byte(raw), &names) != nil {
+ return out
+ }
+ for _, n := range names {
+ if n = strings.TrimSpace(n); n != "" {
+ out[n] = true
+ }
+ }
+ return out
+}
+
+func (a *App) saveCloudIgnored(set map[string]bool) {
+ names := make([]string, 0, len(set))
+ for n, on := range set {
+ if on {
+ names = append(names, n)
+ }
+ }
+ b, _ := json.Marshal(names)
+ _ = a.store.SetMeta("cloud_projects_ignored", string(b))
+}
+
+// IgnoreCloudProject 忽略某个云端待认领项目(仅本机记忆,可随时恢复)。
+func (a *App) IgnoreCloudProject(name string) error {
+ if e := a.ready(); e != nil {
+ return e
+ }
+ name = strings.TrimSpace(name)
+ if name == "" {
+ return errors.New("NAME_REQUIRED")
+ }
+ set := a.cloudIgnoredSet()
+ set[name] = true
+ a.saveCloudIgnored(set)
+ return nil
+}
+
+// UnignoreCloudProject 恢复被忽略的云端项目,使其重新出现在待认领清单。
+func (a *App) UnignoreCloudProject(name string) error {
+ if e := a.ready(); e != nil {
+ return e
+ }
+ set := a.cloudIgnoredSet()
+ delete(set, strings.TrimSpace(name))
+ a.saveCloudIgnored(set)
+ return nil
+}
+
+// ListCloudIgnoredProjects 返回被忽略的云端项目名(供认领弹窗「已忽略」区展示)。
+func (a *App) ListCloudIgnoredProjects() ([]string, error) {
+ if e := a.ready(); e != nil {
+ return nil, e
+ }
+ out := []string{}
+ for n := range a.cloudIgnoredSet() {
+ out = append(out, n)
+ }
+ return out, nil
+}
+
+// ListCloudPendingProjects 返回云端存在但本机尚未绑定路径、且未被忽略的项目。
func (a *App) ListCloudPendingProjects() ([]projectDocEntry, error) {
if e := a.ready(); e != nil {
return nil, e
@@ -125,7 +193,11 @@ func (a *App) ListCloudPendingProjects() ([]projectDocEntry, error) {
if json.Unmarshal([]byte(raw), &list) != nil {
return out, nil
}
+ ignored := a.cloudIgnoredSet()
for _, it := range list {
+ if ignored[strings.TrimSpace(it.Name)] {
+ continue
+ }
// 本地可能在同步间隙手动建了同名项目,再过滤一遍
var pid int64
if a.store.db.QueryRow(`SELECT id FROM projects WHERE name=? LIMIT 1`, it.Name).Scan(&pid) == sql.ErrNoRows {
@@ -189,6 +261,17 @@ func (a *App) BindCloudProject(name, path string) (Project, error) {
if entry.Favorite {
_, _ = a.store.db.Exec(`INSERT OR IGNORE INTO favorites(project_id,created_at) VALUES(?,?)`, pid, now)
}
+ // 云端身份携带的项目级排除规则一并落地
+ if len(entry.Rules) > 0 {
+ if e := a.applyProjectRules(pid, entry.Rules); e != nil {
+ return Project{}, e
+ }
+ }
+ // 认领后如仍在忽略清单里,顺手移除
+ if set := a.cloudIgnoredSet(); set[name] {
+ delete(set, name)
+ a.saveCloudIgnored(set)
+ }
a.store.Log("info", "同步", "云端项目已绑定本机目录", name+" → "+path)
if a.syncUserID() > 0 {
go a.syncOnce(true)
diff --git a/database.go b/database.go
index ef9ad65..af94358 100644
--- a/database.go
+++ b/database.go
@@ -73,7 +73,8 @@ func OpenStore(path string) (*Store, error) {
// v11:launch_apps 新增 category(我的应用分类筛选)。
// v12:launch_categories 一级分类;launch_apps.category_id;projects.icon;kind_icons。
// v13:pack_tasks / pack_task_logs(打包任务与控制台输出,仅本地,不同步)。
-const schemaVersion = 13
+// v14:exclusion_rules 新增 project_id(0=全局),唯一约束改为 (project_id,pattern)。
+const schemaVersion = 14
func (s *Store) migrate() error {
// PRAGMA 是连接级/文件级配置,不属于迁移,每次打开都需执行。
@@ -104,7 +105,7 @@ func (s *Store) migrate() error {
`CREATE TABLE IF NOT EXISTS contributors(project_id INTEGER NOT NULL, email TEXT NOT NULL, name TEXT NOT NULL, commits INTEGER NOT NULL, added INTEGER NOT NULL, deleted INTEGER NOT NULL, PRIMARY KEY(project_id,email), FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE)`,
`CREATE TABLE IF NOT EXISTS project_insights(project_id INTEGER PRIMARY KEY, health_score INTEGER NOT NULL, generated_at TEXT NOT NULL, high INTEGER NOT NULL, medium INTEGER NOT NULL, low INTEGER NOT NULL, todo_count INTEGER NOT NULL, long_files INTEGER NOT NULL, large_files INTEGER NOT NULL, FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE)`,
`CREATE TABLE IF NOT EXISTS insight_issues(project_id INTEGER NOT NULL, idx INTEGER NOT NULL, severity TEXT NOT NULL, type TEXT NOT NULL, title TEXT NOT NULL, detail TEXT NOT NULL, path TEXT NOT NULL DEFAULT '', line INTEGER NOT NULL DEFAULT 0, suggestion TEXT NOT NULL DEFAULT '', evidence TEXT NOT NULL DEFAULT '', PRIMARY KEY(project_id,idx), FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE)`,
- `CREATE TABLE IF NOT EXISTS exclusion_rules(id INTEGER PRIMARY KEY AUTOINCREMENT, pattern TEXT NOT NULL UNIQUE, category TEXT NOT NULL, builtin INTEGER NOT NULL DEFAULT 0)`,
+ `CREATE TABLE IF NOT EXISTS exclusion_rules(id INTEGER PRIMARY KEY AUTOINCREMENT, pattern TEXT NOT NULL, category TEXT NOT NULL, builtin INTEGER NOT NULL DEFAULT 0, project_id INTEGER NOT NULL DEFAULT 0, UNIQUE(project_id,pattern))`,
`CREATE TABLE IF NOT EXISTS app_logs(id INTEGER PRIMARY KEY AUTOINCREMENT, level TEXT NOT NULL, category TEXT NOT NULL, message TEXT NOT NULL, detail TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL)`,
`CREATE TABLE IF NOT EXISTS git_hotspots(project_id INTEGER NOT NULL, path TEXT NOT NULL, changes INTEGER NOT NULL, added INTEGER NOT NULL, deleted INTEGER NOT NULL, PRIMARY KEY(project_id,path), FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE)`,
`CREATE TABLE IF NOT EXISTS todos(id INTEGER PRIMARY KEY AUTOINCREMENT, uuid TEXT NOT NULL UNIQUE, title TEXT NOT NULL, content TEXT NOT NULL DEFAULT '', project_id INTEGER NOT NULL DEFAULT 0, due_at TEXT NOT NULL DEFAULT '', priority TEXT NOT NULL DEFAULT 'medium', status TEXT NOT NULL DEFAULT 'open', reminded INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, deleted INTEGER NOT NULL DEFAULT 0, dirty INTEGER NOT NULL DEFAULT 1, history TEXT NOT NULL DEFAULT '', team_id INTEGER NOT NULL DEFAULT 0)`,
@@ -193,6 +194,21 @@ func (s *Store) migrate() error {
return fmt.Errorf("database migration: %w", err)
}
}
+ // 排除规则支持项目级(project_id>0);旧表 pattern 全局唯一,需重建为 (project_id,pattern) 唯一。
+ if ok, err := s.columnExists("exclusion_rules", "project_id"); err != nil {
+ return err
+ } else if !ok {
+ for _, q := range []string{
+ `CREATE TABLE exclusion_rules_new(id INTEGER PRIMARY KEY AUTOINCREMENT, pattern TEXT NOT NULL, category TEXT NOT NULL, builtin INTEGER NOT NULL DEFAULT 0, project_id INTEGER NOT NULL DEFAULT 0, UNIQUE(project_id,pattern))`,
+ `INSERT INTO exclusion_rules_new(id,pattern,category,builtin,project_id) SELECT id,pattern,category,builtin,0 FROM exclusion_rules`,
+ `DROP TABLE exclusion_rules`,
+ `ALTER TABLE exclusion_rules_new RENAME TO exclusion_rules`,
+ } {
+ if _, err = s.db.Exec(q); err != nil {
+ return fmt.Errorf("database migration: %w", err)
+ }
+ }
+ }
_, _ = s.db.Exec(`INSERT OR IGNORE INTO project_groups(id,name,created_at,updated_at) VALUES(1,'My Projects',?,?)`, time.Now().Format(time.RFC3339), time.Now().Format(time.RFC3339))
_, _ = s.db.Exec(`UPDATE projects SET group_id=1 WHERE group_id IS NULL OR group_id=0`)
defaults := map[string][]string{"general": {".git", ".idea", ".vscode", "node_modules", "dist", "coverage", "*.min.js", "*.min.css", ".DS_Store"}, "php": {"vendor", "storage/framework", "bootstrap/cache", ".phpunit.cache"}, "go": {"go.sum"}, "vue": {".nuxt", ".next", ".output", "unpackage"}}
@@ -594,8 +610,23 @@ func (s *Store) Structure(id int64) (StructureStats, error) {
return x, r.Err()
}
+// Rules 全局排除规则(project_id=0,含内置)。
func (s *Store) Rules() ([]ExclusionRule, error) {
- r, e := s.db.Query(`SELECT id,pattern,category,builtin FROM exclusion_rules ORDER BY category,builtin DESC,pattern`)
+ return s.queryRules(`SELECT id,pattern,category,builtin,project_id FROM exclusion_rules WHERE project_id=0 ORDER BY category,builtin DESC,pattern`)
+}
+
+// ProjectRules 某项目专属规则。
+func (s *Store) ProjectRules(projectID int64) ([]ExclusionRule, error) {
+ return s.queryRules(`SELECT id,pattern,category,builtin,project_id FROM exclusion_rules WHERE project_id=? ORDER BY category,pattern`, projectID)
+}
+
+// RulesForProject 扫描用:全局规则 + 项目专属规则叠加。
+func (s *Store) RulesForProject(projectID int64) ([]ExclusionRule, error) {
+ return s.queryRules(`SELECT id,pattern,category,builtin,project_id FROM exclusion_rules WHERE project_id IN (0,?) ORDER BY project_id,category,builtin DESC,pattern`, projectID)
+}
+
+func (s *Store) queryRules(q string, args ...any) ([]ExclusionRule, error) {
+ r, e := s.db.Query(q, args...)
if e != nil {
return nil, e
}
@@ -604,7 +635,7 @@ func (s *Store) Rules() ([]ExclusionRule, error) {
for r.Next() {
var x ExclusionRule
var b int
- if e = r.Scan(&x.ID, &x.Pattern, &x.Category, &b); e != nil {
+ if e = r.Scan(&x.ID, &x.Pattern, &x.Category, &b, &x.ProjectID); e != nil {
return nil, e
}
x.Builtin = b == 1
@@ -612,7 +643,13 @@ func (s *Store) Rules() ([]ExclusionRule, error) {
}
return o, r.Err()
}
+
func (s *Store) AddRule(pattern, category string) (ExclusionRule, error) {
+ return s.AddProjectRule(0, pattern, category)
+}
+
+// AddProjectRule projectID=0 为全局规则,>0 为项目专属。
+func (s *Store) AddProjectRule(projectID int64, pattern, category string) (ExclusionRule, error) {
pattern = strings.TrimSpace(pattern)
if pattern == "" {
return ExclusionRule{}, errors.New("PATTERN_REQUIRED")
@@ -620,12 +657,12 @@ func (s *Store) AddRule(pattern, category string) (ExclusionRule, error) {
if category == "" {
category = "custom"
}
- r, e := s.db.Exec(`INSERT INTO exclusion_rules(pattern,category,builtin) VALUES(?,?,0)`, pattern, category)
+ r, e := s.db.Exec(`INSERT INTO exclusion_rules(pattern,category,builtin,project_id) VALUES(?,?,0,?)`, pattern, category, projectID)
if e != nil {
return ExclusionRule{}, e
}
id, _ := r.LastInsertId()
- return ExclusionRule{ID: id, Pattern: pattern, Category: category}, nil
+ return ExclusionRule{ID: id, Pattern: pattern, Category: category, ProjectID: projectID}, nil
}
func (s *Store) DeleteRule(id int64) error {
r, e := s.db.Exec(`DELETE FROM exclusion_rules WHERE id=? AND builtin=0`, id)
@@ -751,7 +788,7 @@ func (s *Store) LogCategories() ([]string, error) {
}
func (s *Store) Settings() (AppSettings, error) {
x := AppSettings{DatabasePath: s.path, Theme: "dark", Locale: "zh-CN", GitScope: "current", AutoRefresh: true, GlassOpacity: 55, LoadingStyle: "fullscreen-orbit",
- MinimizeToTray: true, AutoUpdateMode: "daily", AutoUpdateInterval: 1, AutoUpdateTime: "09:00", AIProvider: "spark", ImageMode: "base64"}
+ MinimizeToTray: true, AutoUpdateMode: "daily", AutoUpdateInterval: 1, AutoUpdateTime: "09:00", AIProvider: "spark", ImageMode: "base64", ProjectSyncMode: "auto"}
r, e := s.db.Query(`SELECT key,value FROM settings`)
if e != nil {
return x, e
@@ -809,6 +846,10 @@ func (s *Store) Settings() (AppSettings, error) {
if v == "base64" || v == "path" || v == "server" {
x.ImageMode = v
}
+ case "projectSyncMode":
+ if v == "auto" || v == "manual" {
+ x.ProjectSyncMode = v
+ }
case "autoUpdateTime":
if len(v) == 5 && v[2] == ':' {
x.AutoUpdateTime = v
@@ -839,10 +880,13 @@ func (s *Store) SaveSettings(x AppSettings) error {
if x.ImageMode != "base64" && x.ImageMode != "path" && x.ImageMode != "server" {
x.ImageMode = "base64"
}
+ if x.ProjectSyncMode != "auto" && x.ProjectSyncMode != "manual" {
+ x.ProjectSyncMode = "auto"
+ }
vals := map[string]string{"theme": x.Theme, "locale": x.Locale, "gitScope": x.GitScope, "autoRefresh": fmt.Sprint(x.AutoRefresh), "glassOpacity": strconv.Itoa(x.GlassOpacity), "loadingStyle": x.LoadingStyle, "loadingStyleSet": "true",
"minimizeToTray": fmt.Sprint(x.MinimizeToTray), "autoUpdateEnabled": fmt.Sprint(x.AutoUpdateEnabled), "autoUpdateMode": x.AutoUpdateMode, "autoUpdateInterval": strconv.Itoa(x.AutoUpdateInterval), "autoUpdateTime": x.AutoUpdateTime,
"aiProvider": x.AIProvider, "sparkKey": x.SparkKey, "deepSeekKey": x.DeepSeekKey, "syncApiKeys": fmt.Sprint(x.SyncAPIKeys),
- "avatarMode": x.AvatarMode, "avatarValue": x.AvatarValue, "imageMode": x.ImageMode}
+ "avatarMode": x.AvatarMode, "avatarValue": x.AvatarValue, "imageMode": x.ImageMode, "projectSyncMode": x.ProjectSyncMode}
if x.AvatarMode != "base64" && x.AvatarMode != "url" && x.AvatarMode != "path" {
vals["avatarMode"], vals["avatarValue"] = "", ""
}
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index 065c0f2..2ce32bb 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -17,6 +17,7 @@ import NoteCenter from './components/NoteCenter.vue'
import LocalPackCenter from './components/LocalPackCenter.vue'
import TeamSwitcher from './components/TeamSwitcher.vue'
import TitleBar from './components/TitleBar.vue'
+import CloudClaimModal from './components/CloudClaimModal.vue'
import { call, isNative, on } from './api'
const route = useRoute()
@@ -369,6 +370,7 @@ watch(activeTask, task => {