修改更新检查功能,自动运行更新源的配置

master
cysamurai 1 month ago
parent e07d0caa9f
commit e8df6c17d5

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "adler2"
@ -306,13 +306,12 @@ dependencies = [
name = "broadcast-client"
version = "0.1.1"
dependencies = [
"arboard",
"chrono",
"fs2",
"serde",
"serde_json",
"tauri",
"tauri-build",
"time",
]
[[package]]
@ -472,8 +471,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-link 0.2.1",
]

@ -173,6 +173,7 @@ pub fn run() {
stop_socket_service,
get_socket_service_status,
check_apt_update,
setup_zyyun_apt_source,
debug_log,
quit_app
])
@ -462,6 +463,110 @@ fn check_apt_update(
}
}
#[cfg(target_os = "linux")]
const ZYYUN_APT_SETUP_PROGRESS_EVENT: &str = "zyyun-apt-setup-progress";
#[cfg(target_os = "linux")]
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
struct AptSetupProgressPayload {
percent: u8,
message: String,
}
#[cfg(target_os = "linux")]
fn bash_single_quoted_apt_line(content: &str) -> String {
format!("'{}'", content.replace('\'', "'\\''"))
}
#[cfg(target_os = "linux")]
fn emit_apt_setup_progress_broadcast(app: &tauri::AppHandle, percent: u8, message: impl Into<String>) {
let msg = message.into();
let _ = app.emit_all(
ZYYUN_APT_SETUP_PROGRESS_EVENT,
AptSetupProgressPayload {
percent,
message: msg.clone(),
},
);
append_socket_log(
app,
"INFO",
format!("[apt-setup] {percent}% {msg}"),
);
}
#[cfg(target_os = "linux")]
fn run_privileged_apt_setup_broadcast(deb_line: &str) -> Result<(), String> {
let quoted = bash_single_quoted_apt_line(deb_line.trim());
let inner = format!(
"set -e; install -d /etc/apt/sources.list.d; printf '%s\\n' {quoted} > /etc/apt/sources.list.d/zyyun.list; apt-get update -y"
);
let output = Command::new("pkexec")
.args(["bash", "-c", &inner])
.output()
.map_err(|e| format!("无法启动 pkexec请确认已安装 policykit-1{e}"))?;
if output.status.success() {
return Ok(());
}
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
Err(format!(
"配置更新源失败(退出码 {:?}。stderr={} stdout={}",
output.status.code(),
if stderr.is_empty() { "(空)" } else { &stderr },
if stdout.is_empty() { "(空)" } else { &stdout }
))
}
/// 首次配置:写入紫云 apt 源并执行 `apt-get update`(需 `pkexec` 授权)。
#[tauri::command]
fn setup_zyyun_apt_source(app: tauri::AppHandle, deb_line: String) -> Result<(), String> {
#[cfg(not(target_os = "linux"))]
{
let _ = (&app, deb_line);
return Err("当前系统不支持自动配置 apt 源。".to_string());
}
#[cfg(target_os = "linux")]
{
let line = deb_line.trim();
if line.is_empty() {
return Err("deb 源行不能为空".to_string());
}
emit_apt_setup_progress_broadcast(&app, 5, "准备配置紫云 apt 软件源…");
emit_apt_setup_progress_broadcast(
&app,
15,
"即将弹出权限确认,请在对话框中输入管理员密码…",
);
let result = run_privileged_apt_setup_broadcast(line);
match &result {
Ok(()) => {
emit_apt_setup_progress_broadcast(&app, 92, "正在完成索引刷新…");
emit_apt_setup_progress_broadcast(&app, 100, "配置完成,可继续检查更新。");
append_socket_log(&app, "INFO", "[apt-setup] 紫云 apt 源配置成功");
}
Err(err) => {
emit_apt_setup_progress_broadcast(&app, 100, format!("配置失败:{err}"));
append_socket_log(
&app,
"ERROR",
format!("[apt-setup] 紫云 apt 源配置失败: {err}"),
);
}
}
result
}
}
fn emit_socket_status(app: &tauri::AppHandle, running: bool) {
let _ = app.emit_all(
SOCKET_STATUS_EVENT,

@ -93,6 +93,8 @@ export interface BroadcastConfig {
segments: SegmentConfigItem[];
windowAreas: ChildWindowAreaConfig[];
subtitleAreas: SubtitleAreaConfig[];
/** 是否已在本机自动配置过紫云 apt 源(用于检查更新时跳过重复配置) */
zyyunAptSourceConfigured: boolean;
}
// 应用启动时使用的默认配置。
@ -137,4 +139,5 @@ export const DEFAULT_BROADCAST_CONFIG: BroadcastConfig = {
segments: [],
windowAreas: [],
subtitleAreas: [],
zyyunAptSourceConfigured: false,
};

@ -269,6 +269,10 @@ function normalizeConfig(raw: unknown): BroadcastConfig {
),
windowAreas: windowAreasRaw.map((item, index) => normalizeWindowArea(item, index)),
subtitleAreas: subtitleAreasRaw.map((item, index) => normalizeSubtitleArea(item, index)),
zyyunAptSourceConfigured:
typeof source.zyyunAptSourceConfigured === "boolean"
? source.zyyunAptSourceConfigured
: DEFAULT_BROADCAST_CONFIG.zyyunAptSourceConfigured,
};
}

