diff --git a/ainews.go b/ainews.go
index c70f8cc..e82196e 100644
--- a/ainews.go
+++ b/ainews.go
@@ -55,7 +55,12 @@ func fetchAINews() []aiNewsItem {
}
aiNewsMu.Unlock()
- url := fmt.Sprintf("%s?key=%s", tianapiAIURL, tianapiKey)
+ key := loadConfig().tianapiKey()
+ if key == "" {
+ log.Println("未配置天聚数行 API Key")
+ return nil
+ }
+ url := fmt.Sprintf("%s?key=%s", tianapiAIURL, key)
data, err := httpGet(url)
if err != nil {
log.Println("AI资讯请求失败:", err)
@@ -124,25 +129,33 @@ func pushAINews(items []aiNewsItem) {
func aiNewsLoop() {
cfg := loadConfig()
- if cfg.HideAINews {
- return
- }
-
- // 先推送缓存
- if cached := loadAINewsCache(); cached != nil {
- pushAINews(cached)
+ if !cfg.HideAINews {
+ // 先推送缓存
+ if cached := loadAINewsCache(); cached != nil {
+ pushAINews(cached)
+ }
+ } else if cached := loadAINewsCache(); cached != nil {
+ aiNewsMu.Lock()
+ aiNewsCache = cached
+ aiNewsCacheAt = time.Now()
+ aiNewsMu.Unlock()
}
time.Sleep(8 * time.Second)
cfg = loadConfig()
- if cfg.HideAINews {
- return
- }
-
- items := fetchAINews()
- if items != nil {
- pushAINews(items)
+ if !cfg.HideAINews {
+ var items []aiNewsItem
+ for i := 0; i < 3; i++ {
+ items = fetchAINews()
+ if items != nil {
+ break
+ }
+ time.Sleep(30 * time.Second)
+ }
+ if items != nil {
+ pushAINews(items)
+ }
}
ticker := time.NewTicker(2 * time.Hour)
diff --git a/assets/icons/tray-icon-128.png b/assets/icons/tray-icon-128.png
new file mode 100644
index 0000000..5936954
Binary files /dev/null and b/assets/icons/tray-icon-128.png differ
diff --git a/assets/icons/tray-icon-16.png b/assets/icons/tray-icon-16.png
new file mode 100644
index 0000000..659413e
Binary files /dev/null and b/assets/icons/tray-icon-16.png differ
diff --git a/assets/icons/tray-icon-20.png b/assets/icons/tray-icon-20.png
new file mode 100644
index 0000000..09110b8
Binary files /dev/null and b/assets/icons/tray-icon-20.png differ
diff --git a/assets/icons/tray-icon-24.png b/assets/icons/tray-icon-24.png
new file mode 100644
index 0000000..86fd39f
Binary files /dev/null and b/assets/icons/tray-icon-24.png differ
diff --git a/assets/icons/tray-icon-256.png b/assets/icons/tray-icon-256.png
new file mode 100644
index 0000000..7628eb9
Binary files /dev/null and b/assets/icons/tray-icon-256.png differ
diff --git a/assets/icons/tray-icon-32.png b/assets/icons/tray-icon-32.png
new file mode 100644
index 0000000..3ed59ee
Binary files /dev/null and b/assets/icons/tray-icon-32.png differ
diff --git a/assets/icons/tray-icon-40.png b/assets/icons/tray-icon-40.png
new file mode 100644
index 0000000..81be0ea
Binary files /dev/null and b/assets/icons/tray-icon-40.png differ
diff --git a/assets/icons/tray-icon-48.png b/assets/icons/tray-icon-48.png
new file mode 100644
index 0000000..19fb3e9
Binary files /dev/null and b/assets/icons/tray-icon-48.png differ
diff --git a/assets/icons/tray-icon-64.png b/assets/icons/tray-icon-64.png
new file mode 100644
index 0000000..6b002ab
Binary files /dev/null and b/assets/icons/tray-icon-64.png differ
diff --git a/assets/icons/tray-icon.png b/assets/icons/tray-icon.png
new file mode 100644
index 0000000..400a9b6
Binary files /dev/null and b/assets/icons/tray-icon.png differ
diff --git a/assets/icons/tray-source.png b/assets/icons/tray-source.png
new file mode 100644
index 0000000..ae01e29
Binary files /dev/null and b/assets/icons/tray-source.png differ
diff --git a/assets/icons/tray.ico b/assets/icons/tray.ico
new file mode 100644
index 0000000..bae6b50
Binary files /dev/null and b/assets/icons/tray.ico differ
diff --git a/bing.go b/bing.go
index 0cb32d5..95fee0c 100644
--- a/bing.go
+++ b/bing.go
@@ -213,25 +213,14 @@ func bingToggleFavorite() string {
r := &h.Records[h.CurrentIdx]
r.Favorited = !r.Favorited
saveBingHistory(h)
- state := "收藏"
- if r.Favorited {
- state = "取消收藏"
- }
- log.Printf("Bing 壁纸: %s (date=%s)", state, r.Date)
if r.Favorited {
+ log.Printf("Bing 壁纸: 已收藏 (date=%s)", r.Date)
return "☆ 取消收藏"
}
+ log.Printf("Bing 壁纸: 已取消收藏 (date=%s)", r.Date)
return "★ 收藏当前壁纸"
}
-func bingCopyrightInfo() string {
- r := getCurrentBingRecord()
- if r != nil {
- return fmt.Sprintf("%s (%s)", r.Copyright, r.Date)
- }
- return ""
-}
-
func bingCurrentState() string {
h := loadBingHistory()
if h.CurrentIdx < 0 || h.CurrentIdx >= len(h.Records) {
@@ -305,30 +294,34 @@ func bingWallpaperLoop() {
}
// 定时切换壁纸 (1 小时间隔)
- ticker := time.NewTicker(1 * time.Hour)
- for range ticker.C {
- cfg := loadConfig()
- if cfg.WallpaperType != WPBing || !cfg.BingAutoRefresh {
- continue
+ go func() {
+ ticker := time.NewTicker(1 * time.Hour)
+ for range ticker.C {
+ cfg := loadConfig()
+ if cfg.WallpaperType != WPBing || !cfg.BingAutoRefresh {
+ continue
+ }
+ bingMu.Lock()
+ h := loadBingHistory()
+ if len(h.Records) <= 1 {
+ bingMu.Unlock()
+ continue
+ }
+ h.CurrentIdx = (h.CurrentIdx + 1) % len(h.Records)
+ saveBingHistory(h)
+ bingMu.Unlock()
+ bingReloadImage()
+ log.Printf("Bing 自动切换: idx=%d/%d", h.CurrentIdx, len(h.Records))
}
- h := loadBingHistory()
- if len(h.Records) <= 1 {
- continue
- }
- // 顺序切换到下一张
- nextIdx := (h.CurrentIdx + 1) % len(h.Records)
- h.CurrentIdx = nextIdx
- saveBingHistory(h)
- bingReloadImage()
- log.Printf("Bing 自动切换: idx=%d/%d", nextIdx, len(h.Records))
- }
+ }()
- // 定时拉取新壁纸 (4 小时间隔)
- fetchTicker := time.NewTicker(4 * time.Hour)
- for range fetchTicker.C {
- cfg := loadConfig()
- if cfg.WallpaperType == WPBing {
- go fetchBingHistory()
+ go func() {
+ fetchTicker := time.NewTicker(4 * time.Hour)
+ for range fetchTicker.C {
+ cfg := loadConfig()
+ if cfg.WallpaperType == WPBing {
+ go fetchBingHistory()
+ }
}
- }
+ }()
}
diff --git a/build.ps1 b/build.ps1
new file mode 100644
index 0000000..e588d8f
--- /dev/null
+++ b/build.ps1
@@ -0,0 +1,73 @@
+<#
+.SYNOPSIS
+ u-desktop 构建脚本
+.PARAMETER Pack
+ 打包为 zip(含 WebView2 bootstrapper)
+#>
+param(
+ [switch]$Pack
+)
+
+$ErrorActionPreference = "Stop"
+$project = "u-desktop"
+$buildDir = "dist"
+
+# 清理
+if (Test-Path $buildDir) { Remove-Item $buildDir -Recurse -Force }
+New-Item -ItemType Directory -Force -Path $buildDir | Out-Null
+
+# 版本号
+$gitHash = git rev-parse --short HEAD
+$version = "$(Get-Date -Format 'yyyyMMdd').$gitHash"
+Write-Host "=== 构建 $project v$version ===" -ForegroundColor Cyan
+
+# 构建(隐藏控制台窗口)
+$env:GOOS = "windows"
+$env:GOARCH = "amd64"
+go build -ldflags="-s -w -H windowsgui -X main.version=$version" -o "$buildDir/$project.exe" .
+if ($LASTEXITCODE -ne 0) { Write-Host "构建失败" -ForegroundColor Red; exit 1 }
+
+$sizeBefore = [math]::Round((Get-Item "$buildDir/$project.exe").Length / 1MB, 1)
+Write-Host "构建完成: $buildDir/$project.exe ($sizeBefore MB)" -ForegroundColor Green
+
+# UPX 压缩
+if (Get-Command upx -ErrorAction SilentlyContinue) {
+ Write-Host "UPX 压缩中..." -ForegroundColor Yellow
+ upx --best "$buildDir/$project.exe"
+ $sizeAfter = [math]::Round((Get-Item "$buildDir/$project.exe").Length / 1MB, 1)
+ Write-Host "UPX 完成: $sizeBefore MB -> $sizeAfter MB" -ForegroundColor Green
+} else {
+ Write-Host "UPX 未安装,跳过压缩" -ForegroundColor Yellow
+}
+
+if (-not $Pack) { exit 0 }
+
+# --- 打包 ---
+$packDir = "$buildDir\$project-win10-amd64"
+if (Test-Path $packDir) { Remove-Item $packDir -Recurse -Force }
+New-Item -ItemType Directory -Force -Path $packDir | Out-Null
+
+# 复制 exe
+Copy-Item "$buildDir\$project.exe" $packDir
+
+# 下载 WebView2 bootstrapper
+$bootstrapper = "$packDir\MicrosoftEdgeWebview2Setup.exe"
+Write-Host "下载 WebView2 bootstrapper..." -ForegroundColor Yellow
+Invoke-WebRequest -Uri "https://go.microsoft.com/fwlink/p/?LinkId=2124703" -OutFile $bootstrapper -UseBasicParsing
+
+# 写入安装说明
+@"
+u-desktop v$version
+
+== 首次运行(Windows 10)==
+如果程序无法启动,请先运行 MicrosoftEdgeWebview2Setup.exe 安装 WebView2 运行时。
+Windows 11 无需安装,直接运行 $project.exe 即可。
+"@ | Out-File "$packDir\README.txt" -Encoding UTF8
+
+# 打包 zip
+$zipName = "$project-win10-amd64-v$version.zip"
+Compress-Archive -Path $packDir -DestinationPath "$buildDir\$zipName" -Force
+Write-Host "打包完成: $buildDir\$zipName" -ForegroundColor Green
+
+# 清理临时目录
+Remove-Item $packDir -Recurse -Force
diff --git a/config.go b/config.go
index 26a7149..8f2bba2 100644
--- a/config.go
+++ b/config.go
@@ -1,12 +1,9 @@
package main
import (
- "bytes"
- "encoding/binary"
+ _ "embed"
"encoding/json"
- "image"
- "image/color"
- "image/png"
+ "log"
"os"
"path/filepath"
)
@@ -39,42 +36,50 @@ const (
)
type SavedColor struct {
- Color1 string `json:"color1"`
- Color2 string `json:"color2"`
- Gradient bool `json:"gradient"`
+ Color1 string `json:"color1"`
+ Color2 string `json:"color2"`
+ Gradient bool `json:"gradient"`
}
type Config struct {
- Zodiac string `json:"zodiac"`
- City string `json:"city"`
- WallpaperType WallpaperType `json:"wallpaperType"`
- Theme ThemeName `json:"theme"`
- ImagePath string `json:"imagePath"`
- Color1 string `json:"color1"`
- Color2 string `json:"color2"`
- ColorGradient bool `json:"colorGradient"`
- WallpaperText string `json:"wallpaperText"`
- Layout Layout `json:"layout"`
- HideWallpaper bool `json:"hideWallpaper"`
- HideTime bool `json:"hideTime"`
- HideWeather bool `json:"hideWeather"`
- HideZodiac bool `json:"hideZodiac"`
- HideAINews bool `json:"hideAINews"`
- ShowSeconds bool `json:"showSeconds"`
- PhotoDir string `json:"photoDir"`
- PhotoInterval int `json:"photoInterval"`
- HidePhoto bool `json:"hidePhoto"`
- KnowledgeKeyword string `json:"knowledgeKeyword"`
- KnowledgePrompt string `json:"knowledgePrompt"`
- HideKnowledge bool `json:"hideKnowledge"`
- SavedColors []SavedColor `json:"savedColors"`
- BingAutoRefresh bool `json:"bingAutoRefresh"`
+ Zodiac string `json:"zodiac"`
+ City string `json:"city"`
+ WallpaperType WallpaperType `json:"wallpaperType"`
+ Theme ThemeName `json:"theme"`
+ ImagePath string `json:"imagePath"`
+ Color1 string `json:"color1"`
+ Color2 string `json:"color2"`
+ ColorGradient bool `json:"colorGradient"`
+ WallpaperText string `json:"wallpaperText"`
+ Layout Layout `json:"layout"`
+ HideWallpaper bool `json:"hideWallpaper"`
+ HideTime bool `json:"hideTime"`
+ HideWeather bool `json:"hideWeather"`
+ HideZodiac bool `json:"hideZodiac"`
+ HideAINews bool `json:"hideAINews"`
+ ShowSeconds bool `json:"showSeconds"`
+ PhotoDir string `json:"photoDir"`
+ PhotoInterval int `json:"photoInterval"`
+ HidePhoto bool `json:"hidePhoto"`
+ PhotoFrameMode bool `json:"photoFrameMode"`
+ KnowledgeKeyword string `json:"knowledgeKeyword"`
+ KnowledgePrompt string `json:"knowledgePrompt"`
+ HideKnowledge bool `json:"hideKnowledge"`
+ SavedColors []SavedColor `json:"savedColors"`
+ BingAutoRefresh bool `json:"bingAutoRefresh"`
+ AutoStart bool `json:"autoStart"`
+ QWeatherKey string `json:"qweatherKey,omitempty"`
+ TianapiKey string `json:"tianapiKey,omitempty"`
+ CPAKey string `json:"cpaKey,omitempty"`
}
const defaultZodiac = "射手座"
var configPath string
+//go:embed assets/icons/tray.ico
+var trayIcon []byte
+
func configDir() string {
return filepath.Dir(configPath)
}
@@ -117,30 +122,29 @@ func defaultConfig() *Config {
}
func saveConfig(cfg *Config) error {
- data, _ := json.MarshalIndent(cfg, "", " ")
+ data, err := json.MarshalIndent(cfg, "", " ")
+ if err != nil {
+ log.Println("配置序列化失败:", err)
+ return err
+ }
return os.WriteFile(configPath, data, 0644)
}
-func generateIcon() []byte {
- img := image.NewRGBA(image.Rect(0, 0, 16, 16))
- c := color.RGBA{R: 88, G: 101, B: 242, A: 255}
- for y := 0; y < 16; y++ {
- for x := 0; x < 16; x++ {
- img.Set(x, y, c)
- }
+func getSecret(envName, cfgValue string) string {
+ if v := os.Getenv(envName); v != "" {
+ return v
}
- var buf bytes.Buffer
- png.Encode(&buf, img)
- pngData := buf.Bytes()
- ico := make([]byte, 22+len(pngData))
- binary.LittleEndian.PutUint16(ico[0:], 0)
- binary.LittleEndian.PutUint16(ico[2:], 1)
- binary.LittleEndian.PutUint16(ico[4:], 1)
- ico[6], ico[7], ico[8], ico[9] = 16, 16, 0, 0
- binary.LittleEndian.PutUint16(ico[10:], 1)
- binary.LittleEndian.PutUint16(ico[12:], 32)
- binary.LittleEndian.PutUint32(ico[14:], uint32(len(pngData)))
- binary.LittleEndian.PutUint32(ico[18:], 22)
- copy(ico[22:], pngData)
- return ico
+ return cfgValue
+}
+
+func (c *Config) qweatherKey() string {
+ return getSecret("U_DESKTOP_QWEATHER_KEY", c.QWeatherKey)
+}
+
+func (c *Config) tianapiKey() string {
+ return getSecret("U_DESKTOP_TIANAPI_KEY", c.TianapiKey)
+}
+
+func (c *Config) cpaKey() string {
+ return getSecret("U_DESKTOP_CPA_KEY", c.CPAKey)
}
diff --git a/config_icon_test.go b/config_icon_test.go
new file mode 100644
index 0000000..da16079
--- /dev/null
+++ b/config_icon_test.go
@@ -0,0 +1,56 @@
+package main
+
+import (
+ "bytes"
+ "encoding/binary"
+ "image/png"
+ "testing"
+)
+
+func TestTrayIconIsMultiSizeICO(t *testing.T) {
+ ico := trayIcon
+ if len(ico) < 6 {
+ t.Fatalf("icon is too short: %d bytes", len(ico))
+ }
+ if got := binary.LittleEndian.Uint16(ico[0:2]); got != 0 {
+ t.Fatalf("reserved field = %d, want 0", got)
+ }
+ if got := binary.LittleEndian.Uint16(ico[2:4]); got != 1 {
+ t.Fatalf("icon type = %d, want 1", got)
+ }
+ count := int(binary.LittleEndian.Uint16(ico[4:6]))
+ if count < 3 {
+ t.Fatalf("entry count = %d, want at least 3", count)
+ }
+
+ seen := map[int]bool{}
+ for i := 0; i < count; i++ {
+ entry := 6 + i*16
+ size := int(ico[entry])
+ if size == 0 {
+ size = 256
+ }
+ seen[size] = true
+ dataSize := int(binary.LittleEndian.Uint32(ico[entry+8 : entry+12]))
+ dataOffset := int(binary.LittleEndian.Uint32(ico[entry+12 : entry+16]))
+ if dataOffset+dataSize > len(ico) {
+ t.Fatalf("entry %d data range %d..%d exceeds icon length %d", i, dataOffset, dataOffset+dataSize, len(ico))
+ }
+ img, err := png.Decode(bytes.NewReader(ico[dataOffset : dataOffset+dataSize]))
+ if err != nil {
+ t.Fatalf("entry %d PNG decode failed: %v", i, err)
+ }
+ if got := img.Bounds().Dx(); got != int(size) {
+ t.Fatalf("entry %d decoded width = %d, want %d", i, got, size)
+ }
+ if got := img.Bounds().Dy(); got != int(size) {
+ t.Fatalf("entry %d decoded height = %d, want %d", i, got, size)
+ }
+ }
+
+ for _, size := range []int{16, 32, 48, 256} {
+ if !seen[size] {
+ t.Fatalf("icon is missing %dx%d entry", size, size)
+ }
+ }
+}
diff --git a/dialog.go b/dialog.go
index 2c8aad4..2d2b5fc 100644
--- a/dialog.go
+++ b/dialog.go
@@ -10,11 +10,11 @@ import (
var (
comdlg32 = windows.NewLazySystemDLL("comdlg32.dll")
- procGetOpenFileNameW = comdlg32.NewProc("GetOpenFileNameW")
- procChooseColorW = comdlg32.NewProc("ChooseColorW")
+ procGetOpenFileNameW = comdlg32.NewProc("GetOpenFileNameW")
+ procChooseColorW = comdlg32.NewProc("ChooseColorW")
- shell32 = windows.NewLazySystemDLL("shell32.dll")
- procSHBrowseForFolderW = shell32.NewProc("SHBrowseForFolderW")
+ shell32 = windows.NewLazySystemDLL("shell32.dll")
+ procSHBrowseForFolderW = shell32.NewProc("SHBrowseForFolderW")
procSHGetPathFromIDListW = shell32.NewProc("SHGetPathFromIDListW")
ole32dll = windows.NewLazySystemDLL("ole32.dll")
@@ -24,10 +24,14 @@ var (
func slicePtr(s interface{}) uintptr {
switch v := s.(type) {
case []uint16:
- if len(v) == 0 { return 0 }
+ if len(v) == 0 {
+ return 0
+ }
return uintptr(unsafe.Pointer(&v[0]))
case []uint32:
- if len(v) == 0 { return 0 }
+ if len(v) == 0 {
+ return 0
+ }
return uintptr(unsafe.Pointer(&v[0]))
}
return 0
diff --git a/horoscope.go b/horoscope.go
index 5f9702f..c0214db 100644
--- a/horoscope.go
+++ b/horoscope.go
@@ -7,11 +7,9 @@ import (
"os"
"path/filepath"
"strings"
- "sync"
"time"
)
-const tianapiKey = "da21ff665b09cbdcc29952a105aad97b"
const tianapiStarURL = "https://apis.tianapi.com/star/index"
var zodiacToSign = map[string]string{
@@ -20,8 +18,6 @@ var zodiacToSign = map[string]string{
"射手座": "sagittarius", "摩羯座": "capricorn", "水瓶座": "aquarius", "双鱼座": "pisces",
}
-var horoscopeMu sync.Mutex
-
type tianapiStarResp struct {
Code int `json:"code"`
Result struct {
@@ -68,17 +64,18 @@ func loadHoroscopeCache() *horoscopeInfo {
return &info
}
-func isCacheToday(info *horoscopeInfo) bool {
- return info != nil
-}
-
func fetchHoroscope(zodiac string) *horoscopeInfo {
sign := zodiacToSign[zodiac]
if sign == "" {
sign = "sagittarius"
}
- url := fmt.Sprintf("%s?key=%s&astro=%s", tianapiStarURL, tianapiKey, sign)
+ key := loadConfig().tianapiKey()
+ if key == "" {
+ log.Println("未配置天聚数行 API Key")
+ return nil
+ }
+ url := fmt.Sprintf("%s?key=%s&astro=%s", tianapiStarURL, key, sign)
data, err := httpGet(url)
if err != nil {
log.Println("星座运势请求失败:", err)
@@ -138,29 +135,27 @@ func pushHoroscope(zodiac string) {
func horoscopeLoop() {
cfg := loadConfig()
- if cfg.HideZodiac {
- return
- }
cached := loadHoroscopeCache()
- if cached != nil {
+ if cached != nil && !cfg.HideZodiac {
pushHoroscopeInfo(cached)
}
time.Sleep(5 * time.Second)
cfg = loadConfig()
- if cfg.HideZodiac {
- return
- }
-
today := time.Now().Format("2006-01-02")
- if cached != nil && cached.Date == today && cached.Zodiac == cfg.Zodiac {
- return
+ if !cfg.HideZodiac && !(cached != nil && cached.Date == today && cached.Zodiac == cfg.Zodiac) {
+ for i := 0; i < 3; i++ {
+ pushHoroscope(cfg.Zodiac)
+ cached := loadHoroscopeCache()
+ if cached != nil && cached.Date == time.Now().Format("2006-01-02") && cached.Zodiac == cfg.Zodiac {
+ break
+ }
+ time.Sleep(30 * time.Second)
+ }
}
- pushHoroscope(cfg.Zodiac)
-
ticker := time.NewTicker(24 * time.Hour)
for range ticker.C {
cfg := loadConfig()
diff --git a/knowledge.go b/knowledge.go
index be4a528..4bb97ef 100644
--- a/knowledge.go
+++ b/knowledge.go
@@ -9,14 +9,16 @@ import (
"math/rand"
"net/http"
"path/filepath"
+ "strings"
"time"
+ "unicode/utf8"
_ "modernc.org/sqlite"
)
const cpaURL = "https://cpa.1216.top/v1/chat/completions"
-const cpaKey = "alink-shared-key-1"
const cpaModel = "glm-4.5-air"
+const minKnowledgeRunes = 80
type knowledgeData struct {
Content string `json:"content"`
@@ -59,15 +61,24 @@ func getRandomKnowledgeCard(keyword string) string {
if knowledgeDB == nil {
return ""
}
- var content string
- err := knowledgeDB.QueryRow(
- "SELECT content FROM knowledge_cards WHERE keyword = ? ORDER BY RANDOM() LIMIT 1",
+ rows, err := knowledgeDB.Query(
+ "SELECT content FROM knowledge_cards WHERE keyword = ? ORDER BY RANDOM() LIMIT 20",
keyword,
- ).Scan(&content)
+ )
if err != nil {
return ""
}
- return content
+ defer rows.Close()
+ for rows.Next() {
+ var content string
+ if rows.Scan(&content) == nil {
+ content = normalizeKnowledgeContent(content)
+ if isQualityKnowledgeCard(content) {
+ return content
+ }
+ }
+ }
+ return ""
}
func getKnowledgeCardCount(keyword string) int {
@@ -86,21 +97,60 @@ func getKnowledgeCardCount(keyword string) int {
}
func fetchKnowledgeFromLLM(keyword string, cfg *Config) string {
- basePrompt := fmt.Sprintf(
- "根据关键词「%s」,生成一条有趣的知识小卡片。要求:控制在80字以内,简洁有趣,有知识性。直接输出内容,不要加标题、序号或其他格式。",
- keyword,
- )
- if cfg.KnowledgePrompt != "" {
- basePrompt += "\n附加要求:" + cfg.KnowledgePrompt
+ basePrompt := buildKnowledgePrompt(keyword, cfg.KnowledgePrompt)
+ messages := []map[string]string{
+ {
+ "role": "system",
+ "content": "你是严谨的中文知识卡片作者,输出必须具体、准确、有信息密度。不要写空泛鸡汤,不要只给一句定义。",
+ },
+ {"role": "user", "content": basePrompt},
}
body := map[string]interface{}{
- "model": cpaModel,
- "max_tokens": 256,
- "messages": []map[string]string{
- {"role": "user", "content": basePrompt},
- },
+ "model": cpaModel,
+ "max_tokens": 512,
+ "temperature": 0.55,
+ "messages": messages,
}
+ content := requestKnowledgeCompletion(body)
+ content = normalizeKnowledgeContent(content)
+ if isQualityKnowledgeCard(content) {
+ return content
+ }
+
+ log.Printf("知识卡片质量不足,重试: %q", content)
+ messages = append(messages, map[string]string{
+ "role": "user",
+ "content": fmt.Sprintf(
+ "上一条太短或信息密度不足。请重写一条「%s」知识卡片:120-180个中文字符,必须包含一个明确机制/原理、一个具体例子或应用场景、一个结论。只输出正文。",
+ keyword,
+ ),
+ })
+ body["messages"] = messages
+ content = requestKnowledgeCompletion(body)
+ content = normalizeKnowledgeContent(content)
+ if isQualityKnowledgeCard(content) {
+ return content
+ }
+
+ return ""
+}
+
+func buildKnowledgePrompt(keyword, customPrompt string) string {
+ basePrompt := fmt.Sprintf(`围绕关键词「%s」生成一条桌面知识卡片。
+硬性要求:
+1. 120-180个中文字符,分成2-3句;
+2. 必须讲清一个具体机制、原理、权衡或实践经验;
+3. 必须包含一个具体例子、场景或判断标准;
+4. 避免“很重要、非常有用、提升效率”这类空泛表述;
+5. 不要标题、序号、Markdown、表情,直接输出正文。`, keyword)
+ if customPrompt != "" {
+ basePrompt += "\n附加风格要求(不能覆盖上面的字数和质量要求):" + customPrompt
+ }
+ return basePrompt
+}
+
+func requestKnowledgeCompletion(body map[string]interface{}) string {
jsonData, _ := json.Marshal(body)
req, err := http.NewRequest("POST", cpaURL, bytes.NewReader(jsonData))
@@ -108,7 +158,12 @@ func fetchKnowledgeFromLLM(keyword string, cfg *Config) string {
log.Println("知识API请求创建失败:", err)
return ""
}
- req.Header.Set("Authorization", "Bearer "+cpaKey)
+ key := loadConfig().cpaKey()
+ if key == "" {
+ log.Println("未配置知识卡片 API Key")
+ return ""
+ }
+ req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
resp, err := httpClient.Do(req)
@@ -135,6 +190,41 @@ func fetchKnowledgeFromLLM(keyword string, cfg *Config) string {
return ""
}
+// normalizeKnowledgeContent 清洗 LLM 输出的格式残留(Markdown标记、标题前缀、多余空白)
+func normalizeKnowledgeContent(content string) string {
+ content = strings.TrimSpace(content)
+ content = strings.Trim(content, "` \t\r\n")
+ content = strings.TrimPrefix(content, "知识卡片:")
+ content = strings.TrimPrefix(content, "知识卡片:")
+ content = strings.TrimPrefix(content, "正文:")
+ content = strings.TrimPrefix(content, "正文:")
+ content = strings.ReplaceAll(content, "\r\n", "\n")
+ lines := strings.FieldsFunc(content, func(r rune) bool {
+ return r == '\n' || r == '\r' || r == '\t'
+ })
+ content = strings.Join(lines, " ")
+ content = strings.Join(strings.Fields(content), " ")
+ return strings.TrimSpace(content)
+}
+
+// isQualityKnowledgeCard 检查字数 ≥80 且空泛表述 <3 条
+func isQualityKnowledgeCard(content string) bool {
+ if utf8.RuneCountInString(content) < minKnowledgeRunes {
+ return false
+ }
+ weakPhrases := []string{"很重要", "非常重要", "很有用", "提升效率", "值得关注", "可以帮助", "广泛应用"}
+ weakHits := 0
+ for _, phrase := range weakPhrases {
+ if strings.Contains(content, phrase) {
+ weakHits++
+ }
+ }
+ if weakHits >= 3 {
+ return false
+ }
+ return strings.ContainsAny(content, "。;;::,,")
+}
+
func pushKnowledgeJSON(content, keyword string) {
data, _ := json.Marshal(knowledgeData{Content: content, Keyword: keyword})
evalJS(fmt.Sprintf(`if(window.updateKnowledgeFromGo) window.updateKnowledgeFromGo(%s)`, string(data)))
diff --git a/main.go b/main.go
index adfc7af..01faf04 100644
--- a/main.go
+++ b/main.go
@@ -1,24 +1,57 @@
package main
import (
+ "fmt"
"log"
"os"
"path/filepath"
+ "unsafe"
"github.com/getlantern/systray"
+ "github.com/jchv/go-webview2/webviewloader"
"golang.org/x/sys/windows"
)
+func showError(msg string) {
+ title, _ := windows.UTF16PtrFromString("u-desktop")
+ text, _ := windows.UTF16PtrFromString(msg)
+ procMessageBoxW.Call(0, uintptr(unsafe.Pointer(text)), uintptr(unsafe.Pointer(title)), 0x10)
+}
+
+func setupLog() {
+ exePath, _ := os.Executable()
+ logFile, err := os.Create(filepath.Join(filepath.Dir(exePath), "u-desktop.log"))
+ if err != nil {
+ return
+ }
+ log.SetOutput(logFile)
+ log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds)
+}
+
func main() {
- log.SetFlags(log.Ltime | log.Lmicroseconds)
+ setupLog()
+
+ defer func() {
+ if r := recover(); r != nil {
+ log.Println("PANIC:", r)
+ showError(fmt.Sprintf("程序崩溃: %v\n详情见 u-desktop.log", r))
+ }
+ }()
+
+ ver, err := webviewloader.GetInstalledVersion()
+ if err != nil || ver == "" {
+ showError("未检测到 WebView2 运行时,请先安装 Microsoft Edge WebView2 Runtime。\n\n下载地址: https://developer.microsoft.com/microsoft-edge/webview2/")
+ os.Exit(1)
+ }
+ log.Println("WebView2:", ver)
mutexName, _ := windows.UTF16PtrFromString("Global\\u-desktop-single-instance")
mutex, err := windows.CreateMutex(nil, false, mutexName)
if err != nil {
- log.Fatal("创建互斥锁失败:", err)
+ showError("创建互斥锁失败: " + err.Error())
+ os.Exit(1)
}
if windows.GetLastError() == windows.ERROR_ALREADY_EXISTS {
- log.Println("已有实例运行,退出")
windows.CloseHandle(mutex)
return
}
@@ -29,5 +62,7 @@ func main() {
os.MkdirAll(cfgDir, 0755)
configPath = filepath.Join(cfgDir, "settings.json")
procSetProcessDPIAware.Call()
+
+ log.Println("启动 systray...")
systray.Run(onSystrayReady, nil)
}
diff --git a/photo.go b/photo.go
index d73a7bb..eb3e1b4 100644
--- a/photo.go
+++ b/photo.go
@@ -1,7 +1,6 @@
package main
import (
- "encoding/base64"
"encoding/json"
"fmt"
"log"
@@ -14,12 +13,15 @@ import (
)
var (
- photoMu sync.Mutex
- photoFiles []string
- photoIdx int
- photoDir string
- photoStop chan struct{}
- photoDone chan struct{}
+ photoMu sync.Mutex
+ photoFiles []string
+ photoIdx int
+ photoDir string
+ photoStop chan struct{}
+ photoDone chan struct{}
+ photoCacheMu sync.Mutex
+ photoCacheMap map[string]string
+ photoCacheDir string
)
func scanPhotoDir(dir string) []string {
@@ -42,25 +44,31 @@ func scanPhotoDir(dir string) []string {
return files
}
-func photoToDataURI(dir, name string) string {
- path := filepath.Join(dir, name)
- data, err := os.ReadFile(path)
- if err != nil {
- return ""
+func preCachePhotos(dir string, files []string) {
+ cache := make(map[string]string, len(files))
+ for _, name := range files {
+ uri := imageToDataURI(filepath.Join(dir, name))
+ if uri != "" {
+ cache[name] = uri
+ }
}
- ext := strings.ToLower(filepath.Ext(name))
- mime := "image/jpeg"
- switch ext {
- case ".png":
- mime = "image/png"
- case ".gif":
- mime = "image/gif"
- case ".webp":
- mime = "image/webp"
- case ".bmp":
- mime = "image/bmp"
+ photoCacheMu.Lock()
+ photoCacheMap = cache
+ photoCacheDir = dir
+ photoCacheMu.Unlock()
+ log.Printf("相册: 预缓存 %d/%d 张", len(cache), len(files))
+}
+
+func getCachedPhotoURI(dir, name string) string {
+ photoCacheMu.Lock()
+ if photoCacheMap != nil && photoCacheDir == dir {
+ if uri, ok := photoCacheMap[name]; ok {
+ photoCacheMu.Unlock()
+ return uri
+ }
}
- return fmt.Sprintf("data:%s;base64,%s", mime, base64.StdEncoding.EncodeToString(data))
+ photoCacheMu.Unlock()
+ return imageToDataURI(filepath.Join(dir, name))
}
func pushCurrentPhoto(interval int) {
@@ -78,7 +86,7 @@ func pushCurrentPhoto(interval int) {
idx = 0
}
- src := photoToDataURI(dir, files[idx])
+ src := getCachedPhotoURI(dir, files[idx])
if src == "" {
return
}
@@ -118,7 +126,13 @@ func startPhotoLoop() {
photoDone = done
photoMu.Unlock()
+ photoCacheMu.Lock()
+ photoCacheMap = nil
+ photoCacheDir = cfg.PhotoDir
+ photoCacheMu.Unlock()
+
log.Printf("相册: 共 %d 张, 间隔 %ds", len(files), interval)
+ go preCachePhotos(cfg.PhotoDir, files)
pushCurrentPhoto(interval)
go func() {
@@ -149,8 +163,16 @@ func stopPhotoLoop() {
done := photoDone
photoStop = nil
photoDone = nil
+ photoFiles = nil
+ photoIdx = 0
+ photoDir = ""
photoMu.Unlock()
+ photoCacheMu.Lock()
+ photoCacheMap = nil
+ photoCacheDir = ""
+ photoCacheMu.Unlock()
+
if stop != nil {
close(stop)
}
diff --git a/scripts/build_tray_icon.py b/scripts/build_tray_icon.py
new file mode 100644
index 0000000..8418d17
--- /dev/null
+++ b/scripts/build_tray_icon.py
@@ -0,0 +1,98 @@
+from __future__ import annotations
+
+import argparse
+from pathlib import Path
+
+from PIL import Image, ImageChops
+
+
+ICON_SIZES = (16, 20, 24, 32, 40, 48, 64, 128, 256)
+
+
+def remove_green_background(image: Image.Image) -> Image.Image:
+ rgba = image.convert("RGBA")
+ pixels = rgba.load()
+ width, height = rgba.size
+
+ for y in range(height):
+ for x in range(width):
+ r, g, b, a = pixels[x, y]
+ green_score = g - max(r, b)
+ is_key = g > 90 and green_score > 35
+ is_fringe = g > 32 and g > r * 1.35 and g > b * 1.20
+ if is_key or is_fringe:
+ edge = max(0, min(255, (green_score - 18) * 5))
+ alpha = max(0, 255 - edge)
+ if alpha < 56:
+ pixels[x, y] = (0, 0, 0, 0)
+ else:
+ despilled_g = min(g, max(r, b))
+ pixels[x, y] = (r, despilled_g, b, min(a, alpha))
+
+ return rgba
+
+
+def crop_to_alpha(image: Image.Image, padding_ratio: float) -> Image.Image:
+ alpha = image.getchannel("A")
+ bbox = alpha.getbbox()
+ if not bbox:
+ raise ValueError("source image has no visible pixels after background removal")
+
+ cropped = image.crop(bbox)
+ side = max(cropped.size)
+ padding = round(side * padding_ratio)
+ canvas_side = side + padding * 2
+ canvas = Image.new("RGBA", (canvas_side, canvas_side), (0, 0, 0, 0))
+ canvas.alpha_composite(cropped, ((canvas_side - cropped.width) // 2, (canvas_side - cropped.height) // 2))
+ return canvas
+
+
+def trim_transparent_padding(image: Image.Image) -> Image.Image:
+ empty = Image.new("RGBA", image.size, (0, 0, 0, 0))
+ bbox = ImageChops.difference(image, empty).getbbox()
+ if bbox is None:
+ return image
+ return image.crop(bbox)
+
+
+def build_icons(source: Path, out_dir: Path, padding_ratio: float) -> None:
+ out_dir.mkdir(parents=True, exist_ok=True)
+
+ source_image = Image.open(source)
+ transparent = remove_green_background(source_image)
+ master = crop_to_alpha(transparent, padding_ratio)
+ master = trim_transparent_padding(master)
+
+ master_path = out_dir / "tray-icon.png"
+ master.save(master_path)
+
+ resized_images: list[Image.Image] = []
+ for size in ICON_SIZES:
+ resized = master.resize((size, size), Image.Resampling.LANCZOS)
+ resized.save(out_dir / f"tray-icon-{size}.png")
+ resized_images.append(resized)
+
+ ico_path = out_dir / "tray.ico"
+ resized_images[-1].save(
+ ico_path,
+ format="ICO",
+ sizes=[(size, size) for size in ICON_SIZES],
+ append_images=resized_images[:-1],
+ )
+
+ print(f"wrote {master_path}")
+ print(f"wrote {ico_path}")
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(description="Build u-desktop tray icon assets from a generated source image.")
+ parser.add_argument("--source", type=Path, required=True, help="Generated source PNG on a green chroma-key background.")
+ parser.add_argument("--out-dir", type=Path, default=Path("assets/icons"), help="Directory for PNG sizes and tray.ico.")
+ parser.add_argument("--padding", type=float, default=0.04, help="Transparent padding ratio around the cropped icon.")
+ args = parser.parse_args()
+
+ build_icons(args.source, args.out_dir, args.padding)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/check-resources.ps1 b/scripts/check-resources.ps1
new file mode 100644
index 0000000..0bdeafc
--- /dev/null
+++ b/scripts/check-resources.ps1
@@ -0,0 +1,8 @@
+$p = Get-Process -Name 'u-desktop' -ErrorAction SilentlyContinue
+if (-not $p) { Write-Host "u-desktop not running"; exit 0 }
+Write-Host "PID: $($p.Id)"
+Write-Host "CPU time: $([math]::Round($p.CPU, 2))s"
+Write-Host "WorkingSet: $([math]::Round($p.WorkingSet64/1MB, 1)) MB"
+Write-Host "PrivateMem: $([math]::Round($p.PrivateMemorySize64/1MB, 1)) MB"
+Write-Host "Threads: $($p.Threads.Count)"
+Write-Host "Handles: $($p.HandleCount)"
diff --git a/settings.go b/settings.go
index 54b32cf..387cfac 100644
--- a/settings.go
+++ b/settings.go
@@ -24,6 +24,40 @@ var (
settingsHwnd uintptr
)
+const runKeyPath = `Software\Microsoft\Windows\CurrentVersion\Run`
+
+func isAutoStartEnabled() bool {
+ k, err := registry.OpenKey(registry.CURRENT_USER, runKeyPath, registry.QUERY_VALUE)
+ if err != nil {
+ return false
+ }
+ defer k.Close()
+ _, _, err = k.GetStringValue("u-desktop")
+ return err == nil
+}
+
+func setAutoStart(enabled bool) {
+ exePath, _ := os.Executable()
+ if enabled {
+ k, _, err := registry.CreateKey(registry.CURRENT_USER, runKeyPath, registry.SET_VALUE)
+ if err != nil {
+ log.Println("注册表写入失败:", err)
+ return
+ }
+ defer k.Close()
+ k.SetStringValue("u-desktop", exePath)
+ log.Println("已开启开机启动")
+ } else {
+ k, err := registry.OpenKey(registry.CURRENT_USER, runKeyPath, registry.SET_VALUE)
+ if err != nil {
+ return
+ }
+ defer k.Close()
+ k.DeleteValue("u-desktop")
+ log.Println("已关闭开机启动")
+ }
+}
+
func isSystemLightTheme() bool {
k, err := registry.OpenKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Themes\Personalize`, registry.QUERY_VALUE)
if err != nil {
@@ -49,6 +83,25 @@ var themeNames = []struct {
{ThemeText, "文字"},
}
+func refreshVisibleCards(cfg, oldCfg *Config) {
+ if oldCfg.HideWeather && !cfg.HideWeather {
+ go fetchAndPushWeather(getCurrentCity())
+ }
+ if oldCfg.HideZodiac && !cfg.HideZodiac {
+ triggerHoroscopeRefresh(cfg.Zodiac)
+ }
+ if oldCfg.HideKnowledge && !cfg.HideKnowledge {
+ if cfg.KnowledgeKeyword != "" {
+ triggerKnowledgeRefresh()
+ } else {
+ pushKnowledgePlaceholder()
+ }
+ }
+ if oldCfg.HideAINews && !cfg.HideAINews {
+ triggerAINewsRefresh()
+ }
+}
+
func openSettingsWindow() {
settingsMu.Lock()
if settingsOpen && settingsHwnd != 0 {
@@ -113,34 +166,36 @@ func openSettingsWindow() {
}
data, _ := json.Marshal(map[string]interface{}{
- "lightTheme": isSystemLightTheme(),
- "wallpaper": !cfg.HideWallpaper,
- "time": !cfg.HideTime,
- "weather": !cfg.HideWeather,
- "zodiacCard": !cfg.HideZodiac,
- "knowledgeCard": !cfg.HideKnowledge,
- "layout": string(cfg.Layout),
- "zodiac": cfg.Zodiac,
- "city": cfg.City,
- "provinces": provinces,
- "citiesByProv": citiesByProv,
- "wallpaperType": string(cfg.WallpaperType),
- "theme": string(cfg.Theme),
- "themes": tl,
- "color1": cfg.Color1,
- "color2": cfg.Color2,
- "colorGradient": cfg.ColorGradient,
- "savedColors": cfg.SavedColors,
- "bingAutoRefresh": cfg.BingAutoRefresh,
+ "lightTheme": isSystemLightTheme(),
+ "wallpaper": !cfg.HideWallpaper,
+ "time": !cfg.HideTime,
+ "weather": !cfg.HideWeather,
+ "zodiacCard": !cfg.HideZodiac,
+ "knowledgeCard": !cfg.HideKnowledge,
+ "layout": string(cfg.Layout),
+ "zodiac": cfg.Zodiac,
+ "city": cfg.City,
+ "provinces": provinces,
+ "citiesByProv": citiesByProv,
+ "wallpaperType": string(cfg.WallpaperType),
+ "theme": string(cfg.Theme),
+ "themes": tl,
+ "color1": cfg.Color1,
+ "color2": cfg.Color2,
+ "colorGradient": cfg.ColorGradient,
+ "savedColors": cfg.SavedColors,
+ "bingAutoRefresh": cfg.BingAutoRefresh,
"knowledgeKeyword": cfg.KnowledgeKeyword,
"knowledgePrompt": cfg.KnowledgePrompt,
- "wallpaperText": cfg.WallpaperText,
- "imagePath": cfg.ImagePath,
- "showSeconds": cfg.ShowSeconds,
- "ainewsCard": !cfg.HideAINews,
- "photoDir": cfg.PhotoDir,
- "photoInterval": cfg.PhotoInterval,
- "photoCard": !cfg.HidePhoto,
+ "wallpaperText": cfg.WallpaperText,
+ "imagePath": cfg.ImagePath,
+ "showSeconds": cfg.ShowSeconds,
+ "ainewsCard": !cfg.HideAINews,
+ "photoDir": cfg.PhotoDir,
+ "photoInterval": cfg.PhotoInterval,
+ "photoCard": !cfg.HidePhoto,
+ "photoFrameMode": cfg.PhotoFrameMode,
+ "autoStart": isAutoStartEnabled(),
})
return string(data)
})
@@ -151,6 +206,7 @@ func openSettingsWindow() {
return ""
}
cfg := loadConfig()
+ oldCfg := *cfg
if v, ok := data["wallpaper"]; ok {
cfg.HideWallpaper = !v
evalJS(fmt.Sprintf("if(window.setWallpaperVisible) setWallpaperVisible(%v)", v))
@@ -179,9 +235,30 @@ func openSettingsWindow() {
cfg.ShowSeconds = v
evalJS(fmt.Sprintf("if(window.setShowSeconds) setShowSeconds(%v)", v))
}
+ if v, ok := data["autoStart"]; ok {
+ setAutoStart(v)
+ }
+ if v, ok := data["photoFrameMode"]; ok {
+ cfg.PhotoFrameMode = v
+ evalJS(fmt.Sprintf("if(window.setPhotoFrameMode) setPhotoFrameMode(%v)", v))
+ if v {
+ cfg.HideWallpaper = true
+ cfg.HideTime = true
+ cfg.HideWeather = true
+ cfg.HideZodiac = true
+ cfg.HideAINews = true
+ cfg.HideKnowledge = true
+ evalJS(`if(window.setWallpaperVisible) setWallpaperVisible(false)`)
+ for _, card := range []string{"time", "weather", "zodiac", "ainews", "knowledge"} {
+ evalJS(fmt.Sprintf("if(window.setCardVisible) setCardVisible('%s',false)", card))
+ }
+ }
+ }
if v, ok := data["photoCard"]; ok {
cfg.HidePhoto = !v
+ saveConfig(cfg)
evalJS(fmt.Sprintf("if(window.setCardVisible) setCardVisible('photo',%v)", v))
+ refreshVisibleCards(cfg, &oldCfg)
if cfg.PhotoDir != "" {
if v {
restartPhotoLoop()
@@ -189,8 +266,10 @@ func openSettingsWindow() {
stopPhotoLoop()
}
}
+ } else {
+ saveConfig(cfg)
+ refreshVisibleCards(cfg, &oldCfg)
}
- saveConfig(cfg)
return ""
})
@@ -416,8 +495,8 @@ func openSettingsWindow() {
hwnd := uintptr(w.Window())
// disable resize
- style, _, _ := procGetWindowLongPtrW.Call(hwnd, uintptr(0xFFFFFFF0))
- procSetWindowLongPtrW.Call(hwnd, uintptr(0xFFFFFFF0), style & ^uintptr(0x00040000|0x00010000))
+ style, _, _ := procGetWindowLongPtrW.Call(hwnd, gwlStyle)
+ procSetWindowLongPtrW.Call(hwnd, gwlStyle, style & ^uintptr(wsSizebox|wsMaxbox))
// resizeToFit: JS measures content, Go adjusts window frame
w.Bind("resizeToFit", func(contentW, contentH int) string {
diff --git a/systray.go b/systray.go
index 1fa9ddd..a81abbf 100644
--- a/systray.go
+++ b/systray.go
@@ -1,7 +1,6 @@
package main
import (
- "fmt"
"log"
"os"
"os/exec"
@@ -16,7 +15,7 @@ import (
)
func onSystrayReady() {
- systray.SetIcon(generateIcon())
+ systray.SetIcon(trayIcon)
systray.SetTooltip("动态壁纸引擎")
mSettings := systray.AddMenuItem("桌面设置", "打开设置窗口")
@@ -58,15 +57,20 @@ func onSystrayReady() {
func startWebView() {
runtime.LockOSThread()
+ log.Println("startWebView: 开始")
workerw := findWorkerW()
if workerw == 0 {
- log.Fatal("WorkerW not found")
+ log.Println("ERROR: WorkerW not found")
+ showError("无法找到桌面窗口(WorkerW),程序无法启动。")
+ os.Exit(1)
}
+ log.Printf("WorkerW: 0x%x", workerw)
screenW, screenH := getScreenSize()
log.Printf("Screen: %dx%d", screenW, screenH)
+ log.Println("创建 WebView2...")
wv = webview2.NewWithOptions(webview2.WebViewOptions{
AutoFocus: false,
WindowOptions: webview2.WindowOptions{
@@ -76,8 +80,20 @@ func startWebView() {
},
})
if wv == nil {
- log.Fatal("WebView2 create failed")
+ showError("WebView2 创建失败,请确保已安装 WebView2 Runtime。")
+ os.Exit(1)
}
+ log.Println("WebView2 创建成功")
+
+ // 立即获取 HWND 并隐藏窗口,避免闪烁
+ wvHwnd = uintptr(wv.Window())
+ procShowWindow.Call(wvHwnd, 0) // SW_HIDE
+ procSetWindowLongPtrW.Call(wvHwnd, gwlStyle, wsPopup|wsVisible|wsChild)
+
+ // 立即嵌入 WorkerW
+ procSetParent.Call(wvHwnd, workerw)
+ procMoveWindow.Call(wvHwnd, uintptr(^uint(0)), uintptr(^uint(0)), uintptr(screenW+2), uintptr(screenH+2), 1)
+ log.Printf("已嵌入 WorkerW: HWND=0x%x, %dx%d", wvHwnd, screenW, screenH)
wv.Bind("setZodiacFromGo", func(zodiac string) error {
cfg := loadConfig()
@@ -85,33 +101,22 @@ func startWebView() {
return saveConfig(cfg)
})
- wv.SetHtml(buildWallpaperHTML(loadConfig()))
- time.Sleep(1 * time.Second)
+ log.Println("设置 HTML...")
+ cfg := loadConfig()
+ wv.SetHtml(buildWallpaperHTML(cfg))
+ time.Sleep(500 * time.Millisecond)
- wvHwnd = uintptr(wv.Window())
- procSetWindowLongPtrW.Call(wvHwnd, uintptr(0xFFFFFFF0), uintptr(0x80000000|0x10000000|0x02000000))
+ // 嵌入完成后再显示
procShowWindow.Call(wvHwnd, 5)
- procMoveWindow.Call(wvHwnd, uintptr(^uint(0)), uintptr(^uint(0)), uintptr(screenW+2), uintptr(screenH+2), 1)
- log.Printf("壁纸已嵌入: HWND=0x%x, %dx%d", wvHwnd, screenW, screenH)
+ log.Println("壁纸窗口已显示")
go func() {
time.Sleep(500 * time.Millisecond)
- cfg := loadConfig()
- evalJS(fmt.Sprintf(`window.userZodiac = %q;`, cfg.Zodiac))
+ reloadAllCards()
}()
go fullscreenMonitor()
- go func() {
- time.Sleep(3 * time.Second)
- workerw := findWorkerW()
- if workerw != 0 {
- oldParent, _, _ := procSetParent.Call(wvHwnd, workerw)
- log.Printf("SetParent: 0x%x -> 0x%x (old=0x%x)", wvHwnd, workerw, oldParent)
- procMoveWindow.Call(wvHwnd, uintptr(^uint(0)), uintptr(^uint(0)), uintptr(screenW+2), uintptr(screenH+2), 1)
- }
- }()
-
type msg struct {
hwnd uintptr
message uint32
@@ -139,15 +144,15 @@ func startWebView() {
}
}
}
- if m.message == wmSetHtml {
- select {
- case html := <-htmlQueue:
- wv.SetHtml(html)
- default:
+ if m.message == wmSetHtml {
+ select {
+ case html := <-htmlQueue:
+ wv.SetHtml(html)
+ default:
+ }
+ goto nextMsg
}
- goto nextMsg
- }
-nextMsg:
+ nextMsg:
procTranslateMessage.Call(uintptr(unsafe.Pointer(&m)))
procDispatchMessageW.Call(uintptr(unsafe.Pointer(&m)))
}
diff --git a/wallpaper.go b/wallpaper.go
index f66d6fd..a0ced6a 100644
--- a/wallpaper.go
+++ b/wallpaper.go
@@ -3,6 +3,7 @@ package main
import (
_ "embed"
"encoding/base64"
+ "encoding/json"
"fmt"
"log"
"os"
@@ -44,35 +45,14 @@ var themeMap = map[ThemeName]string{
func buildWallpaperHTML(cfg *Config) string {
var bg string
- switch cfg.WallpaperType {
- case WPTheme:
+ if cfg.WallpaperType == WPTheme {
if t, ok := themeMap[cfg.Theme]; ok {
bg = t
} else {
bg = themeAurora
}
- case WPImage:
- if cfg.ImagePath != "" {
- src := imageToDataURI(cfg.ImagePath)
- if src != "" {
- bg = fmt.Sprintf(``, src)
- }
- }
- case WPBing:
- if p := getCurrentBingPath(); p != "" {
- if _, err := os.Stat(p); err == nil {
- src := imageToDataURI(p)
- if src != "" {
- bg = fmt.Sprintf(`
`, src)
- }
- }
- }
- case WPColor:
- if cfg.ColorGradient && cfg.Color2 != "" {
- bg = fmt.Sprintf(`