conf.go 32 KB

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