@ -465,15 +465,32 @@
<el-button type="danger" @click="quitApplication">退</el-button>
<span class="save-hint">{{ saveMessage }}</span>
</div>
<el-dialog
v-model="aptSetupDialogVisible"
title="正在配置更新源"
width="480px"
: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>
</main>
</template>
<script setup lang="ts">
import { nextTick, onMounted, onUnmounted, reactive, ref, watch } from "vue";
import { appWindow, currentMonitor } from "@tauri-apps/api/window";
import { getVersion } from "@tauri-apps/api/app";
import { invoke } from "@tauri-apps/api/tauri";
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
import { ElMessage } from "element-plus";
import { ElMessage, ElMessageBox, ElDialog, ElProgress } from "element-plus";
import type {
BroadcastConfig,
ChildWindowAreaConfig,
@ -504,7 +521,12 @@ interface AptUpdateCheckResult {
updateCommand: string;
}
//
interface AptSetupProgressPayload {
percent: number;
message: string;
}
const ZYYUN_APT_SETUP_PROGRESS_EVENT = "zyyun-apt-setup-progress";
const screenWidth = ref(normalizeScreenWidth(window.screen.width || 1920));
const { config, patchConfig } = useBroadcastConfig();
const saveMessage = ref("修改后请点击“保存配置”。");
@ -512,6 +534,11 @@ const activeTab = ref("base");
const socketRunning = ref(false);
const checkingUpdate = ref(false);
const appVersion = ref("0.1.0");
const aptSetupDialogVisible = ref(false);
const aptSetupPercent = ref(0);
const aptSetupHint = ref("");
const aptSetupFinished = ref(false);
const aptSetupProgressStatus = ref<"success" | "exception" | undefined>(undefined);
const APT_SOURCE_ENTRY =
"deb [arch=amd64 signed-by=/usr/share/keyrings/zyyun-archive-keyring.gpg] http://80.12.140.29:80/apt v10 main";
const APT_SOURCE_SETUP_COMMAND = `echo "${APT_SOURCE_ENTRY}" | sudo tee /etc/apt/sources.list.d/zyyun.list && sudo apt update`;
@ -818,6 +845,31 @@ async function closeConfigWindow() {
}
}
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 : "";
});
try {
await invoke("setup_zyyun_apt_source", { debLine: debLine.trim() });
aptSetupProgressStatus.value = "success";
} catch (error) {
aptSetupProgressStatus.value = "exception";
throw error;
} finally {
unlisten();
aptSetupFinished.value = true;
}
}
async function handleCheckUpdate() {
if (checkingUpdate.value) {
return;
@ -829,18 +881,54 @@ async function handleCheckUpdate() {
packageName: "broadcast-client",
currentVersion: appVersion.value,
});
const result = await invoke<AptUpdateCheckResult>("check_apt_update", {
packageName: "broadcast-client",
currentVersion: appVersion.value,
});
const invokeCheck = (): Promise<AptUpdateCheckResult> =>
invoke<AptUpdateCheckResult>("check_apt_update", {
packageName: "broadcast-client",
currentVersion: appVersion.value,
});
let result = await invokeCheck();
console.info("[update] 检查更新结果", result);
if (!result.sourceAvailable) {
console.warn("[update] 未检测到可用更新源", { packageName: result.packageName });
ElMessage.warning(
`未检测到可用更新源,请先执行:${APT_SOURCE_SETUP_COMMAND}`,
);
return;
const alreadyConfigured = config.value.zyyunAptSourceConfigured === true;
if (alreadyConfigured) {
console.warn("[update] 已配置过 apt 源但候选版本仍不可用");
ElMessage.warning(
"本机已自动配置过紫云软件源,但 apt 仍无法解析候选版本。请在终端执行 sudo apt update 后重试,或检查 GPG 密钥。",
);
return;
}
try {
await ElMessageBox.confirm(
"未检测到可用更新源。是否使用管理员权限自动写入紫云软件源并执行 apt 更新?(需要桌面环境的 pkexec 授权)",
"配置更新源",
{
type: "warning",
confirmButtonText: "开始配置",
cancelButtonText: "取消",
distinguishCancelAndClose: true,
},
);
} catch {
ElMessage.info("已取消。可手动在终端执行:" + APT_SOURCE_SETUP_COMMAND);
return;
}
await runZyyunAptSourceSetupWithProgress(APT_SOURCE_ENTRY);
await patchConfig({ zyyunAptSourceConfigured: true });
result = await invokeCheck();
console.info("[update] 自动配置后再次检查", result);
if (!result.sourceAvailable) {
ElMessage.warning(
`自动配置后仍不可用。请手动检查网络与密钥,或执行:${APT_SOURCE_SETUP_COMMAND}`,
);
return;
}
}
if (!result.hasUpdate) {
@ -971,6 +1059,7 @@ async function saveConfig() {
segmentHeight,
showRuler: draft.showRuler,
autoStartSocket: draft.autoStartSocket === true,
zyyunAptSourceConfigured: draft.zyyunAptSourceConfigured === true,
deliveryTargets: {
syncScreen: selectedTargetSet.has("syncScreen"),
compositeScreen: selectedTargetSet.has("compositeScreen"),
@ -1004,6 +1093,7 @@ watch(
draft.subtitleAreas = cloneSubtitleAreas(next.subtitleAreas);
activeSubtitleAreaTab.value = draft.subtitleAreas[0]?.id ?? "";
draft.compositeScreen = cloneCompositeScreen(next.compositeScreen);
draft.zyyunAptSourceConfigured = next.zyyunAptSourceConfigured;
syncingFromStore.value = false;
},
{ immediate: true, deep: true },
@ -1048,6 +1138,12 @@ watch(
);
onMounted(async () => {
try {
appVersion.value = await getVersion();
} catch {
appVersion.value = "0.1.0";
}
try {
const monitor = await currentMonitor();
const width = monitor?.size?.width;

@ -1,3 +1,4 @@
use serde::Serialize;
use std::{
env,
fs::{self, OpenOptions},
@ -128,5 +129,23 @@ pub fn app_log(level: String, message: String) -> Result<(), String> {
"[{formatted_time}] [unix_s={ts_secs}] [unix_ms={ts_millis}] [{}] {single_line_message}",
level.to_uppercase()
)
.map_err(|error| format!("写入日志失败: {error}"))
.map_err(|error| format!("写入日志失败: {error}"))
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LogPaths {
pub log_dir: String,
pub log_file: String,
}
/// 返回与 `app_log` 相同的日志目录及当前 `app.log` 绝对路径,供前端打开文件夹或文件。
#[tauri::command]
pub fn get_log_paths() -> Result<LogPaths, String> {
let dir = get_state_dir();
let file = dir.join(LOG_FILE_NAME);
Ok(LogPaths {
log_dir: dir.to_string_lossy().to_string(),
log_file: file.to_string_lossy().to_string(),
})
}

@ -5,6 +5,63 @@ use tauri::AppHandle;
use super::logger::app_log;
/// 与前端 `listen` 事件名一致,用于 apt 源自动配置进度(仅 Linux 构建使用)。
#[cfg(target_os = "linux")]
pub const ZYYUN_APT_SETUP_PROGRESS_EVENT: &str = "zyyun-apt-setup-progress";
#[cfg(target_os = "linux")]
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct AptSetupProgressPayload {
pub percent: u8,
pub message: String,
}
#[cfg(target_os = "linux")]
fn emit_apt_setup_progress(app: &AppHandle, percent: u8, message: impl Into<String>) {
let msg = message.into();
let payload = AptSetupProgressPayload {
percent,
message: msg.clone(),
};
let _ = app.emit_all(ZYYUN_APT_SETUP_PROGRESS_EVENT, payload);
let _ = app_log(
"info".to_string(),
format!("[apt-setup] {percent}% {msg}"),
);
}
#[cfg(target_os = "linux")]
fn bash_single_quoted(content: &str) -> String {
format!("'{}'", content.replace('\'', "'\\''"))
}
#[cfg(target_os = "linux")]
fn run_privileged_apt_setup(deb_line: &str) -> Result<(), String> {
let quoted = bash_single_quoted(deb_line.trim());
let inner = format!(
"set -e; install -d /etc/apt/sources.list.d; printf '%s\\n' {quoted} > /etc/apt/sources.list.d/zyyun.list; apt-get update -y"
);
let output = Command::new("pkexec")
.args(["bash", "-c", &inner])
.output()
.map_err(|e| format!("无法启动 pkexec请确认已安装 policykit-1{e}"))?;
if output.status.success() {
return Ok(());
}
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
Err(format!(
"配置更新源失败(退出码 {:?}。stderr={} stdout={}",
output.status.code(),
if stderr.is_empty() { "(空)" } else { &stderr },
if stdout.is_empty() { "(空)" } else { &stdout }
))
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AptUpdateCheckResult {
@ -124,3 +181,50 @@ pub fn check_apt_update(
Ok(result)
}
}
/// 首次配置:写入紫云 apt 源并执行 `apt-get update`(需图形环境 `pkexec` 授权)。
#[tauri::command]
pub fn setup_zyyun_apt_source(app: AppHandle, deb_line: String) -> Result<(), String> {
#[cfg(not(target_os = "linux"))]
{
let _ = (&app, deb_line);
return Err("当前系统不支持自动配置 apt 源。".to_string());
}
#[cfg(target_os = "linux")]
{
let line = deb_line.trim();
if line.is_empty() {
return Err("deb 源行不能为空".to_string());
}
emit_apt_setup_progress(&app, 5, "准备配置紫云 apt 软件源…");
emit_apt_setup_progress(
&app,
15,
"即将弹出权限确认,请在对话框中输入管理员密码…",
);
let result = run_privileged_apt_setup(line);
match &result {
Ok(()) => {
emit_apt_setup_progress(&app, 92, "正在完成索引刷新…");
emit_apt_setup_progress(&app, 100, "配置完成,可继续检查更新。");
let _ = app_log(
"info".to_string(),
"[apt-setup] 紫云 apt 源配置成功".to_string(),
);
}
Err(err) => {
emit_apt_setup_progress(&app, 100, format!("配置失败:{err}"));
let _ = app_log(
"error".to_string(),
format!("[apt-setup] 紫云 apt 源配置失败: {err}"),
);
}
}
result
}
}

@ -9,10 +9,10 @@ use std::{
use commands::{
config::{config_get_all, config_merge},
events::{emit_to_window, list_windows},
logger::app_log,
logger::{app_log, get_log_paths},
session::{session_clear, session_get, session_set},
sync::{start_screen_sync, stop_screen_sync},
update::check_apt_update,
update::{check_apt_update, setup_zyyun_apt_source},
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,
@ -79,9 +79,11 @@ pub fn run() {
config_get_all,
config_merge,
app_log,
get_log_paths,
emit_to_window,
list_windows,
check_apt_update,
setup_zyyun_apt_source,
open_ticket_window,
close_ticket_window,
close_taxer_info_window,

@ -1,5 +1,11 @@
import { ask, message } from "@tauri-apps/api/dialog";
import type { NativeConfirmOptions } from "./types";
import { invoke } from "@tauri-apps/api/tauri";
import { open } from "@tauri-apps/api/shell";
import type {
AppLogPaths,
NativeConfirmOptions,
ShowErrorNativeOptions,
} from "./types";
/**
*
@ -16,13 +22,85 @@ export async function confirmNative(options: NativeConfirmOptions): Promise<bool
}
}
async function fetchLogPaths(): Promise<AppLogPaths> {
return await invoke<AppLogPaths>("get_log_paths");
}
const ERROR_DIALOG_MAX_LINES = 6;
/** 报错弹窗正文:最多 6 行;超过时保留前 5 行完整内容,第 6 行仅显示「...」。 */
function truncateErrorTextForDialog(text: string): string {
const normalized = text.replace(/\r\n/g, "\n");
const lines = normalized.split("\n");
if (lines.length <= ERROR_DIALOG_MAX_LINES) {
return normalized;
}
return `${lines.slice(0, ERROR_DIALOG_MAX_LINES - 1).join("\n")}\n...`;
}
/**
*
* `options.logActions === "file"` 6 6 ...
* / app.log
*/
export async function showErrorNative(content: string, title = "错误"): Promise<void> {
export async function showErrorNative(
content: string,
title = "错误",
options?: ShowErrorNativeOptions,
): Promise<void> {
const body = truncateErrorTextForDialog(content);
const logActions = options?.logActions ?? "none";
if (logActions === "none") {
try {
await message(body, { title, type: "error" });
} catch (error) {
throw new Error(`打开错误提示框失败: ${String(error)}`);
}
return;
}
let paths: AppLogPaths;
try {
await message(content, { title });
paths = await fetchLogPaths();
} catch {
try {
await message(body, { title, type: "error" });
} catch (error) {
throw new Error(`打开错误提示框失败: ${String(error)}`);
}
return;
}
try {
const openFile = await ask(body, {
title,
type: "error",
okLabel: "打开日志文件",
cancelLabel: "确定",
});
if (openFile) {
await open(paths.logFile);
}
} catch (error) {
throw new Error(`打开错误提示框失败: ${String(error)}`);
try {
const fallback = truncateErrorTextForDialog(
`${content}\n无法打开日志文件${String(error)}`,
);
await message(fallback, {
title,
type: "error",
});
} catch (inner) {
throw new Error(`打开错误提示框失败: ${String(inner)}`);
}
}
}
/**
* app.log `logActions: "file"`
*/
export async function showErrorNativeWithLog(
content: string,
title = "错误",
): Promise<void> {
return showErrorNative(content, title, { logActions: "file" });
}

@ -37,6 +37,19 @@ export interface NativeConfirmOptions {
cancelLabel?: string;
}
/** `get_log_paths` 返回结构(与后端 camelCase 一致) */
export interface AppLogPaths {
logDir: string;
logFile: string;
}
/** `showErrorNative` 第三参数:是否在原生报错中附带日志路径并允许打开 app.log */
export type ShowErrorLogActions = "none" | "file";
export interface ShowErrorNativeOptions {
logActions?: ShowErrorLogActions;
}
export interface TicketActionPayload {
action: "call" | "evaluate";
ticketUid?: number;

@ -1,11 +1,13 @@
<script setup lang="ts">
import { Close, Lock, Minus, User } from "@element-plus/icons-vue";
import { ElForm, ElMessage, ElMessageBox } from "element-plus";
import { ElDialog, ElForm, ElMessage, ElMessageBox, ElProgress } from "element-plus";
import { getVersion } from "@tauri-apps/api/app";
import { writeText } from "@tauri-apps/api/clipboard";
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 } from "../host/dialog";
import { getAllConfig, mergeConfig } from "../host/config";
import { log } from "../host/logger";
import { setSession } from "../host/session";
@ -31,9 +33,23 @@ const cachedWinKey = ref("");
const options = ref<Array<{ label: string; value: string }>>([]);
const appVersion = ref("0.1.0");
const checkingUpdate = ref(false);
const APT_SOURCE_ENTRY =
const ZYYUN_APT_SETUP_PROGRESS_EVENT = "zyyun-apt-setup-progress";
const aptSetupDialogVisible = ref(false);
const aptSetupPercent = ref(0);
const aptSetupHint = ref("");
const aptSetupFinished = ref(false);
const aptSetupProgressStatus = ref<"success" | "exception" | undefined>(undefined);
/** 默认紫云 apt 源行;可通过配置项 apt_source_deb_line 覆盖(见 ~/.config/com.ziyun.callclient/config.json */
const DEFAULT_APT_SOURCE_DEB_LINE =
"deb [arch=amd64 signed-by=/usr/share/keyrings/zyyun-archive-keyring.gpg] http://80.12.140.29:80/apt v10 main";
const APT_SOURCE_SETUP_COMMAND = `echo "${APT_SOURCE_ENTRY}" | sudo tee /etc/apt/sources.list.d/zyyun.list && sudo apt update`;
const aptSourceDebLine = ref(DEFAULT_APT_SOURCE_DEB_LINE);
function buildAptSourceSetupCommand(debLine: string): string {
const escaped = debLine.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
return `echo "${escaped}" | sudo tee /etc/apt/sources.list.d/zyyun.list && sudo apt update`;
}
interface AptUpdateCheckResult {
packageName: string;
@ -45,6 +61,11 @@ interface AptUpdateCheckResult {
updateCommand: string;
}
interface AptSetupProgressPayload {
percent: number;
message: string;
}
let sessionState: SessionState = {
empUid: null,
winUid: null,
@ -306,6 +327,34 @@ function handleInputFocus(event: Event): void {
target.select();
}
/**
* 自动写入紫云 apt 源并 apt-get updateRust 通过事件推送进度
*/
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 : "";
});
try {
await invoke("setup_zyyun_apt_source", { debLine: debLine.trim() });
aptSetupProgressStatus.value = "success";
} catch (error) {
aptSetupProgressStatus.value = "exception";
throw error;
} finally {
unlisten();
aptSetupFinished.value = true;
}
}
/**
* 检查 apt 仓库中是否有新版本并引导复制升级命令
*/
@ -317,41 +366,98 @@ async function handleCheckUpdate(): Promise<void> {
checkingUpdate.value = true;
try {
await log("info", `检查更新开始: package=call-client, currentVersion=${appVersion.value}`);
const result = await invoke<AptUpdateCheckResult>("check_apt_update", {
packageName: "call-client",
currentVersion: appVersion.value,
});
const invokeCheck = (): Promise<AptUpdateCheckResult> =>
invoke<AptUpdateCheckResult>("check_apt_update", {
packageName: "call-client",
currentVersion: appVersion.value,
});
let result = await invokeCheck();
await log(
"info",
`检查更新结果: package=${result.packageName}, installed=${result.installedVersion}, candidate=${result.candidateVersion}, hasUpdate=${result.hasUpdate}, sourceAvailable=${result.sourceAvailable}`,
);
if (!result.sourceAvailable) {
await log("warn", `检查更新提示: 未检测到可用更新源, package=${result.packageName}`);
const cfg = await getAllConfig();
const alreadyConfigured = cfg.zyyun_apt_source_configured === true;
if (alreadyConfigured) {
await log(
"warn",
"检查更新: 已记录自动配置过 apt 源,但候选版本仍不可用,请人工排查网络或 GPG 密钥",
);
showMessage(
"warning",
"本机已自动配置过紫云软件源,但 apt 仍无法解析候选版本。请在终端执行 sudo apt update 后重试,或检查 /usr/share/keyrings/zyyun-archive-keyring.gpg 是否存在。",
);
return;
}
try {
await ElMessageBox.confirm(
[
"未检测到可用更新源,请先配置固定更新地址:",
APT_SOURCE_ENTRY,
"",
"点击“复制命令”后,在终端执行:",
APT_SOURCE_SETUP_COMMAND,
].join("\n"),
"更新源未配置",
"未检测到可用更新源。是否使用管理员权限自动写入紫云软件源并执行 apt 更新?(需要桌面环境的 pkexec 授权)",
"配置更新源",
{
confirmButtonText: "复制命令",
cancelButtonText: "关闭",
distinguishCancelAndClose: true,
type: "warning",
confirmButtonText: "开始配置",
cancelButtonText: "取消",
distinguishCancelAndClose: true,
},
);
await writeText(APT_SOURCE_SETUP_COMMAND);
showMessage("success", "更新源初始化命令已复制到剪贴板");
} catch {
//
await log("info", "用户取消自动配置 apt 源");
const setupCmd = buildAptSourceSetupCommand(aptSourceDebLine.value);
const body = [
"已取消自动配置。若需手动配置,可将下面一行写入软件源,并在终端执行复制出的命令:",
aptSourceDebLine.value,
"",
"点击「复制命令」后,在终端执行:",
setupCmd,
].join("\n");
const copy = await confirmNative({
title: "更新源未配置",
message: body,
okLabel: "复制命令",
cancelLabel: "关闭",
});
if (copy) {
await writeText(setupCmd);
showMessage("success", "更新源初始化命令已复制到剪贴板");
}
return;
}
await runZyyunAptSourceSetupWithProgress(aptSourceDebLine.value);
await mergeConfig({ zyyun_apt_source_configured: true });
await log("info", "已写入配置: zyyun_apt_source_configured=true");
result = await invokeCheck();
await log(
"info",
`自动配置后再次检查: installed=${result.installedVersion}, candidate=${result.candidateVersion}, sourceAvailable=${result.sourceAvailable}`,
);
if (!result.sourceAvailable) {
await log("warn", "自动配置 apt 源后仍无法解析候选版本");
const setupCmd = buildAptSourceSetupCommand(aptSourceDebLine.value);
const body = [
"自动配置已完成,但 apt 仍无法解析候选版本。请检查网络、GPG 密钥,或在终端执行:",
setupCmd,
].join("\n");
const copy = await confirmNative({
title: "更新源仍不可用",
message: body,
okLabel: "复制命令",
cancelLabel: "关闭",
});
if (copy) {
await writeText(setupCmd);
showMessage("success", "命令已复制到剪贴板");
}
return;
}
return;
}
if (!result.hasUpdate) {
@ -364,32 +470,28 @@ async function handleCheckUpdate(): Promise<void> {
"info",
`检查更新提示: 检测到新版本, installed=${result.installedVersion}, candidate=${result.candidateVersion}`,
);
try {
await ElMessageBox.confirm(
[
`检测到新版本:${result.candidateVersion}`,
`当前版本:${result.installedVersion}`,
"",
"点击“复制命令”后,在终端执行升级:",
result.updateCommand,
].join("\n"),
"发现新版本",
{
confirmButtonText: "复制命令",
cancelButtonText: "关闭",
distinguishCancelAndClose: true,
type: "info",
},
);
const body = [
`检测到新版本:${result.candidateVersion}`,
`当前版本:${result.installedVersion}`,
"",
"点击「复制命令」后,在终端执行升级:",
result.updateCommand,
].join("\n");
const copy = await confirmNative({
title: "发现新版本",
message: body,
okLabel: "复制命令",
cancelLabel: "关闭",
});
if (copy) {
await writeText(result.updateCommand);
showMessage("success", "升级命令已复制到剪贴板");
} catch {
//
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
showMessage("error", `检查更新失败:${message}`);
await showErrorNative(`检查更新失败:${message}`, "检查更新", {
logActions: "file",
});
await log("error", `检查更新失败: ${message}`);
} finally {
checkingUpdate.value = false;
@ -404,6 +506,12 @@ onMounted(async () => {
}
const config = await getAllConfig();
if (
typeof config.apt_source_deb_line === "string" &&
config.apt_source_deb_line.trim() !== ""
) {
aptSourceDebLine.value = config.apt_source_deb_line.trim();
}
if (typeof config.last_username === "string" && config.last_username.trim()) {
loginForm.username = config.last_username;
}
@ -595,7 +703,24 @@ onUnmounted(() => {
</div>
</div>
</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>
</template>
<style lang="scss" scoped>
@ -785,6 +910,13 @@ onUnmounted(() => {
margin-top: 5px;
}
.apt-setup-hint {
margin-top: 12px;
font-size: 13px;
color: #606266;
line-height: 1.45;
}
:global(.narrow-window-message) {
min-width: 0 !important;
width: calc(100vw - 32px) !important;

@ -23,7 +23,7 @@ import { ElMessage } from "element-plus";
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
import { api } from "../api";
import { getAllConfig } from "../host/config";
import { confirmNative, showErrorNative } from "../host/dialog";
import { confirmNative, showErrorNative, showErrorNativeWithLog } from "../host/dialog";
import { emitTaxerTicketContext, listenMainAction } from "../host/events";
import { log } from "../host/logger";
import { getSession } from "../host/session";
@ -934,7 +934,7 @@ async function handleMoreCommand(
});
} catch (error) {
await logErr(`更多菜单处理失败: ${command}`, error);
await showErrorNative("操作失败,请查看日志");
await showErrorNativeWithLog("操作失败,请查看日志");
updateLog(`操作失败: ${command}`);
} finally {
if (command !== "logout" && command !== "syncMainScreen") {

@ -107,7 +107,7 @@ import { ElConfigProvider } from "element-plus";
import zhCn from "element-plus/dist/locale/zh-cn.mjs";
import { computed, onMounted, onUnmounted, ref } from "vue";
import { api } from "../api";
import { showErrorNative } from "../host/dialog";
import { showErrorNative, showErrorNativeWithLog } from "../host/dialog";
import { emitCallAction, emitEvaluateAction } from "../host/events";
import { log } from "../host/logger";
import type { LogLevel } from "../host/types";
@ -480,7 +480,7 @@ onMounted(async () => {
"error",
`TicketListView onMounted 失败: ${error instanceof Error ? error.message : String(error)}`,
);
await showErrorNative("票号列表加载失败,请查看日志");
await showErrorNativeWithLog("票号列表加载失败,请查看日志");
loading.value = false;
}
});

Loading…
Cancel
Save