新增: 自动升级体系 + WorkerW重建检测 + 知识卡片优化

自动升级:
- 版本变量注入 (-ldflags -X main.version)
- 远程 version.json 检查 + 弹窗提示(非强制右下角/强制居中)
- updater.exe 嵌入主程序, 运行时释放到临时目录
- 下载+SHA256校验+备份+替换+回滚 全流程
- 托盘菜单"检查更新"

稳定性:
- WorkerW 重建自动检测(每10s)并重新嵌入
- 左键托盘点击防抖
- 设置窗口已打开时正确前置

优化:
- 知识卡片prompt改为面向开发者的硬核内容
- 配置目录统一到 ~/.u-desktop/
- 设置窗口WebView2数据目录改为configDir下
This commit is contained in:
2026-05-28 19:06:26 +08:00
parent aee7997195
commit f5a473c4b8
23 changed files with 859 additions and 90 deletions

View File

@@ -4,28 +4,41 @@ function parseArg(data: any) {
return typeof data === 'string' ? JSON.parse(data) : data
}
// JSON 去重:相同数据不触发 Vue 重渲染
const lastJSON = new Map<string, string>()
function shouldUpdate(key: string, data: any): boolean {
const json = JSON.stringify(data)
if (lastJSON.get(key) === json) return false
lastJSON.set(key, json)
return true
}
export function registerGoBridge() {
const w = window as any
w.updateHoroscopeFromGo = (data: any) => {
state.horoscope = parseArg(data)
const parsed = parseArg(data)
if (shouldUpdate('horoscope', parsed)) state.horoscope = parsed
}
w.updateAINewsFromGo = (items: any) => {
const parsed = parseArg(items)
if (parsed?.length) state.aiNews = parsed
if (parsed?.length && shouldUpdate('ainews', parsed)) state.aiNews = parsed
}
w.updateKnowledgeFromGo = (data: any) => {
state.knowledge = parseArg(data)
const parsed = parseArg(data)
if (shouldUpdate('knowledge', parsed)) state.knowledge = parsed
}
w.updateWeatherFromGo = (data: any) => {
state.weather = parseArg(data)
const parsed = parseArg(data)
if (shouldUpdate('weather', parsed)) state.weather = parsed
}
w.updatePhotoFromGo = (data: any) => {
state.photo = parseArg(data)
const parsed = parseArg(data)
if (parsed?.src && shouldUpdate('photo', parsed)) state.photo = parsed
}
w.setCardVisible = (card: string, visible: boolean) => {

View File

@@ -1,10 +1,15 @@
<template>
<div>
<div class="knowledge-card">
<div class="knowledge-header">
💡 知识卡片
<span class="knowledge-label">知识卡片</span>
<span class="knowledge-keyword-tag" v-if="state.knowledge.keyword">#{{ state.knowledge.keyword }}</span>
</div>
<div class="knowledge-content">{{ state.knowledge.content || '请设置知识关键字' }}</div>
<div class="knowledge-divider"></div>
<div class="knowledge-body" v-if="state.knowledge.content">
<span class="knowledge-quote">"</span>
<span class="knowledge-text">{{ state.knowledge.content }}</span>
</div>
<div class="knowledge-placeholder" v-else>请在设置中配置知识关键字</div>
</div>
</template>
@@ -13,19 +18,49 @@ import { state } from '../composables/useOverlayState'
</script>
<style scoped>
.knowledge-card {
display: flex; flex-direction: column;
height: 100%;
}
.knowledge-header {
display: flex; align-items: center; gap: 8px;
margin-bottom: 12px;
}
.knowledge-label {
font-size: 11px; color: var(--text-faint);
margin-bottom: 10px; display: flex;
align-items: center; gap: 6px;
letter-spacing: 0; font-weight: 700;
font-weight: 700; letter-spacing: 1px;
text-transform: uppercase;
}
.knowledge-keyword-tag {
background: rgba(255,255,255,0.09);
padding: 3px 8px; border-radius: 6px;
font-size: 10px; color: var(--text-soft);
background: rgba(255,216,107,0.12);
color: var(--accent-warm);
padding: 2px 9px; border-radius: 4px;
font-size: 10px; font-weight: 600;
border: 1px solid rgba(255,216,107,0.15);
}
.knowledge-content {
font-size: 16px; color: var(--text-main);
line-height: 1.55; text-shadow: 0 1px 4px rgba(0,0,0,0.5);
.knowledge-divider {
height: 1px;
background: linear-gradient(90deg, var(--accent-warm), rgba(255,216,107,0.08), transparent);
margin-bottom: 16px;
}
.knowledge-body {
position: relative;
padding-left: 4px;
}
.knowledge-quote {
position: absolute; top: -10px; left: -6px;
font-size: 52px; line-height: 1;
color: var(--accent-warm); opacity: 0.18;
font-family: Georgia, serif; pointer-events: none;
}
.knowledge-text {
font-size: 15px; color: var(--text-main);
line-height: 1.7; text-shadow: 0 1px 4px rgba(0,0,0,0.5);
display: -webkit-box; -webkit-line-clamp: 8;
-webkit-box-orient: vertical; overflow: hidden;
}
.knowledge-placeholder {
font-size: 13px; color: var(--text-faint);
font-style: italic; padding-top: 8px;
}
</style>

View File

@@ -68,10 +68,7 @@ import PhotoCard from './PhotoCard.vue'
width: 100%; min-width: 0; text-align: right;
padding: 22px 28px 24px;
}
#card-knowledge :deep(.knowledge-content) {
display: -webkit-box; -webkit-line-clamp: 8;
-webkit-box-orient: vertical; overflow: hidden;
}
#card-knowledge { overflow: hidden; }
#layout-multi :deep(#card-photo) {
grid-area: photo;
position: relative; top: auto; left: auto;

View File

@@ -35,9 +35,7 @@ html, body {
position: relative;
box-sizing: border-box;
overflow: hidden;
background: linear-gradient(145deg, rgba(255,255,255,0.075), rgba(255,255,255,0.025)), var(--card-bg);
backdrop-filter: blur(28px) saturate(1.2);
-webkit-backdrop-filter: blur(28px) saturate(1.2);
background: linear-gradient(145deg, rgba(255,255,255,0.075), rgba(255,255,255,0.025)), var(--card-bg-strong);
border: 1px solid var(--card-line);
border-radius: 8px;
padding: 22px 24px;

View File

@@ -11,13 +11,13 @@
</div>
<div class="flex justify-between items-center px-3.5 py-2 border-t border-[var(--card-divider)]">
<div><div class="text-xs font-medium text-[var(--text-muted)]">知识关键字</div><div class="text-[10px] text-[var(--text-weak)] mt-px">AI 将根据关键字生成知识小卡片</div></div>
<input type="text" v-model="s.knowledgeKeyword" @input="debounced(go.saveKnowledgeKeyword, s.knowledgeKeyword)"
<input type="text" v-model="s.knowledgeKeyword" @input="debounced('kw', go.saveKnowledgeKeyword, s.knowledgeKeyword)"
placeholder="如: 历史、科学、冷知识"
class="bg-[var(--input-bg)] border border-[var(--input-border)] rounded-md text-[var(--text)] text-[11px] py-1 px-2 outline-none w-[140px] focus:border-[var(--input-border-focus)]">
</div>
<div class="flex justify-between items-center px-3.5 py-2 border-t border-[var(--card-divider)]">
<div><div class="text-xs font-medium text-[var(--text-muted)]">知识提示词</div><div class="text-[10px] text-[var(--text-weak)] mt-px">补充风格或方向系统会保证内容密度</div></div>
<input type="text" v-model="s.knowledgePrompt" @input="debounced(go.saveKnowledgePrompt, s.knowledgePrompt)"
<input type="text" v-model="s.knowledgePrompt" @input="debounced('kp', go.saveKnowledgePrompt, s.knowledgePrompt)"
placeholder="如: 偏实践、给出判断标准"
class="bg-[var(--input-bg)] border border-[var(--input-border)] rounded-md text-[var(--text)] text-[11px] py-1 px-2 outline-none w-[140px] focus:border-[var(--input-border-focus)]">
</div>
@@ -76,12 +76,10 @@ function onDocClick(e: MouseEvent) {
onMounted(() => {
document.addEventListener('click', onDocClick)
// Find initial city
for (const p of Object.keys(props.s.citiesByProv)) {
for (const c of (props.s.citiesByProv as any)[p]) {
if (c.id === props.s.city) { activeProv.value = p; selectedCityName.value = c.name; break }
}
if (activeProv.value) break
const entries = Object.entries(props.s.citiesByProv as Record<string, { id: string; name: string }[]>)
for (const [p, cs] of entries) {
const found = cs.find(c => c.id === props.s.city)
if (found) { activeProv.value = p; selectedCityName.value = found.name; break }
}
})
onUnmounted(() => document.removeEventListener('click', onDocClick))

View File

@@ -29,12 +29,20 @@
</template>
<script setup lang="ts">
import { watch } from 'vue'
import { go } from '../composables/useGoBridge'
import ToggleSwitch from './ToggleSwitch.vue'
import type { SettingsData } from '@shared/types'
const props = defineProps<{ s: SettingsData }>()
watch(
() => [props.s.photoCard, props.s.photoFrameMode] as [boolean, boolean],
([photoCard, photoFrameMode]) => {
go.saveToggles({ photoCard, photoFrameMode })
}
)
async function onPickDir() {
const dir = await go.pickPhotoDir()
if (dir) props.s.photoDir = dir

View File

@@ -26,7 +26,7 @@
</div>
<div v-if="s.theme === 'text'" class="flex justify-between items-center px-3.5 py-2 border-t border-[var(--card-divider)]">
<div class="text-xs font-medium text-[var(--text-muted)]">自定义文字</div>
<input type="text" v-model="s.wallpaperText" @input="debounced(go.saveWallpaperText, s.wallpaperText)"
<input type="text" v-model="s.wallpaperText" @input="debounced('wt', go.saveWallpaperText, s.wallpaperText)"
placeholder="输入显示文字"
class="bg-[var(--input-bg)] border border-[var(--input-border)] rounded-md text-[var(--text)] text-[11px] py-1 px-2 outline-none w-[140px] focus:border-[var(--input-border-focus)]">
</div>
@@ -135,7 +135,10 @@ async function loadBingInfo() {
const raw = await go.getBingInfo()
const st = JSON.parse(raw) as BingState
bing.value = st
if (st.url) bingThumb.value = st.url.replace('_1920x1080', '_400x240')
if (st.url) {
const t = st.url.replace('_1920x1080', '_400x240')
bingThumb.value = t !== st.url ? t : await go.bingThumbDataURI(st.url.split('/').pop() || '')
}
}
async function onBingPrev() { bing.value = JSON.parse(await go.bingPrev()) }

View File

@@ -1,5 +1,5 @@
const timers = new Map<string, ReturnType<typeof setTimeout>>()
export function debounced(fn: (v: string) => void, val: string, ms = 500) {
clearTimeout(timers.get(fn.name))
timers.set(fn.name, setTimeout(() => fn(val), ms))
export function debounced(key: string, fn: (v: string) => void, val: string, ms = 500) {
clearTimeout(timers.get(key))
timers.set(key, setTimeout(() => fn(val), ms))
}