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