From 17d49b481faa218c299dc232953a46d28b788468 Mon Sep 17 00:00:00 2001
From: Jeffrey Chen <78434827+TCOTC@users.noreply.github.com>
Date: Wed, 4 Mar 2026 20:48:12 +0800
Subject: [PATCH 1/7] :recycle: Bazaar adds parameter validation (#17132)
---
kernel/api/bazaar.go | 182 ++++++++++++++++++++++++++-----------------
kernel/util/net.go | 53 +++++++++++++
2 files changed, 162 insertions(+), 73 deletions(-)
diff --git a/kernel/api/bazaar.go b/kernel/api/bazaar.go
index 9d00cb985..26fbc9872 100644
--- a/kernel/api/bazaar.go
+++ b/kernel/api/bazaar.go
@@ -25,6 +25,14 @@ import (
"github.com/siyuan-note/siyuan/kernel/util"
)
+var validPackageTypes = map[string]bool{
+ "plugins": true,
+ "themes": true,
+ "icons": true,
+ "templates": true,
+ "widgets": true,
+}
+
func batchUpdatePackage(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
@@ -34,7 +42,10 @@ func batchUpdatePackage(c *gin.Context) {
return
}
- frontend := arg["frontend"].(string)
+ var frontend string
+ if !util.ParseJsonArgs(arg, ret, util.BindJsonArg("frontend", true, &frontend)) {
+ return
+ }
model.BatchUpdateBazaarPackages(frontend)
}
@@ -47,7 +58,10 @@ func getUpdatedPackage(c *gin.Context) {
return
}
- frontend := arg["frontend"].(string)
+ var frontend string
+ if !util.ParseJsonArgs(arg, ret, util.BindJsonArg("frontend", true, &frontend)) {
+ return
+ }
plugins, widgets, icons, themes, templates := model.UpdatedPackages(frontend)
ret.Data = map[string]interface{}{
"plugins": plugins,
@@ -67,11 +81,21 @@ func getBazaarPackageREADME(c *gin.Context) {
return
}
- repoURL := arg["repoURL"].(string)
- repoHash := arg["repoHash"].(string)
- packageType := arg["packageType"].(string)
+ var repoURL, repoHash, pkgType string
+ if !util.ParseJsonArgs(arg, ret,
+ util.BindJsonArg("repoURL", true, &repoURL),
+ util.BindJsonArg("repoHash", true, &repoHash),
+ util.BindJsonArg("packageType", true, &pkgType),
+ ) {
+ return
+ }
+ if !validPackageTypes[pkgType] {
+ ret.Code = -1
+ ret.Msg = "Invalid package type"
+ return
+ }
ret.Data = map[string]interface{}{
- "html": model.GetPackageREADME(repoURL, repoHash, packageType),
+ "html": model.GetPackageREADME(repoURL, repoHash, pkgType),
}
}
@@ -84,10 +108,12 @@ func getBazaarPlugin(c *gin.Context) {
return
}
- frontend := arg["frontend"].(string)
- var keyword string
- if keywordArg := arg["keyword"]; nil != keywordArg {
- keyword = keywordArg.(string)
+ var frontend, keyword string
+ if !util.ParseJsonArgs(arg, ret,
+ util.BindJsonArg("frontend", true, &frontend),
+ util.BindJsonArg("keyword", false, &keyword),
+ ) {
+ return
}
ret.Data = map[string]interface{}{
@@ -104,10 +130,12 @@ func getInstalledPlugin(c *gin.Context) {
return
}
- frontend := arg["frontend"].(string)
- var keyword string
- if keywordArg := arg["keyword"]; nil != keywordArg {
- keyword = keywordArg.(string)
+ var frontend, keyword string
+ if !util.ParseJsonArgs(arg, ret,
+ util.BindJsonArg("frontend", true, &frontend),
+ util.BindJsonArg("keyword", false, &keyword),
+ ) {
+ return
}
ret.Data = map[string]interface{}{
@@ -124,14 +152,16 @@ func installBazaarPlugin(c *gin.Context) {
return
}
- var keyword string
- if keywordArg := arg["keyword"]; nil != keywordArg {
- keyword = keywordArg.(string)
+ var frontend, keyword, repoURL, repoHash, packageName string
+ if !util.ParseJsonArgs(arg, ret,
+ util.BindJsonArg("frontend", true, &frontend),
+ util.BindJsonArg("keyword", false, &keyword),
+ util.BindJsonArg("repoURL", true, &repoURL),
+ util.BindJsonArg("repoHash", true, &repoHash),
+ util.BindJsonArg("packageName", true, &packageName),
+ ) {
+ return
}
-
- repoURL := arg["repoURL"].(string)
- repoHash := arg["repoHash"].(string)
- packageName := arg["packageName"].(string)
err := model.InstallBazaarPlugin(repoURL, repoHash, packageName)
if err != nil {
ret.Code = 1
@@ -139,8 +169,6 @@ func installBazaarPlugin(c *gin.Context) {
return
}
- frontend := arg["frontend"].(string)
-
util.PushMsg(model.Conf.Language(69), 3000)
ret.Data = map[string]interface{}{
"packages": model.BazaarPlugins(frontend, keyword),
@@ -156,13 +184,14 @@ func uninstallBazaarPlugin(c *gin.Context) {
return
}
- var keyword string
- if keywordArg := arg["keyword"]; nil != keywordArg {
- keyword = keywordArg.(string)
+ var frontend, keyword, packageName string
+ if !util.ParseJsonArgs(arg, ret,
+ util.BindJsonArg("frontend", true, &frontend),
+ util.BindJsonArg("keyword", false, &keyword),
+ util.BindJsonArg("packageName", true, &packageName),
+ ) {
+ return
}
-
- frontend := arg["frontend"].(string)
- packageName := arg["packageName"].(string)
err := model.UninstallBazaarPlugin(packageName, frontend)
if err != nil {
ret.Code = -1
@@ -185,8 +214,8 @@ func getBazaarWidget(c *gin.Context) {
}
var keyword string
- if keywordArg := arg["keyword"]; nil != keywordArg {
- keyword = keywordArg.(string)
+ if !util.ParseJsonArgs(arg, ret, util.BindJsonArg("keyword", false, &keyword)) {
+ return
}
ret.Data = map[string]interface{}{
@@ -204,8 +233,8 @@ func getInstalledWidget(c *gin.Context) {
}
var keyword string
- if keywordArg := arg["keyword"]; nil != keywordArg {
- keyword = keywordArg.(string)
+ if !util.ParseJsonArgs(arg, ret, util.BindJsonArg("keyword", false, &keyword)) {
+ return
}
ret.Data = map[string]interface{}{
@@ -222,14 +251,15 @@ func installBazaarWidget(c *gin.Context) {
return
}
- var keyword string
- if keywordArg := arg["keyword"]; nil != keywordArg {
- keyword = keywordArg.(string)
+ var keyword, repoURL, repoHash, packageName string
+ if !util.ParseJsonArgs(arg, ret,
+ util.BindJsonArg("keyword", false, &keyword),
+ util.BindJsonArg("repoURL", true, &repoURL),
+ util.BindJsonArg("repoHash", true, &repoHash),
+ util.BindJsonArg("packageName", true, &packageName),
+ ) {
+ return
}
-
- repoURL := arg["repoURL"].(string)
- repoHash := arg["repoHash"].(string)
- packageName := arg["packageName"].(string)
err := model.InstallBazaarWidget(repoURL, repoHash, packageName)
if err != nil {
ret.Code = 1
@@ -252,12 +282,13 @@ func uninstallBazaarWidget(c *gin.Context) {
return
}
- var keyword string
- if keywordArg := arg["keyword"]; nil != keywordArg {
- keyword = keywordArg.(string)
+ var keyword, packageName string
+ if !util.ParseJsonArgs(arg, ret,
+ util.BindJsonArg("keyword", false, &keyword),
+ util.BindJsonArg("packageName", true, &packageName),
+ ) {
+ return
}
-
- packageName := arg["packageName"].(string)
err := model.UninstallBazaarWidget(packageName)
if err != nil {
ret.Code = -1
@@ -280,8 +311,8 @@ func getBazaarIcon(c *gin.Context) {
}
var keyword string
- if keywordArg := arg["keyword"]; nil != keywordArg {
- keyword = keywordArg.(string)
+ if !util.ParseJsonArgs(arg, ret, util.BindJsonArg("keyword", false, &keyword)) {
+ return
}
ret.Data = map[string]interface{}{
@@ -299,8 +330,8 @@ func getInstalledIcon(c *gin.Context) {
}
var keyword string
- if keywordArg := arg["keyword"]; nil != keywordArg {
- keyword = keywordArg.(string)
+ if !util.ParseJsonArgs(arg, ret, util.BindJsonArg("keyword", false, &keyword)) {
+ return
}
ret.Data = map[string]interface{}{
@@ -317,14 +348,15 @@ func installBazaarIcon(c *gin.Context) {
return
}
- var keyword string
- if keywordArg := arg["keyword"]; nil != keywordArg {
- keyword = keywordArg.(string)
+ var keyword, repoURL, repoHash, packageName string
+ if !util.ParseJsonArgs(arg, ret,
+ util.BindJsonArg("keyword", false, &keyword),
+ util.BindJsonArg("repoURL", true, &repoURL),
+ util.BindJsonArg("repoHash", true, &repoHash),
+ util.BindJsonArg("packageName", true, &packageName),
+ ) {
+ return
}
-
- repoURL := arg["repoURL"].(string)
- repoHash := arg["repoHash"].(string)
- packageName := arg["packageName"].(string)
err := model.InstallBazaarIcon(repoURL, repoHash, packageName)
if err != nil {
ret.Code = 1
@@ -348,12 +380,13 @@ func uninstallBazaarIcon(c *gin.Context) {
return
}
- var keyword string
- if keywordArg := arg["keyword"]; nil != keywordArg {
- keyword = keywordArg.(string)
+ var keyword, packageName string
+ if !util.ParseJsonArgs(arg, ret,
+ util.BindJsonArg("keyword", false, &keyword),
+ util.BindJsonArg("packageName", true, &packageName),
+ ) {
+ return
}
-
- packageName := arg["packageName"].(string)
err := model.UninstallBazaarIcon(packageName)
if err != nil {
ret.Code = -1
@@ -377,8 +410,8 @@ func getBazaarTemplate(c *gin.Context) {
}
var keyword string
- if keywordArg := arg["keyword"]; nil != keywordArg {
- keyword = keywordArg.(string)
+ if !util.ParseJsonArgs(arg, ret, util.BindJsonArg("keyword", false, &keyword)) {
+ return
}
ret.Data = map[string]interface{}{
@@ -396,8 +429,8 @@ func getInstalledTemplate(c *gin.Context) {
}
var keyword string
- if keywordArg := arg["keyword"]; nil != keywordArg {
- keyword = keywordArg.(string)
+ if !util.ParseJsonArgs(arg, ret, util.BindJsonArg("keyword", false, &keyword)) {
+ return
}
ret.Data = map[string]interface{}{
@@ -510,15 +543,17 @@ func installBazaarTheme(c *gin.Context) {
return
}
- var keyword string
- if keywordArg := arg["keyword"]; nil != keywordArg {
- keyword = keywordArg.(string)
+ var keyword, repoURL, repoHash, packageName string
+ var mode float64
+ if !util.ParseJsonArgs(arg, ret,
+ util.BindJsonArg("keyword", false, &keyword),
+ util.BindJsonArg("repoURL", true, &repoURL),
+ util.BindJsonArg("repoHash", true, &repoHash),
+ util.BindJsonArg("packageName", true, &packageName),
+ util.BindJsonArg("mode", true, &mode),
+ ) {
+ return
}
-
- repoURL := arg["repoURL"].(string)
- repoHash := arg["repoHash"].(string)
- packageName := arg["packageName"].(string)
- mode := arg["mode"].(float64)
update := false
if nil != arg["update"] {
update = arg["update"].(bool)
@@ -530,6 +565,7 @@ func installBazaarTheme(c *gin.Context) {
return
}
+ // TODO 安装新主题之后,不应该始终取消外观模式“跟随系统” https://github.com/siyuan-note/siyuan/issues/16990
// 安装集市主题后不跟随系统切换外观模式
model.Conf.Appearance.ModeOS = false
model.Conf.Save()
diff --git a/kernel/util/net.go b/kernel/util/net.go
index 1f39ca627..a57dfa58a 100644
--- a/kernel/util/net.go
+++ b/kernel/util/net.go
@@ -17,6 +17,7 @@
package util
import (
+ "fmt"
"net"
"net/http"
"net/url"
@@ -213,6 +214,58 @@ func JsonArg(c *gin.Context, result *gulu.Result) (arg map[string]interface{}, o
return
}
+// ParseJsonArg 使用泛型从 JSON 参数中提取指定键的值。
+// - 如果 required 为 true 但参数缺失,则会在 ret.Msg 中写入 “[key] is required”
+// - 如果参数存在但类型不匹配,则会在 ret.Msg 中写入 “[key] should be [T]”
+// - 返回值 ok 为 false 时,表示提取失败或类型不匹配
+func ParseJsonArg[T any](key string, required bool, arg map[string]interface{}, ret *gulu.Result) (value T, ok bool) {
+ raw, exists := arg[key]
+ if !exists || raw == nil {
+ if required {
+ ret.Code = -1
+ ret.Msg = key + " is required"
+ } else {
+ ok = true
+ }
+ return
+ }
+
+ value, ok = raw.(T)
+ if !ok {
+ var zero T
+ ret.Code = -1
+ ret.Msg = fmt.Sprintf("%s should be %T", key, zero)
+ }
+ return
+}
+
+// JsonArgParseFunc 为单次提取函数,用于 ParseJsonArgs 批量提取。
+type JsonArgParseFunc func(arg map[string]interface{}, ret *gulu.Result) bool
+
+// BindJsonArg 创建一个提取函数:从 arg 取 key 并写入 dest,供 ParseJsonArgs 使用。
+func BindJsonArg[T any](key string, required bool, dest *T) JsonArgParseFunc {
+ return func(arg map[string]interface{}, ret *gulu.Result) bool {
+ v, ok := ParseJsonArg[T](key, required, arg, ret)
+ if !ok {
+ return false
+ }
+ *dest = v
+ return true
+ }
+}
+
+// ParseJsonArgs 按顺序执行多个提取函数。
+// - 任一失败返回 false 并在 ret 中写入错误信息
+// - 全部成功返回 true
+func ParseJsonArgs(arg map[string]interface{}, ret *gulu.Result, extractors ...JsonArgParseFunc) bool {
+ for _, ext := range extractors {
+ if !ext(arg, ret) {
+ return false
+ }
+ }
+ return true
+}
+
func InvalidIDPattern(idArg string, result *gulu.Result) bool {
if ast.IsNodeIDPattern(idArg) {
return false
From ba733eedfd9b1285a983dd5e6c86e56852cbe86e Mon Sep 17 00:00:00 2001
From: Jeffrey Chen <78434827+TCOTC@users.noreply.github.com>
Date: Wed, 4 Mar 2026 20:53:06 +0800
Subject: [PATCH 2/7] :art: Normalize bazaar naming and reorganize
kernel/bazaar structure (#17133)
---
app/src/dialog/processSystem.ts | 8 +-
kernel/bazaar/package.go | 435 --------------------------------
kernel/bazaar/readme.go | 160 ++++++++++++
kernel/bazaar/stage.go | 332 ++++++++++++++++++++++++
4 files changed, 496 insertions(+), 439 deletions(-)
create mode 100644 kernel/bazaar/readme.go
create mode 100644 kernel/bazaar/stage.go
diff --git a/app/src/dialog/processSystem.ts b/app/src/dialog/processSystem.ts
index 64814a0f2..7a5d73bcc 100644
--- a/app/src/dialog/processSystem.ts
+++ b/app/src/dialog/processSystem.ts
@@ -537,14 +537,14 @@ export const setTitle = (title: string) => {
};
export const downloadProgress = (data: { id: string, percent: number }) => {
- const bazzarSideElement = document.querySelector("#configBazaarReadme .item__side");
- if (!bazzarSideElement) {
+ const bazaarSideElement = document.querySelector("#configBazaarReadme .item__side");
+ if (!bazaarSideElement) {
return;
}
- if (data.id !== JSON.parse(bazzarSideElement.getAttribute("data-obj")).repoURL) {
+ if (data.id !== JSON.parse(bazaarSideElement.getAttribute("data-obj")).repoURL) {
return;
}
- const btnElement = bazzarSideElement.querySelector('[data-type="install"]') as HTMLElement;
+ const btnElement = bazaarSideElement.querySelector('[data-type="install"]') as HTMLElement;
if (btnElement) {
if (data.percent >= 1) {
btnElement.parentElement.classList.add("fn__none");
diff --git a/kernel/bazaar/package.go b/kernel/bazaar/package.go
index 59004a03a..6ecaae1c7 100644
--- a/kernel/bazaar/package.go
+++ b/kernel/bazaar/package.go
@@ -18,7 +18,6 @@ package bazaar
import (
"bytes"
- "context"
"errors"
"fmt"
"os"
@@ -28,7 +27,6 @@ import (
"time"
"github.com/88250/gulu"
- "github.com/88250/lute"
"github.com/araddon/dateparse"
"github.com/imroc/req/v3"
gcache "github.com/patrickmn/go-cache"
@@ -37,9 +35,6 @@ import (
"github.com/siyuan-note/logging"
"github.com/siyuan-note/siyuan/kernel/util"
"golang.org/x/mod/semver"
- "golang.org/x/sync/singleflight"
- textUnicode "golang.org/x/text/encoding/unicode"
- "golang.org/x/text/transform"
)
// LocaleStrings 表示按语种 key 的字符串表,key 为语种如 "default"、"en_US"、"zh_CN" 等
@@ -274,388 +269,6 @@ func ThemeJSON(themeDirName string) (ret *Theme, err error) {
return
}
-var cachedStageIndex = map[string]*StageIndex{}
-var stageIndexCacheTime int64
-var stageIndexLock = sync.RWMutex{}
-
-type StageBazaarResult struct {
- StageIndex *StageIndex // stage 索引
- BazaarIndex map[string]*bazaarPackage // bazaar 索引
- Online bool // online 状态
- StageErr error // stage 错误
-}
-
-var stageBazaarFlight singleflight.Group
-var onlineCheckFlight singleflight.Group
-
-// getStageAndBazaar 获取 stage 索引和 bazaar 索引,相同 pkgType 的并发调用会合并为一次实际请求 (single-flight)
-func getStageAndBazaar(pkgType string) (result StageBazaarResult) {
- key := "stageBazaar:" + pkgType
- v, err, _ := stageBazaarFlight.Do(key, func() (interface{}, error) {
- return getStageAndBazaar0(pkgType), nil
- })
- if err != nil {
- return
- }
- result = v.(StageBazaarResult)
- return
-}
-
-// getStageAndBazaar0 执行一次 stage 和 bazaar 索引拉取
-func getStageAndBazaar0(pkgType string) (result StageBazaarResult) {
- stageIndex, stageErr := getStageIndexFromCache(pkgType)
- bazaarIndex := getBazaarIndexFromCache()
- if nil != stageIndex && nil != bazaarIndex {
- // 两者都从缓存返回,不需要 online 检查
- return StageBazaarResult{
- StageIndex: stageIndex,
- BazaarIndex: bazaarIndex,
- Online: true,
- StageErr: stageErr,
- }
- }
-
- ctx, cancel := context.WithCancel(context.Background())
- defer cancel()
- var onlineResult bool
- onlineDone := make(chan bool, 1)
- wg := &sync.WaitGroup{}
- wg.Add(3)
- go func() {
- defer wg.Done()
- onlineResult = isBazzarOnline()
- onlineDone <- true
- }()
- go func() {
- defer wg.Done()
- stageIndex, stageErr = getStageIndex(ctx, pkgType)
- }()
- go func() {
- defer wg.Done()
- bazaarIndex = getBazaarIndex(ctx)
- }()
-
- <-onlineDone
- if !onlineResult {
- // 不在线时立即取消其他请求并返回结果,避免等待 HTTP 请求超时
- cancel()
- return StageBazaarResult{
- StageIndex: stageIndex,
- BazaarIndex: bazaarIndex,
- Online: false,
- StageErr: stageErr,
- }
- }
-
- // 在线时等待所有请求完成
- wg.Wait()
-
- return StageBazaarResult{
- StageIndex: stageIndex,
- BazaarIndex: bazaarIndex,
- Online: onlineResult,
- StageErr: stageErr,
- }
-}
-
-// getStageIndexFromCache 仅从缓存获取 stage 索引,过期或无缓存时返回 nil
-func getStageIndexFromCache(pkgType string) (ret *StageIndex, err error) {
- stageIndexLock.RLock()
- cacheTime := stageIndexCacheTime
- cached := cachedStageIndex[pkgType]
- stageIndexLock.RUnlock()
- if util.RhyCacheDuration >= time.Now().Unix()-cacheTime && nil != cached {
- ret = cached
- }
- return
-}
-
-// getStageIndex 获取 stage 索引
-func getStageIndex(ctx context.Context, pkgType string) (ret *StageIndex, err error) {
- if cached, cacheErr := getStageIndexFromCache(pkgType); nil != cached {
- ret = cached
- err = cacheErr
- return
- }
-
- var rhyRet map[string]interface{}
- rhyRet, err = util.GetRhyResult(ctx, false)
- if nil != err {
- return
- }
-
- stageIndexLock.Lock()
- defer stageIndexLock.Unlock()
-
- bazaarHash := rhyRet["bazaar"].(string)
- ret = &StageIndex{}
- request := httpclient.NewBrowserRequest()
- u := util.BazaarOSSServer + "/bazaar@" + bazaarHash + "/stage/" + pkgType + ".json"
- resp, reqErr := request.SetContext(ctx).SetSuccessResult(ret).Get(u)
- if nil != reqErr {
- logging.LogErrorf("get community stage index [%s] failed: %s", u, reqErr)
- err = reqErr
- return
- }
- if 200 != resp.StatusCode {
- logging.LogErrorf("get community stage index [%s] failed: %d", u, resp.StatusCode)
- err = errors.New("get stage index failed")
- return
- }
-
- stageIndexCacheTime = time.Now().Unix()
- cachedStageIndex[pkgType] = ret
- return
-}
-
-func isOutdatedTheme(theme *Theme, bazaarThemes []*Theme) bool {
- if !strings.HasPrefix(theme.URL, "https://github.com/") {
- return false
- }
-
- repo := strings.TrimPrefix(theme.URL, "https://github.com/")
- parts := strings.Split(repo, "/")
- if 2 != len(parts) || "" == strings.TrimSpace(parts[1]) {
- return false
- }
-
- for _, pkg := range bazaarThemes {
- if theme.Name == pkg.Name && 0 > semver.Compare("v"+theme.Version, "v"+pkg.Version) {
- theme.RepoHash = pkg.RepoHash
- return true
- }
- }
- return false
-}
-
-func isOutdatedIcon(icon *Icon, bazaarIcons []*Icon) bool {
- if !strings.HasPrefix(icon.URL, "https://github.com/") {
- return false
- }
-
- repo := strings.TrimPrefix(icon.URL, "https://github.com/")
- parts := strings.Split(repo, "/")
- if 2 != len(parts) || "" == strings.TrimSpace(parts[1]) {
- return false
- }
-
- for _, pkg := range bazaarIcons {
- if icon.Name == pkg.Name && 0 > semver.Compare("v"+icon.Version, "v"+pkg.Version) {
- icon.RepoHash = pkg.RepoHash
- return true
- }
- }
- return false
-}
-
-func isOutdatedPlugin(plugin *Plugin, bazaarPlugins []*Plugin) bool {
- if !strings.HasPrefix(plugin.URL, "https://github.com/") {
- return false
- }
-
- repo := strings.TrimPrefix(plugin.URL, "https://github.com/")
- parts := strings.Split(repo, "/")
- if 2 != len(parts) || "" == strings.TrimSpace(parts[1]) {
- return false
- }
-
- for _, pkg := range bazaarPlugins {
- if plugin.Name == pkg.Name && 0 > semver.Compare("v"+plugin.Version, "v"+pkg.Version) {
- plugin.RepoHash = pkg.RepoHash
- return true
- }
- }
- return false
-}
-
-func isOutdatedWidget(widget *Widget, bazaarWidgets []*Widget) bool {
- if !strings.HasPrefix(widget.URL, "https://github.com/") {
- return false
- }
-
- repo := strings.TrimPrefix(widget.URL, "https://github.com/")
- parts := strings.Split(repo, "/")
- if 2 != len(parts) || "" == strings.TrimSpace(parts[1]) {
- return false
- }
-
- for _, pkg := range bazaarWidgets {
- if widget.Name == pkg.Name && 0 > semver.Compare("v"+widget.Version, "v"+pkg.Version) {
- widget.RepoHash = pkg.RepoHash
- return true
- }
- }
- return false
-}
-
-func isOutdatedTemplate(template *Template, bazaarTemplates []*Template) bool {
- if !strings.HasPrefix(template.URL, "https://github.com/") {
- return false
- }
-
- repo := strings.TrimPrefix(template.URL, "https://github.com/")
- parts := strings.Split(repo, "/")
- if 2 != len(parts) || "" == strings.TrimSpace(parts[1]) {
- return false
- }
-
- for _, pkg := range bazaarTemplates {
- if template.Name == pkg.Name && 0 > semver.Compare("v"+template.Version, "v"+pkg.Version) {
- template.RepoHash = pkg.RepoHash
- return true
- }
- }
- return false
-}
-
-func isBazzarOnline() bool {
- v, err, _ := onlineCheckFlight.Do("bazaarOnline", func() (interface{}, error) {
- return isBazzarOnline0(), nil
- })
- if err != nil {
- return false
- }
- return v.(bool)
-}
-
-func isBazzarOnline0() (ret bool) {
- // Improve marketplace loading when offline https://github.com/siyuan-note/siyuan/issues/12050
- ret = util.IsOnline(util.BazaarOSSServer+"/204", true, 3000)
- if !ret {
- util.PushErrMsg(util.Langs[util.Lang][24], 5000)
- }
- return
-}
-
-func GetPackageREADME(repoURL, repoHash, packageType string) (ret string) {
- repoURLHash := repoURL + "@" + repoHash
-
- stageIndexLock.RLock()
- stageIndex := cachedStageIndex[packageType]
- stageIndexLock.RUnlock()
- if nil == stageIndex {
- return
- }
-
- url := strings.TrimPrefix(repoURLHash, "https://github.com/")
- var repo *StageRepo
- for _, r := range stageIndex.Repos {
- if r.URL == url {
- repo = r
- break
- }
- }
- if nil == repo || nil == repo.Package {
- return
- }
-
- readme := getPreferredReadme(repo.Package.Readme)
-
- data, err := downloadPackage(repoURLHash+"/"+readme, false, "")
- if err != nil {
- ret = fmt.Sprintf("Load bazaar package's preferred README(%s) failed: %s", readme, err.Error())
- // 回退到 Default README
- var defaultReadme string
- if len(repo.Package.Readme) > 0 {
- defaultReadme = repo.Package.Readme["default"]
- }
- if "" == strings.TrimSpace(defaultReadme) {
- defaultReadme = "README.md"
- }
- if readme != defaultReadme {
- data, err = downloadPackage(repoURLHash+"/"+defaultReadme, false, "")
- if err != nil {
- ret += fmt.Sprintf("
Load bazaar package's default README(%s) failed: %s", defaultReadme, err.Error())
- }
- }
- // 回退到 README.md
- if err != nil && readme != "README.md" && defaultReadme != "README.md" {
- data, err = downloadPackage(repoURLHash+"/README.md", false, "")
- if err != nil {
- ret += fmt.Sprintf("
Load bazaar package's README.md failed: %s", err.Error())
- return
- }
- } else if err != nil {
- return
- }
- }
-
- if 2 < len(data) {
- if 255 == data[0] && 254 == data[1] {
- data, _, err = transform.Bytes(textUnicode.UTF16(textUnicode.LittleEndian, textUnicode.ExpectBOM).NewDecoder(), data)
- } else if 254 == data[0] && 255 == data[1] {
- data, _, err = transform.Bytes(textUnicode.UTF16(textUnicode.BigEndian, textUnicode.ExpectBOM).NewDecoder(), data)
- }
- }
-
- ret, err = renderREADME(repoURL, data)
- return
-}
-
-func loadInstalledReadme(installPath, basePath string, readme LocaleStrings) (ret string) {
- readmeFilename := getPreferredReadme(readme)
- readmeData, readErr := os.ReadFile(filepath.Join(installPath, readmeFilename))
- if nil == readErr {
- ret, _ = renderLocalREADME(basePath, readmeData)
- return
- }
-
- logging.LogWarnf("read installed %s failed: %s", readmeFilename, readErr)
- ret = fmt.Sprintf("File %s not found", readmeFilename)
- // 回退到 Default README
- var defaultReadme string
- if len(readme) > 0 {
- defaultReadme = strings.TrimSpace(readme["default"])
- }
- if "" == defaultReadme {
- defaultReadme = "README.md"
- }
- if readmeFilename != defaultReadme {
- readmeData, readErr = os.ReadFile(filepath.Join(installPath, defaultReadme))
- if nil == readErr {
- ret, _ = renderLocalREADME(basePath, readmeData)
- return
- }
- logging.LogWarnf("read installed %s failed: %s", defaultReadme, readErr)
- ret += fmt.Sprintf("
File %s not found", defaultReadme)
- }
- // 回退到 README.md
- if nil != readErr && readmeFilename != "README.md" && defaultReadme != "README.md" {
- readmeData, readErr = os.ReadFile(filepath.Join(installPath, "README.md"))
- if nil == readErr {
- ret, _ = renderLocalREADME(basePath, readmeData)
- return
- }
- logging.LogWarnf("read installed README.md failed: %s", readErr)
- ret += "
File README.md not found"
- }
- return
-}
-
-func renderREADME(repoURL string, mdData []byte) (ret string, err error) {
- mdData = bytes.TrimPrefix(mdData, []byte("\xef\xbb\xbf"))
- luteEngine := lute.New()
- luteEngine.SetSoftBreak2HardBreak(false)
- luteEngine.SetCodeSyntaxHighlight(false)
- linkBase := "https://cdn.jsdelivr.net/gh/" + strings.TrimPrefix(repoURL, "https://github.com/")
- luteEngine.SetLinkBase(linkBase)
- ret = luteEngine.Md2HTML(string(mdData))
- ret = util.LinkTarget(ret, linkBase)
- return
-}
-
-func renderLocalREADME(basePath string, mdData []byte) (ret string, err error) {
- mdData = bytes.TrimPrefix(mdData, []byte("\xef\xbb\xbf"))
- luteEngine := lute.New()
- luteEngine.SetSoftBreak2HardBreak(false)
- luteEngine.SetCodeSyntaxHighlight(false)
- linkBase := basePath
- luteEngine.SetLinkBase(linkBase)
- ret = luteEngine.Md2HTML(string(mdData))
- ret = util.LinkTarget(ret, linkBase)
- return
-}
-
var (
packageLocks = map[string]*sync.Mutex{}
packageLocksLock = sync.Mutex{}
@@ -779,54 +392,6 @@ func formatUpdated(updated string) (ret string) {
return
}
-type bazaarPackage struct {
- Name string `json:"name"`
- Downloads int `json:"downloads"`
-}
-
-var cachedBazaarIndex = map[string]*bazaarPackage{}
-var bazaarIndexCacheTime int64
-var bazaarIndexLock = sync.RWMutex{}
-
-// getBazaarIndexFromCache 仅从缓存获取 bazaar 索引,过期或无缓存时返回 nil
-func getBazaarIndexFromCache() (ret map[string]*bazaarPackage) {
- bazaarIndexLock.RLock()
- cacheTime := bazaarIndexCacheTime
- cached := cachedBazaarIndex
- hasData := 0 < len(cached)
- bazaarIndexLock.RUnlock()
- if util.RhyCacheDuration >= time.Now().Unix()-cacheTime && hasData {
- ret = cached
- } else {
- ret = nil
- }
- return
-}
-
-// getBazaarIndex 获取 bazaar 索引
-func getBazaarIndex(ctx context.Context) map[string]*bazaarPackage {
- if cached := getBazaarIndexFromCache(); nil != cached {
- return cached
- }
-
- bazaarIndexLock.Lock()
- defer bazaarIndexLock.Unlock()
-
- request := httpclient.NewBrowserRequest()
- u := util.BazaarStatServer + "/bazaar/index.json"
- resp, reqErr := request.SetContext(ctx).SetSuccessResult(&cachedBazaarIndex).Get(u)
- if nil != reqErr {
- logging.LogErrorf("get bazaar index [%s] failed: %s", u, reqErr)
- return cachedBazaarIndex
- }
- if 200 != resp.StatusCode {
- logging.LogErrorf("get bazaar index [%s] failed: %d", u, resp.StatusCode)
- return cachedBazaarIndex
- }
- bazaarIndexCacheTime = time.Now().Unix()
- return cachedBazaarIndex
-}
-
// Add marketplace package config item `minAppVersion` https://github.com/siyuan-note/siyuan/issues/8330
func disallowInstallBazaarPackage(pkg *Package) bool {
// 如果包没有指定 minAppVersion,则允许安装
diff --git a/kernel/bazaar/readme.go b/kernel/bazaar/readme.go
new file mode 100644
index 000000000..ff3a350cb
--- /dev/null
+++ b/kernel/bazaar/readme.go
@@ -0,0 +1,160 @@
+// SiYuan - Refactor your thinking
+// Copyright (c) 2020-present, b3log.org
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package bazaar
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/88250/lute"
+ "github.com/siyuan-note/logging"
+ "github.com/siyuan-note/siyuan/kernel/util"
+ textUnicode "golang.org/x/text/encoding/unicode"
+ "golang.org/x/text/transform"
+)
+
+func GetPackageREADME(repoURL, repoHash, packageType string) (ret string) {
+ repoURLHash := repoURL + "@" + repoHash
+
+ stageIndexLock.RLock()
+ stageIndex := cachedStageIndex[packageType]
+ stageIndexLock.RUnlock()
+ if nil == stageIndex {
+ return
+ }
+
+ url := strings.TrimPrefix(repoURLHash, "https://github.com/")
+ var repo *StageRepo
+ for _, r := range stageIndex.Repos {
+ if r.URL == url {
+ repo = r
+ break
+ }
+ }
+ if nil == repo || nil == repo.Package {
+ return
+ }
+
+ readme := getPreferredReadme(repo.Package.Readme)
+
+ data, err := downloadPackage(repoURLHash+"/"+readme, false, "")
+ if err != nil {
+ ret = fmt.Sprintf("Load bazaar package's preferred README(%s) failed: %s", readme, err.Error())
+ // 回退到 Default README
+ var defaultReadme string
+ if len(repo.Package.Readme) > 0 {
+ defaultReadme = repo.Package.Readme["default"]
+ }
+ if "" == strings.TrimSpace(defaultReadme) {
+ defaultReadme = "README.md"
+ }
+ if readme != defaultReadme {
+ data, err = downloadPackage(repoURLHash+"/"+defaultReadme, false, "")
+ if err != nil {
+ ret += fmt.Sprintf("
Load bazaar package's default README(%s) failed: %s", defaultReadme, err.Error())
+ }
+ }
+ // 回退到 README.md
+ if err != nil && readme != "README.md" && defaultReadme != "README.md" {
+ data, err = downloadPackage(repoURLHash+"/README.md", false, "")
+ if err != nil {
+ ret += fmt.Sprintf("
Load bazaar package's README.md failed: %s", err.Error())
+ return
+ }
+ } else if err != nil {
+ return
+ }
+ }
+
+ if 2 < len(data) {
+ if 255 == data[0] && 254 == data[1] {
+ data, _, err = transform.Bytes(textUnicode.UTF16(textUnicode.LittleEndian, textUnicode.ExpectBOM).NewDecoder(), data)
+ } else if 254 == data[0] && 255 == data[1] {
+ data, _, err = transform.Bytes(textUnicode.UTF16(textUnicode.BigEndian, textUnicode.ExpectBOM).NewDecoder(), data)
+ }
+ }
+
+ ret, err = renderREADME(repoURL, data)
+ return
+}
+
+func loadInstalledReadme(installPath, basePath string, readme LocaleStrings) (ret string) {
+ readmeFilename := getPreferredReadme(readme)
+ readmeData, readErr := os.ReadFile(filepath.Join(installPath, readmeFilename))
+ if nil == readErr {
+ ret, _ = renderLocalREADME(basePath, readmeData)
+ return
+ }
+
+ logging.LogWarnf("read installed %s failed: %s", readmeFilename, readErr)
+ ret = fmt.Sprintf("File %s not found", readmeFilename)
+ // 回退到 Default README
+ var defaultReadme string
+ if len(readme) > 0 {
+ defaultReadme = strings.TrimSpace(readme["default"])
+ }
+ if "" == defaultReadme {
+ defaultReadme = "README.md"
+ }
+ if readmeFilename != defaultReadme {
+ readmeData, readErr = os.ReadFile(filepath.Join(installPath, defaultReadme))
+ if nil == readErr {
+ ret, _ = renderLocalREADME(basePath, readmeData)
+ return
+ }
+ logging.LogWarnf("read installed %s failed: %s", defaultReadme, readErr)
+ ret += fmt.Sprintf("
File %s not found", defaultReadme)
+ }
+ // 回退到 README.md
+ if nil != readErr && readmeFilename != "README.md" && defaultReadme != "README.md" {
+ readmeData, readErr = os.ReadFile(filepath.Join(installPath, "README.md"))
+ if nil == readErr {
+ ret, _ = renderLocalREADME(basePath, readmeData)
+ return
+ }
+ logging.LogWarnf("read installed README.md failed: %s", readErr)
+ ret += "
File README.md not found"
+ }
+ return
+}
+
+func renderREADME(repoURL string, mdData []byte) (ret string, err error) {
+ mdData = bytes.TrimPrefix(mdData, []byte("\xef\xbb\xbf"))
+ luteEngine := lute.New()
+ luteEngine.SetSoftBreak2HardBreak(false)
+ luteEngine.SetCodeSyntaxHighlight(false)
+ linkBase := "https://cdn.jsdelivr.net/gh/" + strings.TrimPrefix(repoURL, "https://github.com/")
+ luteEngine.SetLinkBase(linkBase)
+ ret = luteEngine.Md2HTML(string(mdData))
+ ret = util.LinkTarget(ret, linkBase)
+ return
+}
+
+func renderLocalREADME(basePath string, mdData []byte) (ret string, err error) {
+ mdData = bytes.TrimPrefix(mdData, []byte("\xef\xbb\xbf"))
+ luteEngine := lute.New()
+ luteEngine.SetSoftBreak2HardBreak(false)
+ luteEngine.SetCodeSyntaxHighlight(false)
+ linkBase := basePath
+ luteEngine.SetLinkBase(linkBase)
+ ret = luteEngine.Md2HTML(string(mdData))
+ ret = util.LinkTarget(ret, linkBase)
+ return
+}
diff --git a/kernel/bazaar/stage.go b/kernel/bazaar/stage.go
new file mode 100644
index 000000000..6b90f4797
--- /dev/null
+++ b/kernel/bazaar/stage.go
@@ -0,0 +1,332 @@
+// SiYuan - Refactor your thinking
+// Copyright (c) 2020-present, b3log.org
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package bazaar
+
+import (
+ "context"
+ "errors"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/siyuan-note/httpclient"
+ "github.com/siyuan-note/logging"
+ "github.com/siyuan-note/siyuan/kernel/util"
+ "golang.org/x/mod/semver"
+ "golang.org/x/sync/singleflight"
+)
+
+var cachedStageIndex = map[string]*StageIndex{}
+var stageIndexCacheTime int64
+var stageIndexLock = sync.RWMutex{}
+
+type StageBazaarResult struct {
+ StageIndex *StageIndex // stage 索引
+ BazaarIndex map[string]*bazaarPackage // bazaar 索引
+ Online bool // online 状态
+ StageErr error // stage 错误
+}
+
+var stageBazaarFlight singleflight.Group
+var onlineCheckFlight singleflight.Group
+
+// getStageAndBazaar 获取 stage 索引和 bazaar 索引,相同 pkgType 的并发调用会合并为一次实际请求 (single-flight)
+func getStageAndBazaar(pkgType string) (result StageBazaarResult) {
+ key := "stageBazaar:" + pkgType
+ v, err, _ := stageBazaarFlight.Do(key, func() (interface{}, error) {
+ return getStageAndBazaar0(pkgType), nil
+ })
+ if err != nil {
+ return
+ }
+ result = v.(StageBazaarResult)
+ return
+}
+
+// getStageAndBazaar0 执行一次 stage 和 bazaar 索引拉取
+func getStageAndBazaar0(pkgType string) (result StageBazaarResult) {
+ stageIndex, stageErr := getStageIndexFromCache(pkgType)
+ bazaarIndex := getBazaarIndexFromCache()
+ if nil != stageIndex && nil != bazaarIndex {
+ // 两者都从缓存返回,不需要 online 检查
+ return StageBazaarResult{
+ StageIndex: stageIndex,
+ BazaarIndex: bazaarIndex,
+ Online: true,
+ StageErr: stageErr,
+ }
+ }
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ var onlineResult bool
+ onlineDone := make(chan bool, 1)
+ wg := &sync.WaitGroup{}
+ wg.Add(3)
+ go func() {
+ defer wg.Done()
+ onlineResult = isBazzarOnline()
+ onlineDone <- true
+ }()
+ go func() {
+ defer wg.Done()
+ stageIndex, stageErr = getStageIndex(ctx, pkgType)
+ }()
+ go func() {
+ defer wg.Done()
+ bazaarIndex = getBazaarIndex(ctx)
+ }()
+
+ <-onlineDone
+ if !onlineResult {
+ // 不在线时立即取消其他请求并返回结果,避免等待 HTTP 请求超时
+ cancel()
+ return StageBazaarResult{
+ StageIndex: stageIndex,
+ BazaarIndex: bazaarIndex,
+ Online: false,
+ StageErr: stageErr,
+ }
+ }
+
+ // 在线时等待所有请求完成
+ wg.Wait()
+
+ return StageBazaarResult{
+ StageIndex: stageIndex,
+ BazaarIndex: bazaarIndex,
+ Online: onlineResult,
+ StageErr: stageErr,
+ }
+}
+
+// getStageIndexFromCache 仅从缓存获取 stage 索引,过期或无缓存时返回 nil
+func getStageIndexFromCache(pkgType string) (ret *StageIndex, err error) {
+ stageIndexLock.RLock()
+ cacheTime := stageIndexCacheTime
+ cached := cachedStageIndex[pkgType]
+ stageIndexLock.RUnlock()
+ if util.RhyCacheDuration >= time.Now().Unix()-cacheTime && nil != cached {
+ ret = cached
+ }
+ return
+}
+
+// getStageIndex 获取 stage 索引
+func getStageIndex(ctx context.Context, pkgType string) (ret *StageIndex, err error) {
+ if cached, cacheErr := getStageIndexFromCache(pkgType); nil != cached {
+ ret = cached
+ err = cacheErr
+ return
+ }
+
+ var rhyRet map[string]interface{}
+ rhyRet, err = util.GetRhyResult(ctx, false)
+ if nil != err {
+ return
+ }
+
+ stageIndexLock.Lock()
+ defer stageIndexLock.Unlock()
+
+ bazaarHash := rhyRet["bazaar"].(string)
+ ret = &StageIndex{}
+ request := httpclient.NewBrowserRequest()
+ u := util.BazaarOSSServer + "/bazaar@" + bazaarHash + "/stage/" + pkgType + ".json"
+ resp, reqErr := request.SetContext(ctx).SetSuccessResult(ret).Get(u)
+ if nil != reqErr {
+ logging.LogErrorf("get community stage index [%s] failed: %s", u, reqErr)
+ err = reqErr
+ return
+ }
+ if 200 != resp.StatusCode {
+ logging.LogErrorf("get community stage index [%s] failed: %d", u, resp.StatusCode)
+ err = errors.New("get stage index failed")
+ return
+ }
+
+ stageIndexCacheTime = time.Now().Unix()
+ cachedStageIndex[pkgType] = ret
+ return
+}
+
+func isOutdatedTheme(theme *Theme, bazaarThemes []*Theme) bool {
+ if !strings.HasPrefix(theme.URL, "https://github.com/") {
+ return false
+ }
+
+ repo := strings.TrimPrefix(theme.URL, "https://github.com/")
+ parts := strings.Split(repo, "/")
+ if 2 != len(parts) || "" == strings.TrimSpace(parts[1]) {
+ return false
+ }
+
+ for _, pkg := range bazaarThemes {
+ if theme.Name == pkg.Name && 0 > semver.Compare("v"+theme.Version, "v"+pkg.Version) {
+ theme.RepoHash = pkg.RepoHash
+ return true
+ }
+ }
+ return false
+}
+
+func isOutdatedIcon(icon *Icon, bazaarIcons []*Icon) bool {
+ if !strings.HasPrefix(icon.URL, "https://github.com/") {
+ return false
+ }
+
+ repo := strings.TrimPrefix(icon.URL, "https://github.com/")
+ parts := strings.Split(repo, "/")
+ if 2 != len(parts) || "" == strings.TrimSpace(parts[1]) {
+ return false
+ }
+
+ for _, pkg := range bazaarIcons {
+ if icon.Name == pkg.Name && 0 > semver.Compare("v"+icon.Version, "v"+pkg.Version) {
+ icon.RepoHash = pkg.RepoHash
+ return true
+ }
+ }
+ return false
+}
+
+func isOutdatedPlugin(plugin *Plugin, bazaarPlugins []*Plugin) bool {
+ if !strings.HasPrefix(plugin.URL, "https://github.com/") {
+ return false
+ }
+
+ repo := strings.TrimPrefix(plugin.URL, "https://github.com/")
+ parts := strings.Split(repo, "/")
+ if 2 != len(parts) || "" == strings.TrimSpace(parts[1]) {
+ return false
+ }
+
+ for _, pkg := range bazaarPlugins {
+ if plugin.Name == pkg.Name && 0 > semver.Compare("v"+plugin.Version, "v"+pkg.Version) {
+ plugin.RepoHash = pkg.RepoHash
+ return true
+ }
+ }
+ return false
+}
+
+func isOutdatedWidget(widget *Widget, bazaarWidgets []*Widget) bool {
+ if !strings.HasPrefix(widget.URL, "https://github.com/") {
+ return false
+ }
+
+ repo := strings.TrimPrefix(widget.URL, "https://github.com/")
+ parts := strings.Split(repo, "/")
+ if 2 != len(parts) || "" == strings.TrimSpace(parts[1]) {
+ return false
+ }
+
+ for _, pkg := range bazaarWidgets {
+ if widget.Name == pkg.Name && 0 > semver.Compare("v"+widget.Version, "v"+pkg.Version) {
+ widget.RepoHash = pkg.RepoHash
+ return true
+ }
+ }
+ return false
+}
+
+func isOutdatedTemplate(template *Template, bazaarTemplates []*Template) bool {
+ if !strings.HasPrefix(template.URL, "https://github.com/") {
+ return false
+ }
+
+ repo := strings.TrimPrefix(template.URL, "https://github.com/")
+ parts := strings.Split(repo, "/")
+ if 2 != len(parts) || "" == strings.TrimSpace(parts[1]) {
+ return false
+ }
+
+ for _, pkg := range bazaarTemplates {
+ if template.Name == pkg.Name && 0 > semver.Compare("v"+template.Version, "v"+pkg.Version) {
+ template.RepoHash = pkg.RepoHash
+ return true
+ }
+ }
+ return false
+}
+
+func isBazzarOnline() bool {
+ v, err, _ := onlineCheckFlight.Do("bazaarOnline", func() (interface{}, error) {
+ return isBazzarOnline0(), nil
+ })
+ if err != nil {
+ return false
+ }
+ return v.(bool)
+}
+
+func isBazzarOnline0() (ret bool) {
+ // Improve marketplace loading when offline https://github.com/siyuan-note/siyuan/issues/12050
+ ret = util.IsOnline(util.BazaarOSSServer+"/204", true, 3000)
+ if !ret {
+ util.PushErrMsg(util.Langs[util.Lang][24], 5000)
+ }
+ return
+}
+
+type bazaarPackage struct {
+ Name string `json:"name"`
+ Downloads int `json:"downloads"`
+}
+
+var cachedBazaarIndex = map[string]*bazaarPackage{}
+var bazaarIndexCacheTime int64
+var bazaarIndexLock = sync.RWMutex{}
+
+// getBazaarIndexFromCache 仅从缓存获取 bazaar 索引,过期或无缓存时返回 nil
+func getBazaarIndexFromCache() (ret map[string]*bazaarPackage) {
+ bazaarIndexLock.RLock()
+ cacheTime := bazaarIndexCacheTime
+ cached := cachedBazaarIndex
+ hasData := 0 < len(cached)
+ bazaarIndexLock.RUnlock()
+ if util.RhyCacheDuration >= time.Now().Unix()-cacheTime && hasData {
+ ret = cached
+ } else {
+ ret = nil
+ }
+ return
+}
+
+// getBazaarIndex 获取 bazaar 索引
+func getBazaarIndex(ctx context.Context) map[string]*bazaarPackage {
+ if cached := getBazaarIndexFromCache(); nil != cached {
+ return cached
+ }
+
+ bazaarIndexLock.Lock()
+ defer bazaarIndexLock.Unlock()
+
+ request := httpclient.NewBrowserRequest()
+ u := util.BazaarStatServer + "/bazaar/index.json"
+ resp, reqErr := request.SetContext(ctx).SetSuccessResult(&cachedBazaarIndex).Get(u)
+ if nil != reqErr {
+ logging.LogErrorf("get bazaar index [%s] failed: %s", u, reqErr)
+ return cachedBazaarIndex
+ }
+ if 200 != resp.StatusCode {
+ logging.LogErrorf("get bazaar index [%s] failed: %d", u, resp.StatusCode)
+ return cachedBazaarIndex
+ }
+ bazaarIndexCacheTime = time.Now().Unix()
+ return cachedBazaarIndex
+}
From 26c378a8209e788bc1e5e6f879618cf7f9ee3de4 Mon Sep 17 00:00:00 2001
From: Jeffrey Chen <78434827+TCOTC@users.noreply.github.com>
Date: Wed, 4 Mar 2026 22:33:35 +0800
Subject: [PATCH 3/7] :art: Unified file listener logic (#17134)
---
kernel/api/ui.go | 2 +-
kernel/main.go | 1 +
kernel/model/appearance.go | 186 +++++++++-----------------
kernel/model/assets_watcher.go | 31 +++--
kernel/model/assets_watcher_darwin.go | 37 ++---
kernel/model/bazzar.go | 9 +-
kernel/model/emojis_watcher.go | 33 +++--
kernel/model/emojis_watcher_darwin.go | 37 ++---
kernel/model/themes_watcher.go | 161 ++++++++++++++++++++++
kernel/model/themes_watcher_darwin.go | 156 +++++++++++++++++++++
10 files changed, 462 insertions(+), 191 deletions(-)
create mode 100644 kernel/model/themes_watcher.go
create mode 100644 kernel/model/themes_watcher_darwin.go
diff --git a/kernel/api/ui.go b/kernel/api/ui.go
index dcbec4ebe..8a4d6302a 100644
--- a/kernel/api/ui.go
+++ b/kernel/api/ui.go
@@ -76,5 +76,5 @@ func reloadIcon(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
- model.ReloadIcon()
+ model.LoadIcons()
}
diff --git a/kernel/main.go b/kernel/main.go
index c010d9f78..466b9ddb2 100644
--- a/kernel/main.go
+++ b/kernel/main.go
@@ -55,5 +55,6 @@ func main() {
model.WatchAssets()
model.WatchEmojis()
+ model.WatchThemes()
model.HandleSignal()
}
diff --git a/kernel/model/appearance.go b/kernel/model/appearance.go
index 2751ddd01..60da7e0f1 100644
--- a/kernel/model/appearance.go
+++ b/kernel/model/appearance.go
@@ -21,11 +21,9 @@ import (
"os"
"path/filepath"
"strings"
- "sync"
"time"
"github.com/88250/gulu"
- "github.com/fsnotify/fsnotify"
"github.com/siyuan-note/filelock"
"github.com/siyuan-note/logging"
"github.com/siyuan-note/siyuan/kernel/bazaar"
@@ -41,15 +39,17 @@ func InitAppearance() {
return
}
- unloadThemes()
from := filepath.Join(util.WorkingDir, "appearance")
if err := filelock.Copy(from, util.AppearancePath); err != nil {
logging.LogErrorf("copy appearance resources from [%s] to [%s] failed: %s", from, util.AppearancePath, err)
util.ReportFileSysFatalError(err)
return
}
- loadThemes()
+ loadThemes()
+ LoadIcons()
+
+ Conf.m.Lock()
if !containTheme(Conf.Appearance.ThemeDark, Conf.Appearance.DarkThemes) {
Conf.Appearance.ThemeDark = "midnight"
Conf.Appearance.ThemeJS = false
@@ -58,11 +58,10 @@ func InitAppearance() {
Conf.Appearance.ThemeLight = "daylight"
Conf.Appearance.ThemeJS = false
}
-
- loadIcons()
if !gulu.Str.Contains(Conf.Appearance.Icon, Conf.Appearance.Icons) {
Conf.Appearance.Icon = "material"
}
+ Conf.m.Unlock()
Conf.Save()
@@ -78,36 +77,6 @@ func containTheme(name string, themes []*conf.AppearanceTheme) bool {
return false
}
-var themeWatchers = sync.Map{} // [string]*fsnotify.Watcher{}
-
-func closeThemeWatchers() {
- themeWatchers.Range(func(key, value interface{}) bool {
- if err := value.(*fsnotify.Watcher).Close(); err != nil {
- logging.LogErrorf("close file watcher failed: %s", err)
- }
- return true
- })
-}
-
-func unloadThemes() {
- if !util.IsPathRegularDirOrSymlinkDir(util.ThemesPath) {
- return
- }
-
- themeDirs, err := os.ReadDir(util.ThemesPath)
- if err != nil {
- logging.LogErrorf("read appearance themes folder failed: %s", err)
- return
- }
-
- for _, themeDir := range themeDirs {
- if !util.IsDirRegularOrSymlink(themeDir) {
- continue
- }
- unwatchTheme(filepath.Join(util.ThemesPath, themeDir.Name()))
- }
-}
-
func loadThemes() {
themeDirs, err := os.ReadDir(util.ThemesPath)
if err != nil {
@@ -116,9 +85,13 @@ func loadThemes() {
return
}
- Conf.Appearance.DarkThemes = nil
- Conf.Appearance.LightThemes = nil
+ var darkThemes, lightThemes []*conf.AppearanceTheme
var daylightTheme, midnightTheme *conf.AppearanceTheme
+ var themeVer string
+ var themeJS bool
+ mode := Conf.Appearance.Mode
+ themeLight := Conf.Appearance.ThemeLight
+ themeDark := Conf.Appearance.ThemeDark
for _, themeDir := range themeDirs {
if !util.IsDirRegularOrSymlink(themeDir) {
continue
@@ -129,10 +102,9 @@ func loadThemes() {
continue
}
- modes := themeConf.Modes
- for _, mode := range modes {
+ for _, mode := range themeConf.Modes {
t := &conf.AppearanceTheme{Name: name}
- if "midnight" == name || "daylight" == name {
+ if isBuiltInTheme(name) {
t.Label = name + Conf.Language(281)
} else {
t.Label = name
@@ -156,32 +128,37 @@ func loadThemes() {
}
if "dark" == mode {
- Conf.Appearance.DarkThemes = append(Conf.Appearance.DarkThemes, t)
+ darkThemes = append(darkThemes, t)
} else if "light" == mode {
- Conf.Appearance.LightThemes = append(Conf.Appearance.LightThemes, t)
+ lightThemes = append(lightThemes, t)
}
}
- if 0 == Conf.Appearance.Mode {
- if Conf.Appearance.ThemeLight == name {
- Conf.Appearance.ThemeVer = themeConf.Version
- Conf.Appearance.ThemeJS = gulu.File.IsExist(filepath.Join(util.ThemesPath, name, "theme.js"))
+ if 0 == mode {
+ if themeLight == name {
+ themeVer = themeConf.Version
+ themeJS = gulu.File.IsExist(filepath.Join(util.ThemesPath, name, "theme.js"))
}
} else {
- if Conf.Appearance.ThemeDark == name {
- Conf.Appearance.ThemeVer = themeConf.Version
- Conf.Appearance.ThemeJS = gulu.File.IsExist(filepath.Join(util.ThemesPath, name, "theme.js"))
+ if themeDark == name {
+ themeVer = themeConf.Version
+ themeJS = gulu.File.IsExist(filepath.Join(util.ThemesPath, name, "theme.js"))
}
}
-
- go watchTheme(filepath.Join(util.ThemesPath, name))
}
- Conf.Appearance.LightThemes = append([]*conf.AppearanceTheme{daylightTheme}, Conf.Appearance.LightThemes...)
- Conf.Appearance.DarkThemes = append([]*conf.AppearanceTheme{midnightTheme}, Conf.Appearance.DarkThemes...)
+ lightThemes = append([]*conf.AppearanceTheme{daylightTheme}, lightThemes...)
+ darkThemes = append([]*conf.AppearanceTheme{midnightTheme}, darkThemes...)
+
+ Conf.m.Lock()
+ Conf.Appearance.DarkThemes = darkThemes
+ Conf.Appearance.LightThemes = lightThemes
+ Conf.Appearance.ThemeVer = themeVer
+ Conf.Appearance.ThemeJS = themeJS
+ Conf.m.Unlock()
}
-func loadIcons() {
+func LoadIcons() {
iconDirs, err := os.ReadDir(util.IconsPath)
if err != nil {
logging.LogErrorf("read appearance icons folder failed: %s", err)
@@ -189,7 +166,9 @@ func loadIcons() {
return
}
- Conf.Appearance.Icons = nil
+ var icons []string
+ var iconVer string
+ currentIcon := Conf.Appearance.Icon
for _, iconDir := range iconDirs {
if !util.IsDirRegularOrSymlink(iconDir) {
continue
@@ -199,77 +178,15 @@ func loadIcons() {
if err != nil || nil == iconConf {
continue
}
- Conf.Appearance.Icons = append(Conf.Appearance.Icons, name)
- if Conf.Appearance.Icon == name {
- Conf.Appearance.IconVer = iconConf.Version
+ icons = append(icons, name)
+ if currentIcon == name {
+ iconVer = iconConf.Version
}
}
-}
-
-func ReloadIcon() {
- loadIcons()
-}
-
-func unwatchTheme(folder string) {
- val, _ := themeWatchers.Load(folder)
- if nil != val {
- themeWatcher := val.(*fsnotify.Watcher)
- themeWatcher.Close()
- }
-}
-
-func watchTheme(folder string) {
- val, _ := themeWatchers.Load(folder)
- var themeWatcher *fsnotify.Watcher
- if nil != val {
- themeWatcher = val.(*fsnotify.Watcher)
- themeWatcher.Close()
- }
-
- var err error
- if themeWatcher, err = fsnotify.NewWatcher(); err != nil {
- logging.LogErrorf("add theme file watcher for folder [%s] failed: %s", folder, err)
- return
- }
- themeWatchers.Store(folder, themeWatcher)
-
- done := make(chan bool)
- go func() {
- for {
- select {
- case event, ok := <-themeWatcher.Events:
- if !ok {
- return
- }
-
- //logging.LogInfof(event.String())
- if event.Op&fsnotify.Write == fsnotify.Write && (strings.HasSuffix(event.Name, "theme.css")) {
- var themeName string
- if themeName = isCurrentUseTheme(event.Name); "" == themeName {
- break
- }
-
- if strings.HasSuffix(event.Name, "theme.css") {
- util.BroadcastByType("main", "refreshtheme", 0, "", map[string]interface{}{
- "theme": "/appearance/themes/" + themeName + "/theme.css?" + fmt.Sprintf("%d", time.Now().Unix()),
- })
- break
- }
- }
- case err, ok := <-themeWatcher.Errors:
- if !ok {
- return
- }
- logging.LogErrorf("watch theme file failed: %s", err)
- }
- }
- }()
-
- //logging.LogInfof("add file watcher [%s]", folder)
- if err := themeWatcher.Add(folder); err != nil {
- logging.LogErrorf("add theme files watcher for folder [%s] failed: %s", folder, err)
- }
- <-done
+ Conf.m.Lock()
+ Conf.Appearance.Icons = icons
+ Conf.Appearance.IconVer = iconVer
+ Conf.m.Unlock()
}
func isCurrentUseTheme(themePath string) string {
@@ -285,3 +202,22 @@ func isCurrentUseTheme(themePath string) string {
}
return ""
}
+
+func broadcastRefreshThemeIfCurrent(themeCssPath string) {
+ if !strings.HasSuffix(themeCssPath, "theme.css") {
+ return
+ }
+ // 只处理主题根目录中的 theme.css
+ themeDir := filepath.Clean(filepath.Dir(themeCssPath))
+ themesRoot := filepath.Clean(util.ThemesPath)
+ if themeDir != filepath.Join(themesRoot, filepath.Base(themeDir)) {
+ return
+ }
+ themeName := isCurrentUseTheme(themeCssPath)
+ if themeName == "" {
+ return
+ }
+ util.BroadcastByType("main", "refreshtheme", 0, "", map[string]interface{}{
+ "theme": "/appearance/themes/" + themeName + "/theme.css?" + fmt.Sprintf("%d", time.Now().Unix()),
+ })
+}
diff --git a/kernel/model/assets_watcher.go b/kernel/model/assets_watcher.go
index 8adff63e5..781c2d041 100644
--- a/kernel/model/assets_watcher.go
+++ b/kernel/model/assets_watcher.go
@@ -37,23 +37,30 @@ func WatchAssets() {
return
}
- go func() {
- watchAssets()
- }()
+ go watchAssets()
}
func watchAssets() {
+ CloseWatchAssets()
assetsDir := filepath.Join(util.DataDir, "assets")
- if nil != assetsWatcher {
- assetsWatcher.Close()
- }
var err error
- if assetsWatcher, err = fsnotify.NewWatcher(); err != nil {
+ assetsWatcher, err = fsnotify.NewWatcher()
+ if err != nil {
logging.LogErrorf("add assets watcher for folder [%s] failed: %s", assetsDir, err)
return
}
+ if !gulu.File.IsDir(assetsDir) {
+ os.MkdirAll(assetsDir, 0755)
+ }
+
+ if err = assetsWatcher.Add(assetsDir); err != nil {
+ logging.LogErrorf("add assets watcher for folder [%s] failed: %s", assetsDir, err)
+ CloseWatchAssets()
+ return
+ }
+
go func() {
defer logging.Recover()
@@ -95,19 +102,11 @@ func watchAssets() {
}
}
}()
-
- if !gulu.File.IsDir(assetsDir) {
- os.MkdirAll(assetsDir, 0755)
- }
-
- if err = assetsWatcher.Add(assetsDir); err != nil {
- logging.LogErrorf("add assets watcher for folder [%s] failed: %s", assetsDir, err)
- }
- //logging.LogInfof("added file watcher [%s]", assetsDir)
}
func CloseWatchAssets() {
if nil != assetsWatcher {
assetsWatcher.Close()
+ assetsWatcher = nil
}
}
diff --git a/kernel/model/assets_watcher_darwin.go b/kernel/model/assets_watcher_darwin.go
index 32e620e22..8d355af35 100644
--- a/kernel/model/assets_watcher_darwin.go
+++ b/kernel/model/assets_watcher_darwin.go
@@ -19,9 +19,11 @@
package model
import (
+ "os"
"path/filepath"
"time"
+ "github.com/88250/gulu"
"github.com/radovskyb/watcher"
"github.com/siyuan-note/logging"
"github.com/siyuan-note/siyuan/kernel/cache"
@@ -31,20 +33,31 @@ import (
var assetsWatcher *watcher.Watcher
func WatchAssets() {
- go func() {
- watchAssets()
- }()
+ if !isFileWatcherAvailable() {
+ return
+ }
+
+ go watchAssets()
}
func watchAssets() {
- if nil != assetsWatcher {
- assetsWatcher.Close()
- }
- assetsWatcher = watcher.New()
-
+ CloseWatchAssets()
assetsDir := filepath.Join(util.DataDir, "assets")
+ assetsWatcher = watcher.New()
+
+ if !gulu.File.IsDir(assetsDir) {
+ os.MkdirAll(assetsDir, 0755)
+ }
+
+ if err := assetsWatcher.Add(assetsDir); err != nil {
+ logging.LogErrorf("add assets watcher for folder [%s] failed: %s", assetsDir, err)
+ return
+ }
+
go func() {
+ defer logging.Recover()
+
for {
select {
case event, ok := <-assetsWatcher.Event:
@@ -75,13 +88,6 @@ func watchAssets() {
}
}
}()
-
- if err := assetsWatcher.Add(assetsDir); err != nil {
- logging.LogErrorf("add assets watcher for folder [%s] failed: %s", assetsDir, err)
- return
- }
-
- //logging.LogInfof("added file watcher [%s]", assetsDir)
if err := assetsWatcher.Start(10 * time.Second); err != nil {
logging.LogErrorf("start assets watcher for folder [%s] failed: %s", assetsDir, err)
return
@@ -91,5 +97,6 @@ func watchAssets() {
func CloseWatchAssets() {
if nil != assetsWatcher {
assetsWatcher.Close()
+ assetsWatcher = nil
}
}
diff --git a/kernel/model/bazzar.go b/kernel/model/bazzar.go
index c873e5dc0..ac504919a 100644
--- a/kernel/model/bazzar.go
+++ b/kernel/model/bazzar.go
@@ -432,7 +432,7 @@ func InstalledThemes(keyword string) (ret []*bazaar.Theme) {
}
func InstallBazaarTheme(repoURL, repoHash, themeName string, mode int, update bool) error {
- closeThemeWatchers()
+ CloseWatchThemes()
installPath := filepath.Join(util.ThemesPath, themeName)
err := bazaar.InstallTheme(repoURL, repoHash, installPath, Conf.System.ID)
@@ -458,7 +458,7 @@ func InstallBazaarTheme(repoURL, repoHash, themeName string, mode int, update bo
}
func UninstallBazaarTheme(themeName string) error {
- closeThemeWatchers()
+ CloseWatchThemes()
installPath := filepath.Join(util.ThemesPath, themeName)
err := bazaar.UninstallTheme(installPath)
@@ -580,3 +580,8 @@ func getSearchKeywords(query string) (ret []string) {
}
return
}
+
+// isBuiltInTheme 通过包名或目录名判断是否为内置主题
+func isBuiltInTheme(name string) bool {
+ return "daylight" == name || "midnight" == name
+}
diff --git a/kernel/model/emojis_watcher.go b/kernel/model/emojis_watcher.go
index 8b23fd479..2974e2954 100644
--- a/kernel/model/emojis_watcher.go
+++ b/kernel/model/emojis_watcher.go
@@ -32,27 +32,34 @@ import (
var emojisWatcher *fsnotify.Watcher
func WatchEmojis() {
- if util.ContainerAndroid == util.Container || util.ContainerIOS == util.Container || util.ContainerHarmony == util.Container {
+ if !isFileWatcherAvailable() {
return
}
- go func() {
- watchEmojis()
- }()
+ go watchEmojis()
}
func watchEmojis() {
+ CloseWatchEmojis()
emojisDir := filepath.Join(util.DataDir, "emojis")
- if nil != emojisWatcher {
- emojisWatcher.Close()
- }
var err error
- if emojisWatcher, err = fsnotify.NewWatcher(); err != nil {
+ emojisWatcher, err = fsnotify.NewWatcher()
+ if err != nil {
logging.LogErrorf("add emojis watcher for folder [%s] failed: %s", emojisDir, err)
return
}
+ if !gulu.File.IsDir(emojisDir) {
+ os.MkdirAll(emojisDir, 0755)
+ }
+
+ if err = emojisWatcher.Add(emojisDir); err != nil {
+ logging.LogErrorf("add emojis watcher for folder [%s] failed: %s", emojisDir, err)
+ CloseWatchEmojis()
+ return
+ }
+
go func() {
defer logging.Recover()
@@ -77,19 +84,11 @@ func watchEmojis() {
}
}
}()
-
- if !gulu.File.IsDir(emojisDir) {
- os.MkdirAll(emojisDir, 0755)
- }
-
- if err = emojisWatcher.Add(emojisDir); err != nil {
- logging.LogErrorf("add emojis watcher for folder [%s] failed: %s", emojisDir, err)
- }
- //logging.LogInfof("added file watcher [%s]", emojisDir)
}
func CloseWatchEmojis() {
if nil != emojisWatcher {
emojisWatcher.Close()
+ emojisWatcher = nil
}
}
diff --git a/kernel/model/emojis_watcher_darwin.go b/kernel/model/emojis_watcher_darwin.go
index 652b3c917..419180dfe 100644
--- a/kernel/model/emojis_watcher_darwin.go
+++ b/kernel/model/emojis_watcher_darwin.go
@@ -19,9 +19,11 @@
package model
import (
+ "os"
"path/filepath"
"time"
+ "github.com/88250/gulu"
"github.com/radovskyb/watcher"
"github.com/siyuan-note/logging"
"github.com/siyuan-note/siyuan/kernel/util"
@@ -30,20 +32,31 @@ import (
var emojisWatcher *watcher.Watcher
func WatchEmojis() {
- go func() {
- watchEmojis()
- }()
+ if !isFileWatcherAvailable() {
+ return
+ }
+
+ go watchEmojis()
}
func watchEmojis() {
- if nil != emojisWatcher {
- emojisWatcher.Close()
- }
- emojisWatcher = watcher.New()
-
emojisDir := filepath.Join(util.DataDir, "emojis")
+ CloseWatchEmojis()
+ emojisWatcher = watcher.New()
+
+ if !gulu.File.IsDir(emojisDir) {
+ os.MkdirAll(emojisDir, 0755)
+ }
+
+ if err := emojisWatcher.Add(emojisDir); err != nil {
+ logging.LogErrorf("add emojis watcher for folder [%s] failed: %s", emojisDir, err)
+ return
+ }
+
go func() {
+ defer logging.Recover()
+
for {
select {
case _, ok := <-emojisWatcher.Event:
@@ -61,13 +74,6 @@ func watchEmojis() {
}
}
}()
-
- if err := emojisWatcher.Add(emojisDir); err != nil {
- logging.LogErrorf("add emojis watcher for folder [%s] failed: %s", emojisDir, err)
- return
- }
-
- //logging.LogInfof("added file watcher [%s]", emojisDir)
if err := emojisWatcher.Start(10 * time.Second); err != nil {
logging.LogErrorf("start emojis watcher for folder [%s] failed: %s", emojisDir, err)
return
@@ -77,5 +83,6 @@ func watchEmojis() {
func CloseWatchEmojis() {
if nil != emojisWatcher {
emojisWatcher.Close()
+ emojisWatcher = nil
}
}
diff --git a/kernel/model/themes_watcher.go b/kernel/model/themes_watcher.go
new file mode 100644
index 000000000..2e98dc78d
--- /dev/null
+++ b/kernel/model/themes_watcher.go
@@ -0,0 +1,161 @@
+// SiYuan - Refactor your thinking
+// Copyright (c) 2020-present, b3log.org
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+//go:build !darwin
+
+package model
+
+import (
+ "os"
+ "path/filepath"
+ "time"
+
+ "github.com/88250/gulu"
+ "github.com/fsnotify/fsnotify"
+ "github.com/siyuan-note/logging"
+ "github.com/siyuan-note/siyuan/kernel/util"
+)
+
+var themesWatcher *fsnotify.Watcher
+
+func WatchThemes() {
+ if !isFileWatcherAvailable() {
+ return
+ }
+
+ go watchThemes()
+}
+
+func watchThemes() {
+ CloseWatchThemes()
+ themesDir := util.ThemesPath
+
+ var err error
+ themesWatcher, err = fsnotify.NewWatcher()
+ if err != nil {
+ logging.LogErrorf("add themes watcher for folder [%s] failed: %s", themesDir, err)
+ return
+ }
+
+ if !gulu.File.IsDir(themesDir) {
+ os.MkdirAll(themesDir, 0755)
+ }
+
+ if err = themesWatcher.Add(themesDir); err != nil {
+ logging.LogErrorf("add themes root watcher for folder [%s] failed: %s", themesDir, err)
+ CloseWatchThemes()
+ return
+ }
+
+ // 为每个子目录添加监听,以便收到 theme.css 的变更
+ addThemesSubdirs(themesWatcher, themesDir)
+
+ go func() {
+ defer logging.Recover()
+
+ var (
+ timer *time.Timer
+ lastEvent fsnotify.Event
+ )
+ timer = time.NewTimer(100 * time.Millisecond)
+ <-timer.C // timer should be expired at first
+
+ for {
+ select {
+ case event, ok := <-themesWatcher.Events:
+ if !ok {
+ return
+ }
+
+ // 新目录创建时加入监听
+ if event.Op&fsnotify.Create == fsnotify.Create {
+ if isThemesDirectSubdir(event.Name) {
+ if addErr := themesWatcher.Add(event.Name); addErr != nil {
+ logging.LogWarnf("add themes watcher for new folder [%s] failed: %s", event.Name, addErr)
+ }
+ }
+ }
+
+ lastEvent = event
+ timer.Reset(time.Millisecond * 100)
+ case err, ok := <-themesWatcher.Errors:
+ if !ok {
+ return
+ }
+ logging.LogErrorf("watch themes failed: %s", err)
+ case <-timer.C:
+ handleThemesEvent(lastEvent)
+ }
+ }
+ }()
+}
+
+// addThemesSubdirs 为 themes 下每个子目录添加监听
+func addThemesSubdirs(w *fsnotify.Watcher, themesDir string) {
+ entries, err := os.ReadDir(themesDir)
+ if err != nil {
+ logging.LogErrorf("read themes folder failed: %s", err)
+ return
+ }
+ for _, e := range entries {
+ if !util.IsDirRegularOrSymlink(e) {
+ continue
+ }
+ subdir := filepath.Join(themesDir, e.Name())
+ if addErr := w.Add(subdir); addErr != nil {
+ logging.LogWarnf("add themes watcher for folder [%s] failed: %s", subdir, addErr)
+ }
+ }
+}
+
+// isThemesDirectSubdir 判断 path 是否为 themes 下的直接子目录
+func isThemesDirectSubdir(path string) bool {
+ if !gulu.File.IsDir(path) {
+ return false
+ }
+ rel, err := filepath.Rel(util.ThemesPath, path)
+ if err != nil {
+ return false
+ }
+ if filepath.Base(path) != rel {
+ return false
+ }
+ entries, err := os.ReadDir(util.ThemesPath)
+ if err != nil {
+ return false
+ }
+ name := filepath.Base(path)
+ for _, e := range entries {
+ if e.Name() == name {
+ return util.IsDirRegularOrSymlink(e)
+ }
+ }
+ return false
+}
+
+func handleThemesEvent(event fsnotify.Event) {
+ if event.Op&fsnotify.Write != fsnotify.Write {
+ return
+ }
+ broadcastRefreshThemeIfCurrent(event.Name)
+}
+
+func CloseWatchThemes() {
+ if nil != themesWatcher {
+ themesWatcher.Close()
+ themesWatcher = nil
+ }
+}
diff --git a/kernel/model/themes_watcher_darwin.go b/kernel/model/themes_watcher_darwin.go
new file mode 100644
index 000000000..332b96030
--- /dev/null
+++ b/kernel/model/themes_watcher_darwin.go
@@ -0,0 +1,156 @@
+// SiYuan - Refactor your thinking
+// Copyright (c) 2020-present, b3log.org
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+//go:build darwin
+
+package model
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "github.com/88250/gulu"
+ "github.com/radovskyb/watcher"
+ "github.com/siyuan-note/logging"
+ "github.com/siyuan-note/siyuan/kernel/util"
+)
+
+var themesWatcher *watcher.Watcher
+
+func WatchThemes() {
+ if !isFileWatcherAvailable() {
+ return
+ }
+
+ go watchThemes()
+}
+
+func watchThemes() {
+ CloseWatchThemes()
+ themesDir := util.ThemesPath
+
+ themesWatcher = watcher.New()
+
+ if !gulu.File.IsDir(themesDir) {
+ os.MkdirAll(themesDir, 0755)
+ }
+
+ if err := themesWatcher.Add(themesDir); err != nil {
+ logging.LogErrorf("add themes watcher for folder [%s] failed: %s", themesDir, err)
+ return
+ }
+
+ // 为每个子目录添加监听,以便收到 theme.css 的变更
+ addThemesSubdirs(themesWatcher, themesDir)
+
+ go func() {
+ defer logging.Recover()
+
+ for {
+ select {
+ case event, ok := <-themesWatcher.Event:
+ if !ok {
+ return
+ }
+
+ // 新目录创建时加入监听
+ if watcher.Create == event.Op {
+ if isThemesDirectSubdir(event.Path) {
+ if addErr := themesWatcher.Add(event.Path); addErr != nil {
+ logging.LogWarnf("add themes watcher for new folder [%s] failed: %s", event.Path, addErr)
+ }
+ }
+ }
+
+ handleThemesEvent(event)
+ case err, ok := <-themesWatcher.Error:
+ if !ok {
+ return
+ }
+ logging.LogErrorf("watch themes failed: %s", err)
+ case <-themesWatcher.Closed:
+ return
+ }
+ }
+ }()
+
+ if err := themesWatcher.Start(10 * time.Second); err != nil {
+ logging.LogErrorf("start themes watcher for folder [%s] failed: %s", themesDir, err)
+ return
+ }
+}
+
+// addThemesSubdirs 为 themes 下每个子目录添加监听
+func addThemesSubdirs(w *watcher.Watcher, themesDir string) {
+ entries, err := os.ReadDir(themesDir)
+ if err != nil {
+ logging.LogErrorf("read themes folder failed: %s", err)
+ return
+ }
+ for _, e := range entries {
+ if !util.IsDirRegularOrSymlink(e) {
+ continue
+ }
+ subdir := filepath.Join(themesDir, e.Name())
+ if addErr := w.Add(subdir); addErr != nil {
+ logging.LogWarnf("add themes watcher for folder [%s] failed: %s", subdir, addErr)
+ }
+ }
+}
+
+// isThemesDirectSubdir 判断 path 是否为 themes 下的直接子目录
+func isThemesDirectSubdir(path string) bool {
+ if !gulu.File.IsDir(path) {
+ return false
+ }
+ rel, err := filepath.Rel(util.ThemesPath, path)
+ if err != nil {
+ return false
+ }
+ if filepath.Base(path) != rel {
+ return false
+ }
+ entries, err := os.ReadDir(util.ThemesPath)
+ if err != nil {
+ return false
+ }
+ name := filepath.Base(path)
+ for _, e := range entries {
+ if e.Name() == name {
+ return util.IsDirRegularOrSymlink(e)
+ }
+ }
+ return false
+}
+
+func handleThemesEvent(event watcher.Event) {
+ if watcher.Write != event.Op {
+ return
+ }
+ if !strings.HasSuffix(event.Path, "theme.css") {
+ return
+ }
+ broadcastRefreshThemeIfCurrent(event.Path)
+}
+
+func CloseWatchThemes() {
+ if nil != themesWatcher {
+ themesWatcher.Close()
+ themesWatcher = nil
+ }
+}
From 5e0d1e64de71b2c1a9c85745780b80ae789530a3 Mon Sep 17 00:00:00 2001
From: Jeffrey Chen <78434827+TCOTC@users.noreply.github.com>
Date: Wed, 4 Mar 2026 22:47:44 +0800
Subject: [PATCH 4/7] :recycle: Normalize bazaar naming (#17136)
---
kernel/model/{bazzar.go => bazaar.go} | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename kernel/model/{bazzar.go => bazaar.go} (100%)
diff --git a/kernel/model/bazzar.go b/kernel/model/bazaar.go
similarity index 100%
rename from kernel/model/bazzar.go
rename to kernel/model/bazaar.go
From 487e27cb000e3d0d45b05e31cbed04189d10f687 Mon Sep 17 00:00:00 2001
From: Jeffrey Chen <78434827+TCOTC@users.noreply.github.com>
Date: Thu, 5 Mar 2026 10:11:44 +0800
Subject: [PATCH 5/7] :recycle: refactor reloadPlugin (#17137)
---
app/src/config/bazaar.ts | 2 +-
app/src/plugin/loader.ts | 18 +++----
kernel/api/petal.go | 6 +--
kernel/model/bazaar.go | 2 +-
kernel/model/push_reload.go | 101 ++++++++++++------------------------
kernel/model/repository.go | 14 ++---
6 files changed, 54 insertions(+), 89 deletions(-)
diff --git a/app/src/config/bazaar.ts b/app/src/config/bazaar.ts
index 153567568..0ba9a74f3 100644
--- a/app/src/config/bazaar.ts
+++ b/app/src/config/bazaar.ts
@@ -771,7 +771,7 @@ type="checkbox">
app.plugins.find((item: Plugin) => {
if (item.name === dataObj.name) {
reloadPlugin(app, {
- upsertCodePlugins: [dataObj.name],
+ reloadPlugins: [dataObj.name],
});
return true;
}
diff --git a/app/src/plugin/loader.ts b/app/src/plugin/loader.ts
index d3dfc2f77..fbe746232 100644
--- a/app/src/plugin/loader.ts
+++ b/app/src/plugin/loader.ts
@@ -223,12 +223,12 @@ export const afterLoadPlugin = (plugin: Plugin) => {
};
export const reloadPlugin = async (app: App, data: {
- upsertCodePlugins?: string[],
- upsertDataPlugins?: string[],
- unloadPlugins?: string[],
- uninstallPlugins?: string[],
+ uninstallPlugins?: string[], // 插件卸载
+ unloadPlugins?: string[], // 插件禁用
+ reloadPlugins?: string[], // 插件启用,或插件代码变更
+ dataChangePlugins?: string[], // 插件存储数据变更
} = {}) => {
- const {upsertCodePlugins = [], upsertDataPlugins = [], unloadPlugins = [], uninstallPlugins = []} = data;
+ const {uninstallPlugins = [], unloadPlugins = [], reloadPlugins = [], dataChangePlugins = []} = data;
// 禁用
unloadPlugins.forEach((item) => {
uninstall(app, item, true);
@@ -237,12 +237,12 @@ export const reloadPlugin = async (app: App, data: {
uninstallPlugins.forEach((item) => {
uninstall(app, item, false);
});
- upsertCodePlugins.forEach((item) => {
+ reloadPlugins.forEach((item) => {
uninstall(app, item, true);
});
- loadPlugins(app, upsertCodePlugins, false).then(() => {
+ loadPlugins(app, reloadPlugins, false).then(() => {
app.plugins.forEach(item => {
- if (upsertCodePlugins.includes(item.name)) {
+ if (reloadPlugins.includes(item.name)) {
afterLoadPlugin(item);
getAllEditor().forEach(editor => {
editor.protyle.toolbar.update(editor.protyle);
@@ -251,7 +251,7 @@ export const reloadPlugin = async (app: App, data: {
});
});
app.plugins.forEach(item => {
- if (upsertDataPlugins.includes(item.name)) {
+ if (dataChangePlugins.includes(item.name)) {
try {
item.onDataChanged();
} catch (e) {
diff --git a/kernel/api/petal.go b/kernel/api/petal.go
index b9e45a488..bf8397f19 100644
--- a/kernel/api/petal.go
+++ b/kernel/api/petal.go
@@ -67,10 +67,10 @@ func setPetalEnabled(c *gin.Context) {
app = arg["app"].(string)
}
if enabled {
- upsertPluginCodeSet := hashset.New(packageName)
- model.PushReloadPlugin(upsertPluginCodeSet, nil, nil, nil, app)
+ reloadPluginSet := hashset.New(packageName)
+ model.PushReloadPlugin(nil, nil, reloadPluginSet, nil, app)
} else {
unloadPluginSet := hashset.New(packageName)
- model.PushReloadPlugin(nil, nil, unloadPluginSet, nil, app)
+ model.PushReloadPlugin(nil, unloadPluginSet, nil, nil, app)
}
}
diff --git a/kernel/model/bazaar.go b/kernel/model/bazaar.go
index ac504919a..7eaf4e3ae 100644
--- a/kernel/model/bazaar.go
+++ b/kernel/model/bazaar.go
@@ -267,7 +267,7 @@ func UninstallBazaarPlugin(pluginName, frontend string) error {
savePetals(petals)
uninstallPluginSet := hashset.New(pluginName)
- PushReloadPlugin(nil, nil, nil, uninstallPluginSet, "")
+ PushReloadPlugin(uninstallPluginSet, nil, nil, nil, "")
return nil
}
diff --git a/kernel/model/push_reload.go b/kernel/model/push_reload.go
index 29180556a..bc0cd8b56 100644
--- a/kernel/model/push_reload.go
+++ b/kernel/model/push_reload.go
@@ -43,85 +43,50 @@ func PushReloadSnippet(snippet *conf.Snpt) {
util.BroadcastByType("main", "setSnippet", 0, "", snippet)
}
-func PushReloadPlugin(upsertCodePluginSet, upsertDataPluginSet, unloadPluginNameSet, uninstallPluginNameSet *hashset.Set, excludeApp string) {
- // 集合去重
- if nil != uninstallPluginNameSet {
- for _, n := range uninstallPluginNameSet.Values() {
- pluginName := n.(string)
- if nil != upsertCodePluginSet {
- upsertCodePluginSet.Remove(pluginName)
- }
- if nil != upsertDataPluginSet {
- upsertDataPluginSet.Remove(pluginName)
- }
- if nil != unloadPluginNameSet {
- unloadPluginNameSet.Remove(pluginName)
+func PushReloadPlugin(uninstallPluginNameSet, unloadPluginNameSet, reloadPluginSet, dataChangePluginSet *hashset.Set, excludeApp string) {
+ // 按优先级从高到低排列,同一插件只保留在优先级最高的集合中
+ orderedSets := []*hashset.Set{uninstallPluginNameSet, unloadPluginNameSet, reloadPluginSet, dataChangePluginSet}
+ slices := make([][]string, len(orderedSets))
+ // 按顺序遍历所有集合
+ for i, set := range orderedSets {
+ if nil != set {
+ // 遍历当前集合的所有插件名称
+ for _, n := range set.Values() {
+ name := n.(string)
+ // 将该插件从所有后续集合中移除
+ for _, lowerSet := range orderedSets[i+1:] {
+ if nil != lowerSet {
+ lowerSet.Remove(name)
+ }
+ }
}
}
- }
- if nil != unloadPluginNameSet {
- for _, n := range unloadPluginNameSet.Values() {
- pluginName := n.(string)
- if nil != upsertCodePluginSet {
- upsertCodePluginSet.Remove(pluginName)
- }
- if nil != upsertDataPluginSet {
- upsertDataPluginSet.Remove(pluginName)
- }
- }
- }
- if nil != upsertCodePluginSet {
- for _, n := range upsertCodePluginSet.Values() {
- pluginName := n.(string)
- if nil != upsertDataPluginSet {
- upsertDataPluginSet.Remove(pluginName)
+
+ // 将当前集合转换为字符串切片
+ if nil == set {
+ slices[i] = []string{}
+ } else {
+ strs := make([]string, 0, set.Size())
+ for _, n := range set.Values() {
+ strs = append(strs, n.(string))
}
+ slices[i] = strs
}
}
- upsertCodePlugins, upsertDataPlugins, unloadPlugins, uninstallPlugins := []string{}, []string{}, []string{}, []string{}
- if nil != upsertCodePluginSet {
- for _, n := range upsertCodePluginSet.Values() {
- upsertCodePlugins = append(upsertCodePlugins, n.(string))
- }
- }
- if nil != upsertDataPluginSet {
- for _, n := range upsertDataPluginSet.Values() {
- upsertDataPlugins = append(upsertDataPlugins, n.(string))
- }
- }
- if nil != unloadPluginNameSet {
- for _, n := range unloadPluginNameSet.Values() {
- unloadPlugins = append(unloadPlugins, n.(string))
- }
- }
- if nil != uninstallPluginNameSet {
- for _, n := range uninstallPluginNameSet.Values() {
- uninstallPlugins = append(uninstallPlugins, n.(string))
- }
+ logging.LogInfof("reload plugins, uninstalls=%v, unloads=%v, reloads=%v, dataChanges=%v", slices[0], slices[1], slices[2], slices[3])
+ payload := map[string]interface{}{
+ "uninstallPlugins": slices[0], // 插件卸载
+ "unloadPlugins": slices[1], // 插件禁用
+ "reloadPlugins": slices[2], // 插件启用,或插件代码变更
+ "dataChangePlugins": slices[3], // 插件存储数据变更
}
- pushReloadPlugin0(upsertCodePlugins, upsertDataPlugins, unloadPlugins, uninstallPlugins, excludeApp)
-}
-
-func pushReloadPlugin0(upsertCodePlugins, upsertDataPlugins, unloadPlugins, uninstallPlugins []string, excludeApp string) {
- logging.LogInfof("reload plugins [codeChanges=%v, dataChanges=%v, unloads=%v, uninstalls=%v]", upsertCodePlugins, upsertDataPlugins, unloadPlugins, uninstallPlugins)
if "" == excludeApp {
- util.BroadcastByType("main", "reloadPlugin", 0, "", map[string]interface{}{
- "upsertCodePlugins": upsertCodePlugins,
- "upsertDataPlugins": upsertDataPlugins,
- "unloadPlugins": unloadPlugins,
- "uninstallPlugins": uninstallPlugins,
- })
+ util.BroadcastByType("main", "reloadPlugin", 0, "", payload)
return
}
-
- util.BroadcastByTypeAndExcludeApp(excludeApp, "main", "reloadPlugin", 0, "", map[string]interface{}{
- "upsertCodePlugins": upsertCodePlugins,
- "upsertDataPlugins": upsertDataPlugins,
- "unloadPlugins": unloadPlugins,
- "uninstallPlugins": uninstallPlugins,
- })
+ util.BroadcastByTypeAndExcludeApp(excludeApp, "main", "reloadPlugin", 0, "", payload)
}
func refreshDocInfo(tree *parse.Tree) {
diff --git a/kernel/model/repository.go b/kernel/model/repository.go
index 6e44edbee..52d2c624d 100644
--- a/kernel/model/repository.go
+++ b/kernel/model/repository.go
@@ -1596,8 +1596,8 @@ func processSyncMergeResult(exit, byHand bool, mergeResult *dejavu.MergeResult,
var upsertTrees int
// 可能需要重新加载部分功能
var needReloadFlashcard, needReloadOcrTexts, needReloadPlugin, needReloadSnippet bool
- upsertCodePluginSet := hashset.New() // 插件代码变更 data/plugins/
- upsertDataPluginSet := hashset.New() // 插件存储数据变更 data/storage/petal/
+ reloadPluginSet := hashset.New() // 插件代码变更 data/plugins/
+ dataChangePluginSet := hashset.New() // 插件存储数据变更 data/storage/petal/
needUnindexBoxes, needIndexBoxes := map[string]bool{}, map[string]bool{}
for _, file := range mergeResult.Upserts {
upserts = append(upserts, file.Path)
@@ -1620,7 +1620,7 @@ func processSyncMergeResult(exit, byHand bool, mergeResult *dejavu.MergeResult,
needReloadPlugin = true
if parts := strings.Split(file.Path, "/"); 3 < len(parts) {
if pluginName := parts[3]; "petals.json" != pluginName {
- upsertDataPluginSet.Add(pluginName)
+ dataChangePluginSet.Add(pluginName)
}
}
}
@@ -1628,7 +1628,7 @@ func processSyncMergeResult(exit, byHand bool, mergeResult *dejavu.MergeResult,
if strings.HasPrefix(file.Path, "/plugins/") {
if parts := strings.Split(file.Path, "/"); 2 < len(parts) {
needReloadPlugin = true
- upsertCodePluginSet.Add(parts[2])
+ reloadPluginSet.Add(parts[2])
}
}
@@ -1667,7 +1667,7 @@ func processSyncMergeResult(exit, byHand bool, mergeResult *dejavu.MergeResult,
needReloadPlugin = true
if parts := strings.Split(file.Path, "/"); 3 < len(parts) {
if pluginName := parts[3]; "petals.json" != pluginName {
- upsertDataPluginSet.Add(pluginName)
+ dataChangePluginSet.Add(pluginName)
}
}
}
@@ -1698,7 +1698,7 @@ func processSyncMergeResult(exit, byHand bool, mergeResult *dejavu.MergeResult,
for _, upsertPetal := range mergeResult.UpsertPetals {
needReloadPlugin = true
- upsertCodePluginSet.Add(upsertPetal)
+ reloadPluginSet.Add(upsertPetal)
}
for _, removePetal := range mergeResult.RemovePetals {
needReloadPlugin = true
@@ -1715,7 +1715,7 @@ func processSyncMergeResult(exit, byHand bool, mergeResult *dejavu.MergeResult,
}
if needReloadPlugin {
- PushReloadPlugin(upsertCodePluginSet, upsertDataPluginSet, unloadPluginSet, uninstallPluginSet, "")
+ PushReloadPlugin(uninstallPluginSet, unloadPluginSet, reloadPluginSet, dataChangePluginSet, "")
}
if needReloadSnippet {
From 51295adb4b8fbc4cd12cff4b37f27fa63d842843 Mon Sep 17 00:00:00 2001
From: Daniel <845765@qq.com>
Date: Thu, 5 Mar 2026 10:13:57 +0800
Subject: [PATCH 6/7] :art: Support sending notifications on Android
https://github.com/siyuan-note/siyuan/issues/17114
Signed-off-by: Daniel <845765@qq.com>
---
app/src/mobile/util/onMessage.ts | 2 +-
app/src/types/index.d.ts | 2 +-
kernel/api/notification.go | 8 ++++++++
3 files changed, 10 insertions(+), 2 deletions(-)
diff --git a/app/src/mobile/util/onMessage.ts b/app/src/mobile/util/onMessage.ts
index 196759437..3b47e4020 100644
--- a/app/src/mobile/util/onMessage.ts
+++ b/app/src/mobile/util/onMessage.ts
@@ -26,7 +26,7 @@ export const onMessage = (app: App, data: IWebSocketData) => {
break;
case "sendDeviceNotification":
if (window.JSAndroid.sendNotification) {
- window.JSAndroid.sendNotification(data.data.title, data.data.body, data.data.delayInSeconds);
+ window.JSAndroid.sendNotification(data.data.channel, data.data.title, data.data.body, data.data.delayInSeconds);
}
break;
case "backgroundtask":
diff --git a/app/src/types/index.d.ts b/app/src/types/index.d.ts
index 84f4a8c1f..9348ebd9a 100644
--- a/app/src/types/index.d.ts
+++ b/app/src/types/index.d.ts
@@ -255,7 +255,7 @@ interface Window {
getScreenWidthPx(): number
exit(): void
setWebViewFocusable(enable: boolean): void
- sendNotification(title: string, body: string, delayInSeconds: number): void
+ sendNotification(channel: string, title: string, body: string, delayInSeconds: number): void
};
JSHarmony: {
showKeyboard(): void
diff --git a/kernel/api/notification.go b/kernel/api/notification.go
index f71192252..4b0a5a48c 100644
--- a/kernel/api/notification.go
+++ b/kernel/api/notification.go
@@ -40,6 +40,13 @@ func sendDeviceNotification(c *gin.Context) {
return
}
+ var channel string
+ if nil != arg["channel"] {
+ channel = strings.TrimSpace(arg["channel"].(string))
+ } else {
+ channel = "SiYuan Notifications"
+ }
+
var title string
if nil != arg["title"] {
title = strings.TrimSpace(arg["title"].(string))
@@ -66,6 +73,7 @@ func sendDeviceNotification(c *gin.Context) {
}
util.BroadcastByType("main", "sendDeviceNotification", 0, "", map[string]interface{}{
+ "channel": channel,
"title": title,
"body": body,
"delayInSeconds": delayInSeconds,
From 5d98257e51eb401933c776f130d273fbe523d768 Mon Sep 17 00:00:00 2001
From: Daniel <845765@qq.com>
Date: Thu, 5 Mar 2026 10:54:54 +0800
Subject: [PATCH 7/7] :art: Support sending notifications on HarmonyOS
https://github.com/siyuan-note/siyuan/issues/17125
Signed-off-by: Daniel <845765@qq.com>
---
app/src/mobile/util/onMessage.ts | 4 ++++
app/src/types/index.d.ts | 1 +
kernel/api/notification.go | 4 ++--
3 files changed, 7 insertions(+), 2 deletions(-)
diff --git a/app/src/mobile/util/onMessage.ts b/app/src/mobile/util/onMessage.ts
index 3b47e4020..dabaf88dd 100644
--- a/app/src/mobile/util/onMessage.ts
+++ b/app/src/mobile/util/onMessage.ts
@@ -28,6 +28,10 @@ export const onMessage = (app: App, data: IWebSocketData) => {
if (window.JSAndroid.sendNotification) {
window.JSAndroid.sendNotification(data.data.channel, data.data.title, data.data.body, data.data.delayInSeconds);
}
+ if (window.JSHarmony.sendNotification) {
+ window.JSHarmony.sendNotification(data.data.channel, data.data.title, data.data.body, data.data.delayInSeconds);
+ }
+
break;
case "backgroundtask":
if (!document.querySelector("#keyboardToolbar").classList.contains("fn__none") ||
diff --git a/app/src/types/index.d.ts b/app/src/types/index.d.ts
index 9348ebd9a..78336d690 100644
--- a/app/src/types/index.d.ts
+++ b/app/src/types/index.d.ts
@@ -274,6 +274,7 @@ interface Window {
getScreenWidthPx(): number
exit(): void
setWebViewFocusable(enable: boolean): void
+ sendNotification(channel: string, title: string, body: string, delayInSeconds: number): void
};
Protyle: import("../protyle/method").default;
diff --git a/kernel/api/notification.go b/kernel/api/notification.go
index 4b0a5a48c..9922396e2 100644
--- a/kernel/api/notification.go
+++ b/kernel/api/notification.go
@@ -34,9 +34,9 @@ func sendDeviceNotification(c *gin.Context) {
return
}
- if util.ContainerAndroid != util.Container {
+ if util.ContainerAndroid != util.Container && util.ContainerHarmony != util.Container {
ret.Code = -1
- ret.Msg = "Just support Android"
+ ret.Msg = "Just support Android and HarmonyOS"
return
}