A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
at lambda-fork/main 1177 lines 35 kB view raw
1// SiYuan - Refactor your thinking 2// Copyright (c) 2020-present, b3log.org 3// 4// This program is free software: you can redistribute it and/or modify 5// it under the terms of the GNU Affero General Public License as published by 6// the Free Software Foundation, either version 3 of the License, or 7// (at your option) any later version. 8// 9// This program is distributed in the hope that it will be useful, 10// but WITHOUT ANY WARRANTY; without even the implied warranty of 11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12// GNU Affero General Public License for more details. 13// 14// You should have received a copy of the GNU Affero General Public License 15// along with this program. If not, see <https://www.gnu.org/licenses/>. 16 17package model 18 19import ( 20 "bytes" 21 "crypto/sha1" 22 "fmt" 23 "os" 24 "path/filepath" 25 "runtime" 26 "sort" 27 "strconv" 28 "strings" 29 "sync" 30 "time" 31 32 "github.com/88250/gulu" 33 "github.com/88250/lute" 34 "github.com/88250/lute/ast" 35 "github.com/Xuanwo/go-locale" 36 "github.com/sashabaranov/go-openai" 37 "github.com/siyuan-note/eventbus" 38 "github.com/siyuan-note/filelock" 39 "github.com/siyuan-note/logging" 40 "github.com/siyuan-note/siyuan/kernel/conf" 41 "github.com/siyuan-note/siyuan/kernel/sql" 42 "github.com/siyuan-note/siyuan/kernel/task" 43 "github.com/siyuan-note/siyuan/kernel/treenode" 44 "github.com/siyuan-note/siyuan/kernel/util" 45 "golang.org/x/mod/semver" 46 "golang.org/x/text/language" 47) 48 49var Conf *AppConf 50 51// AppConf 维护应用元数据,保存在 ~/.siyuan/conf.json。 52type AppConf struct { 53 LogLevel string `json:"logLevel"` // 日志级别:off, trace, debug, info, warn, error, fatal 54 Appearance *conf.Appearance `json:"appearance"` // 外观 55 Langs []*conf.Lang `json:"langs"` // 界面语言列表 56 Lang string `json:"lang"` // 选择的界面语言,同 Appearance.Lang 57 FileTree *conf.FileTree `json:"fileTree"` // 文档面板 58 Tag *conf.Tag `json:"tag"` // 标签面板 59 Editor *conf.Editor `json:"editor"` // 编辑器配置 60 Export *conf.Export `json:"export"` // 导出配置 61 Graph *conf.Graph `json:"graph"` // 关系图配置 62 UILayout *conf.UILayout `json:"uiLayout"` // 界面布局。不要直接使用,使用 GetUILayout() 和 SetUILayout() 方法 63 UserData string `json:"userData"` // 社区用户信息,对 User 加密存储 64 User *conf.User `json:"-"` // 社区用户内存结构,不持久化。不要直接使用,使用 GetUser() 和 SetUser() 方法 65 ReadOnly bool `json:"readonly"` // 是否是以只读模式运行 66 LocalIPs []string `json:"localIPs"` // 本地 IP 列表 67 AccessAuthCode string `json:"accessAuthCode"` // 访问授权码 68 System *conf.System `json:"system"` // 系统配置 69 Keymap *conf.Keymap `json:"keymap"` // 快捷键配置 70 Sync *conf.Sync `json:"sync"` // 同步配置 71 Search *conf.Search `json:"search"` // 搜索配置 72 Flashcard *conf.Flashcard `json:"flashcard"` // 闪卡配置 73 AI *conf.AI `json:"ai"` // 人工智能配置 74 Bazaar *conf.Bazaar `json:"bazaar"` // 集市配置 75 Stat *conf.Stat `json:"stat"` // 统计 76 Api *conf.API `json:"api"` // API 77 Repo *conf.Repo `json:"repo"` // 数据仓库 78 Publish *conf.Publish `json:"publish"` // 发布服务 79 OpenHelp bool `json:"openHelp"` // 启动后是否需要打开用户指南 80 ShowChangelog bool `json:"showChangelog"` // 是否显示版本更新日志 81 CloudRegion int `json:"cloudRegion"` // 云端区域,0:中国大陆,1:北美 82 Snippet *conf.Snpt `json:"snippet"` // 代码片段 83 DataIndexState int `json:"dataIndexState"` // 数据索引状态,0:已索引,1:未索引 84 85 m *sync.Mutex 86} 87 88func NewAppConf() *AppConf { 89 return &AppConf{LogLevel: "debug", m: &sync.Mutex{}} 90} 91 92func (conf *AppConf) GetUILayout() *conf.UILayout { 93 conf.m.Lock() 94 defer conf.m.Unlock() 95 return conf.UILayout 96} 97 98func (conf *AppConf) SetUILayout(uiLayout *conf.UILayout) { 99 conf.m.Lock() 100 defer conf.m.Unlock() 101 conf.UILayout = uiLayout 102} 103 104func (conf *AppConf) GetUser() *conf.User { 105 conf.m.Lock() 106 defer conf.m.Unlock() 107 return conf.User 108} 109 110func (conf *AppConf) SetUser(user *conf.User) { 111 conf.m.Lock() 112 defer conf.m.Unlock() 113 conf.User = user 114} 115 116func InitConf() { 117 initLang() 118 119 Conf = NewAppConf() 120 confPath := filepath.Join(util.ConfDir, "conf.json") 121 if gulu.File.IsExist(confPath) { 122 if data, err := os.ReadFile(confPath); err != nil { 123 logging.LogErrorf("load conf [%s] failed: %s", confPath, err) 124 } else { 125 if err = gulu.JSON.UnmarshalJSON(data, Conf); err != nil { 126 logging.LogErrorf("parse conf [%s] failed: %s", confPath, err) 127 } else { 128 logging.LogInfof("loaded conf [%s]", confPath) 129 } 130 } 131 } 132 133 if "" != util.Lang { 134 initialized := false 135 if util.ContainerAndroid == util.Container || util.ContainerIOS == util.Container || util.ContainerHarmony == util.Container { 136 // 移动端以上次设置的外观语言为准 137 if "" != Conf.Lang && util.Lang != Conf.Lang { 138 util.Lang = Conf.Lang 139 logging.LogInfof("use the last specified language [%s]", util.Lang) 140 initialized = true 141 } 142 } 143 144 if !initialized { 145 Conf.Lang = util.Lang 146 logging.LogInfof("initialized the specified language [%s]", util.Lang) 147 } 148 } else { 149 if "" == Conf.Lang { 150 // 未指定外观语言时使用系统语言 151 152 if userLang, err := locale.Detect(); err == nil { 153 var supportLangs []language.Tag 154 for lang := range util.Langs { 155 if tag, err := language.Parse(lang); err == nil { 156 supportLangs = append(supportLangs, tag) 157 } else { 158 logging.LogErrorf("load language [%s] failed: %s", lang, err) 159 } 160 } 161 matcher := language.NewMatcher(supportLangs) 162 lang, _, _ := matcher.Match(userLang) 163 base, _ := lang.Base() 164 region, _ := lang.Region() 165 util.Lang = base.String() + "_" + region.String() 166 Conf.Lang = util.Lang 167 logging.LogInfof("initialized language [%s] based on device locale", Conf.Lang) 168 } else { 169 logging.LogDebugf("check device locale failed [%s], using default language [en_US]", err) 170 util.Lang = "en_US" 171 Conf.Lang = util.Lang 172 } 173 } 174 util.Lang = Conf.Lang 175 } 176 177 Conf.Langs = loadLangs() 178 if nil == Conf.Appearance { 179 Conf.Appearance = conf.NewAppearance() 180 } 181 var langOK bool 182 for _, l := range Conf.Langs { 183 if Conf.Lang == l.Name { 184 langOK = true 185 break 186 } 187 } 188 if !langOK { 189 Conf.Lang = "en_US" 190 util.Lang = Conf.Lang 191 } 192 Conf.Appearance.Lang = Conf.Lang 193 if nil == Conf.UILayout { 194 Conf.UILayout = &conf.UILayout{} 195 } 196 if nil == Conf.Keymap { 197 Conf.Keymap = &conf.Keymap{} 198 } 199 if "" == Conf.Appearance.CodeBlockThemeDark { 200 Conf.Appearance.CodeBlockThemeDark = "dracula" 201 } 202 if "" == Conf.Appearance.CodeBlockThemeLight { 203 Conf.Appearance.CodeBlockThemeLight = "github" 204 } 205 if nil == Conf.FileTree { 206 Conf.FileTree = conf.NewFileTree() 207 } 208 if 1 > Conf.FileTree.MaxListCount { 209 Conf.FileTree.MaxListCount = 512 210 } 211 if 1 > Conf.FileTree.MaxOpenTabCount { 212 Conf.FileTree.MaxOpenTabCount = 8 213 } 214 if 32 < Conf.FileTree.MaxOpenTabCount { 215 Conf.FileTree.MaxOpenTabCount = 32 216 } 217 Conf.FileTree.DocCreateSavePath = util.TrimSpaceInPath(Conf.FileTree.DocCreateSavePath) 218 Conf.FileTree.RefCreateSavePath = util.TrimSpaceInPath(Conf.FileTree.RefCreateSavePath) 219 util.UseSingleLineSave = Conf.FileTree.UseSingleLineSave 220 if 2 > Conf.FileTree.LargeFileWarningSize { 221 Conf.FileTree.LargeFileWarningSize = 8 222 } 223 util.LargeFileWarningSize = Conf.FileTree.LargeFileWarningSize 224 225 util.CurrentCloudRegion = Conf.CloudRegion 226 227 if nil == Conf.Tag { 228 Conf.Tag = conf.NewTag() 229 } 230 231 if nil == Conf.Editor { 232 Conf.Editor = conf.NewEditor() 233 } 234 if 1 > len(Conf.Editor.Emoji) { 235 Conf.Editor.Emoji = []string{} 236 } 237 for i, emoji := range Conf.Editor.Emoji { 238 if strings.Contains(emoji, ".") { 239 // XSS through emoji name https://github.com/siyuan-note/siyuan/issues/15034 240 emoji = util.FilterUploadEmojiFileName(emoji) 241 Conf.Editor.Emoji[i] = emoji 242 } 243 } 244 if 9 > Conf.Editor.FontSize || 72 < Conf.Editor.FontSize { 245 Conf.Editor.FontSize = 16 246 } 247 if "" == Conf.Editor.PlantUMLServePath { 248 Conf.Editor.PlantUMLServePath = "https://www.plantuml.com/plantuml/svg/~1" 249 } 250 if 1 > Conf.Editor.BlockRefDynamicAnchorTextMaxLen { 251 Conf.Editor.BlockRefDynamicAnchorTextMaxLen = 64 252 } 253 if 5120 < Conf.Editor.BlockRefDynamicAnchorTextMaxLen { 254 Conf.Editor.BlockRefDynamicAnchorTextMaxLen = 5120 255 } 256 if 1440 < Conf.Editor.GenerateHistoryInterval { 257 Conf.Editor.GenerateHistoryInterval = 1440 258 } 259 if 1 > Conf.Editor.HistoryRetentionDays { 260 Conf.Editor.HistoryRetentionDays = 30 261 } 262 if 3650 < Conf.Editor.HistoryRetentionDays { 263 Conf.Editor.HistoryRetentionDays = 3650 264 } 265 if conf.MinDynamicLoadBlocks > Conf.Editor.DynamicLoadBlocks { 266 Conf.Editor.DynamicLoadBlocks = conf.MinDynamicLoadBlocks 267 } 268 if 0 > Conf.Editor.BacklinkExpandCount { 269 Conf.Editor.BacklinkExpandCount = 0 270 } 271 if -1 > Conf.Editor.BackmentionExpandCount { 272 Conf.Editor.BackmentionExpandCount = -1 273 } 274 if nil == Conf.Editor.Markdown { 275 Conf.Editor.Markdown = &util.Markdown{} 276 } 277 util.MarkdownSettings = Conf.Editor.Markdown 278 279 if nil == Conf.Export { 280 Conf.Export = conf.NewExport() 281 } 282 if 0 == Conf.Export.BlockRefMode || 1 == Conf.Export.BlockRefMode || 5 == Conf.Export.BlockRefMode { 283 // 废弃导出选项引用块转换为原始块和引述块 https://github.com/siyuan-note/siyuan/issues/3155 284 // 锚点哈希模式和脚注模式合并 https://github.com/siyuan-note/siyuan/issues/13331 285 Conf.Export.BlockRefMode = 4 // 改为脚注+锚点哈希 286 } 287 if "" == Conf.Export.PandocBin { 288 Conf.Export.PandocBin = util.PandocBinPath 289 } 290 291 if nil == Conf.Graph || nil == Conf.Graph.Local || nil == Conf.Graph.Global { 292 Conf.Graph = conf.NewGraph() 293 } 294 295 if nil == Conf.System { 296 Conf.System = conf.NewSystem() 297 if util.ContainerIOS != util.Container { 298 Conf.OpenHelp = true 299 } 300 } else { 301 if 0 < semver.Compare("v"+util.Ver, "v"+Conf.System.KernelVersion) { 302 logging.LogInfof("upgraded from version [%s] to [%s]", Conf.System.KernelVersion, util.Ver) 303 Conf.ShowChangelog = true 304 } else if 0 > semver.Compare("v"+util.Ver, "v"+Conf.System.KernelVersion) { 305 logging.LogInfof("downgraded from version [%s] to [%s]", Conf.System.KernelVersion, util.Ver) 306 } 307 308 Conf.System.KernelVersion = util.Ver 309 Conf.System.IsInsider = util.IsInsider 310 } 311 if nil == Conf.System.NetworkProxy { 312 Conf.System.NetworkProxy = &conf.NetworkProxy{} 313 } 314 if "" == Conf.System.ID { 315 Conf.System.ID = util.GetDeviceID() 316 } 317 if "" == Conf.System.Name { 318 Conf.System.Name = util.GetDeviceName() 319 } 320 if util.ContainerStd == util.Container { 321 Conf.System.ID = util.GetDeviceID() 322 Conf.System.Name = util.GetDeviceName() 323 } 324 Conf.System.DisabledFeatures = util.DisabledFeatures 325 if 1 > len(Conf.System.DisabledFeatures) { 326 Conf.System.DisabledFeatures = []string{} 327 } 328 329 if nil == Conf.Snippet { 330 Conf.Snippet = conf.NewSnpt() 331 } 332 333 Conf.System.AppDir = util.WorkingDir 334 Conf.System.ConfDir = util.ConfDir 335 Conf.System.HomeDir = util.HomeDir 336 Conf.System.WorkspaceDir = util.WorkspaceDir 337 Conf.System.DataDir = util.DataDir 338 Conf.System.Container = util.Container 339 Conf.System.IsMicrosoftStore = util.ISMicrosoftStore 340 if util.ISMicrosoftStore { 341 logging.LogInfof("using Microsoft Store edition") 342 } 343 Conf.System.OS = runtime.GOOS 344 Conf.System.OSPlatform = util.GetOSPlatform() 345 346 if "" != Conf.UserData { 347 Conf.SetUser(loadUserFromConf()) 348 } 349 350 if nil == Conf.Sync { 351 Conf.Sync = conf.NewSync() 352 } 353 if 0 == Conf.Sync.Mode { 354 Conf.Sync.Mode = 1 355 } 356 if 30 > Conf.Sync.Interval { 357 Conf.Sync.Interval = 30 358 } 359 if 60*60*12 < Conf.Sync.Interval { 360 Conf.Sync.Interval = 60 * 60 * 12 361 } 362 if nil == Conf.Sync.S3 { 363 Conf.Sync.S3 = &conf.S3{PathStyle: true, SkipTlsVerify: true} 364 } 365 Conf.Sync.S3.Endpoint = util.NormalizeEndpoint(Conf.Sync.S3.Endpoint) 366 Conf.Sync.S3.Timeout = util.NormalizeTimeout(Conf.Sync.S3.Timeout) 367 Conf.Sync.S3.ConcurrentReqs = util.NormalizeConcurrentReqs(Conf.Sync.S3.ConcurrentReqs, conf.ProviderS3) 368 if nil == Conf.Sync.WebDAV { 369 Conf.Sync.WebDAV = &conf.WebDAV{SkipTlsVerify: true} 370 } 371 Conf.Sync.WebDAV.Endpoint = util.NormalizeEndpoint(Conf.Sync.WebDAV.Endpoint) 372 Conf.Sync.WebDAV.Timeout = util.NormalizeTimeout(Conf.Sync.WebDAV.Timeout) 373 Conf.Sync.WebDAV.ConcurrentReqs = util.NormalizeConcurrentReqs(Conf.Sync.WebDAV.ConcurrentReqs, conf.ProviderWebDAV) 374 if nil == Conf.Sync.Local { 375 Conf.Sync.Local = &conf.Local{} 376 } 377 Conf.Sync.Local.Endpoint = util.NormalizeLocalPath(Conf.Sync.Local.Endpoint) 378 Conf.Sync.Local.Timeout = util.NormalizeTimeout(Conf.Sync.Local.Timeout) 379 Conf.Sync.Local.ConcurrentReqs = util.NormalizeConcurrentReqs(Conf.Sync.Local.ConcurrentReqs, conf.ProviderLocal) 380 381 if util.ContainerDocker == util.Container { 382 Conf.Sync.Perception = false 383 } 384 385 if nil == Conf.Api { 386 Conf.Api = conf.NewAPI() 387 } 388 389 if nil == Conf.Bazaar { 390 Conf.Bazaar = conf.NewBazaar() 391 } 392 393 if nil == Conf.Publish { 394 Conf.Publish = conf.NewPublish() 395 } 396 if Conf.OpenHelp && Conf.Publish.Enable { 397 Conf.OpenHelp = false 398 } 399 400 if nil == Conf.Repo { 401 Conf.Repo = conf.NewRepo() 402 } 403 if timingEnv := os.Getenv("SIYUAN_SYNC_INDEX_TIMING"); "" != timingEnv { 404 val, err := strconv.Atoi(timingEnv) 405 if err == nil { 406 Conf.Repo.SyncIndexTiming = int64(val) 407 } 408 } 409 if 12000 > Conf.Repo.SyncIndexTiming { 410 Conf.Repo.SyncIndexTiming = 12 * 1000 411 } 412 if 1 > Conf.Repo.IndexRetentionDays { 413 Conf.Repo.IndexRetentionDays = 180 414 } 415 if 1 > Conf.Repo.RetentionIndexesDaily { 416 Conf.Repo.RetentionIndexesDaily = 2 417 } 418 if 0 < len(Conf.Repo.Key) { 419 logging.LogInfof("repo key [%x]", sha1.Sum(Conf.Repo.Key)) 420 } 421 422 if nil == Conf.Search { 423 Conf.Search = conf.NewSearch() 424 } 425 if 1 > Conf.Search.Limit { 426 Conf.Search.Limit = 64 427 } 428 if 32 > Conf.Search.Limit { 429 Conf.Search.Limit = 32 430 } 431 if 1 > Conf.Search.BacklinkMentionKeywordsLimit { 432 Conf.Search.BacklinkMentionKeywordsLimit = 512 433 } 434 435 if nil == Conf.Stat { 436 Conf.Stat = conf.NewStat() 437 } 438 439 if nil == Conf.Flashcard { 440 Conf.Flashcard = conf.NewFlashcard() 441 } 442 if 0 > Conf.Flashcard.NewCardLimit { 443 Conf.Flashcard.NewCardLimit = 20 444 } 445 if 0 > Conf.Flashcard.ReviewCardLimit { 446 Conf.Flashcard.ReviewCardLimit = 200 447 } 448 if 0 >= Conf.Flashcard.RequestRetention || 1 <= Conf.Flashcard.RequestRetention { 449 Conf.Flashcard.RequestRetention = conf.NewFlashcard().RequestRetention 450 } 451 if 0 >= Conf.Flashcard.MaximumInterval || 36500 <= Conf.Flashcard.MaximumInterval { 452 Conf.Flashcard.MaximumInterval = conf.NewFlashcard().MaximumInterval 453 } 454 if "" == Conf.Flashcard.Weights { 455 Conf.Flashcard.Weights = conf.NewFlashcard().Weights 456 } 457 if 19 != len(strings.Split(Conf.Flashcard.Weights, ",")) { 458 defaultWeights := conf.DefaultFSRSWeights() 459 msg := "fsrs store weights length must be [19]" 460 logging.LogWarnf("%s , given [%s], reset to default weights [%s]", msg, Conf.Flashcard.Weights, defaultWeights) 461 Conf.Flashcard.Weights = defaultWeights 462 go func() { 463 util.WaitForUILoaded() 464 task.AppendAsyncTaskWithDelay(task.PushMsg, 2*time.Second, util.PushErrMsg, msg, 15000) 465 }() 466 } 467 isInvalidFlashcardWeights := false 468 for _, w := range strings.Split(Conf.Flashcard.Weights, ",") { 469 if _, err := strconv.ParseFloat(strings.TrimSpace(w), 64); err != nil { 470 isInvalidFlashcardWeights = true 471 break 472 } 473 } 474 if isInvalidFlashcardWeights { 475 defaultWeights := conf.DefaultFSRSWeights() 476 msg := "fsrs store weights contain invalid number" 477 logging.LogWarnf("%s, given [%s], reset to default weights [%s]", msg, Conf.Flashcard.Weights, defaultWeights) 478 Conf.Flashcard.Weights = defaultWeights 479 go func() { 480 util.WaitForUILoaded() 481 task.AppendAsyncTaskWithDelay(task.PushMsg, 2*time.Second, util.PushErrMsg, msg, 15000) 482 }() 483 } 484 485 if nil == Conf.AI { 486 Conf.AI = conf.NewAI() 487 } 488 if "" == Conf.AI.OpenAI.APIModel { 489 Conf.AI.OpenAI.APIModel = openai.GPT3Dot5Turbo 490 } 491 if "" == Conf.AI.OpenAI.APIUserAgent { 492 Conf.AI.OpenAI.APIUserAgent = util.UserAgent 493 } 494 if strings.HasPrefix(Conf.AI.OpenAI.APIUserAgent, "SiYuan/") { 495 Conf.AI.OpenAI.APIUserAgent = util.UserAgent 496 } 497 if "" == Conf.AI.OpenAI.APIProvider { 498 Conf.AI.OpenAI.APIProvider = "OpenAI" 499 } 500 if 0 > Conf.AI.OpenAI.APIMaxTokens { 501 Conf.AI.OpenAI.APIMaxTokens = 0 502 } 503 if 0 >= Conf.AI.OpenAI.APITemperature || 2 < Conf.AI.OpenAI.APITemperature { 504 Conf.AI.OpenAI.APITemperature = 1.0 505 } 506 if 1 > Conf.AI.OpenAI.APIMaxContexts || 64 < Conf.AI.OpenAI.APIMaxContexts { 507 Conf.AI.OpenAI.APIMaxContexts = 7 508 } 509 510 if "" != Conf.AI.OpenAI.APIKey { 511 logging.LogInfof("OpenAI API enabled\n"+ 512 " userAgent=%s\n"+ 513 " baseURL=%s\n"+ 514 " timeout=%ds\n"+ 515 " proxy=%s\n"+ 516 " model=%s\n"+ 517 " maxTokens=%d\n"+ 518 " temperature=%.1f\n"+ 519 " maxContexts=%d", 520 Conf.AI.OpenAI.APIUserAgent, 521 Conf.AI.OpenAI.APIBaseURL, 522 Conf.AI.OpenAI.APITimeout, 523 Conf.AI.OpenAI.APIProxy, 524 Conf.AI.OpenAI.APIModel, 525 Conf.AI.OpenAI.APIMaxTokens, 526 Conf.AI.OpenAI.APITemperature, 527 Conf.AI.OpenAI.APIMaxContexts) 528 } 529 530 Conf.ReadOnly = util.ReadOnly 531 532 if "" != util.AccessAuthCode { 533 Conf.AccessAuthCode = util.AccessAuthCode 534 } 535 Conf.AccessAuthCode = strings.TrimSpace(Conf.AccessAuthCode) 536 Conf.AccessAuthCode = util.RemoveInvalid(Conf.AccessAuthCode) 537 538 Conf.LocalIPs = util.GetLocalIPs() 539 540 if 1 == Conf.DataIndexState { 541 // 上次未正常完成数据索引 542 go func() { 543 util.WaitForUILoaded() 544 if util.ContainerIOS == util.Container || util.ContainerAndroid == util.Container || util.ContainerHarmony == util.Container { 545 task.AppendAsyncTaskWithDelay(task.PushMsg, 2*time.Second, util.PushMsg, Conf.language(245), 15000) 546 } else { 547 task.AppendAsyncTaskWithDelay(task.PushMsg, 2*time.Second, util.PushMsg, Conf.language(244), 15000) 548 } 549 }() 550 } 551 552 Conf.DataIndexState = 0 553 554 Conf.Save() 555 logging.SetLogLevel(Conf.LogLevel) 556 557 util.SetNetworkProxy(Conf.System.NetworkProxy.String()) 558 559 go util.InitPandoc() 560 go util.InitTesseract() 561} 562 563func initLang() { 564 p := filepath.Join(util.WorkingDir, "appearance", "langs") 565 dir, err := os.Open(p) 566 if err != nil { 567 logging.LogErrorf("open language configuration folder [%s] failed: %s", p, err) 568 util.ReportFileSysFatalError(err) 569 return 570 } 571 defer dir.Close() 572 573 langNames, err := dir.Readdirnames(-1) 574 if err != nil { 575 logging.LogErrorf("list language configuration folder [%s] failed: %s", p, err) 576 util.ReportFileSysFatalError(err) 577 return 578 } 579 580 for _, langName := range langNames { 581 jsonPath := filepath.Join(p, langName) 582 data, err := os.ReadFile(jsonPath) 583 if err != nil { 584 logging.LogErrorf("read language configuration [%s] failed: %s", jsonPath, err) 585 continue 586 } 587 langMap := map[string]interface{}{} 588 if err := gulu.JSON.UnmarshalJSON(data, &langMap); err != nil { 589 logging.LogErrorf("parse language configuration failed [%s] failed: %s", jsonPath, err) 590 continue 591 } 592 593 kernelMap := map[int]string{} 594 label := langMap["_label"].(string) 595 kernelLangs := langMap["_kernel"].(map[string]interface{}) 596 for k, v := range kernelLangs { 597 num, convErr := strconv.Atoi(k) 598 if nil != convErr { 599 logging.LogErrorf("parse language configuration [%s] item [%d] failed: %s", p, num, convErr) 600 continue 601 } 602 kernelMap[num] = v.(string) 603 } 604 kernelMap[-1] = label 605 name := langName[:strings.LastIndex(langName, ".")] 606 util.Langs[name] = kernelMap 607 608 util.TimeLangs[name] = langMap["_time"].(map[string]interface{}) 609 util.TaskActionLangs[name] = langMap["_taskAction"].(map[string]interface{}) 610 util.TrayMenuLangs[name] = langMap["_trayMenu"].(map[string]interface{}) 611 util.AttrViewLangs[name] = langMap["_attrView"].(map[string]interface{}) 612 } 613} 614 615func loadLangs() (ret []*conf.Lang) { 616 for name, langMap := range util.Langs { 617 lang := &conf.Lang{Label: langMap[-1], Name: name} 618 ret = append(ret, lang) 619 } 620 sort.Slice(ret, func(i, j int) bool { 621 return ret[i].Name < ret[j].Name 622 }) 623 return 624} 625 626var exitLock = sync.Mutex{} 627 628// Close 退出内核进程. 629// 630// force:是否不执行同步过程而直接退出 631// 632// setCurrentWorkspace:是否将当前工作空间放到工作空间列表的最后一个 633// 634// execInstallPkg:是否执行新版本安装包 635// 636// 0:默认按照设置项 System.DownloadInstallPkg 检查并推送提示 637// 1:不执行新版本安装 638// 2:执行新版本安装 639// 640// 返回值 exitCode: 641// 642// 0:正常退出 643// 1:同步执行失败 644// 2:提示新安装包 645// 646// 当 force 为 true(强制退出)并且 execInstallPkg 为 0(默认检查更新)并且同步失败并且新版本安装版已经准备就绪时,执行新版本安装 https://github.com/siyuan-note/siyuan/issues/10288 647func Close(force, setCurrentWorkspace bool, execInstallPkg int) (exitCode int) { 648 exitLock.Lock() 649 defer exitLock.Unlock() 650 651 logging.LogInfof("exiting kernel [force=%v, setCurrentWorkspace=%v, execInstallPkg=%d]", force, setCurrentWorkspace, execInstallPkg) 652 util.PushMsg(Conf.Language(95), 10000*60) 653 FlushTxQueue() 654 655 if !force { 656 if Conf.Sync.Enabled && 3 != Conf.Sync.Mode && 657 ((IsSubscriber() && conf.ProviderSiYuan == Conf.Sync.Provider) || conf.ProviderSiYuan != Conf.Sync.Provider) { 658 syncData(true, false) 659 if 0 != ExitSyncSucc { 660 exitCode = 1 661 return 662 } 663 } 664 } 665 666 // Close the user guide when exiting https://github.com/siyuan-note/siyuan/issues/10322 667 closeUserGuide() 668 669 // Improve indexing completeness when exiting https://github.com/siyuan-note/siyuan/issues/12039 670 sql.FlushQueue() 671 672 util.IsExiting.Store(true) 673 waitSecondForExecInstallPkg := false 674 if !skipNewVerInstallPkg() && "" != newVerInstallPkgPath { 675 if 2 == execInstallPkg || (force && 0 == execInstallPkg) { // 执行新版本安装 676 waitSecondForExecInstallPkg = true 677 if gulu.OS.IsWindows() { 678 util.PushMsg(Conf.Language(130), 1000*30) 679 } 680 go execNewVerInstallPkg(newVerInstallPkgPath) 681 } else if 0 == execInstallPkg { // 新版本安装包已经准备就绪 682 exitCode = 2 683 logging.LogInfof("the new version install pkg is ready [%s], waiting for the user's next instruction", newVerInstallPkgPath) 684 return 685 } 686 } 687 688 Conf.Close() 689 sql.CloseDatabase() 690 util.SaveAssetsTexts() 691 clearWorkspaceTemp() 692 clearCorruptedNotebooks() 693 clearPortJSON() 694 695 if setCurrentWorkspace { 696 // 将当前工作空间放到工作空间列表的最后一个 697 // Open the last workspace by default https://github.com/siyuan-note/siyuan/issues/10570 698 workspacePaths, err := util.ReadWorkspacePaths() 699 if err != nil { 700 logging.LogErrorf("read workspace paths failed: %s", err) 701 } else { 702 workspacePaths = gulu.Str.RemoveElem(workspacePaths, util.WorkspaceDir) 703 workspacePaths = append(workspacePaths, util.WorkspaceDir) 704 util.WriteWorkspacePaths(workspacePaths) 705 } 706 } 707 708 util.UnlockWorkspace() 709 710 time.Sleep(500 * time.Millisecond) 711 if waitSecondForExecInstallPkg { 712 // 桌面端退出拉起更新安装时有时需要重启两次 https://github.com/siyuan-note/siyuan/issues/6544 713 // 这里多等待一段时间,等待安装程序启动 714 if gulu.OS.IsWindows() { 715 time.Sleep(30 * time.Second) 716 } 717 } 718 closeSyncWebSocket() 719 go func() { 720 time.Sleep(500 * time.Millisecond) 721 logging.LogInfof("exited kernel") 722 if nil != util.WebSocketServer { 723 util.WebSocketServer.Close() 724 } 725 util.HttpServing = false 726 os.Exit(logging.ExitCodeOk) 727 }() 728 return 729} 730 731var customEmojis = sync.Map{} 732 733func AddCustomEmoji(emojiName, imgSrc string) { 734 customEmojis.Store(emojiName, imgSrc) 735} 736 737func ClearCustomEmojis() { 738 customEmojis.Clear() 739} 740 741func NewLute() (ret *lute.Lute) { 742 ret = util.NewLute() 743 ret.SetCodeSyntaxHighlightLineNum(Conf.Editor.CodeSyntaxHighlightLineNum) 744 ret.SetChineseParagraphBeginningSpace(Conf.Export.ParagraphBeginningSpace) 745 ret.SetProtyleMarkNetImg(Conf.Editor.DisplayNetImgMark) 746 ret.SetSpellcheck(Conf.Editor.Spellcheck) 747 748 customEmojiMap := map[string]string{} 749 customEmojis.Range(func(key, value interface{}) bool { 750 customEmojiMap[key.(string)] = value.(string) 751 return true 752 }) 753 ret.PutEmojis(customEmojiMap) 754 return 755} 756 757func enableLuteInlineSyntax(luteEngine *lute.Lute) { 758 luteEngine.SetInlineAsterisk(true) 759 luteEngine.SetInlineUnderscore(true) 760 luteEngine.SetSup(true) 761 luteEngine.SetSub(true) 762 luteEngine.SetTag(true) 763 luteEngine.SetInlineMath(true) 764 luteEngine.SetGFMStrikethrough(true) 765} 766 767func (conf *AppConf) Save() { 768 if util.ReadOnly { 769 return 770 } 771 772 Conf.m.Lock() 773 defer Conf.m.Unlock() 774 775 newData, _ := gulu.JSON.MarshalIndentJSON(Conf, "", " ") 776 confPath := filepath.Join(util.ConfDir, "conf.json") 777 oldData, err := filelock.ReadFile(confPath) 778 if err != nil { 779 conf.save0(newData) 780 return 781 } 782 783 if bytes.Equal(newData, oldData) { 784 return 785 } 786 787 conf.save0(newData) 788} 789 790func (conf *AppConf) save0(data []byte) { 791 confPath := filepath.Join(util.ConfDir, "conf.json") 792 if err := filelock.WriteFile(confPath, data); err != nil { 793 logging.LogErrorf("write conf [%s] failed: %s", confPath, err) 794 util.ReportFileSysFatalError(err) 795 return 796 } 797} 798 799func (conf *AppConf) Close() { 800 conf.Save() 801} 802 803func (conf *AppConf) Box(boxID string) *Box { 804 for _, box := range conf.GetOpenedBoxes() { 805 if box.ID == boxID { 806 return box 807 } 808 } 809 return nil 810} 811 812func (conf *AppConf) GetBox(boxID string) *Box { 813 for _, box := range conf.GetBoxes() { 814 if box.ID == boxID { 815 return box 816 } 817 } 818 return nil 819} 820 821func (conf *AppConf) BoxNames(boxIDs []string) (ret map[string]string) { 822 ret = map[string]string{} 823 824 boxes := conf.GetOpenedBoxes() 825 for _, boxID := range boxIDs { 826 for _, box := range boxes { 827 if box.ID == boxID { 828 ret[boxID] = box.Name 829 break 830 } 831 } 832 } 833 return 834} 835 836func (conf *AppConf) GetBoxes() (ret []*Box) { 837 ret = []*Box{} 838 notebooks, err := ListNotebooks() 839 if err != nil { 840 return 841 } 842 843 for _, notebook := range notebooks { 844 id := notebook.ID 845 name := notebook.Name 846 closed := notebook.Closed 847 box := &Box{ID: id, Name: name, Closed: closed} 848 ret = append(ret, box) 849 } 850 return 851} 852 853func (conf *AppConf) GetOpenedBoxes() (ret []*Box) { 854 ret = []*Box{} 855 notebooks, err := ListNotebooks() 856 if err != nil { 857 return 858 } 859 860 for _, notebook := range notebooks { 861 if !notebook.Closed { 862 ret = append(ret, notebook) 863 } 864 } 865 return 866} 867 868func (conf *AppConf) GetClosedBoxes() (ret []*Box) { 869 ret = []*Box{} 870 notebooks, err := ListNotebooks() 871 if err != nil { 872 return 873 } 874 875 for _, notebook := range notebooks { 876 if notebook.Closed { 877 ret = append(ret, notebook) 878 } 879 } 880 return 881} 882 883func (conf *AppConf) Language(num int) (ret string) { 884 ret = conf.language(num) 885 ret = strings.ReplaceAll(ret, "${accountServer}", util.GetCloudAccountServer()) 886 return 887} 888 889func (conf *AppConf) language(num int) (ret string) { 890 ret = util.Langs[conf.Lang][num] 891 if "" != ret { 892 return 893 } 894 ret = util.Langs["en_US"][num] 895 return 896} 897 898func InitBoxes() { 899 blockCount := treenode.CountBlocks() 900 initialized := 0 < blockCount 901 for _, box := range Conf.GetOpenedBoxes() { 902 box.UpdateHistoryGenerated() // 初始化历史生成时间为当前时间 903 904 if !initialized { 905 indexBox(box.ID) 906 } 907 } 908 909 logging.LogInfof("tree/block count [%d/%d]", treenode.CountTrees(), blockCount) 910} 911 912func IsSubscriber() bool { 913 u := Conf.GetUser() 914 return nil != u && (-1 == u.UserSiYuanProExpireTime || 0 < u.UserSiYuanProExpireTime) && 0 == u.UserSiYuanSubscriptionStatus 915} 916 917func IsPaidUser() bool { 918 // S3/WebDAV data sync and backup are available for a fee https://github.com/siyuan-note/siyuan/issues/8780 919 920 if IsSubscriber() { 921 return true 922 } 923 924 u := Conf.GetUser() 925 if nil == u { 926 return false 927 } 928 return 1 == u.UserSiYuanOneTimePayStatus 929} 930 931const ( 932 MaskedUserData = "" 933 MaskedAccessAuthCode = "*******" 934) 935 936func GetMaskedConf() (ret *AppConf, err error) { 937 // 脱敏处理 938 data, err := gulu.JSON.MarshalIndentJSON(Conf, "", " ") 939 if err != nil { 940 logging.LogErrorf("marshal conf failed: %s", err) 941 return 942 } 943 ret = &AppConf{} 944 if err = gulu.JSON.UnmarshalJSON(data, ret); err != nil { 945 logging.LogErrorf("unmarshal conf failed: %s", err) 946 return 947 } 948 949 ret.UserData = MaskedUserData 950 if "" != ret.AccessAuthCode { 951 ret.AccessAuthCode = MaskedAccessAuthCode 952 } 953 return 954} 955 956// REF: https://github.com/siyuan-note/siyuan/issues/11364 957// HideConfSecret 隐藏设置中的秘密信息 958func HideConfSecret(c *AppConf) { 959 c.AI = &conf.AI{} 960 c.Api = &conf.API{} 961 c.Flashcard = &conf.Flashcard{} 962 c.LocalIPs = []string{} 963 c.Publish = &conf.Publish{} 964 c.Repo = &conf.Repo{} 965 c.Sync = &conf.Sync{} 966 c.System.AppDir = "" 967 c.System.ConfDir = "" 968 c.System.DataDir = "" 969 c.System.HomeDir = "" 970 c.System.Name = "" 971 c.System.NetworkProxy = &conf.NetworkProxy{} 972} 973 974func clearPortJSON() { 975 pid := fmt.Sprintf("%d", os.Getpid()) 976 portJSON := filepath.Join(util.HomeDir, ".config", "siyuan", "port.json") 977 pidPorts := map[string]string{} 978 var data []byte 979 var err error 980 981 if gulu.File.IsExist(portJSON) { 982 data, err = os.ReadFile(portJSON) 983 if err != nil { 984 logging.LogWarnf("read port.json failed: %s", err) 985 } else { 986 if err = gulu.JSON.UnmarshalJSON(data, &pidPorts); err != nil { 987 logging.LogWarnf("unmarshal port.json failed: %s", err) 988 } 989 } 990 } 991 992 delete(pidPorts, pid) 993 if data, err = gulu.JSON.MarshalIndentJSON(pidPorts, "", " "); err != nil { 994 logging.LogWarnf("marshal port.json failed: %s", err) 995 } else { 996 if err = os.WriteFile(portJSON, data, 0644); err != nil { 997 logging.LogWarnf("write port.json failed: %s", err) 998 } 999 } 1000} 1001 1002func clearCorruptedNotebooks() { 1003 // 数据同步时展开文档树操作可能导致数据丢失 https://github.com/siyuan-note/siyuan/issues/7129 1004 1005 dirs, err := os.ReadDir(util.DataDir) 1006 if err != nil { 1007 logging.LogErrorf("read dir [%s] failed: %s", util.DataDir, err) 1008 return 1009 } 1010 for _, dir := range dirs { 1011 if util.IsReservedFilename(dir.Name()) { 1012 continue 1013 } 1014 1015 if !dir.IsDir() { 1016 continue 1017 } 1018 1019 if !ast.IsNodeIDPattern(dir.Name()) { 1020 continue 1021 } 1022 1023 boxDirPath := filepath.Join(util.DataDir, dir.Name()) 1024 boxConfPath := filepath.Join(boxDirPath, ".siyuan", "conf.json") 1025 if !filelock.IsExist(boxConfPath) { 1026 logging.LogWarnf("found a corrupted box [%s]", boxDirPath) 1027 continue 1028 } 1029 } 1030} 1031 1032func clearWorkspaceTemp() { 1033 os.RemoveAll(filepath.Join(util.TempDir, "bazaar")) 1034 os.RemoveAll(filepath.Join(util.TempDir, "export")) 1035 os.RemoveAll(filepath.Join(util.TempDir, "convert")) 1036 os.RemoveAll(filepath.Join(util.TempDir, "import")) 1037 os.RemoveAll(filepath.Join(util.TempDir, "repo")) 1038 os.RemoveAll(filepath.Join(util.TempDir, "os")) 1039 os.RemoveAll(filepath.Join(util.TempDir, "base64")) 1040 os.RemoveAll(filepath.Join(util.TempDir, "blocktree.msgpack")) // v2.7.2 前旧版的块树数据 1041 os.RemoveAll(filepath.Join(util.TempDir, "blocktree")) // v3.1.0 前旧版的块树数据 1042 1043 // 退出时自动删除超过 7 天的安装包 https://github.com/siyuan-note/siyuan/issues/6128 1044 install := filepath.Join(util.TempDir, "install") 1045 if gulu.File.IsDir(install) { 1046 monthAgo := time.Now().Add(-time.Hour * 24 * 7) 1047 entries, err := os.ReadDir(install) 1048 if err != nil { 1049 logging.LogErrorf("read dir [%s] failed: %s", install, err) 1050 } else { 1051 for _, entry := range entries { 1052 info, _ := entry.Info() 1053 if nil != info && !info.IsDir() && info.ModTime().Before(monthAgo) { 1054 if err = os.RemoveAll(filepath.Join(install, entry.Name())); err != nil { 1055 logging.LogErrorf("remove old install pkg [%s] failed: %s", filepath.Join(install, entry.Name()), err) 1056 } 1057 } 1058 } 1059 } 1060 } 1061 1062 tmps, err := filepath.Glob(filepath.Join(util.TempDir, "*.tmp")) 1063 if err != nil { 1064 logging.LogErrorf("glob temp files failed: %s", err) 1065 } 1066 for _, tmp := range tmps { 1067 if err = os.RemoveAll(tmp); err != nil { 1068 logging.LogErrorf("remove temp file [%s] failed: %s", tmp, err) 1069 } else { 1070 logging.LogInfof("removed temp file [%s]", tmp) 1071 } 1072 } 1073 1074 tmps, err = filepath.Glob(filepath.Join(util.DataDir, ".siyuan", "*.tmp")) 1075 if err != nil { 1076 logging.LogErrorf("glob temp files failed: %s", err) 1077 } 1078 for _, tmp := range tmps { 1079 if err = os.RemoveAll(tmp); err != nil { 1080 logging.LogErrorf("remove temp file [%s] failed: %s", tmp, err) 1081 } else { 1082 logging.LogInfof("removed temp file [%s]", tmp) 1083 } 1084 } 1085 1086 // 老版本遗留文件清理 1087 os.RemoveAll(filepath.Join(util.DataDir, "assets", ".siyuan", "assets.json")) 1088 os.RemoveAll(filepath.Join(util.DataDir, ".siyuan", "history")) 1089 os.RemoveAll(filepath.Join(util.WorkspaceDir, "backup")) 1090 os.RemoveAll(filepath.Join(util.WorkspaceDir, "sync")) 1091 os.RemoveAll(filepath.Join(util.DataDir, "%")) // v3.0.6 生成的错误历史文件夹 1092 1093 logging.LogInfof("cleared workspace temp") 1094} 1095 1096func closeUserGuide() { 1097 defer logging.Recover() 1098 1099 dirs, err := os.ReadDir(util.DataDir) 1100 if err != nil { 1101 logging.LogErrorf("read dir [%s] failed: %s", util.DataDir, err) 1102 return 1103 } 1104 1105 for _, dir := range dirs { 1106 if !IsUserGuide(dir.Name()) { 1107 continue 1108 } 1109 1110 boxID := dir.Name() 1111 boxDirPath := filepath.Join(util.DataDir, boxID) 1112 boxConf := conf.NewBoxConf() 1113 boxConfPath := filepath.Join(boxDirPath, ".siyuan", "conf.json") 1114 if !filelock.IsExist(boxConfPath) { 1115 logging.LogWarnf("found a corrupted user guide box [%s]", boxDirPath) 1116 if removeErr := filelock.Remove(boxDirPath); nil != removeErr { 1117 logging.LogErrorf("remove corrupted user guide box [%s] failed: %s", boxDirPath, removeErr) 1118 } else { 1119 logging.LogInfof("removed corrupted user guide box [%s]", boxDirPath) 1120 } 1121 continue 1122 } 1123 1124 data, readErr := filelock.ReadFile(boxConfPath) 1125 if nil != readErr { 1126 logging.LogErrorf("read box conf [%s] failed: %s", boxConfPath, readErr) 1127 if removeErr := filelock.Remove(boxDirPath); nil != removeErr { 1128 logging.LogErrorf("remove corrupted user guide box [%s] failed: %s", boxDirPath, removeErr) 1129 } else { 1130 logging.LogInfof("removed corrupted user guide box [%s]", boxDirPath) 1131 } 1132 continue 1133 } 1134 if readErr = gulu.JSON.UnmarshalJSON(data, boxConf); nil != readErr { 1135 logging.LogErrorf("parse box conf [%s] failed: %s", boxConfPath, readErr) 1136 if removeErr := filelock.Remove(boxDirPath); nil != removeErr { 1137 logging.LogErrorf("remove corrupted user guide box [%s] failed: %s", boxDirPath, removeErr) 1138 } else { 1139 logging.LogInfof("removed corrupted user guide box [%s]", boxDirPath) 1140 } 1141 continue 1142 } 1143 1144 if boxConf.Closed { 1145 continue 1146 } 1147 1148 msgId := util.PushMsg(Conf.language(233), 30000) 1149 evt := util.NewCmdResult("unmount", 0, util.PushModeBroadcast) 1150 evt.Data = map[string]interface{}{ 1151 "box": boxID, 1152 } 1153 util.PushEvent(evt) 1154 1155 unindex(boxID) 1156 1157 sql.FlushQueue() 1158 if removeErr := filelock.Remove(boxDirPath); nil != removeErr { 1159 logging.LogErrorf("remove corrupted user guide box [%s] failed: %s", boxDirPath, removeErr) 1160 } 1161 1162 util.PushClearMsg(msgId) 1163 logging.LogInfof("closed user guide box [%s]", boxID) 1164 } 1165} 1166 1167func init() { 1168 subscribeConfEvents() 1169} 1170 1171func subscribeConfEvents() { 1172 eventbus.Subscribe(util.EvtConfPandocInitialized, func() { 1173 logging.LogInfof("pandoc initialized, set pandoc bin to [%s]", util.PandocBinPath) 1174 Conf.Export.PandocBin = util.PandocBinPath 1175 Conf.Save() 1176 }) 1177}