修改更新模式

master
cysamurai 1 month ago
parent 0330639928
commit 44af8a34ab

@ -1,7 +1,7 @@
{
"name": "call-client",
"private": true,
"version": "0.1.3",
"version": "0.1.4",
"type": "module",
"scripts": {
"dev": "vite",

@ -392,7 +392,7 @@ dependencies = [
[[package]]
name = "call-client"
version = "0.1.3"
version = "0.1.4"
dependencies = [
"chrono",
"fs2",

@ -1,6 +1,6 @@
[package]
name = "call-client"
version = "0.1.3"
version = "0.1.4"
description = "A Tauri App"
authors = ["you"]
edition = "2021"

@ -3,6 +3,7 @@ use std::{
env,
fs,
path::{Path, PathBuf},
sync::{Mutex, OnceLock},
};
use serde_json::{Map, Value};
@ -72,6 +73,15 @@ fn write_config(value: &Value) -> Result<(), String> {
fs::write(path, content).map_err(|error| format!("写入配置文件失败: {error}"))
}
static CONFIG_IO_MUTEX: OnceLock<Mutex<()>> = OnceLock::new();
fn lock_config_io() -> Result<std::sync::MutexGuard<'static, ()>, String> {
let mutex = CONFIG_IO_MUTEX.get_or_init(|| Mutex::new(()));
mutex
.lock()
.map_err(|_| "配置文件锁异常poisoned".to_string())
}
fn merge_value(target: &mut Value, patch: Value) {
match (target, patch) {
(Value::Object(target_map), Value::Object(patch_map)) => {
@ -91,6 +101,7 @@ fn merge_value(target: &mut Value, patch: Value) {
#[tauri::command]
pub fn config_get_all() -> Result<BTreeMap<String, Value>, String> {
let _guard = lock_config_io()?;
let value = read_config()?;
let Value::Object(map) = value else {
return Ok(BTreeMap::new());
@ -99,8 +110,18 @@ pub fn config_get_all() -> Result<BTreeMap<String, Value>, String> {
Ok(map.into_iter().collect())
}
/// 与 `config_merge` 相同磁盘语义,供后台线程等非 invoke 路径调用;带全局锁避免与前端合并写交错。
pub fn merge_config_disk(partial: Value) -> Result<(), String> {
let _guard = lock_config_io()?;
let mut current = read_config()?;
merge_value(&mut current, partial);
write_config(&current)?;
Ok(())
}
#[tauri::command]
pub fn config_merge(partial: Value) -> Result<BTreeMap<String, Value>, String> {
let _guard = lock_config_io()?;
let mut current = read_config()?;
merge_value(&mut current, partial);
write_config(&current)?;

@ -1,9 +1,8 @@
use serde::Serialize;
#[cfg(target_os = "linux")]
use std::process::Command;
use tauri::AppHandle;
#[cfg(target_os = "linux")]
use tauri::Manager;
use tauri::{AppHandle, Manager};
use serde_json::json;
use super::logger::app_log;
@ -214,6 +213,14 @@ pub fn check_apt_update(
_app: AppHandle,
package_name: String,
current_version: String,
) -> Result<AptUpdateCheckResult, String> {
check_apt_update_internal(package_name, current_version)
}
/// 供 `check_apt_update` 命令与启动后台线程复用。
pub(crate) fn check_apt_update_internal(
package_name: String,
current_version: String,
) -> Result<AptUpdateCheckResult, String> {
let _ = app_log(
"info".to_string(),
@ -412,6 +419,61 @@ pub fn check_apt_update(
}
}
/// 与前端 `listen('pending-call-client-update-synced')` 一致:后台 apt 检测已落盘 config 后广播。
pub const PENDING_CALL_CLIENT_UPDATE_SYNCED_EVENT: &str = "pending-call-client-update-synced";
/// 将 `check_apt_update` 结果同步到 `pending_call_client_update`(与前端约定一致)。
pub fn apply_pending_call_client_update_config(r: &AptUpdateCheckResult) -> Result<(), String> {
if r.has_update && r.source_available {
super::config::merge_config_disk(json!({
"pending_call_client_update": {
"candidateVersion": r.candidate_version
}
}))
} else if r.source_available {
super::config::merge_config_disk(json!({
"pending_call_client_update": null
}))
} else {
Ok(())
}
}
/// 启动后后台执行 apt 检测并写 config不阻塞 UI完成后 `emit_all` 便于登录页刷新角标。
pub fn spawn_background_pending_call_client_update_sync(app: AppHandle) {
std::thread::spawn(move || {
let version = app.package_info().version.to_string();
match check_apt_update_internal("call-client".to_string(), version) {
Ok(r) => match apply_pending_call_client_update_config(&r) {
Ok(()) => {
let _ = app_log(
"info".to_string(),
"[update-badge] 后台 apt 检查已完成并写入 config如需".to_string(),
);
let payload = json!({
"hasUpdate": r.has_update,
"sourceAvailable": r.source_available,
"candidateVersion": r.candidate_version,
});
let _ = app.emit_all(PENDING_CALL_CLIENT_UPDATE_SYNCED_EVENT, payload);
}
Err(e) => {
let _ = app_log(
"error".to_string(),
format!("[update-badge] 后台同步 pending 配置失败: {e}"),
);
}
},
Err(msg) => {
let _ = app_log(
"debug".to_string(),
format!("[update-badge] 后台 apt 检查未执行: {msg}"),
);
}
}
});
}
/// 首次配置:写入紫云 apt 源并执行 `apt-get update`(需图形环境 `pkexec` 授权)。
#[tauri::command]
pub fn setup_zyyun_apt_source(app: AppHandle, deb_line: String) -> Result<(), String> {

@ -12,7 +12,10 @@ use commands::{
logger::{app_log, get_log_paths},
session::{session_clear, session_get, session_set},
sync::{cleanup_screen_sync, start_screen_sync, stop_screen_sync},
update::{check_apt_update, setup_zyyun_apt_source, upgrade_call_client_via_apt},
update::{
check_apt_update, setup_zyyun_apt_source,
spawn_background_pending_call_client_update_sync, upgrade_call_client_via_apt,
},
window::{
close_taxer_info_window, close_ticket_window, ensure_main_window, focus_window,
open_login_window, open_main_window, open_taxer_info_window, open_ticket_window, quit_app,
@ -107,6 +110,7 @@ pub fn run() {
}
ensure_main_window(app.handle())?;
spawn_background_pending_call_client_update_sync(app.handle());
Ok(())
})
.invoke_handler(tauri::generate_handler![

@ -2,7 +2,7 @@
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"package": {
"productName": "call-client",
"version": "0.1.3"
"version": "0.1.4"
},
"build": {
"beforeDevCommand": "npm run dev",

@ -63,4 +63,12 @@ export interface TaxerTicketContextPayload {
export type AppConfig = Record<string, JsonValue>;
/**
* `config.json` `pending_call_client_update`
*
*/
export interface PendingCallClientUpdateStored {
candidateVersion: string;
}
export type LogLevel = "debug" | "info" | "warn" | "error";

@ -1,5 +1,11 @@
<script setup lang="ts">
import { Close, Lock, Minus, User } from "@element-plus/icons-vue";
import {
Close,
Lock,
Minus,
User,
WarningFilled,
} from "@element-plus/icons-vue";
import { ElDialog, ElForm, ElMessage, ElProgress } from "element-plus";
// host/dialog message ElMessage.error
import { getVersion } from "@tauri-apps/api/app";
@ -7,11 +13,16 @@ import { listen } from "@tauri-apps/api/event";
import { invoke } from "@tauri-apps/api/tauri";
import { computed, onMounted, onUnmounted, reactive, ref } from "vue";
import { api } from "../api";
import { confirmNative, showErrorNative, showInfoNative, showWarningNative } from "../host/dialog";
import { getAllConfig, mergeConfig } from "../host/config";
import {
confirmNative,
showErrorNative,
showInfoNative,
showWarningNative,
} from "../host/dialog";
import { log } from "../host/logger";
import { setSession } from "../host/session";
import type { SessionState } from "../host/types";
import type { AppConfig, JsonValue, SessionState } from "../host/types";
import {
closeWindow,
minimizeWindow,
@ -19,6 +30,13 @@ import {
startWindowDragging,
} from "../host/window";
/** `config.json` 中持久化「有待升级的 call-client 候选版本」 */
const PENDING_CALL_CLIENT_UPDATE_KEY = "pending_call_client_update";
/** 与 Rust `commands::update::PENDING_CALL_CLIENT_UPDATE_SYNCED_EVENT` 一致 */
const PENDING_CALL_CLIENT_UPDATE_SYNCED_EVENT =
"pending-call-client-update-synced";
/** el-form 需要稳定引用,勿使用每次渲染新建的 `{ username, password }` 对象 */
const loginForm = reactive({
username: "admin",
@ -32,6 +50,9 @@ const selectedWin = ref("");
const cachedWinKey = ref("");
const options = ref<Array<{ label: string; value: string }>>([]);
const appVersion = ref("0.1.0");
/** 与 config / apt 检查结果同步:有可升级的候选版本时为 true仅 UI 提示) */
const newVersionBadgeVisible = ref(false);
const newVersionCandidateText = ref("");
const checkingUpdate = ref(false);
const ZYYUN_APT_SETUP_PROGRESS_EVENT = "zyyun-apt-setup-progress";
@ -39,7 +60,9 @@ const aptSetupDialogVisible = ref(false);
const aptSetupPercent = ref(0);
const aptSetupHint = ref("");
const aptSetupFinished = ref(false);
const aptSetupProgressStatus = ref<"success" | "exception" | undefined>(undefined);
const aptSetupProgressStatus = ref<"success" | "exception" | undefined>(
undefined,
);
/** 默认紫云 apt 源行;可通过配置项 apt_source_deb_line 覆盖(见 ~/.config/com.ziyun.callclient/config.json */
const DEFAULT_APT_SOURCE_DEB_LINE =
@ -61,6 +84,89 @@ interface AptSetupProgressPayload {
message: string;
}
interface PendingCallClientUpdateSyncedPayload {
hasUpdate?: boolean;
sourceAvailable?: boolean;
candidateVersion?: string;
}
/** Rust 后台 apt 检测落盘 config 后广播,用于刷新角标(与页面生命周期无关) */
function applyBadgeFromPendingSyncEvent(
p: PendingCallClientUpdateSyncedPayload | null | undefined,
): void {
if (p == null || p.sourceAvailable !== true) {
return;
}
if (p.hasUpdate === true) {
const v =
typeof p.candidateVersion === "string" ? p.candidateVersion.trim() : "";
if (v) {
newVersionBadgeVisible.value = true;
newVersionCandidateText.value = v;
return;
}
}
clearNewVersionBadge();
}
const unlistenPendingUpdateSynced = ref<(() => void) | undefined>(undefined);
function syncNewVersionBadgeFromResult(r: AptUpdateCheckResult): void {
const show = Boolean(r.hasUpdate && r.sourceAvailable);
newVersionBadgeVisible.value = show;
newVersionCandidateText.value = show ? r.candidateVersion : "";
}
function clearNewVersionBadge(): void {
newVersionBadgeVisible.value = false;
newVersionCandidateText.value = "";
}
/** 与 apt 结果同步角标,并在「源可用」时写入或清除 config 中的待升级记录 */
async function syncBadgeAndPersistPendingFromResult(
r: AptUpdateCheckResult,
): Promise<void> {
syncNewVersionBadgeFromResult(r);
if (r.hasUpdate && r.sourceAvailable) {
await persistPendingUpdateToConfig(r.candidateVersion);
} else if (r.sourceAvailable) {
await clearPendingUpdateInConfig();
}
}
function readPendingCandidateFromConfig(config: AppConfig): string | null {
const raw = config[PENDING_CALL_CLIENT_UPDATE_KEY];
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
const c = (raw as Record<string, JsonValue>).candidateVersion;
if (typeof c === "string" && c.trim() !== "") {
return c.trim();
}
}
return null;
}
function applyPendingUpdateBadgeFromConfig(config: AppConfig): void {
const candidate = readPendingCandidateFromConfig(config);
if (candidate) {
newVersionBadgeVisible.value = true;
newVersionCandidateText.value = candidate;
}
}
async function persistPendingUpdateToConfig(
candidateVersion: string,
): Promise<void> {
await mergeConfig({
[PENDING_CALL_CLIENT_UPDATE_KEY]: { candidateVersion },
});
}
async function clearPendingUpdateInConfig(): Promise<void> {
await mergeConfig({
[PENDING_CALL_CLIENT_UPDATE_KEY]: null,
});
}
let sessionState: SessionState = {
empUid: null,
winUid: null,
@ -137,7 +243,10 @@ async function handleLogin(): Promise<void> {
"warn",
`登录失败: 接口未返回有效 queueToken, user=${loginForm.username}`,
);
await showErrorNative("登录失败:服务器未返回有效凭据,请稍后重试。", "登录");
await showErrorNative(
"登录失败:服务器未返回有效凭据,请稍后重试。",
"登录",
);
return;
}
@ -235,7 +344,9 @@ async function handleWindowLogin(): Promise<void> {
const resolvedWinUid =
Number.isFinite(callerWin) && callerWin > 0 ? callerWin : winUid;
const callerEmp = Number(callerInit.empUid);
const resolvedEmpUid = Number.isFinite(callerEmp) ? callerEmp : Number(sessionState.empUid ?? -1);
const resolvedEmpUid = Number.isFinite(callerEmp)
? callerEmp
: Number(sessionState.empUid ?? -1);
sessionState = {
...sessionState,
@ -331,22 +442,32 @@ function handleInputFocus(event: Event): void {
/**
* 自动写入紫云 apt 源并 apt-get updateRust 通过事件推送进度
*/
async function runZyyunAptSourceSetupWithProgress(debLine: string): Promise<void> {
async function runZyyunAptSourceSetupWithProgress(
debLine: string,
): Promise<void> {
aptSetupDialogVisible.value = true;
aptSetupFinished.value = false;
aptSetupPercent.value = 0;
aptSetupHint.value = "准备中…";
aptSetupProgressStatus.value = undefined;
const unlisten = await listen<AptSetupProgressPayload>(ZYYUN_APT_SETUP_PROGRESS_EVENT, (e) => {
const p = e.payload;
aptSetupPercent.value = Math.min(100, Math.max(0, Number(p.percent) || 0));
aptSetupHint.value = typeof p.message === "string" ? p.message : "";
});
const unlisten = await listen<AptSetupProgressPayload>(
ZYYUN_APT_SETUP_PROGRESS_EVENT,
(e) => {
const p = e.payload;
aptSetupPercent.value = Math.min(
100,
Math.max(0, Number(p.percent) || 0),
);
aptSetupHint.value = typeof p.message === "string" ? p.message : "";
},
);
const trimmed = debLine.trim();
const debPreview =
trimmed.length > 220 ? `${trimmed.slice(0, 220)}…(共 ${trimmed.length} 字符)` : trimmed;
trimmed.length > 220
? `${trimmed.slice(0, 220)}…(共 ${trimmed.length} 字符)`
: trimmed;
await log(
"info",
`[apt-setup][前端] 即将 invoke setup_zyyun_apt_source, debLen=${trimmed.length}, debPreview=${debPreview}`,
@ -355,11 +476,17 @@ async function runZyyunAptSourceSetupWithProgress(debLine: string): Promise<void
try {
await invoke("setup_zyyun_apt_source", { debLine: trimmed });
aptSetupProgressStatus.value = "success";
await log("info", "[apt-setup][前端] setup_zyyun_apt_source invoke 成功结束");
await log(
"info",
"[apt-setup][前端] setup_zyyun_apt_source invoke 成功结束",
);
} catch (error) {
aptSetupProgressStatus.value = "exception";
const message = error instanceof Error ? error.message : String(error);
await log("error", `[apt-setup][前端] setup_zyyun_apt_source invoke 失败: ${message}`);
await log(
"error",
`[apt-setup][前端] setup_zyyun_apt_source invoke 失败: ${message}`,
);
throw error;
} finally {
unlisten();
@ -368,9 +495,9 @@ async function runZyyunAptSourceSetupWithProgress(debLine: string): Promise<void
}
/**
* 检查 apt 仓库中是否有新版本配源与升级均在应用内由固定 Rust 脚本 + pkexec 完成不向用户暴露可手写的 shell 命令
* 点击有新版本角标配源确认并执行 apt 升级检查更新流程
*/
async function handleCheckUpdate(): Promise<void> {
async function handleUpgradeBadgeClick(): Promise<void> {
if (checkingUpdate.value) {
return;
}
@ -405,8 +532,9 @@ async function handleCheckUpdate(): Promise<void> {
);
await showWarningNative(
"本机已自动配置过紫云软件源,但 apt 仍无法解析候选版本。请联系管理员检查内网仓库、网络或密钥文件;勿在终端自行执行来源不明的更新脚本。",
"检查更新",
"升级",
);
await syncBadgeAndPersistPendingFromResult(result);
return;
}
@ -421,17 +549,21 @@ async function handleCheckUpdate(): Promise<void> {
await log("info", "检查更新(前端): 用户在「配置更新源」对话框选择取消");
await showInfoNative(
[
"未执行自动配置。稍后可再次点击「检查更新」,在提示中选择「开始配置」并输入管理员密码,",
"未执行自动配置。稍后可再次点击登录页上的「有新版本」提示,在对话框中选择「开始配置」并输入管理员密码,",
"由本程序自动完成软件源与索引更新。",
"",
"请勿在终端自行编写或执行未经验证的更新命令;若仍无法使用,请联系管理员。",
].join("\n"),
"更新源未配置",
);
await syncBadgeAndPersistPendingFromResult(result);
return;
}
await log("info", "检查更新(前端): 用户确认自动配置 apt 源,开始 runZyyunAptSourceSetupWithProgress");
await log(
"info",
"检查更新(前端): 用户确认自动配置 apt 源,开始 runZyyunAptSourceSetupWithProgress",
);
await runZyyunAptSourceSetupWithProgress(aptSourceDebLine.value);
await mergeConfig({ zyyun_apt_source_configured: true });
await log("info", "已写入配置: zyyun_apt_source_configured=true");
@ -449,17 +581,23 @@ async function handleCheckUpdate(): Promise<void> {
"自动配置已完成,但 apt 仍无法解析候选版本。",
"请联系管理员检查内网 apt 仓库、GPG 与网络;请勿在终端自行执行未经验证的安装脚本。",
"",
"您可稍后再次点击「检查更新」重试。",
"您可稍后再次点击「有新版本」重试。",
].join("\n"),
"更新源不可用",
{ logActions: "file" },
);
await syncBadgeAndPersistPendingFromResult(result);
return;
}
}
await syncBadgeAndPersistPendingFromResult(result);
if (!result.hasUpdate) {
await log("info", `检查更新提示: 当前已是最新版本, version=${result.installedVersion}`);
await log(
"info",
`检查更新提示: 当前已是最新版本, version=${result.installedVersion}`,
);
showMessage("success", `当前已是最新版本(${result.installedVersion}`);
return;
}
@ -476,6 +614,9 @@ async function handleCheckUpdate(): Promise<void> {
"",
"升级将由本程序在授权后固定执行(更新索引并仅升级 call-client需在弹出框中输入管理员密码。",
"请勿在终端自行编写或执行更新脚本。",
"",
"说明:升级完成后 apt 会更新磁盘上的程序,但当前进程仍是旧版本。",
"请在本提示升级流程结束后,完全退出并重新打开本客户端,新版本才会生效。",
].join("\n"),
okLabel: "应用内升级",
cancelLabel: "稍后再说",
@ -496,13 +637,23 @@ async function handleCheckUpdate(): Promise<void> {
} catch {
//
}
showMessage(
"success",
`升级已执行。界面显示版本:${appVersion.value}。若仍为旧版本,请重启本客户端后再试。`,
await showInfoNative(
[
"应用内升级命令已执行完成。",
"",
"apt 已更新本机安装的 call-client 安装包,但当前正在运行的客户端进程仍为旧版本,无法在不退出进程的情况下自动切换为新程序。",
"",
"请现在完全退出本客户端(关闭所有窗口,必要时从托盘退出),然后重新打开,新版本才会生效。",
"",
`界面当前显示的版本号为:${appVersion.value}(来自当前进程,在重启前可能仍显示旧版本,属正常现象。)`,
].join("\n"),
"升级完成",
);
await clearPendingUpdateInConfig();
clearNewVersionBadge();
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
await showErrorNative(`检查更新失败:${message}`, "检查更新", {
await showErrorNative(`升级失败:${message}`, "升级", {
logActions: "file",
});
await log("error", `检查更新失败: ${message}`);
@ -536,10 +687,19 @@ onMounted(async () => {
selectedWin.value = config.selected_win_key;
}
window.addEventListener("keydown", handleKeyPress);
applyPendingUpdateBadgeFromConfig(config);
unlistenPendingUpdateSynced.value = await listen<PendingCallClientUpdateSyncedPayload>(
PENDING_CALL_CLIENT_UPDATE_SYNCED_EVENT,
(event) => {
applyBadgeFromPendingSyncEvent(event.payload);
},
);
});
onUnmounted(() => {
window.removeEventListener("keydown", handleKeyPress);
unlistenPendingUpdateSynced.value?.();
unlistenPendingUpdateSynced.value = undefined;
});
</script>
@ -700,16 +860,25 @@ onUnmounted(() => {
<div class="version-info">
<div class="version-row">
<div class="version">版本号V{{ appVersion }}</div>
<el-button
text
type="primary"
size="small"
:loading="checkingUpdate"
@click="handleCheckUpdate"
>
检查更新
</el-button>
<div class="version">
<span class="version-number-label">版本号V{{ appVersion }}</span>
<span
v-if="newVersionBadgeVisible"
role="button"
tabindex="0"
class="version-update-badge version-update-badge-red version-update-badge-clickable"
:title="`可升级至 ${newVersionCandidateText},点击执行升级`"
:aria-busy="checkingUpdate"
@click="handleUpgradeBadgeClick"
@keydown.enter.prevent="handleUpgradeBadgeClick"
@keydown.space.prevent="handleUpgradeBadgeClick"
>
<el-icon class="version-update-icon">
<WarningFilled />
</el-icon>
<span class="version-update-text">有新版本</span>
</span>
</div>
</div>
<div class="copyright">© 2023 紫云科技 版权所有</div>
</div>
@ -717,23 +886,23 @@ onUnmounted(() => {
</div>
</div>
<el-dialog
v-model="aptSetupDialogVisible"
title="正在配置更新源"
width="440px"
class="apt-setup-dialog"
:close-on-click-modal="false"
:close-on-press-escape="aptSetupFinished"
:show-close="aptSetupFinished"
>
<el-progress
:percentage="aptSetupPercent"
:status="aptSetupProgressStatus"
:stroke-width="16"
/>
<p class="apt-setup-hint">{{ aptSetupHint }}</p>
</el-dialog>
</div>
<el-dialog
v-model="aptSetupDialogVisible"
title="正在配置更新源"
width="440px"
class="apt-setup-dialog"
:close-on-click-modal="false"
:close-on-press-escape="aptSetupFinished"
:show-close="aptSetupFinished"
>
<el-progress
:percentage="aptSetupPercent"
:status="aptSetupProgressStatus"
:stroke-width="16"
/>
<p class="apt-setup-hint">{{ aptSetupHint }}</p>
</el-dialog>
</div>
</template>
<style lang="scss" scoped>
@ -912,6 +1081,7 @@ onUnmounted(() => {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 6px;
}
@ -919,6 +1089,52 @@ onUnmounted(() => {
margin-bottom: 0;
}
.version-number-label {
color: inherit;
}
.version-update-badge {
display: inline-flex;
align-items: center;
gap: 3px;
margin-left: 6px;
vertical-align: middle;
}
.version-update-badge-red {
color: #f56c6c;
}
.version-update-badge-clickable {
cursor: pointer;
border-radius: 4px;
padding: 2px 4px;
&:hover {
background: rgba(245, 108, 108, 0.12);
}
&:focus-visible {
outline: 2px solid rgba(245, 108, 108, 0.55);
outline-offset: 2px;
}
&[aria-busy="true"] {
cursor: wait;
pointer-events: none;
}
}
.version-update-icon {
font-size: 15px;
}
.version-update-text {
font-size: 12px;
font-weight: 600;
letter-spacing: 0.02em;
}
.copyright {
margin-top: 5px;
}

Loading…
Cancel
Save