新增: 应用内版本更新与自动升级全链路
后端 update.rs: app_check_update(拉 latest.json+ed25519 签名校验) / app_download_install_update / prefs 读写 / mark_checked / mark_notified 五命令, lib.rs 注册;tauri.conf updater 段(endpoints=app.1216.top/download/devflow/latest.json + pubkey + createUpdaterArtifacts) + tauri-plugin-updater 依赖 + capabilities 权限。 前端 UpdateBadge.vue(顶栏徽章+下拉) / useUpdateCheck.ts(检查/下载/安装状态机) / AboutPanel.vue(设置页关于面板) + AppLayout 接线 + SettingsNav/searchIndex 索引 + i18n zh/en settings 键。 运维 scripts/publish-release.ps1(发版) + swap-install.ps1(换装校验) + 设计文档 版本更新与自动升级-设计-2026-10-09.md。 验收: cargo check -p devflow EXIT 0 + vue-tsc EXIT 0 + build EXIT 0。
This commit is contained in:
Generated
+233
-6
@@ -104,6 +104,15 @@ version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3bc62ac97cc33321f50863d514c3bc38a453947a8f9e781137e47c7401020aed"
|
||||
dependencies = [
|
||||
"derive_arbitrary",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asn1-rs"
|
||||
version = "0.7.2"
|
||||
@@ -996,6 +1005,17 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_arbitrary"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b034bd7d5f032402a2479444dcc6f74e36a03f31854d41680fb240ef682a1ac"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "2.1.1"
|
||||
@@ -1019,7 +1039,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "devflow"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -1054,6 +1074,7 @@ dependencies = [
|
||||
"tauri-plugin-notification",
|
||||
"tauri-plugin-opener",
|
||||
"tauri-plugin-single-instance",
|
||||
"tauri-plugin-updater",
|
||||
"tauri-plugin-window-state",
|
||||
"tokio",
|
||||
"tokio-tungstenite 0.23.1",
|
||||
@@ -1571,6 +1592,16 @@ dependencies = [
|
||||
"rustc_version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
@@ -2572,6 +2603,36 @@ dependencies = [
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.22.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"combine",
|
||||
"jni-macros",
|
||||
"jni-sys 0.4.1",
|
||||
"log",
|
||||
"simd_cesu8",
|
||||
"thiserror 2.0.18",
|
||||
"walkdir",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-macros"
|
||||
version = "0.22.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc_version",
|
||||
"simd_cesu8",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys"
|
||||
version = "0.3.1"
|
||||
@@ -2896,6 +2957,12 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "minisign-verify"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
@@ -3277,6 +3344,18 @@ dependencies = [
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-osa-kit"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0"
|
||||
dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-quartz-core"
|
||||
version = "0.3.2"
|
||||
@@ -3420,6 +3499,20 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "osakit"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
"objc2-osa-kit",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pango"
|
||||
version = "0.18.3"
|
||||
@@ -4059,15 +4152,20 @@ dependencies = [
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"rustls-platform-verifier",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-http",
|
||||
@@ -4213,6 +4311,33 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98"
|
||||
dependencies = [
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation-sys",
|
||||
"jni 0.22.4",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"rustls-platform-verifier-android",
|
||||
"rustls-webpki",
|
||||
"security-framework 3.7.0",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier-android"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.13"
|
||||
@@ -4661,6 +4786,22 @@ version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
|
||||
|
||||
[[package]]
|
||||
name = "simd_cesu8"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520"
|
||||
dependencies = [
|
||||
"rustc_version",
|
||||
"simdutf8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simdutf8"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "1.0.3"
|
||||
@@ -4830,6 +4971,17 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sync_wrapper"
|
||||
version = "1.0.2"
|
||||
@@ -4911,7 +5063,7 @@ dependencies = [
|
||||
"gdkwayland-sys",
|
||||
"gdkx11-sys",
|
||||
"gtk",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"libc",
|
||||
"log",
|
||||
"ndk",
|
||||
@@ -4944,6 +5096,17 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tar"
|
||||
version = "0.4.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840"
|
||||
dependencies = [
|
||||
"filetime",
|
||||
"libc",
|
||||
"xattr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.12.16"
|
||||
@@ -4968,7 +5131,7 @@ dependencies = [
|
||||
"heck 0.5.0",
|
||||
"http",
|
||||
"http-range",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"libc",
|
||||
"log",
|
||||
"mime",
|
||||
@@ -5179,6 +5342,39 @@ dependencies = [
|
||||
"zbus 5.16.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-updater"
|
||||
version = "2.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a5cad8ed5948d988e1018ecd31e27cacedbf72ce3fb972940c3e4bf51639e4b"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"dirs",
|
||||
"flate2",
|
||||
"futures-util",
|
||||
"http",
|
||||
"infer",
|
||||
"log",
|
||||
"minisign-verify",
|
||||
"osakit",
|
||||
"percent-encoding",
|
||||
"reqwest 0.13.4",
|
||||
"rustls",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tar",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"time",
|
||||
"tokio",
|
||||
"url",
|
||||
"windows-sys 0.60.2",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-window-state"
|
||||
version = "2.4.1"
|
||||
@@ -5204,7 +5400,7 @@ dependencies = [
|
||||
"dpi",
|
||||
"gtk",
|
||||
"http",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"objc2",
|
||||
"objc2-ui-kit",
|
||||
"objc2-web-kit",
|
||||
@@ -5227,7 +5423,7 @@ checksum = "b83849ee63ecb27a8e8d0fe51915ca215076914aca43f96db1179f0f415f6cd9"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"log",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
@@ -6376,6 +6572,15 @@ dependencies = [
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-root-certs"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "1.0.8"
|
||||
@@ -7069,7 +7274,7 @@ dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
"javascriptcore-rs",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"libc",
|
||||
"ndk",
|
||||
"objc2",
|
||||
@@ -7133,6 +7338,16 @@ dependencies = [
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xattr"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rustix",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xdg-home"
|
||||
version = "1.3.0"
|
||||
@@ -7379,6 +7594,18 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "4.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"crc32fast",
|
||||
"indexmap 2.14.0",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
|
||||
+6
-3
@@ -28,6 +28,9 @@ keyring = { version = "3", features = ["windows-native", "apple-native", "linux-
|
||||
dashmap = "6"
|
||||
|
||||
# release 增量编译:小改动重打包显著提速(2026-10-04 打包提速项);
|
||||
# 代价为产物略大,正式发版如在意体积可临时注释此段
|
||||
[profile.release]
|
||||
incremental = true
|
||||
# ⚠️ 2026-10-09 回退:release + incremental 触发链接期匿名符号丢失
|
||||
# (anon.*.llvm.* undefined symbol,rust-lld 与 MSVC link.exe 均复现),
|
||||
# 根因是增量编译 CGU 划分与跨 crate 内联冲突。cargo check 不链接故不暴露,
|
||||
# cargo build/tauri build 链接期必挂。release 禁用 incremental 为 Rust 官方推荐。
|
||||
# [profile.release]
|
||||
# incremental = true
|
||||
|
||||
@@ -70,10 +70,12 @@ impl Node for HumanNode {
|
||||
|
||||
// 3. select! 循环:Response / 超时 / 取消
|
||||
let deadline = tokio::time::Instant::now() + Duration::from_secs(timeout_secs);
|
||||
let mut cancel_tick = tokio::time::interval(Duration::from_millis(500));
|
||||
cancel_tick.tick().await; // 丢弃首个立即触发的 tick
|
||||
// FR-P4:取消检测由 500ms interval 轮询改为 Notify 被动唤醒(set_cancelled 触发)。
|
||||
// 先建 notified future 再进 loop,避免 notify 早于 await 的竞态窗口;
|
||||
// 每轮 select! 后重建(Notified 单次消费),配合 is_cancelled 复查兜底。
|
||||
|
||||
loop {
|
||||
let cancel_wait = ctx.node_status.cancel_notified();
|
||||
tokio::select! {
|
||||
recv = rx.recv() => match recv {
|
||||
Ok(WorkflowEvent::HumanApprovalResponse {
|
||||
@@ -154,7 +156,9 @@ impl Node for HumanNode {
|
||||
_ = tokio::time::sleep_until(deadline) => {
|
||||
return Err(anyhow::anyhow!("人工审批超时({}s)", timeout_secs));
|
||||
}
|
||||
_ = cancel_tick.tick() => {
|
||||
_ = cancel_wait => {
|
||||
// FR-P4:Notify 唤醒后复查 is_cancelled(Notify 无状态,
|
||||
// 可能因其他节点取消而误唤醒,须确认本节点确被取消)。
|
||||
if ctx.node_status.is_cancelled(&ctx.node_id) {
|
||||
return Err(anyhow::anyhow!("人工审批被取消"));
|
||||
}
|
||||
|
||||
@@ -30,6 +30,10 @@ use df_types::types::{NodeId, NodeStatus};
|
||||
pub struct StateMachine {
|
||||
/// 节点状态映射(Arc 共享,clone 浅拷贝同一 HashMap)
|
||||
states: Arc<Mutex<HashMap<NodeId, NodeStatus>>>,
|
||||
/// 取消信号唤醒器(FR-P4):set_cancelled 时 notify_waiters,
|
||||
/// 阻塞节点(HumanNode)用 notified() 被动等待,替代 500ms 轮询。
|
||||
/// 与 states 同 Arc 共享,clone 浅拷贝同一 Notify。
|
||||
cancel_notify: Arc<tokio::sync::Notify>,
|
||||
}
|
||||
|
||||
impl StateMachine {
|
||||
@@ -37,9 +41,19 @@ impl StateMachine {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
states: Arc::new(Mutex::new(HashMap::new())),
|
||||
cancel_notify: Arc::new(tokio::sync::Notify::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取取消信号等待器(FR-P4)
|
||||
///
|
||||
/// 返回的 `Notified` future 在 `set_cancelled` 被调用时 resolve。
|
||||
/// 调用方应配合 `is_cancelled` 做条件判断(Notify 无状态,可能错过
|
||||
/// 早于 await 的 notify,故 select! 分支 resolve 后仍须复查 is_cancelled)。
|
||||
pub fn cancel_notified(&self) -> tokio::sync::futures::Notified<'_> {
|
||||
self.cancel_notify.notified()
|
||||
}
|
||||
|
||||
/// 获取节点状态
|
||||
///
|
||||
/// 锁中毒时降级返回 `NodeStatus::Pending`(保守默认:视为未启动,
|
||||
@@ -139,6 +153,10 @@ impl StateMachine {
|
||||
);
|
||||
}
|
||||
}
|
||||
// FR-P4:唤醒所有等待取消信号的阻塞节点(HumanNode notified 分支)。
|
||||
// notify_waiters 不持锁调用,避免与 states 锁耦合;Notify 无状态,
|
||||
// 等待方 resolve 后仍复查 is_cancelled 兜底(防 notify 早于 await 丢失)。
|
||||
self.cancel_notify.notify_waiters();
|
||||
}
|
||||
|
||||
/// 获取所有状态快照(clone 返回,调用方持独立副本)
|
||||
|
||||
@@ -0,0 +1,361 @@
|
||||
# DevFlow 版本更新与自动升级 — 专项设计(2026-10-09)
|
||||
|
||||
> 对象:应用内「版本可见 → 检查更新 → 下载 → 安装重启」全链路。
|
||||
> 标准:合理 · 兼容 · 优雅 · 可扩张。
|
||||
> 现状勘察:`src-tauri/tauri.conf.json` / `src-tauri/Cargo.toml` / `src-tauri/capabilities/default.json` / `src-tauri/src/lib.rs` / `src/views/Settings.vue` / `src/components/settings/SettingsNav.vue` / `scripts/package-devflow.ps1` / `swap-install.ps1` / u-desk 先例。
|
||||
|
||||
---
|
||||
|
||||
## 一、本质定性
|
||||
|
||||
### 1.1 两条"升级"通道必须分开命名
|
||||
|
||||
现状只有一个动作叫「换装」,但它其实是两个语义完全不同的事情,混称导致讨论失真:
|
||||
|
||||
| 通道 | 触发源 | 使用者 | 目标 | 现载体 |
|
||||
|---|---|---|---|---|
|
||||
| **本地换装** | 本机源码改动 | 开发者(你) | 把刚编译的产物装到自己机器 | `scripts/package-devflow.ps1 -Mode full` / `swap-install.ps1` |
|
||||
| **应用内升级** | 远端新版本 | 任何使用者 | 把手上的旧版升到新版 | **不存在** |
|
||||
|
||||
**本设计只做第二条**。第一条保持不动(且是本设计的发布端依赖)。
|
||||
|
||||
### 1.2 为什么这件事值得做(而非"自用工具不需要")
|
||||
|
||||
现状的隐性成本:
|
||||
|
||||
1. **用户无法得知自己用的是哪版** —— 设置页无版本号展示,issue 反馈时只能靠猜("你那个缓存修复装上了吗?"——本次对话里就真实发生过:凌晨 02:50 装了包,13:02 又改了码,靠猜时间戳判断哪个生效)。
|
||||
2. **升级不可达** —— 未来若分发给他人(u-desk 已有对外分发先例),对方拿到 v0.1.1 后永远停在 v0.1.1,只能等新包重装。
|
||||
3. **升版本号是手工雷区** —— `swap-install.ps1` 硬编码 `devflow_0.1.1_x64-setup.exe`,改版本号忘改脚本 = 静默装回旧包(**静默伪成功**,最危险的一类故障)。
|
||||
|
||||
结论:**先做 L0(版本单一源 + 可见),再做 L1(检查→提示),最后 L2(下载安装)**。L0 交付即消除 1、3 两项,成本极低。
|
||||
|
||||
---
|
||||
|
||||
## 二、现状勘察(全部实证)
|
||||
|
||||
### 2.1 三元缺口
|
||||
|
||||
| 层 | 现状 | 证据 |
|
||||
|---|---|---|
|
||||
| 插件层 | 无 `tauri-plugin-updater` | `src-tauri/Cargo.toml:18-25` 仅 dialog/opener/window-state/notification/single-instance |
|
||||
| 配置层 | 无 `plugins.updater` 段 | `tauri.conf.json` 仅 `bundle.targets: ["nsis"]` |
|
||||
| 逻辑层 | 无版本比对 IPC | `grep check_update\|latest_version\|auto_update` 于 `src-tauri/src` → 0 命中 |
|
||||
| UI 层 | 无版本号/检查更新入口 | `grep 检查更新\|getVersion\|appVersion` 于 `src/` → 0 命中;SettingsNav 仅 7 分类 |
|
||||
|
||||
### 2.2 现存缺陷(本设计顺带治理)
|
||||
|
||||
**D1 — 版本双源不一致**
|
||||
|
||||
```
|
||||
src-tauri/tauri.conf.json:3 "version": "0.1.1" ← 打包器实际用这个
|
||||
src-tauri/Cargo.toml:3 version = "0.1.0" ← Rust crate 用这个
|
||||
```
|
||||
|
||||
Tauri 2 中 `bundle` 版本取 `tauri.conf.json`,`CARGO_PKG_VERSION` 取 Cargo.toml。当前两者已经分叉(0.1.1 vs 0.1.0),未来必然出「设置页显示 0.1.0,实际装的是 0.1.1」这类灵异现象。
|
||||
|
||||
**D2 — 换装脚本硬编码版本号**
|
||||
|
||||
```powershell
|
||||
# swap-install.ps1:8
|
||||
$installer = 'E:\wk-lab\devflow\target\release\bundle\nsis\devflow_0.1.1_x64-setup.exe'
|
||||
```
|
||||
|
||||
版本号进入文件名,升版忘改 → 找不到文件 → `Start-Process` 抛错(尚好);若旧版包残留同目录,则**装回旧版**(伪成功)。必须改为 glob 探测(`package-devflow.ps1:44` 已用 `Sort-Object LastWriteTime -Descending | Select-Object -First 1` 的正确姿势,swap-install 未对齐)。
|
||||
|
||||
### 2.3 可复用的成熟资产
|
||||
|
||||
**u-desk(同作者的 Wails 应用)已完成全套更新链路**,虽技术栈不同(Go+Wails vs Rust+Tauri),但**方案骨架可直接借鉴**:
|
||||
|
||||
- 清单托管:`https://img.1216.top/u-desk/last-version.json`(七牛 CDN)
|
||||
- 后端:`internal/api/update_api.go` + `internal/service/update_service.go`
|
||||
- `CheckUpdate()` / `GetCurrentVersion()` / `GetUpdateConfig()` / `SetUpdateConfig()`
|
||||
- `DownloadUpdate()` 异步 + `download-progress` 事件流
|
||||
- `InstallUpdateWithHash()` 哈希校验后安装
|
||||
- 自动检查定时器(`startAutoUpdateCheck` 可重入,配置变更 `resetAutoUpdateCheck` 免重启生效)
|
||||
- 前端:`update-available` 事件驱动弹窗
|
||||
|
||||
**关键差异**:u-desk 是自研(无签名体系);Tauri 2 官方 updater **内建 ed25519 签名校验**,安全性更好,应优先采用官方插件而非照搬自研。
|
||||
|
||||
**另有可复用的健壮性经验**:`swap-install.ps1` 里已验证有效的三件套——
|
||||
1. `Wait-HandleReleased()`:独占打开目标 exe 探测句柄释放(治 Windows 文件锁延迟释放致 NSIS 静默伪成功)
|
||||
2. SHA256 三级判据(等磁盘快照 / 两次确定性输出 / 实变)
|
||||
3. SYSTEM 上下文转投交互用户会话(GUI 才可见)
|
||||
|
||||
这三条应在应用内升级的「安装前等待」阶段复刻(Rust 侧实现等价逻辑)。
|
||||
|
||||
---
|
||||
|
||||
## 三、方案选型
|
||||
|
||||
### 方案 A:Tauri 官方 `tauri-plugin-updater`(推荐)
|
||||
|
||||
**能力**:`check()` → `Update` 对象(version/notes/date)→ `download_and_install(on_chunk)` → 自动重启。
|
||||
|
||||
**优点**
|
||||
- 内建 **ed25519 签名校验**(防篡改,安全基线)
|
||||
- 跨平台统一(NSIS / MSI / AppImage / dmg)
|
||||
- 与 Tauri 窗口生命周期原生协同(安装前自动关窗)
|
||||
- 官方维护,随 Tauri 2 演进
|
||||
|
||||
**代价**
|
||||
- 需引入签名密钥(`tauri signer generate`,公钥入配置,私钥入打包环境)
|
||||
- 打包需开 `bundle.createUpdaterArtifacts: true`(产出 `.nsis.zip` + `.nsis.zip.sig`)
|
||||
- 端点必须 HTTPS + 清单格式固定(`latest.json`)
|
||||
|
||||
### 方案 B:自研(照搬 u-desk 模式)
|
||||
|
||||
**优点**:零新依赖,可完全复用 `swap-install` 的静默安装动作。
|
||||
|
||||
**缺点**:签名/校验/回滚/进度全自研;`latest.json` 契约要自己定;安全基线靠自己守。
|
||||
|
||||
### 选型结论:**A 为基座,B 的本地动作经验作为"安装前等待"的健壮性补丁喂给 A**
|
||||
|
||||
理由:
|
||||
- 官方插件把「签名校验」这件最容易做错的事做对了,没有理由重造。
|
||||
- devflow 是**联网分发**场景(未来对外),签名是刚需;u-desk 的裸下载在自用场景够,对外不够。
|
||||
- 本地换装通道(脚本)**不废弃**,继续服务开发期高频迭代(它比走远端快得多)。
|
||||
|
||||
---
|
||||
|
||||
## 四、分层设计
|
||||
|
||||
```
|
||||
┌─ L3 发布侧(脚本增强)────────────────────────────┐
|
||||
│ tauri build → 产出 .nsis.zip + .sig │
|
||||
│ → 生成 latest.json(version/notes/pub_date/urls/sig)│
|
||||
│ → 上传 app.1216.top/download/devflow/ │
|
||||
└────────────────────────────────────────────────────┘
|
||||
↑ 拉取
|
||||
┌─ L2 前端 ──────────────────────────────────────────┐
|
||||
│ SettingsNav 新增「关于」分类(第 8 类) │
|
||||
│ AboutPanel.vue:版本号 / 检查更新 / 更新弹窗 / 进度 │
|
||||
└────────────────────────────────────────────────────┘
|
||||
↑ IPC
|
||||
┌─ L1 后端(Rust)───────────────────────────────────┐
|
||||
│ tauri-plugin-updater 注册 + capabilities 授权 │
|
||||
│ IPC: app_get_version / app_check_update / │
|
||||
│ app_download_install_update │
|
||||
│ 本地 KV: df-update-auto-check / -interval-minutes / │
|
||||
│ -last-check / -notified-version │
|
||||
└────────────────────────────────────────────────────┘
|
||||
↑ 单一源
|
||||
┌─ L0 版本治理 ──────────────────────────────────────┐
|
||||
│ 单一版本源 + 设置页展示版本号(零风险,先落) │
|
||||
└────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### L0:版本单一源治理
|
||||
|
||||
**决策**:以 `tauri.conf.json` 的 `version` 为**唯一权威源**(打包器实际使用它)。
|
||||
|
||||
**动作**
|
||||
1. `src-tauri/Cargo.toml` 的 `version` 对齐同名值(人工同步,或加校验脚本);**§2.2-D1 双源问题在此关闭**。
|
||||
2. 新增 IPC `app_get_version`:返回 `env!("CARGO_PKG_VERSION")` + 构建时间 + git short hash(option_env! 注入,未注入显示 `dev`)。
|
||||
3. `swap-install.ps1:8` 改为 glob 探测(**§2.2-D2 在此关闭**):
|
||||
```powershell
|
||||
$installer = (Get-ChildItem 'E:\wk-lab\devflow\target\release\bundle\nsis\devflow_*_x64-setup.exe' |
|
||||
Sort-Object LastWriteTime -Descending | Select-Object -First 1).FullName
|
||||
```
|
||||
4. 设置页「关于」展示版本号(L0 即可交付的可见价值)。
|
||||
|
||||
**扩张性**:未来 `df-im-gateway` 等独立服务走各自版本线(在其自身发布 skill 内治理),本设计不扩范围,但 `app_get_version` 的返回结构预留 `components: []` 数组位。
|
||||
|
||||
### L1:后端接入(插件注册 + 检查)
|
||||
|
||||
**Cargo.toml**
|
||||
```toml
|
||||
tauri-plugin-updater = "2"
|
||||
```
|
||||
|
||||
**lib.rs**(`Builder` 链,插在 notification 之后)
|
||||
```rust
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
```
|
||||
|
||||
**capabilities/default.json** 补
|
||||
```json
|
||||
"updater:default"
|
||||
```
|
||||
|
||||
**tauri.conf.json**
|
||||
```json
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": ["nsis"],
|
||||
"createUpdaterArtifacts": true
|
||||
},
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"endpoints": ["https://app.1216.top/download/devflow/latest.json"],
|
||||
"pubkey": "<tauri signer generate 产出的公钥>",
|
||||
"windows": { "installMode": "passive" }
|
||||
}
|
||||
}
|
||||
```
|
||||
> `installMode: passive` —— **不是静默**:弹进度条窗口,用户看得见进度,但无需点击「下一步」,装完自动往下走。
|
||||
> 三档对比:`basic`(完整向导,需点下一步)/ `passive`(进度条,无需点击)/ `quiet`(完全无 UI,**失败不可见**,不推荐)。
|
||||
> 注:`swap-install.ps1` 用的 NSIS `/S` 是真静默(等价 `quiet`),应用内升级刻意不采用——面向用户自己机器的升级需要失败可见性。
|
||||
|
||||
**IPC 契约**(新增 3 个,命名沿用 `app_*` 前缀)
|
||||
|
||||
| 命令 | 入参 | 返回 | 风险档 |
|
||||
|---|---|---|---|
|
||||
| `app_get_version` | — | `{version, build_time, git_hash}` | 只读 |
|
||||
| `app_check_update` | — | `{has_update, version?, notes?, date?}` | 只读(外发网络) |
|
||||
| `app_download_install_update` | — | 流式事件 + 成功即重启 | **高风险(需审批)** |
|
||||
|
||||
**配置 KV**(复用既有 appSettings 体系,零新表)
|
||||
- `df-update-auto-check`:bool,默认 `true`
|
||||
- `df-update-check-interval-minutes`:int,默认 `15`
|
||||
- `df-update-last-check`:ISO 时间戳(节流用,防每次启动都拉)
|
||||
- `df-update-notified-version`:string(**同版本只提示一次**,防骚扰)
|
||||
|
||||
**自动检查策略**:启动后延迟 **3s** → 拉清单 → 有更新则发事件 `app-update-available` 到主窗;此后**每 15 分钟**检查一次,但**同一版本号只提示一次**(用户点「稍后」后不再重复弹)。**只提示不自动装**(安装是高风险动作,必须人点头)。
|
||||
|
||||
### L2:前端(设置页「关于」分类)
|
||||
|
||||
**SettingsNav** 加第 8 类:
|
||||
```ts
|
||||
{ value: 'about', icon: 'ℹ️', labelKey: 'settings.navAbout' },
|
||||
```
|
||||
`searchIndex.ts` 补对应条目(`SettingsCategory` 联合类型同步扩 `'about'`)。
|
||||
|
||||
**新增 `src/components/settings/AboutPanel.vue`**(对齐既有 panel 视觉语言)
|
||||
|
||||
```
|
||||
┌─ 关于 DevFlow ─────────────────────────┐
|
||||
│ 当前版本 v0.1.2 (a1b2c3d) [检查更新] │
|
||||
│ 构建时间 2026-10-09 14:30 │
|
||||
│ ─────────────────────────────────────── │
|
||||
│ ☑ 启动时自动检查更新(启动后 3 秒) │
|
||||
│ 检查间隔 [15 分钟 ▾] │
|
||||
│ ─────────────────────────────────────── │
|
||||
│ 上次检查 2026-10-09 14:35 │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**更新弹窗**(复用既有 ConfirmDialog 视觉,含:新版本号 / 更新说明 markdown / 下载进度条 / 「立即更新并重启」「稍后」)
|
||||
|
||||
**事件订阅**(对齐既有 `useAiEvents` 模式)
|
||||
- `app-update-available` → 顶栏小红点 + 弹窗
|
||||
- `app-update-progress` → 进度条(`downloaded` / `total` / 百分比)
|
||||
|
||||
**i18n**:zh-CN / en 双语同步补 `settings.navAbout` / `settings.about*` / `update.*` 系列键。
|
||||
|
||||
### L3:发布侧脚本增强
|
||||
|
||||
新增 `scripts/publish-release.ps1`(或扩展 `package-devflow.ps1` 加 `-Mode release` 通道):
|
||||
|
||||
1. 前置:校验 `tauri.conf.json.version` 与 `Cargo.toml.version` 一致(防 D1 复发)
|
||||
2. tauri build(含 createUpdaterArtifacts → .nsis.zip + .sig)
|
||||
3. 计算 sha256 + 读取 .sig 内容
|
||||
4. 生成 latest.json:
|
||||
{
|
||||
"version": "0.1.2",
|
||||
"notes": "<从 docs/CHANGELOG.md 提取本节>",
|
||||
"pub_date": "2026-10-09T14:30:00Z",
|
||||
"platforms": {
|
||||
"windows-x86_64": {
|
||||
"signature": "<.sig 内容>",
|
||||
"url": "https://app.1216.top/download/devflow/devflow_0.1.2_x64-setup.nsis.zip"
|
||||
}
|
||||
}
|
||||
}
|
||||
5. 上传 latest.json + .nsis.zip + .sig + 裸 exe 到 **app.1216.top** `download/devflow/`(走 publish-app-site 通道:suke-dev / `/var/www/app-site/`)
|
||||
6. `app.1216.top` 的 JSON 已配 `Cache-Control: no-cache` → **无需刷 CDN**,发布即生效
|
||||
```
|
||||
|
||||
**密钥管理**
|
||||
- 一次性:`tauri signer generate -w ~/.tauri/devflow.key` → 公钥入 `tauri.conf.json` 的 `pubkey`
|
||||
- 打包时设环境变量 `TAURI_SIGNING_PRIVATE_KEY`(路径)+ `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`
|
||||
- **私钥绝不入仓**(`.gitignore` 补 `*.key`)
|
||||
|
||||
---
|
||||
|
||||
## 五、兼容性核验
|
||||
|
||||
| 既有资产 | 影响 | 结论 |
|
||||
|---|---|---|
|
||||
| `scripts/package-devflow.ps1` | 不动 verify/full 两通道,L3 新增独立通道 | ✔ 零回归 |
|
||||
| `swap-install.ps1` | 仅改第 8 行硬编码 → glob(D2 修复) | ✔ 行为等价,更健壮 |
|
||||
| `lib.rs` Builder 链 | 追加一个 `.plugin()`,不触碰既有 5 个插件与 68 IPC | ✔ 零侵入 |
|
||||
| `capabilities/default.json` | 追加一行 `updater:default` | ✔ 纯增量 |
|
||||
| SettingsNav 7 类 | 追加第 8 类,搜索/防抖/持久化逻辑自动覆盖(`SETTINGS_INDEX` 驱动) | ✔ 架构自洽 |
|
||||
| appSettings KV 体系 | 新增 4 个 key,复用既有读写 | ✔ 零新表零迁移 |
|
||||
| `tauri.conf.json` targets | 保持 `["nsis"]`,只加 `createUpdaterArtifacts` | ✔ 不扩大构建目标(对齐知识库「targets 收窄需还原」的教训:本项是新增字段非收窄,安全) |
|
||||
| devflow.db / devflow-dev.db | L1/L2 只读 KV,不涉 schema | ✔ 零库变更 |
|
||||
|
||||
**dex 依赖**:无。**P0 依赖**:无(可立即开工)。
|
||||
|
||||
---
|
||||
|
||||
## 六、实施批次(全部可独立交付)
|
||||
|
||||
| 批次 | 规模 | 内容 | 验收 |
|
||||
|---|---|---|---|
|
||||
| **B1** | S | L0 版本治理:Cargo.toml 对齐 0.1.1 / `app_get_version` IPC / swap-install glob 修复 / 设置页版本号展示 | `cargo check -p devflow` 0 error;设置页可见 `v0.1.1`;换装脚本升版后仍能正确取包 |
|
||||
| **B2** | M | L1 插件接入 + `app_check_update` IPC + 自动检查节流 + `app-update-available` 事件 | 手动改远端 `latest.json` 版本号 → 应用内提示可用 |
|
||||
| **B3** | M | L2 下载安装:进度事件 + 更新弹窗 + `app_download_install_update` + 重启 | 端到端:旧版 → 提示 → 下载 → 装 → 重启后版本号变新 |
|
||||
| **B4** | S | L3 发布脚本 + 密钥生成 + latest.json 生成上传 | 一条命令产出并上传可分发的更新清单(app.1216.top 无需刷缓存) |
|
||||
|
||||
**建议序**:B1 立即做(零风险、当天可交付、消除 D1/D2 两个真实缺陷);B2→B3 一起做(拆开无独立价值);B4 在首次真实发版前完成即可。
|
||||
|
||||
**总估算**:B1 0.5 天 / B2+B3 1.5-2 天 / B4 0.5 天 ≈ **2.5-3 天**。
|
||||
|
||||
---
|
||||
|
||||
## 七、风险与边界
|
||||
|
||||
| 风险 | 等级 | 对策 |
|
||||
|---|---|---|
|
||||
| 私钥泄露 | **高** | `.gitignore` 拦 `*.key`;仅打包机存私钥;公钥入仓无妨 |
|
||||
| ~~CDN 缓存陈旧致拉了旧清单~~ | ~~中~~ **已消解** | C1 改走 `app.1216.top`(已配 `Cache-Control: no-cache`),无缓存陈旧问题;可选 `?t=<timestamp>` 兜底 |
|
||||
| 更新包下载中断 | 中 | 插件内建重试;失败保留旧版不破坏现状 |
|
||||
| 装完起不来(坏包) | 中 | `installMode: passive` 失败可见;保留上一版安装包以备手回滚 |
|
||||
| 自动检查打扰 | 低 | 15 分钟周期 + **同版本只提示一次** + 只提示不自动装 + 可关 |
|
||||
| 版本双源复发 | 低 | B1 加校验(发布脚本第 1 步)+ 可选 `build.rs` 断言 |
|
||||
|
||||
**明确不做(本设计范围外)**
|
||||
- 增量更新(delta update)——体量收益不值复杂度
|
||||
- 灰度/分渠道发布
|
||||
- 非主应用组件(`df-im-gateway` 等)的版本线
|
||||
- 回滚 UI(保留手动重装路径即可)
|
||||
|
||||
---
|
||||
|
||||
## 八、决策点(✅ 2026-10-09 拍板定稿)
|
||||
|
||||
| # | 议题 | ✅ 定稿 | 依据 |
|
||||
|---|---|---|---|
|
||||
| **C1** | 清单托管位 | **`app.1216.top/download/devflow/`** | 产品门户语义正确;该站已配 `Cache-Control: no-cache`,发布即生效,无需刷 CDN;通道(suke-dev / `/var/www/app-site/`)现成 |
|
||||
| **C2** | 安装模式 | **`passive`**(进度条窗口,无需点击) | 非静默;`quiet` 完全无感但失败不可见(静默伪成功风险) |
|
||||
| **C3** | B1 是否先行 | **立即开工** | 零风险、当天交付,消 D1/D2 两个真实缺陷 |
|
||||
| **C4** | 自动检查节奏 | **启动后 3s → 周期 15 分钟** + 同版本单次提示 + 关于页手动按钮 | 用户定:3s / 15min |
|
||||
|
||||
### C1 补充决策:双产物并存(站点规范 vs Tauri 强制格式)
|
||||
|
||||
app-site 站规为「不打 zip,直接裸 exe」,而 Tauri updater **强制** `.nsis.zip` + `.sig`。故 `download/devflow/` 并存四项:
|
||||
|
||||
| 文件 | 给谁 | 合规 |
|
||||
|---|---|---|
|
||||
| `devflow_<ver>_x64-setup.exe` | 手动下载的用户 | ✅ 站点「裸 exe」规范 |
|
||||
| `devflow_<ver>_x64-setup.nsis.zip` | updater 拉取 | ✅ Tauri 格式强制 |
|
||||
| `devflow_<ver>_x64-setup.nsis.zip.sig` | updater 验签 | ✅ |
|
||||
| `latest.json` | updater 清单 | ✅ |
|
||||
|
||||
### C4 补充决策:体验设计「环境式更新」
|
||||
|
||||
**核心原则**:检查频率 ≠ 体验。体验由「提醒方式 / 去重 / 过程可见 / 时机可控」四件事决定。
|
||||
|
||||
| 触点 | ✅ 定稿 |
|
||||
|---|---|
|
||||
| 平时 | 完全安静,零存在感 |
|
||||
| 发现新版 | **顶栏非模态徽章**(不弹窗打断) |
|
||||
| 查看详情 | 点徽章展开:版本号 + 更新说明 + 包大小 |
|
||||
| 决定 | 「立即升级」/「稍后」双选(**不自动下载安装**) |
|
||||
| 下载 | 进度条 + 可取消 |
|
||||
| 安装 | `passive` 进度窗口,无需点击 |
|
||||
| 完成 | 重启后 toast「已升级到 vX.Y.Z」 |
|
||||
| 反复 | **同版本只提示一次**(KV `df-update-notified-version`) |
|
||||
|
||||
**为什么必须是「环境式」而非「隐形型(全自动)」**:DevFlow 是**有状态工作工具**(可能正跑长任务/开着会话),任何自动重启都会打断工作。时机控制权必须交给用户。
|
||||
@@ -259,7 +259,7 @@
|
||||
|
||||
> **已完成**:`SELECT *` 改为显式 14 列(id, kind, title, content, tags, status, confidence, reuse_count, verified, source_project, source_ref, reasoning, created_at, updated_at),`embedding` 单独另取。消除隐式依赖,字段级精简待单独立项。commit 4a95f6a(该 commit 内编号为 FR-D2)。
|
||||
|
||||
### 🟡 FR-P3 — 全局单 SQLite 连接串行瓶颈 [仅代理报告待复核]
|
||||
### 🟡 FR-P3 — 全局单 SQLite 连接串行瓶颈 [仅代理报告待复核] ⏸️ 评估后不改(2026-10-08)
|
||||
|
||||
**位置**:`crates/df-storage/src/db.rs:13-60` + `crud.rs` 宏内 `spawn_blocking`+`blocking_lock`
|
||||
|
||||
@@ -269,7 +269,9 @@
|
||||
|
||||
**建议**:r2d2/deadpool-sqlite 连接池(写单连接+Mutex,读多连接并发);最低限度换 `std::sync::Mutex`(spawn_blocking 同步上下文内开销更低,不跨 await)。
|
||||
|
||||
### 🟡 FR-P4 — `human_node` 500ms 忙轮询取消状态 [已核实]
|
||||
> **⏸️ 2026-10-08 评估后判定不改**:① 架构约束——`conn()` 返 `Arc<Mutex<Connection>>`,全仓数十处调用点按此签名取锁,改连接池需重构全部调用点 + 迁移逻辑(`run_migrations` 亦持单锁),改动面大风险高;② 收益极低——SQLite 写操作本就串行(WAL 下写写互斥),连接池对写无收益,读并发在桌面单用户场景无实际压力;③ 现状已缓解——`busy_timeout(5000)` 已解 GUI+MCP server 多进程写冲突(真实痛点)且已落地;④ 收益/风险比不划算,动 DB 核心层高危。保留 `db.rs:60` TODO 注释备忘。
|
||||
|
||||
### 🟡 FR-P4 — `human_node` 500ms 忙轮询取消状态 [已核实] ✅ 已完成(2026-10-08)
|
||||
|
||||
**位置**:`crates/df-nodes/src/human_node.rs:54`
|
||||
|
||||
@@ -279,7 +281,9 @@
|
||||
|
||||
**建议**:取消走独立 `tokio::sync::Notify`/oneshot,HumanNode `notify.notified().await` 零轮询。注:todo.md:142 已记"停止生成 idle 用 Notify 替代轮询",此为同类不同位置(审批节点 vs stopChat)。
|
||||
|
||||
### 🟡 FR-P5 — `build_system_prompt` 每次发消息全表扫项目 [仅代理报告待复核]
|
||||
> **✅ 2026-10-08 已完成**:`StateMachine` 加 `cancel_notify: Arc<tokio::sync::Notify>` 字段(与 states 同 Arc 共享,clone 浅拷贝同一 Notify);`set_cancelled` 置位后 `notify_waiters()` 唤醒;新增 `cancel_notified()` 暴露 `Notified` future。`human_node.rs` select! 循环内 `let cancel_wait = ctx.node_status.cancel_notified()` 替代 `cancel_tick.tick()`,唤醒后仍复查 `is_cancelled` 兜底(Notify 无状态,可能因他节点取消误唤醒)。`cargo check -p df-workflow -p df-nodes` EXIT 0;`cargo test -p df-nodes -p df-workflow` 62+3 passed(含 `human_workflow_cancel_via_shared_state_machine` 取消路径测试)。
|
||||
|
||||
### 🟡 FR-P5 — `build_system_prompt` 每次发消息全表扫项目 [仅代理报告待复核] ✅ 已完成(指纹缓存)
|
||||
|
||||
**位置**:`src-tauri/src/commands/ai/prompt.rs:70-86`
|
||||
|
||||
@@ -289,7 +293,9 @@
|
||||
|
||||
**建议**:项目列表变动时刷新缓存(已有 path 绑定事件可触发),或注入前比对 updated_at 决定是否重算。
|
||||
|
||||
### 🟡 FR-P6 — `AiChat.vue` watch messages 用 JSON.stringify 全量快照 [仅代理报告待复核]
|
||||
> **✅ 已完成(指纹缓存)**:`prompt.rs:388,447` 已落地指纹比对——`SELECT id, updated_at FROM projects WHERE deleted_at IS NULL`(仅取轻量两列,省 `list_active` 全行物化),指纹未变则复用缓存字符串,变更才重建;查询失败回退「强制重建」保守安全。原「全表扫 + 每次重拼」病灶已消除。
|
||||
|
||||
### 🟡 FR-P6 — `AiChat.vue` watch messages 用 JSON.stringify 全量快照 [仅代理报告待复核] ✅ 已完成(已消除)
|
||||
|
||||
**位置**:`src/components/AiChat.vue:670-678`
|
||||
|
||||
@@ -299,6 +305,8 @@
|
||||
|
||||
**建议**:watch 具体派生信号(如 toolCalls 的 status 集合)而非整个 messages deep 快照。
|
||||
|
||||
> **✅ 已完成(已消除)**:`AiChat.vue` 全仓 grep `JSON.stringify` 零命中——原 deep watch 全量快照已随消息渲染重构移除(AiChat 组件已按职责抽离,消息 watch 逻辑迁移至 MessageList 等子组件,改用具体派生信号)。病灶不复存在。
|
||||
|
||||
---
|
||||
|
||||
## 6. 可读性 / DRY(重构项)
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
# 用途: DevFlow 应用内升级发布通道(B4/L3)
|
||||
# 构建 updater 产物(.nsis.zip + .sig) -> 生成 latest.json -> 上传 app.1216.top
|
||||
# 设计: docs/02-架构设计/专项设计/版本更新与自动升级-设计-2026-10-09.md §四 L3
|
||||
#
|
||||
# 用法:
|
||||
# .\scripts\publish-release.ps1 # 用 git 最近提交作 notes
|
||||
# .\scripts\publish-release.ps1 -Notes "修复xx" # 手动指定更新说明
|
||||
# .\scripts\publish-release.ps1 -SkipBuild # 复用已有产物(仅生成清单+上传)
|
||||
#
|
||||
# 前置:
|
||||
# 1. tauri.conf.json 与 Cargo.toml 版本号一致(脚本第 1 步校验,防 D1 复发)
|
||||
# 2. 签名私钥就位: $env:TAURI_SIGNING_PRIVATE_KEY = "$env:USERPROFILE\.tauri\devflow.key"
|
||||
# (无密码密钥无需 TAURI_SIGNING_PRIVATE_KEY_PASSWORD)
|
||||
# 3. ssh-proxy 已配 suke-dev(39.99.243.191),web 根 /var/www/app-site/
|
||||
param(
|
||||
[string]$Notes = "",
|
||||
[switch]$SkipBuild
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$repo = "E:\wk-lab\devflow"
|
||||
$site = "suke-dev"
|
||||
$remoteDir = "/var/www/app-site/download/devflow"
|
||||
$baseUrl = "https://app.1216.top/download/devflow"
|
||||
|
||||
Set-Location $repo
|
||||
|
||||
# ── 1. 版本双源校验(防 D1 复发)──
|
||||
Write-Host "[1/6] 校验版本双源一致..."
|
||||
$confVer = (Get-Content "$repo\src-tauri\tauri.conf.json" -Raw | ConvertFrom-Json).version
|
||||
$cargoVer = (Select-String -Path "$repo\src-tauri\Cargo.toml" -Pattern '^version\s*=\s*"([^"]+)"').Matches[0].Groups[1].Value
|
||||
if ($confVer -ne $cargoVer) {
|
||||
Write-Host "[失败] 版本不一致: tauri.conf.json=$confVer, Cargo.toml=$cargoVer"
|
||||
Write-Host " 请先对齐两处版本号再发布(设计 D1:双源必须同值)"
|
||||
exit 1
|
||||
}
|
||||
$version = $confVer
|
||||
Write-Host " 版本 v$version 双源一致,通过"
|
||||
|
||||
# ── 2. 签名私钥就位检查 ──
|
||||
if (-not $env:TAURI_SIGNING_PRIVATE_KEY) {
|
||||
$keyPath = "$env:USERPROFILE\.tauri\devflow.key"
|
||||
if (Test-Path $keyPath) {
|
||||
$env:TAURI_SIGNING_PRIVATE_KEY = $keyPath
|
||||
Write-Host "[2/6] 签名私钥: $keyPath"
|
||||
} else {
|
||||
Write-Host "[失败] 未找到签名私钥($keyPath),且未设 TAURI_SIGNING_PRIVATE_KEY"
|
||||
Write-Host " 先执行: cargo tauri signer generate -w `$env:USERPROFILE\.tauri\devflow.key"
|
||||
exit 1
|
||||
}
|
||||
} else {
|
||||
Write-Host "[2/6] 签名私钥: 取自环境变量"
|
||||
}
|
||||
|
||||
# ── 3. 构建 updater 产物 ──
|
||||
$nsisDir = "$repo\target\release\bundle\nsis"
|
||||
if (-not $SkipBuild) {
|
||||
Write-Host "[3/6] tauri build(含 createUpdaterArtifacts,可能 10 分钟级)..."
|
||||
cmd /c "cargo tauri build 2>&1"
|
||||
if ($LASTEXITCODE -ne 0) { Write-Host "[失败] tauri build"; exit 1 }
|
||||
} else {
|
||||
Write-Host "[3/6] 跳过构建(-SkipBuild),复用已有产物"
|
||||
}
|
||||
|
||||
# 定位产物:裸 exe / .nsis.zip / .sig
|
||||
$setupExe = Get-ChildItem "$nsisDir\devflow_${version}_x64-setup.exe" -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||
$zipFile = Get-ChildItem "$nsisDir\devflow_${version}_x64-setup.nsis.zip" -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||
$sigFile = Get-ChildItem "$nsisDir\devflow_${version}_x64-setup.nsis.zip.sig" -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||
if (-not $zipFile) { Write-Host "[失败] 未找到 .nsis.zip 产物(确认 createUpdaterArtifacts=true)"; exit 1 }
|
||||
if (-not $sigFile) { Write-Host "[失败] 未找到 .sig 签名文件"; exit 1 }
|
||||
Write-Host " 产物: $($zipFile.Name) ($([math]::Round($zipFile.Length/1MB,1))MB)"
|
||||
|
||||
# ── 4. 生成 latest.json ──
|
||||
Write-Host "[4/6] 生成 latest.json..."
|
||||
$sig = (Get-Content $sigFile.FullName -Raw).Trim()
|
||||
if (-not $Notes) {
|
||||
# 用 git 最近一条提交作 notes(无 CHANGELOG 时的兜底)
|
||||
$Notes = (git log -1 --pretty=format:"%s" 2>&1) -join "`n"
|
||||
if (-not $Notes) { $Notes = "DevFlow v$version" }
|
||||
}
|
||||
$pubDate = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
|
||||
$sha256 = (Get-FileHash $zipFile.FullName -Algorithm SHA256).Hash.ToLower()
|
||||
$latestJson = [ordered]@{
|
||||
version = $version
|
||||
notes = $Notes
|
||||
pub_date = $pubDate
|
||||
platforms = [ordered]@{
|
||||
"windows-x86_64" = [ordered]@{
|
||||
signature = $sig
|
||||
url = "$baseUrl/devflow_${version}_x64-setup.nsis.zip"
|
||||
}
|
||||
}
|
||||
} | ConvertTo-Json -Depth 5
|
||||
$latestPath = "$repo\target\release\bundle\nsis\latest.json"
|
||||
$latestJson | Out-File $latestPath -Encoding utf8 -NoNewline
|
||||
Write-Host " latest.json: version=$version, sha256=$($sha256.Substring(0,16))..."
|
||||
|
||||
# ── 5. 上传到 app.1216.top ──
|
||||
Write-Host "[5/6] 上传到 $site`:$remoteDir ..."
|
||||
$env:MSYS_NO_PATHCONV = "1"
|
||||
# 确保远程目录存在
|
||||
ssh-proxy exec -n $site -c "mkdir -p $remoteDir"
|
||||
if ($LASTEXITCODE -ne 0) { Write-Host "[失败] 创建远程目录"; exit 1 }
|
||||
|
||||
$uploads = @($latestPath, $zipFile.FullName, $sigFile.FullName)
|
||||
if ($setupExe) { $uploads += $setupExe.FullName }
|
||||
foreach ($f in $uploads) {
|
||||
$name = Split-Path $f -Leaf
|
||||
Write-Host " 上传 $name ..."
|
||||
ssh-proxy upload --name $site --src $f --dst "$remoteDir/$name"
|
||||
if ($LASTEXITCODE -ne 0) { Write-Host "[失败] 上传 $name"; exit 1 }
|
||||
}
|
||||
|
||||
# ── 6. 验证 ──
|
||||
Write-Host "[6/6] 验证远端清单..."
|
||||
$remoteCheck = ssh-proxy exec -n $site -c "ls -la $remoteDir && echo '---' && cat $remoteDir/latest.json | head -5"
|
||||
Write-Host $remoteCheck
|
||||
Write-Host ""
|
||||
Write-Host "[发布完成] v$version"
|
||||
Write-Host " 清单地址: $baseUrl/latest.json"
|
||||
Write-Host " 应用内检查更新即可发现新版本(app.1216.top 已配 no-cache,无需刷 CDN)"
|
||||
@@ -0,0 +1,43 @@
|
||||
# DevFlow 换装脚本:kill 旧进程 -> 静默安装 -> 启动验证
|
||||
# 独立后台执行,避免中断当前会话导致换装半途而废
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$log = 'E:/wk-lab/devflow/swap.log'
|
||||
function Log($m) { "$(Get-Date -Format 'HH:mm:ss') $m" | Tee-Object -FilePath $log -Append }
|
||||
|
||||
Log "=== 换装开始 ==="
|
||||
|
||||
# 1. kill 旧进程
|
||||
$procs = Get-Process devflow -ErrorAction SilentlyContinue
|
||||
if ($procs) {
|
||||
Log "发现运行中 devflow 进程: $($procs.Id -join ','),正在结束..."
|
||||
$procs | Stop-Process -Force -ErrorAction SilentlyContinue
|
||||
Start-Sleep -Seconds 3
|
||||
$still = Get-Process devflow -ErrorAction SilentlyContinue
|
||||
if ($still) { Log "警告: 仍有残留进程 $($still.Id -join ',')"; $still | Stop-Process -Force -ErrorAction SilentlyContinue; Start-Sleep -Seconds 2 }
|
||||
Log "旧进程已结束"
|
||||
} else {
|
||||
Log "无运行中 devflow 进程"
|
||||
}
|
||||
|
||||
# 2. 静默安装新包
|
||||
$setup = 'E:/wk-lab/devflow/target/release/bundle/nsis/devflow_0.1.1_x64-setup.exe'
|
||||
if (-not (Test-Path $setup)) { Log "错误: 安装包不存在 $setup"; exit 1 }
|
||||
Log "静默安装: $setup"
|
||||
$p = Start-Process -FilePath $setup -ArgumentList '/S' -PassThru -Wait
|
||||
Log "安装器退出码: $($p.ExitCode)"
|
||||
Start-Sleep -Seconds 3
|
||||
|
||||
# 3. 启动验证
|
||||
$installed = 'C:\Users\23780\AppData\Local\devflow\devflow.exe'
|
||||
if (Test-Path $installed) {
|
||||
$ver = (Get-Item $installed).VersionInfo.FileVersion
|
||||
Log "已安装二进制: $installed (FileVersion=$ver)"
|
||||
Start-Process -FilePath $installed
|
||||
Start-Sleep -Seconds 5
|
||||
$new = Get-Process devflow -ErrorAction SilentlyContinue
|
||||
if ($new) { Log "启动成功, PID=$($new.Id -join ',')" } else { Log "警告: 启动后未检测到进程" }
|
||||
} else {
|
||||
Log "警告: 未找到安装后二进制 $installed"
|
||||
}
|
||||
|
||||
Log "=== 换装结束 ==="
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "devflow"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
description = "DevFlow Desktop App"
|
||||
authors = [""]
|
||||
edition = "2021"
|
||||
@@ -23,6 +23,8 @@ tauri-plugin-window-state = "2"
|
||||
tauri-plugin-notification = "2"
|
||||
# Phase3 单实例:二次启动唤起已有进程主窗口,防多实例抢 18765 端口
|
||||
tauri-plugin-single-instance = "2"
|
||||
# B2 应用内升级:L1 接入 tauri-plugin-updater(内建 ed25519 签名校验)
|
||||
tauri-plugin-updater = "2"
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tokio.workspace = true
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"core:webview:allow-create-webview-window",
|
||||
"dialog:default",
|
||||
"window-state:default",
|
||||
"notification:default"
|
||||
"notification:default",
|
||||
"updater:default"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ pub mod project;
|
||||
pub mod services;
|
||||
pub mod settings;
|
||||
pub mod task;
|
||||
pub mod update;
|
||||
pub mod workflow;
|
||||
|
||||
/// 当前时间戳(毫秒字符串)— 与 df-storage 内部的时间格式保持一致
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
//! 应用内升级 IPC — B2/L1(检查更新 + 自动检查节流)
|
||||
//!
|
||||
//! 设计:docs/02-架构设计/专项设计/版本更新与自动升级-设计-2026-10-09.md §四 L1
|
||||
//!
|
||||
//! 职责边界:
|
||||
//! - `app_check_update`:拉远端 latest.json(Tauri updater 内建签名校验) → 返回是否有新版。
|
||||
//! 只读 + 外发网络,不改本地状态(节流时间戳由调用方按需写)。
|
||||
//! - `app_update_get_prefs` / `app_update_set_prefs`:自动检查偏好 KV 读写。
|
||||
//! - 自动检查节流判定:`should_auto_check` 依据 `df-update-last-check` 与间隔配置。
|
||||
//!
|
||||
//! 安装(下载+装+重启)在 B3,本模块不含。
|
||||
//!
|
||||
//! KV keys(复用既有 app_settings 体系,零新表):
|
||||
//! - `df-update-auto-check`:bool,默认 true
|
||||
//! - `df-update-check-interval-minutes`:int,默认 15
|
||||
//! - `df-update-last-check`:ISO 时间戳(节流用)
|
||||
//! - `df-update-notified-version`:string(同版本只提示一次)
|
||||
|
||||
use tauri::{AppHandle, State};
|
||||
use tauri_plugin_updater::UpdaterExt;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
use super::err_str;
|
||||
|
||||
/// 检查更新结果(IPC 传输用)
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct UpdateCheckResult {
|
||||
/// 是否有可用更新
|
||||
pub has_update: bool,
|
||||
/// 新版本号(无更新时 None)
|
||||
pub version: Option<String>,
|
||||
/// 更新说明(无更新时 None)
|
||||
pub notes: Option<String>,
|
||||
/// 发布日期(无更新时 None)
|
||||
pub date: Option<String>,
|
||||
/// 当前运行版本(便于前端展示对比)
|
||||
pub current_version: String,
|
||||
}
|
||||
|
||||
/// 自动检查偏好(IPC 传输用)
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct UpdatePrefs {
|
||||
/// 启动时自动检查更新
|
||||
pub auto_check: bool,
|
||||
/// 检查间隔(分钟)
|
||||
pub interval_minutes: u32,
|
||||
/// 上次检查时间(ISO 时间戳,空=从未)
|
||||
pub last_check: String,
|
||||
/// 已提示过的版本号(同版本只提示一次)
|
||||
pub notified_version: String,
|
||||
}
|
||||
|
||||
/// KV keys — 集中定义防拼写漂移
|
||||
const KEY_AUTO_CHECK: &str = "df-update-auto-check";
|
||||
const KEY_INTERVAL_MINUTES: &str = "df-update-check-interval-minutes";
|
||||
const KEY_LAST_CHECK: &str = "df-update-last-check";
|
||||
const KEY_NOTIFIED_VERSION: &str = "df-update-notified-version";
|
||||
|
||||
const DEFAULT_INTERVAL_MINUTES: u32 = 15;
|
||||
|
||||
/// 检查是否有新版本(只读 + 外发网络)。
|
||||
///
|
||||
/// 走 Tauri updater 的 `check()`:拉取 `tauri.conf.json` plugins.updater.endpoints
|
||||
/// 配置的 latest.json,内建 ed25519 签名校验(公钥在 pubkey)。无更新返回 `has_update=false`。
|
||||
/// 网络失败/解析失败返回 Err(前端据此提示,不谎报「已是最新」)。
|
||||
#[tauri::command]
|
||||
pub async fn app_check_update(app: AppHandle) -> Result<UpdateCheckResult, String> {
|
||||
let current_version = env!("CARGO_PKG_VERSION").to_string();
|
||||
let updater = app.updater().map_err(err_str)?;
|
||||
match updater.check().await {
|
||||
Ok(Some(update)) => Ok(UpdateCheckResult {
|
||||
has_update: true,
|
||||
version: Some(update.version.clone()),
|
||||
notes: update.body.clone(),
|
||||
date: update.date.map(|d| d.to_string()),
|
||||
current_version,
|
||||
}),
|
||||
Ok(None) => Ok(UpdateCheckResult {
|
||||
has_update: false,
|
||||
version: None,
|
||||
notes: None,
|
||||
date: None,
|
||||
current_version,
|
||||
}),
|
||||
Err(e) => Err(format!("检查更新失败: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// 读取自动检查偏好(缺省:auto_check=true / interval=15 / last_check 空 / notified 空)
|
||||
#[tauri::command]
|
||||
pub async fn app_update_get_prefs(state: State<'_, AppState>) -> Result<UpdatePrefs, String> {
|
||||
let auto_check = state
|
||||
.settings
|
||||
.get(KEY_AUTO_CHECK)
|
||||
.await
|
||||
.map_err(err_str)?
|
||||
.map(|v| v.trim_matches('"') != "false")
|
||||
.unwrap_or(true);
|
||||
let interval_minutes = state
|
||||
.settings
|
||||
.get(KEY_INTERVAL_MINUTES)
|
||||
.await
|
||||
.map_err(err_str)?
|
||||
.and_then(|v| v.trim_matches('"').parse::<u32>().ok())
|
||||
.unwrap_or(DEFAULT_INTERVAL_MINUTES);
|
||||
let last_check = state
|
||||
.settings
|
||||
.get(KEY_LAST_CHECK)
|
||||
.await
|
||||
.map_err(err_str)?
|
||||
.map(|v| v.trim_matches('"').to_string())
|
||||
.unwrap_or_default();
|
||||
let notified_version = state
|
||||
.settings
|
||||
.get(KEY_NOTIFIED_VERSION)
|
||||
.await
|
||||
.map_err(err_str)?
|
||||
.map(|v| v.trim_matches('"').to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(UpdatePrefs {
|
||||
auto_check,
|
||||
interval_minutes,
|
||||
last_check,
|
||||
notified_version,
|
||||
})
|
||||
}
|
||||
|
||||
/// 写自动检查偏好(仅 auto_check / interval_minutes;last_check / notified_version 由内部节流逻辑写)
|
||||
#[tauri::command]
|
||||
pub async fn app_update_set_prefs(
|
||||
state: State<'_, AppState>,
|
||||
auto_check: bool,
|
||||
interval_minutes: u32,
|
||||
) -> Result<(), String> {
|
||||
state
|
||||
.settings
|
||||
.set(KEY_AUTO_CHECK, if auto_check { "true" } else { "false" })
|
||||
.await
|
||||
.map_err(err_str)?;
|
||||
state
|
||||
.settings
|
||||
.set(KEY_INTERVAL_MINUTES, &interval_minutes.to_string())
|
||||
.await
|
||||
.map_err(err_str)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 记录一次检查(写 last_check 时间戳)。前端检查后调用,供下次节流判定。
|
||||
#[tauri::command]
|
||||
pub async fn app_update_mark_checked(
|
||||
state: State<'_, AppState>,
|
||||
timestamp: String,
|
||||
) -> Result<(), String> {
|
||||
state
|
||||
.settings
|
||||
.set(KEY_LAST_CHECK, ×tamp)
|
||||
.await
|
||||
.map_err(err_str)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 记录已提示版本号(同版本只提示一次)。
|
||||
#[tauri::command]
|
||||
pub async fn app_update_mark_notified(
|
||||
state: State<'_, AppState>,
|
||||
version: String,
|
||||
) -> Result<(), String> {
|
||||
state
|
||||
.settings
|
||||
.set(KEY_NOTIFIED_VERSION, &version)
|
||||
.await
|
||||
.map_err(err_str)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ═══ B3:下载安装 + 进度事件 ═══
|
||||
// 设计 §四 L3 / §八 C2:下载+安装(passive 进度条窗口),成功即自动重启。
|
||||
// 前端走「立即升级」确认后调用本命令;过程经 `app-update-progress` 事件流回传。
|
||||
// 高危动作:前端已二次确认,本命令不再额外审批(纯应用内自升级,非工具调用)。
|
||||
|
||||
/// 下载安装进度事件载荷
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct UpdateProgress {
|
||||
/// 阶段:downloading(下载中)/ installing(安装中)
|
||||
pub stage: String,
|
||||
/// 已下载字节数(下载阶段有意义)
|
||||
pub downloaded: u64,
|
||||
/// 总字节数(下载阶段有意义;unknown 时 None)
|
||||
pub total: Option<u64>,
|
||||
}
|
||||
|
||||
/// 下载并安装更新(成功即自动重启)。
|
||||
///
|
||||
/// 流程:check() 复验版本 → download_and_install(带 on_chunk 进度回调 →
|
||||
/// emit `app-update-progress` 事件) → 安装完 Tauri 自动重启(passive 模式弹进度条)。
|
||||
///
|
||||
/// 失败(网络断/签名校验失败/安装失败)返回 Err,旧版不受影响(下载在临时目录,
|
||||
/// 校验通过才替换)。前端据此提示重试。
|
||||
#[tauri::command]
|
||||
pub async fn app_download_install_update(app: AppHandle) -> Result<(), String> {
|
||||
use tauri::Emitter;
|
||||
|
||||
let updater = app.updater().map_err(err_str)?;
|
||||
let update = updater
|
||||
.check()
|
||||
.await
|
||||
.map_err(|e| format!("检查更新失败: {e}"))?
|
||||
.ok_or_else(|| "当前已是最新版本".to_string())?;
|
||||
|
||||
let app_handle = app.clone();
|
||||
let mut downloaded: u64 = 0;
|
||||
update
|
||||
.download_and_install(
|
||||
move |chunk_len, content_len| {
|
||||
downloaded += chunk_len as u64;
|
||||
let _ = app_handle.emit(
|
||||
"app-update-progress",
|
||||
UpdateProgress {
|
||||
stage: "downloading".to_string(),
|
||||
downloaded,
|
||||
total: content_len,
|
||||
},
|
||||
);
|
||||
},
|
||||
move || {
|
||||
// 下载完成,进入安装阶段
|
||||
let _ = app.emit(
|
||||
"app-update-progress",
|
||||
UpdateProgress {
|
||||
stage: "installing".to_string(),
|
||||
downloaded,
|
||||
total: None,
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("下载/安装更新失败: {e}"))?;
|
||||
|
||||
// 安装成功后 Tauri 自动触发重启(passive 模式),此处返回即终结
|
||||
Ok(())
|
||||
}
|
||||
@@ -42,6 +42,8 @@ pub fn run() {
|
||||
.plugin(tauri_plugin_window_state::Builder::default().build())
|
||||
// P1 bg 化:系统通知插件(Rust 侧 notify.rs 条件发送:主窗可见聚焦不发 + 同类节流 + 失败静默)
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
// B2 应用内升级:tauri-plugin-updater(内建 ed25519 签名校验;端点/pubkey 见 tauri.conf.json plugins.updater)
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
// Phase3 单实例:二次启动不新开进程,唤起已有实例主窗口(防多实例抢 MCP 18765 端口)
|
||||
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
|
||||
tray::show_main(app);
|
||||
@@ -545,6 +547,8 @@ pub fn run() {
|
||||
commands::ai::audit::record::list_task_conversations,
|
||||
// AC-5 按工具失败率统计(诊断 IPC,聚合 ai_tool_executions 覆盖全量/时间范围)
|
||||
commands::ai::audit::record::tool_failure_stats,
|
||||
// D2a 外置工具结果读回(审计面板遇句柄按 path 读原文全文)
|
||||
commands::ai::audit::record::ai_read_offloaded_result,
|
||||
// 知识库
|
||||
commands::knowledge::knowledge_list,
|
||||
commands::knowledge::knowledge_get,
|
||||
@@ -567,6 +571,16 @@ pub fn run() {
|
||||
commands::settings::settings_get_all,
|
||||
commands::settings::settings_delete,
|
||||
commands::settings::get_data_dir,
|
||||
// 应用版本号(编译期注入,关于页/更新检查消费)
|
||||
commands::settings::app_get_version,
|
||||
// B2 应用内升级:检查更新 + 自动检查偏好 KV
|
||||
commands::update::app_check_update,
|
||||
commands::update::app_update_get_prefs,
|
||||
commands::update::app_update_set_prefs,
|
||||
commands::update::app_update_mark_checked,
|
||||
commands::update::app_update_mark_notified,
|
||||
// B3 应用内升级:下载安装(进度事件 + 成功重启)
|
||||
commands::update::app_download_install_update,
|
||||
// 审批超时配置(默认 15 分钟,0=禁用;Settings 页可改)
|
||||
commands::settings::ai_get_approval_timeout,
|
||||
commands::settings::ai_set_approval_timeout,
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": ["nsis"],
|
||||
"createUpdaterArtifacts": true,
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
@@ -37,5 +38,16 @@
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
},
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"endpoints": [
|
||||
"https://app.1216.top/download/devflow/latest.json"
|
||||
],
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDRGODM4QzcwMDZBNEYyQTEKUldTaDhxUUdjSXlEVHdZVnRWOHQ3WDlXSllidGlHTVk0VC8rSnZQSVE0ckFJRmVNYXpyRXpBMzYK",
|
||||
"windows": {
|
||||
"installMode": "passive"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ import ApprovalDialog from './components/project/ApprovalDialog.vue'
|
||||
import ErrorBoundary from './components/ErrorBoundary.vue'
|
||||
import AppLayout from './components/layout/AppLayout.vue'
|
||||
import { startApprovalPopup } from '@/composables/ai/useApprovalPopup'
|
||||
import { startUpdateAutoCheck } from '@/composables/useUpdateCheck'
|
||||
import { aiApi } from '@/api'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -266,6 +267,11 @@ onMounted(async () => {
|
||||
console.error('[ApprovalPopup] ready 监听失败:', e)
|
||||
}
|
||||
}
|
||||
// B2 应用内升级:自动检查调度(启动后 3s 首查 + 周期检查,同版本只提示一次)。
|
||||
// 幂等(模块级单例),仅主窗口启动;auto_check=false 时内部自动跳过。
|
||||
if (route.path !== '/approval-popup') {
|
||||
startUpdateAutoCheck()
|
||||
}
|
||||
})
|
||||
|
||||
/// 一次性迁移:遍历遗留的 localStorage key,若有值则导入 appSettings(SQLite),
|
||||
|
||||
@@ -21,6 +21,52 @@ export const settingsApi = {
|
||||
return invoke<string>('get_data_dir')
|
||||
},
|
||||
|
||||
/** 获取应用版本信息(编译期注入:version/build_time/git_hash) */
|
||||
getVersion(): Promise<{ version: string; build_time: string; git_hash: string }> {
|
||||
return invoke<{ version: string; build_time: string; git_hash: string }>('app_get_version')
|
||||
},
|
||||
|
||||
/** B2 检查更新:拉远端 latest.json,返回是否有新版(网络失败抛错,不谎报已是最新) */
|
||||
checkUpdate(): Promise<{
|
||||
has_update: boolean
|
||||
version: string | null
|
||||
notes: string | null
|
||||
date: string | null
|
||||
current_version: string
|
||||
}> {
|
||||
return invoke('app_check_update')
|
||||
},
|
||||
|
||||
/** B2 读自动检查偏好 */
|
||||
getUpdatePrefs(): Promise<{
|
||||
auto_check: boolean
|
||||
interval_minutes: number
|
||||
last_check: string
|
||||
notified_version: string
|
||||
}> {
|
||||
return invoke('app_update_get_prefs')
|
||||
},
|
||||
|
||||
/** B2 写自动检查偏好 */
|
||||
setUpdatePrefs(autoCheck: boolean, intervalMinutes: number): Promise<void> {
|
||||
return invoke('app_update_set_prefs', { autoCheck, intervalMinutes })
|
||||
},
|
||||
|
||||
/** B2 记录一次检查时间戳(节流用) */
|
||||
markUpdateChecked(timestamp: string): Promise<void> {
|
||||
return invoke('app_update_mark_checked', { timestamp })
|
||||
},
|
||||
|
||||
/** B2 记录已提示版本号(同版本只提示一次) */
|
||||
markUpdateNotified(version: string): Promise<void> {
|
||||
return invoke('app_update_mark_notified', { version })
|
||||
},
|
||||
|
||||
/** B3 下载并安装更新(进度经 app-update-progress 事件流;成功即自动重启) */
|
||||
downloadAndInstallUpdate(): Promise<void> {
|
||||
return invoke('app_download_install_update')
|
||||
},
|
||||
|
||||
/** 取全部 key/value(启动时一次性拉回恢复偏好) */
|
||||
getAll(): Promise<Record<string, string>> {
|
||||
return invoke<Record<string, string>>('settings_get_all')
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
<!-- 主窗口:完整布局 -->
|
||||
<div v-else class="df-shell">
|
||||
<AppSidebar />
|
||||
<!-- 全局更新徽章(环境式更新,任意页面可见) -->
|
||||
<UpdateBadge />
|
||||
|
||||
<!-- ═══ Main (页面内容) ═══ -->
|
||||
<!-- 仅在 AI 面板可见且最大化时隐藏主内容;面板关闭时无论 maximized 状态如何都显示 -->
|
||||
@@ -29,6 +31,7 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
import { useAiStore } from '@/stores/ai'
|
||||
import { useAppSettingsStore } from '@/stores/appSettings'
|
||||
import AppSidebar from './AppSidebar.vue'
|
||||
import UpdateBadge from './UpdateBadge.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
isDetached: boolean
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
<template>
|
||||
<!-- ═══ 全局更新徽章(环境式更新,设计 §八 C4)═══
|
||||
发现新版 → 右上角非模态徽章(不弹窗打断);点击展开版本详情 + 升级/稍后。
|
||||
全局挂载(AppLayout),任意页面可见。 -->
|
||||
<Transition name="update-badge">
|
||||
<div v-if="visible" class="update-badge-root">
|
||||
<button
|
||||
type="button"
|
||||
class="update-badge-btn"
|
||||
:class="{ 'is-active': expanded }"
|
||||
:title="$t('settings.updateBadgeTitle')"
|
||||
@click="expanded = !expanded"
|
||||
>
|
||||
<span class="update-badge-dot"></span>
|
||||
<span class="update-badge-text">{{ $t('settings.updateBadgeText', { version: info?.version }) }}</span>
|
||||
</button>
|
||||
|
||||
<Transition name="update-pop">
|
||||
<div v-if="expanded" class="update-pop">
|
||||
<div class="update-pop-head">{{ $t('settings.updateAvailableTitle', { version: info?.version }) }}</div>
|
||||
<div v-if="info?.notes" class="update-pop-notes">{{ info.notes }}</div>
|
||||
|
||||
<!-- 下载进度 -->
|
||||
<div v-if="progress" class="update-pop-progress">
|
||||
<div class="update-pop-bar">
|
||||
<div class="update-pop-fill" :style="{ width: progressPercent + '%' }"></div>
|
||||
</div>
|
||||
<div class="update-pop-progress-text">{{ progressText }}</div>
|
||||
</div>
|
||||
<div v-if="installError" class="update-pop-error">{{ installError }}</div>
|
||||
|
||||
<div class="update-pop-actions">
|
||||
<button class="btn btn-ghost btn-sm" :disabled="installing" @click="onLater">
|
||||
{{ $t('settings.updateLater') }}
|
||||
</button>
|
||||
<button class="btn btn-primary btn-sm" :disabled="installing" @click="onInstall">
|
||||
{{ installing ? $t('settings.updateInstalling') : $t('settings.updateNow') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { settingsApi } from '@/api'
|
||||
import { listen, type UnlistenFn } from '@tauri-apps/api/event'
|
||||
import type { UpdateAvailablePayload } from '@/composables/useUpdateCheck'
|
||||
|
||||
// ============================================================
|
||||
// 全局更新徽章 — 消费 useUpdateCheck 广播的 app-update-available 事件。
|
||||
// 环境式:平时零存在感;有新版才显徽章;点击展开详情;不自动安装。
|
||||
// ============================================================
|
||||
const { t } = useI18n()
|
||||
|
||||
const visible = ref(false)
|
||||
const expanded = ref(false)
|
||||
const info = ref<UpdateAvailablePayload | null>(null)
|
||||
|
||||
const installing = ref(false)
|
||||
const progress = ref<{ stage: string; downloaded: number; total: number | null } | null>(null)
|
||||
const installError = ref('')
|
||||
|
||||
let unlistenAvailable: UnlistenFn | null = null
|
||||
let unlistenProgress: UnlistenFn | null = null
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
unlistenAvailable = await listen<UpdateAvailablePayload>('app-update-available', (e) => {
|
||||
info.value = e.payload
|
||||
visible.value = true
|
||||
})
|
||||
} catch {
|
||||
/* 静默 */
|
||||
}
|
||||
try {
|
||||
unlistenProgress = await listen<{ stage: string; downloaded: number; total: number | null }>(
|
||||
'app-update-progress',
|
||||
(e) => {
|
||||
progress.value = e.payload
|
||||
},
|
||||
)
|
||||
} catch {
|
||||
/* 静默 */
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
unlistenAvailable?.()
|
||||
unlistenProgress?.()
|
||||
unlistenAvailable = null
|
||||
unlistenProgress = null
|
||||
})
|
||||
|
||||
const progressPercent = computed(() => {
|
||||
const p = progress.value
|
||||
if (!p || p.stage !== 'downloading' || !p.total || p.total === 0) return 0
|
||||
return Math.min(100, Math.round((p.downloaded / p.total) * 100))
|
||||
})
|
||||
|
||||
const progressText = computed(() => {
|
||||
const p = progress.value
|
||||
if (!p) return ''
|
||||
if (p.stage === 'installing') return t('settings.updateInstallingStage')
|
||||
if (!p.total) return t('settings.updateDownloadingUnknown')
|
||||
const mb = (n: number) => (n / 1048576).toFixed(1)
|
||||
return t('settings.updateDownloading', {
|
||||
percent: progressPercent.value,
|
||||
done: mb(p.downloaded),
|
||||
total: mb(p.total),
|
||||
})
|
||||
})
|
||||
|
||||
function onLater() {
|
||||
visible.value = false
|
||||
expanded.value = false
|
||||
}
|
||||
|
||||
async function onInstall() {
|
||||
if (installing.value) return
|
||||
installing.value = true
|
||||
installError.value = ''
|
||||
progress.value = null
|
||||
try {
|
||||
await settingsApi.downloadAndInstallUpdate()
|
||||
} catch (e) {
|
||||
installError.value = t('settings.updateInstallFailed', { error: String(e) })
|
||||
installing.value = false
|
||||
progress.value = null
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.update-badge-root {
|
||||
position: fixed;
|
||||
top: 12px;
|
||||
right: 16px;
|
||||
z-index: 2000;
|
||||
}
|
||||
.update-badge-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 5px 12px;
|
||||
border: 0.5px solid var(--df-accent);
|
||||
border-radius: 999px;
|
||||
background: var(--df-bg-card);
|
||||
color: var(--df-accent);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
transition: filter 0.15s;
|
||||
}
|
||||
.update-badge-btn:hover {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
.update-badge-dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--df-accent);
|
||||
animation: update-pulse 1.6s ease-in-out infinite;
|
||||
}
|
||||
@keyframes update-pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.35; }
|
||||
}
|
||||
.update-pop {
|
||||
position: absolute;
|
||||
top: 36px;
|
||||
right: 0;
|
||||
width: 320px;
|
||||
padding: 14px 16px;
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius-lg);
|
||||
background: var(--df-bg-card);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.update-pop-head {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--df-accent);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.update-pop-notes {
|
||||
font-size: 12px;
|
||||
color: var(--df-text-dim);
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
max-height: 160px;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.update-pop-progress {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.update-pop-bar {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
background: var(--df-bg-hover, rgba(128, 128, 128, 0.2));
|
||||
overflow: hidden;
|
||||
}
|
||||
.update-pop-fill {
|
||||
height: 100%;
|
||||
background: var(--df-accent);
|
||||
transition: width 0.2s ease;
|
||||
}
|
||||
.update-pop-progress-text {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
font-family: var(--df-font-mono, monospace);
|
||||
color: var(--df-text-dim);
|
||||
}
|
||||
.update-pop-error {
|
||||
margin-bottom: 12px;
|
||||
padding: 8px 10px;
|
||||
border-radius: var(--df-radius-md, 6px);
|
||||
background: rgba(220, 53, 69, 0.12);
|
||||
font-size: 12px;
|
||||
color: var(--df-danger, #dc3545);
|
||||
line-height: 1.5;
|
||||
word-break: break-all;
|
||||
}
|
||||
.update-pop-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
.update-pop-actions .btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.update-badge-enter-active,
|
||||
.update-badge-leave-active {
|
||||
transition: opacity 0.25s, transform 0.25s;
|
||||
}
|
||||
.update-badge-enter-from,
|
||||
.update-badge-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
.update-pop-enter-active,
|
||||
.update-pop-leave-active {
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
.update-pop-enter-from,
|
||||
.update-pop-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,413 @@
|
||||
<template>
|
||||
<!-- ═══ 关于 DevFlow(版本可见 + 更新入口)═══
|
||||
B1(L0):版本号/构建时间/git hash 展示。
|
||||
B2(L1):检查更新接入 + 自动检查开关/间隔 + 更新可用提示。
|
||||
下载安装链路在 B3(此处「立即升级」按钮先禁用,提示即将上线)。 -->
|
||||
<section class="panel">
|
||||
<div class="panel-header">
|
||||
<h2>{{ $t('settings.sectionAbout') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="general-settings">
|
||||
<!-- ═══ 当前版本(编译期注入,单一源头) ═══ -->
|
||||
<div class="setting-row inline-control-row" data-setting-key="appVersion">
|
||||
<div class="inline-head">
|
||||
<span class="setting-label">{{ $t('settings.labelAppVersion') }}</span>
|
||||
<span class="about-version" :title="versionTitle">{{ versionText }}</span>
|
||||
</div>
|
||||
<span class="setting-desc">{{ $t('settings.descAppVersion') }}</span>
|
||||
</div>
|
||||
|
||||
<!-- ═══ 检查更新 ═══ -->
|
||||
<div class="setting-row inline-control-row" data-setting-key="checkUpdate">
|
||||
<div class="inline-head">
|
||||
<span class="setting-label">{{ $t('settings.labelCheckUpdate') }}</span>
|
||||
<button
|
||||
class="btn btn-ghost btn-sm about-check-btn"
|
||||
:disabled="checking || !versionLoaded"
|
||||
@click="onCheckUpdate"
|
||||
>
|
||||
{{ checking ? $t('settings.checkUpdateChecking') : $t('settings.checkUpdateAction') }}
|
||||
</button>
|
||||
</div>
|
||||
<span class="setting-desc">{{ checkResultText }}</span>
|
||||
</div>
|
||||
|
||||
<!-- ═══ 自动检查开关 ═══ -->
|
||||
<div class="setting-row inline-control-row">
|
||||
<div class="inline-head">
|
||||
<span class="setting-label">{{ $t('settings.labelAutoCheckUpdate') }}</span>
|
||||
<input
|
||||
v-model="autoCheck"
|
||||
type="checkbox"
|
||||
class="setting-checkbox"
|
||||
@change="onAutoCheckChange"
|
||||
/>
|
||||
</div>
|
||||
<span class="setting-desc">{{ $t('settings.descAutoCheckUpdate') }}</span>
|
||||
</div>
|
||||
|
||||
<!-- ═══ 检查间隔(自动检查开启时可用) ═══ -->
|
||||
<div class="setting-row inline-control-row">
|
||||
<div class="inline-head">
|
||||
<span class="setting-label">{{ $t('settings.labelCheckInterval') }}</span>
|
||||
<select
|
||||
v-model.number="intervalMinutes"
|
||||
class="setting-select"
|
||||
:disabled="!autoCheck"
|
||||
@change="onIntervalChange"
|
||||
>
|
||||
<option :value="5">{{ $t('settings.interval5m') }}</option>
|
||||
<option :value="15">{{ $t('settings.interval15m') }}</option>
|
||||
<option :value="30">{{ $t('settings.interval30m') }}</option>
|
||||
<option :value="60">{{ $t('settings.interval60m') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<span class="setting-desc">{{ $t('settings.descCheckInterval') }}</span>
|
||||
</div>
|
||||
|
||||
<!-- ═══ 上次检查时间 ═══ -->
|
||||
<div class="setting-row inline-control-row">
|
||||
<div class="inline-head">
|
||||
<span class="setting-label">{{ $t('settings.labelLastCheck') }}</span>
|
||||
<span class="about-last-check">{{ lastCheckText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ 更新可用提示卡(检测到新版时显示) ═══ -->
|
||||
<Transition name="update-card">
|
||||
<div v-if="updateInfo" class="update-card">
|
||||
<div class="update-card-head">
|
||||
<span class="update-card-title">{{ $t('settings.updateAvailableTitle', { version: updateInfo.version }) }}</span>
|
||||
</div>
|
||||
<div v-if="updateInfo.notes" class="update-card-notes">{{ updateInfo.notes }}</div>
|
||||
|
||||
<!-- 下载进度(下载中/安装中显示) -->
|
||||
<div v-if="progress" class="update-progress">
|
||||
<div class="update-progress-bar">
|
||||
<div class="update-progress-fill" :style="{ width: progressPercent + '%' }"></div>
|
||||
</div>
|
||||
<div class="update-progress-text">{{ progressText }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<div v-if="installError" class="update-error">{{ installError }}</div>
|
||||
|
||||
<div class="update-card-actions">
|
||||
<button
|
||||
class="btn btn-ghost btn-sm"
|
||||
:disabled="installing"
|
||||
@click="onLater"
|
||||
>
|
||||
{{ $t('settings.updateLater') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary btn-sm"
|
||||
:disabled="installing"
|
||||
@click="onInstall"
|
||||
>
|
||||
{{ installing ? $t('settings.updateInstalling') : $t('settings.updateNow') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { settingsApi } from '@/api'
|
||||
import { listen, type UnlistenFn } from '@tauri-apps/api/event'
|
||||
|
||||
// ============================================================
|
||||
// 关于域 — 版本可见 + 检查更新 + 下载安装(B1/L0 + B2/L1 + B3/L2)
|
||||
// 版本信息经 app_get_version IPC 取回(编译期注入)。
|
||||
// 检查更新经 app_check_update IPC(拉远端 latest.json,内建签名校验)。
|
||||
// 自动检查偏好经 app_update_get_prefs / set_prefs 读写(复用 app_settings KV)。
|
||||
// 下载安装经 app_download_install_update IPC;进度经 app-update-progress 事件流。
|
||||
// ============================================================
|
||||
const { t } = useI18n()
|
||||
|
||||
interface AppVersionInfo {
|
||||
version: string
|
||||
build_time: string
|
||||
git_hash: string
|
||||
}
|
||||
interface UpdateInfo {
|
||||
version: string
|
||||
notes: string | null
|
||||
date: string | null
|
||||
}
|
||||
interface UpdateProgressPayload {
|
||||
stage: 'downloading' | 'installing'
|
||||
downloaded: number
|
||||
total: number | null
|
||||
}
|
||||
|
||||
const versionInfo = ref<AppVersionInfo | null>(null)
|
||||
const versionLoaded = ref(false)
|
||||
const checking = ref(false)
|
||||
// 检查结果描述(成功/失败/无更新)
|
||||
const checkResultText = ref('')
|
||||
// 更新可用信息(检测到新版时非空 → 显示提示卡)
|
||||
const updateInfo = ref<UpdateInfo | null>(null)
|
||||
|
||||
// 自动检查偏好
|
||||
const autoCheck = ref(true)
|
||||
const intervalMinutes = ref(15)
|
||||
const lastCheck = ref('')
|
||||
|
||||
// B3 下载安装状态
|
||||
const installing = ref(false)
|
||||
const progress = ref<UpdateProgressPayload | null>(null)
|
||||
const installError = ref('')
|
||||
let unlistenProgress: UnlistenFn | null = null
|
||||
|
||||
onMounted(async () => {
|
||||
// 版本信息
|
||||
try {
|
||||
versionInfo.value = await settingsApi.getVersion()
|
||||
} catch {
|
||||
/* 静默:取版本失败不阻断关于页渲染 */
|
||||
} finally {
|
||||
versionLoaded.value = true
|
||||
}
|
||||
// 自动检查偏好
|
||||
try {
|
||||
const prefs = await settingsApi.getUpdatePrefs()
|
||||
autoCheck.value = prefs.auto_check
|
||||
intervalMinutes.value = prefs.interval_minutes
|
||||
lastCheck.value = prefs.last_check
|
||||
} catch {
|
||||
/* 静默:偏好读取失败用默认值 */
|
||||
}
|
||||
// 下载进度事件订阅
|
||||
try {
|
||||
unlistenProgress = await listen<UpdateProgressPayload>('app-update-progress', (e) => {
|
||||
progress.value = e.payload
|
||||
})
|
||||
} catch {
|
||||
/* 静默:订阅失败不影响手动检查 */
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
unlistenProgress?.()
|
||||
unlistenProgress = null
|
||||
})
|
||||
|
||||
// 主展示:vX.Y.Z;git_hash 非 dev 时附短 hash
|
||||
const versionText = computed(() => {
|
||||
const v = versionInfo.value
|
||||
if (!v) return t('settings.loading')
|
||||
const hash = v.git_hash && v.git_hash !== 'dev' ? ` (${v.git_hash.slice(0, 7)})` : ''
|
||||
return `v${v.version}${hash}`
|
||||
})
|
||||
|
||||
const versionTitle = computed(() => {
|
||||
const v = versionInfo.value
|
||||
if (!v) return ''
|
||||
return `${t('settings.labelBuildTime')}: ${v.build_time}\n${t('settings.labelGitHash')}: ${v.git_hash}`
|
||||
})
|
||||
|
||||
const lastCheckText = computed(() => lastCheck.value || t('settings.lastCheckNever'))
|
||||
|
||||
// 检查更新:调 app_check_update,成功则更新结果文案 + 命中新版显示提示卡并记录已提示版本
|
||||
async function onCheckUpdate() {
|
||||
if (checking.value) return
|
||||
checking.value = true
|
||||
checkResultText.value = t('settings.checkUpdateChecking')
|
||||
try {
|
||||
const r = await settingsApi.checkUpdate()
|
||||
const now = new Date().toISOString()
|
||||
lastCheck.value = now
|
||||
void settingsApi.markUpdateChecked(now)
|
||||
if (r.has_update && r.version) {
|
||||
checkResultText.value = t('settings.checkUpdateFound', { version: r.version })
|
||||
updateInfo.value = { version: r.version, notes: r.notes, date: r.date }
|
||||
void settingsApi.markUpdateNotified(r.version)
|
||||
} else {
|
||||
checkResultText.value = t('settings.checkUpdateLatest', { version: r.current_version })
|
||||
}
|
||||
} catch (e) {
|
||||
// 网络/解析失败:明确报错,不谎报「已是最新」
|
||||
checkResultText.value = t('settings.checkUpdateFailed', { error: String(e) })
|
||||
} finally {
|
||||
checking.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 「稍后」:关闭提示卡(同版本已记 notified,不再自动弹)
|
||||
function onLater() {
|
||||
updateInfo.value = null
|
||||
}
|
||||
|
||||
// 进度百分比(下载阶段;总大小未知时按 0 处理)
|
||||
const progressPercent = computed(() => {
|
||||
const p = progress.value
|
||||
if (!p || p.stage !== 'downloading' || !p.total || p.total === 0) return 0
|
||||
return Math.min(100, Math.round((p.downloaded / p.total) * 100))
|
||||
})
|
||||
|
||||
// 进度文案:下载中(x% / MB) / 安装中
|
||||
const progressText = computed(() => {
|
||||
const p = progress.value
|
||||
if (!p) return ''
|
||||
if (p.stage === 'installing') return t('settings.updateInstallingStage')
|
||||
if (!p.total) return t('settings.updateDownloadingUnknown')
|
||||
const mb = (n: number) => (n / 1048576).toFixed(1)
|
||||
return t('settings.updateDownloading', {
|
||||
percent: progressPercent.value,
|
||||
done: mb(p.downloaded),
|
||||
total: mb(p.total),
|
||||
})
|
||||
})
|
||||
|
||||
// 「立即升级」:调 app_download_install_update,进度经事件流更新;
|
||||
// 成功即自动重启(本函数不再返回后处理);失败展示错误 + 可重试。
|
||||
async function onInstall() {
|
||||
if (installing.value) return
|
||||
installing.value = true
|
||||
installError.value = ''
|
||||
progress.value = null
|
||||
try {
|
||||
await settingsApi.downloadAndInstallUpdate()
|
||||
// 成功路径:Tauri 自动重启,通常不会执行到这里
|
||||
} catch (e) {
|
||||
installError.value = t('settings.updateInstallFailed', { error: String(e) })
|
||||
installing.value = false
|
||||
progress.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function onAutoCheckChange() {
|
||||
try {
|
||||
await settingsApi.setUpdatePrefs(autoCheck.value, intervalMinutes.value)
|
||||
} catch {
|
||||
/* 静默:写失败保留内存态 */
|
||||
}
|
||||
}
|
||||
|
||||
async function onIntervalChange() {
|
||||
try {
|
||||
await settingsApi.setUpdatePrefs(autoCheck.value, intervalMinutes.value)
|
||||
} catch {
|
||||
/* 静默:写失败保留内存态 */
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 复用 settings.css 全局 .panel/.panel-header/.general-settings/.setting-row 等。
|
||||
本组件仅保留版本号显示 + 检查更新按钮 + 更新提示卡的特有样式。 */
|
||||
.about-version {
|
||||
flex: 0 0 auto;
|
||||
margin-left: auto;
|
||||
font-size: 13px;
|
||||
font-family: var(--df-font-mono, monospace);
|
||||
color: var(--df-text);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.about-last-check {
|
||||
flex: 0 0 auto;
|
||||
margin-left: auto;
|
||||
font-size: 12px;
|
||||
font-family: var(--df-font-mono, monospace);
|
||||
color: var(--df-text-dim);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.about-check-btn {
|
||||
flex: 0 0 auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
.about-check-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.setting-checkbox {
|
||||
flex: 0 0 auto;
|
||||
margin-left: auto;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
/* 更新可用提示卡 */
|
||||
.update-card {
|
||||
margin: 12px 0 0;
|
||||
padding: 14px 16px;
|
||||
border: 0.5px solid var(--df-accent);
|
||||
border-radius: var(--df-radius-lg);
|
||||
background: var(--df-bg-card);
|
||||
}
|
||||
.update-card-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.update-card-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--df-accent);
|
||||
}
|
||||
.update-card-notes {
|
||||
font-size: 12px;
|
||||
color: var(--df-text-dim);
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
max-height: 160px;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.update-card-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
.update-card-actions .btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
/* 下载进度条 */
|
||||
.update-progress {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.update-progress-bar {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
background: var(--df-bg-hover, rgba(128, 128, 128, 0.2));
|
||||
overflow: hidden;
|
||||
}
|
||||
.update-progress-fill {
|
||||
height: 100%;
|
||||
background: var(--df-accent);
|
||||
transition: width 0.2s ease;
|
||||
}
|
||||
.update-progress-text {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
font-family: var(--df-font-mono, monospace);
|
||||
color: var(--df-text-dim);
|
||||
}
|
||||
/* 安装错误提示 */
|
||||
.update-error {
|
||||
margin-bottom: 12px;
|
||||
padding: 8px 10px;
|
||||
border-radius: var(--df-radius-md, 6px);
|
||||
background: rgba(220, 53, 69, 0.12);
|
||||
font-size: 12px;
|
||||
color: var(--df-danger, #dc3545);
|
||||
line-height: 1.5;
|
||||
word-break: break-all;
|
||||
}
|
||||
.update-card-enter-active,
|
||||
.update-card-leave-active {
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.update-card-enter-from,
|
||||
.update-card-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -76,6 +76,7 @@ const categories: NavCategory[] = [
|
||||
{ value: 'security', icon: '🔒', labelKey: 'settings.navSecurity' },
|
||||
{ value: 'integrations', icon: '🔌', labelKey: 'settings.navIntegrations' },
|
||||
{ value: 'advanced', icon: '⚙️', labelKey: 'settings.navAdvanced' },
|
||||
{ value: 'about', icon: 'ℹ️', labelKey: 'settings.navAbout' },
|
||||
]
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
@@ -20,6 +20,7 @@ export type SettingsCategory =
|
||||
| 'security'
|
||||
| 'integrations'
|
||||
| 'advanced'
|
||||
| 'about'
|
||||
|
||||
/** 搜索索引项:category 所属分类 + labelKey/descKey(i18n key) + key 唯一标识 */
|
||||
export interface SettingIndexEntry {
|
||||
@@ -34,7 +35,7 @@ export interface SettingIndexEntry {
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置项静态索引 —— 覆盖全部 6 类设置项。
|
||||
* 设置项静态索引 —— 覆盖全部 7 类设置项。
|
||||
* 分类映射(对齐 plan):
|
||||
* appearance 外观(theme/language/aiLanguage/showTokenUsage)
|
||||
* ai-model AI 模型(provider 列表 / 负载均衡池 / 模型拉取)
|
||||
@@ -45,6 +46,7 @@ export interface SettingIndexEntry {
|
||||
* security 安全(allowed_dirs)
|
||||
* integrations 集成(飞书渠道凭证)
|
||||
* advanced 高级(autoExecuteMode/logLevel + 连接)
|
||||
* about 关于(版本号 / 更新检查)
|
||||
*/
|
||||
export const SETTINGS_INDEX: SettingIndexEntry[] = [
|
||||
// ===== 外观 =====
|
||||
@@ -112,4 +114,10 @@ export const SETTINGS_INDEX: SettingIndexEntry[] = [
|
||||
{ category: 'advanced', key: 'approvalTimeout', labelKey: 'settings.labelApprovalTimeout', descKey: 'settings.descApprovalTimeout' },
|
||||
{ category: 'advanced', key: 'fileUploadBase', labelKey: 'settings.labelFileUploadBase', descKey: 'settings.descFileUploadBase' },
|
||||
{ category: 'advanced', key: 'connection', labelKey: 'settings.panelConnection', descKey: 'settings.emptyConnection' },
|
||||
|
||||
// ===== 关于 =====
|
||||
// 版本号 + 更新检查(B1 版本更新与自动升级链路)。key 对齐 AboutSection.vue
|
||||
// 渲染行的 data-setting-key,搜索「版本/更新/升级/version」命中本类。
|
||||
{ category: 'about', key: 'appVersion', labelKey: 'settings.labelAppVersion', descKey: 'settings.descAppVersion' },
|
||||
{ category: 'about', key: 'checkUpdate', labelKey: 'settings.labelCheckUpdate', descKey: 'settings.descCheckUpdate' },
|
||||
]
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
// 应用内升级 — 自动检查调度(B2/L1)
|
||||
//
|
||||
// 设计:docs/02-架构设计/专项设计/版本更新与自动升级-设计-2026-10-09.md §四 L1
|
||||
//
|
||||
// 策略(C4 定稿):启动后延迟 3s 首查 → 此后按 interval_minutes(默认 15)周期查。
|
||||
// 去重:同一版本号只提示一次(KV df-update-notified-version),「稍后」后不再重复弹。
|
||||
// 只提示不自动装(安装是高风险动作,必须人点头)。
|
||||
//
|
||||
// 实现要点:
|
||||
// - 单例调度:模块级 timer,防多组件重复启动。
|
||||
// - 节流判定:读 KV last_check + interval,未到间隔跳过(防每次启动都拉)。
|
||||
// - 更新可用时广播 window 事件 'app-update-available',TopBar 徽章/AboutPanel 消费。
|
||||
// - 停用:auto_check=false 时不启动。
|
||||
|
||||
import { settingsApi } from '@/api'
|
||||
|
||||
/** 更新可用事件载荷 */
|
||||
export interface UpdateAvailablePayload {
|
||||
version: string
|
||||
notes: string | null
|
||||
date: string | null
|
||||
}
|
||||
|
||||
let timer: ReturnType<typeof setTimeout> | null = null
|
||||
let started = false
|
||||
|
||||
/** 首查延迟(ms)—— 设计 C4:启动后 3 秒 */
|
||||
const FIRST_CHECK_DELAY_MS = 3000
|
||||
|
||||
/**
|
||||
* 执行一次自动检查(含节流判定)。
|
||||
* - auto_check=false → 跳过
|
||||
* - 距上次检查不足 interval → 跳过
|
||||
* - 有新版且未提示过该版本 → 广播 app-update-available
|
||||
*/
|
||||
async function runAutoCheck(): Promise<void> {
|
||||
try {
|
||||
const prefs = await settingsApi.getUpdatePrefs()
|
||||
if (!prefs.auto_check) return
|
||||
|
||||
// 节流:距上次检查不足 interval 则跳过
|
||||
if (prefs.last_check) {
|
||||
const last = Date.parse(prefs.last_check)
|
||||
if (!Number.isNaN(last)) {
|
||||
const elapsedMin = (Date.now() - last) / 60000
|
||||
if (elapsedMin < prefs.interval_minutes) return
|
||||
}
|
||||
}
|
||||
|
||||
const r = await settingsApi.checkUpdate()
|
||||
const now = new Date().toISOString()
|
||||
void settingsApi.markUpdateChecked(now)
|
||||
|
||||
if (r.has_update && r.version) {
|
||||
// 同版本只提示一次
|
||||
if (prefs.notified_version === r.version) return
|
||||
void settingsApi.markUpdateNotified(r.version)
|
||||
const payload: UpdateAvailablePayload = {
|
||||
version: r.version,
|
||||
notes: r.notes,
|
||||
date: r.date,
|
||||
}
|
||||
window.dispatchEvent(new CustomEvent('app-update-available', { detail: payload }))
|
||||
}
|
||||
} catch {
|
||||
// 网络失败静默:自动检查不打扰用户(手动检查才报错)
|
||||
}
|
||||
}
|
||||
|
||||
/** 调度下一次检查(按 interval_minutes) */
|
||||
async function scheduleNext(): Promise<void> {
|
||||
let intervalMin = 15
|
||||
try {
|
||||
const prefs = await settingsApi.getUpdatePrefs()
|
||||
intervalMin = prefs.interval_minutes
|
||||
} catch {
|
||||
/* 用默认 15 */
|
||||
}
|
||||
timer = setTimeout(async () => {
|
||||
await runAutoCheck()
|
||||
await scheduleNext()
|
||||
}, Math.max(1, intervalMin) * 60_000)
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动自动检查调度(幂等:重复调用只启动一次)。
|
||||
* 在 App.vue onMounted 调用。
|
||||
*/
|
||||
export function startUpdateAutoCheck(): void {
|
||||
if (started) return
|
||||
started = true
|
||||
timer = setTimeout(async () => {
|
||||
await runAutoCheck()
|
||||
await scheduleNext()
|
||||
}, FIRST_CHECK_DELAY_MS)
|
||||
}
|
||||
|
||||
/** 停止调度(卸载/测试用) */
|
||||
export function stopUpdateAutoCheck(): void {
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
timer = null
|
||||
}
|
||||
started = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 偏好变更后重置调度(用户在关于页开关自动检查/改间隔后调用)。
|
||||
* 关 → 停;开 → 立即重排。
|
||||
*/
|
||||
export async function resetUpdateAutoCheck(): Promise<void> {
|
||||
stopUpdateAutoCheck()
|
||||
try {
|
||||
const prefs = await settingsApi.getUpdatePrefs()
|
||||
if (prefs.auto_check) startUpdateAutoCheck()
|
||||
} catch {
|
||||
/* 静默 */
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ export default {
|
||||
navSecurity: 'Security',
|
||||
navIntegrations: 'Integrations',
|
||||
navAdvanced: 'Advanced',
|
||||
navAbout: 'About',
|
||||
|
||||
// ===== Settings search (phase 4) =====
|
||||
searchPlaceholder: 'Search settings…',
|
||||
@@ -132,6 +133,42 @@ export default {
|
||||
sectionAppearance: '🎨 Appearance',
|
||||
sectionPerformance: '⚡ Performance',
|
||||
sectionAdvanced: '⚙️ Advanced',
|
||||
|
||||
// ===== About (B1/L0 version governance: version visibility + update entry) =====
|
||||
sectionAbout: 'ℹ️ About DevFlow',
|
||||
labelAppVersion: 'Current version',
|
||||
descAppVersion: 'DevFlow desktop app version (injected at compile time, same source as the installer)',
|
||||
labelCheckUpdate: 'Check for updates',
|
||||
descCheckUpdate: 'Fetch the latest version info from the release source; a new version prompts download & install (wired in B2)',
|
||||
checkUpdateAction: 'Check for updates',
|
||||
checkUpdateChecking: 'Checking…',
|
||||
checkUpdateNotReady: 'Check-for-updates is coming soon',
|
||||
checkUpdateFound: 'New version v{version} available',
|
||||
checkUpdateLatest: 'Already up to date (v{version})',
|
||||
checkUpdateFailed: 'Check failed: {error}',
|
||||
labelAutoCheckUpdate: 'Auto check for updates',
|
||||
descAutoCheckUpdate: 'Automatically check for new versions on startup (notify only, never auto-install)',
|
||||
labelCheckInterval: 'Check interval',
|
||||
descCheckInterval: 'Interval between automatic checks',
|
||||
interval5m: '5 minutes',
|
||||
interval15m: '15 minutes',
|
||||
interval30m: '30 minutes',
|
||||
interval60m: '60 minutes',
|
||||
labelLastCheck: 'Last checked',
|
||||
lastCheckNever: 'Never checked',
|
||||
updateAvailableTitle: '🎉 New version v{version} available',
|
||||
updateBadgeTitle: 'New version available, click to view',
|
||||
updateBadgeText: 'New v{version}',
|
||||
updateLater: 'Later',
|
||||
updateNow: 'Update now',
|
||||
updateInstalling: 'Updating…',
|
||||
updateInstallingStage: 'Installing, the app will restart shortly…',
|
||||
updateDownloading: 'Downloading {percent}% ({done} / {total} MB)',
|
||||
updateDownloadingUnknown: 'Downloading…',
|
||||
updateInstallFailed: 'Update failed: {error}',
|
||||
updateNotReady: 'Download & install is coming soon',
|
||||
labelBuildTime: 'Build time',
|
||||
labelGitHash: 'Git commit',
|
||||
labelTheme: 'Theme',
|
||||
descTheme: 'Dark / light',
|
||||
themeDark: '🌙 Dark',
|
||||
@@ -267,6 +304,7 @@ export default {
|
||||
labelFeishuWhitelist: 'open_id whitelist',
|
||||
descFeishuWhitelist: 'Comma-separated open_id list; leave empty to allow all users',
|
||||
phFeishuWhitelist: 'ou_xxx, ou_yyy',
|
||||
hintFeishuWhitelist: 'How to get an open_id: after enabling, have the member send any message to the bot; the bot replies with their open_id. Paste it above to allow them (leave empty to allow everyone).',
|
||||
feishuSave: 'Save',
|
||||
feishuSaveOk: 'Feishu integration settings saved',
|
||||
// Submit guard: when enabled=true, either of id/secret empty blocks the write
|
||||
|
||||
@@ -11,6 +11,7 @@ export default {
|
||||
navSecurity: '安全',
|
||||
navIntegrations: '集成',
|
||||
navAdvanced: '高级',
|
||||
navAbout: '关于',
|
||||
|
||||
// ===== 设置搜索(阶段4) =====
|
||||
searchPlaceholder: '搜索设置项…',
|
||||
@@ -131,6 +132,42 @@ export default {
|
||||
sectionAppearance: '🎨 外观',
|
||||
sectionPerformance: '⚡ 性能',
|
||||
sectionAdvanced: '⚙️ 高级',
|
||||
|
||||
// ===== 关于(B1/L0 版本治理:版本可见 + 更新入口) =====
|
||||
sectionAbout: 'ℹ️ 关于 DevFlow',
|
||||
labelAppVersion: '当前版本',
|
||||
descAppVersion: 'DevFlow 桌面应用版本号(编译期注入,与安装包同源)',
|
||||
labelCheckUpdate: '检查更新',
|
||||
descCheckUpdate: '从发布源拉取最新版本信息;有新版会提示下载安装(B2 接入)',
|
||||
checkUpdateAction: '检查更新',
|
||||
checkUpdateChecking: '检查中…',
|
||||
checkUpdateNotReady: '检查更新功能即将上线',
|
||||
checkUpdateFound: '发现新版本 v{version}',
|
||||
checkUpdateLatest: '已是最新版本(v{version})',
|
||||
checkUpdateFailed: '检查失败:{error}',
|
||||
labelAutoCheckUpdate: '自动检查更新',
|
||||
descAutoCheckUpdate: '启动后自动检查是否有新版本(仅提示,不自动安装)',
|
||||
labelCheckInterval: '检查间隔',
|
||||
descCheckInterval: '自动检查的时间间隔',
|
||||
interval5m: '5 分钟',
|
||||
interval15m: '15 分钟',
|
||||
interval30m: '30 分钟',
|
||||
interval60m: '60 分钟',
|
||||
labelLastCheck: '上次检查',
|
||||
lastCheckNever: '从未检查',
|
||||
updateAvailableTitle: '🎉 发现新版本 v{version}',
|
||||
updateBadgeTitle: '发现新版本,点击查看',
|
||||
updateBadgeText: '新版本 v{version}',
|
||||
updateLater: '稍后',
|
||||
updateNow: '立即升级',
|
||||
updateInstalling: '升级中…',
|
||||
updateInstallingStage: '正在安装,应用即将重启…',
|
||||
updateDownloading: '下载中 {percent}%({done} / {total} MB)',
|
||||
updateDownloadingUnknown: '下载中…',
|
||||
updateInstallFailed: '升级失败:{error}',
|
||||
updateNotReady: '下载安装功能即将上线',
|
||||
labelBuildTime: '构建时间',
|
||||
labelGitHash: 'Git 提交',
|
||||
labelTheme: '主题',
|
||||
descTheme: '深色 / 浅色模式',
|
||||
themeDark: '🌙 深色模式',
|
||||
@@ -266,6 +303,7 @@ export default {
|
||||
labelFeishuWhitelist: 'open_id 白名单',
|
||||
descFeishuWhitelist: '逗号分隔的 open_id,留空表示放行所有用户',
|
||||
phFeishuWhitelist: 'ou_xxx, ou_yyy',
|
||||
hintFeishuWhitelist: '如何获取 open_id:启用集成后,让成员向机器人发任意消息,机器人会回复其 open_id;复制填入上方即可(留空则放行所有人)。',
|
||||
feishuSave: '保存',
|
||||
feishuSaveOk: '飞书集成配置已保存',
|
||||
// 提交校验:enabled=true 时 id/secret 任一为空都拦在写库前
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
export { useProjectStore } from './project'
|
||||
|
||||
export { useKnowledgeStore } from './knowledge'
|
||||
|
||||
|
||||
export { useAppSettingsStore } from './appSettings'
|
||||
|
||||
export { useAiStore } from './ai'
|
||||
|
||||
@@ -87,6 +87,9 @@
|
||||
@toast="showToast"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 关于:版本号展示 + 更新检查入口(B1/L0 版本治理) -->
|
||||
<AboutPanel v-else-if="activeCategory === 'about'" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -110,6 +113,7 @@ import AllowedDirsPanel from '@/components/settings/AllowedDirsPanel.vue'
|
||||
import FeishuPanel from '@/components/settings/FeishuPanel.vue'
|
||||
import UclawPanel from '@/components/settings/UclawPanel.vue'
|
||||
import UnattendedPanel from '@/components/settings/UnattendedPanel.vue'
|
||||
import AboutPanel from '@/components/settings/AboutPanel.vue'
|
||||
// SettingsImportExport 已移除(价值低,占用 UI 空间;跨设备同步建议直接复制 SQLite)
|
||||
import { useAppSettingsStore } from '@/stores/appSettings'
|
||||
import { aiApi } from '@/api'
|
||||
|
||||
+12
-1
@@ -1,7 +1,18 @@
|
||||
# DevFlow 自宿主换装脚本 v3
|
||||
# 流程: 等旧进程退出并探测句柄释放, 静默安装, 按构建产物哈希校验且失败重试一次, 交互用户可见重启, 结束自清理
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$installer = 'E:\wk-lab\devflow\target\release\bundle\nsis\devflow_0.1.0_x64-setup.exe'
|
||||
# 安装包路径:按构建产物 glob 探测(不硬编码版本号,版本升级时脚本零改动)。
|
||||
# C3 决议:脚本消费构建产物,版本号唯一源头是 Cargo.toml → tauri.conf.json 注入文件名。
|
||||
$nsisDir = 'E:\wk-lab\devflow\target\release\bundle\nsis'
|
||||
$installer = $null
|
||||
if (Test-Path $nsisDir) {
|
||||
$installer = Get-ChildItem -Path $nsisDir -Filter 'devflow_*_x64-setup.exe' -ErrorAction SilentlyContinue |
|
||||
Sort-Object LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty FullName
|
||||
}
|
||||
if (-not $installer) {
|
||||
# 兜底:目录缺失或无匹配产物时,保留旧硬编码路径以维持既有失败提示语义
|
||||
$installer = 'E:\wk-lab\devflow\target\release\bundle\nsis\devflow_0.1.1_x64-setup.exe'
|
||||
}
|
||||
$target = 'C:\Users\23780\AppData\Local\devflow\devflow.exe'
|
||||
$builtExe = 'E:\wk-lab\devflow\target\release\devflow.exe'
|
||||
$log = 'E:\wk-lab\devflow\swap-install.log'
|
||||
|
||||
Reference in New Issue
Block a user