Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 5 additions & 20 deletions apps/config/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -528,26 +528,11 @@ async fn verhub_upload_log(content: String) -> Result<(), String> {
.map_err(|e| e.to_string())
}

/// 最新日志文件的末尾若干行
/// 核心最近一次运行的日志:从该次运行的 `[START]` 起至今,压到上报预算以内
#[tauri::command]
async fn recent_log_tail(lines: usize) -> String {
blocking(move || {
let dir = log_dir();
let latest = std::fs::read_dir(&dir)
.ok()
.into_iter()
.flatten()
.flatten()
.filter(|e| e.path().is_file())
.max_by_key(|e| e.metadata().and_then(|m| m.modified()).ok());
let Some(entry) = latest else {
return String::new();
};
let content = std::fs::read_to_string(entry.path()).unwrap_or_default();
let tail: Vec<&str> = content.lines().rev().take(lines).collect();
tail.into_iter().rev().collect::<Vec<_>>().join("\n")
})
.await
async fn current_session_log() -> String {
blocking(move || bosskey_core::logging::latest_session(&log_dir(), verhub::LOG_EXCERPT_MAX))
.await
}

/// 系统版本描述,形如 `Microsoft Windows [版本 10.0.26200.1234]`。
Expand Down Expand Up @@ -613,7 +598,7 @@ pub fn run() {
verhub_feedback_options,
verhub_submit_feedback,
verhub_upload_log,
recent_log_tail,
current_session_log,
])
// 兜底:前端起不来时,5 秒后强制显示窗口。
.setup(|app| {
Expand Down
7 changes: 6 additions & 1 deletion apps/config/src-tauri/src/verhub.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ pub const PLATFORM: Platform = Platform::Windows;

const TIMEOUT: Duration = Duration::from_secs(10);
const LOG_CONTENT_MAX: usize = 4096;
/// 上报正文里留给日志摘录的预算,其余部分留给错误描述与详情。
pub const LOG_EXCERPT_MAX: usize = LOG_CONTENT_MAX * 3 / 5;

type Result<T> = verhub_sdk::Result<T>;

Expand Down Expand Up @@ -356,7 +358,10 @@ mod tests {

#[test]
fn normalize_contact_treats_blank_as_absent() {
assert_eq!(normalize_contact(" ivan@o5g.top "), Some("ivan@o5g.top".into()));
assert_eq!(
normalize_contact(" ivan@o5g.top "),
Some("ivan@o5g.top".into())
);
assert_eq!(normalize_contact(""), None);
assert_eq!(normalize_contact(" \t\n "), None);
}
Expand Down
1 change: 1 addition & 0 deletions apps/config/ui/src/components/AboutPanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@
<div
class="stars"
role="radiogroup"
tabindex="-1"
aria-label={t("about.ratingAria")}
onpointerleave={() => (hoverRating = 0)}
>
Expand Down
5 changes: 2 additions & 3 deletions apps/config/ui/src/components/ErrorReportModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import IconTriangleAlert from "~icons/lucide/triangle-alert";
import Modal from "./Modal.svelte";
import { app, toast } from "../lib/state.svelte.js";
import { recentLogTail, uploadLog } from "../lib/verhub.js";
import { currentSessionLog, uploadLog } from "../lib/verhub.js";
import { t } from "../lib/i18n.svelte.js";

const report = $derived(app.errorReport);
Expand All @@ -12,12 +12,11 @@
let sending = $state(false);
let sent = $state(false);

// 弹框出现时取本地日志尾部供用户过目。
$effect(() => {
if (!report) return;
sent = false;
logTail = "";
recentLogTail(60)
currentSessionLog()
.then((t) => (logTail = t))
.catch(() => (logTail = ""));
});
Expand Down
23 changes: 23 additions & 0 deletions apps/config/ui/src/components/OptionsPanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@

const s = $derived(app.config.setting);

// 日志输出等级,由低到高;value 与核心 Setting::log_level 的取值一致。
// 文案键须为字面量,供 scripts/i18n-check.ps1 静态检查。
const LOG_LEVELS = [
{ value: "debug", label: "options.logLevel.debug" },
{ value: "info", label: "options.logLevel.info" },
{ value: "warn", label: "options.logLevel.warn" },
{ value: "error", label: "options.logLevel.error" },
];
const logDisabled = $derived(s.log_retention_days === 0);

// 自启注册方式的标注;仅在已开启自启时显示。
const autostartMethodText = $derived(
!app.autostart
Expand Down Expand Up @@ -202,6 +212,19 @@
</select>
{/snippet}
</SettingRow>
<SettingRow
label={t("options.logLevel")}
description={t("options.logLevelDesc")}
disabled={logDisabled}
>
{#snippet control()}
<select class="sel" bind:value={s.log_level} disabled={logDisabled}>
{#each LOG_LEVELS as level (level.value)}
<option value={level.value}>{t(level.label)}</option>
{/each}
</select>
{/snippet}
</SettingRow>
</section>

<section class="fcard">
Expand Down
8 changes: 6 additions & 2 deletions apps/config/ui/src/lib/ipc.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const mockConfig = {
allow_move_restore: false,
corner_fast_only: true,
log_retention_days: 7,
log_level: "warn",
autostart_admin: false,
language: "auto",
},
Expand Down Expand Up @@ -189,8 +190,11 @@ function mockInvoke(cmd, args) {
case "verhub_upload_log":
case "open_external":
return null;
case "recent_log_tail":
return "[mock] 2026-07-14 12:00:00 WARN 这是预览环境的假日志";
case "current_session_log":
return [
"[mock] 2026-07-14 12:00:00 [START] 核心启动 3.1.0(配置 schema v3.0.0.0,日志等级 warn)",
"[mock] 2026-07-14 12:00:05 [WARN] 这是预览环境的假日志",
].join("\n");
default:
return null;
}
Expand Down
6 changes: 3 additions & 3 deletions apps/config/ui/src/lib/verhub.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ export function uploadLog(content) {
return invoke("verhub_upload_log", { content });
}

/** 本地日志末尾若干行。 */
export function recentLogTail(lines = 60) {
return invoke("recent_log_tail", { lines });
/** 核心最近一次运行的日志(从该次运行的 [START] 起至今,后端已压到上报预算内)。 */
export function currentSessionLog() {
return invoke("current_session_log");
}

/** 用系统浏览器打开外链。 */
Expand Down
11 changes: 9 additions & 2 deletions apps/config/ui/src/locales/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,13 @@ export default {
"Core logs are stored per day in the logs folder inside the program directory, and older logs are cleaned up on startup. Choose “Off” to disable logging.",
"options.logOff": "Off",
"options.logDays": "{n} days",
"options.logLevel": "Log level",
"options.logLevelDesc":
"Only records entries at the selected level or above. The default is “Warning”, which keeps warnings and errors while leaving out routine hide/restore activity. Lower it temporarily to collect more detail before reporting an issue.",
"options.logLevel.debug": "Debug (most detailed)",
"options.logLevel.info": "Info",
"options.logLevel.warn": "Warning (default)",
"options.logLevel.error": "Error (least detailed)",

"options.languageCard": "Language",
"options.language": "Display language",
Expand Down Expand Up @@ -351,7 +358,7 @@ export default {

"error.title": "Something went wrong",
"error.summary":
"Expand to review what will be sent (the log may contain window titles and program paths — please check first)",
"Expand to review what will be sent (logs contain no window titles, and the user folder is replaced with %USERPROFILE%)",
"error.hint": "The log is sent only when you click report; nothing is sent automatically.",
"error.dontReport": "Don’t report",
"error.reporting": "Reporting…",
Expand All @@ -361,7 +368,7 @@ export default {
"error.reportFailed": "Could not report: {err}",
"error.payloadError": "Error: {message}",
"error.payloadDetail": "Details: {detail}",
"error.payloadLog": "Recent log:",
"error.payloadLog": "Log of the core's current run:",

"state.suspendFailed": "Could not pause hotkey monitoring: {err}",
"state.pssuspendFound": "pssuspend64.exe detected",
Expand Down
11 changes: 9 additions & 2 deletions apps/config/ui/src/locales/zh-CN.js
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,13 @@ export default {
"核心日志按天保存在程序目录的 logs 文件夹内,启动时自动清理更早的日志;选择「关闭」则不记录日志。",
"options.logOff": "关闭",
"options.logDays": "{n} 天",
"options.logLevel": "日志输出等级",
"options.logLevelDesc":
"只记录所选等级及以上的日志。默认「警告」,即仅记录警告与错误;日常隐藏、恢复等流水不写入。反馈问题前可临时调低以取得更详细的记录。",
"options.logLevel.debug": "调试(最详细)",
"options.logLevel.info": "信息",
"options.logLevel.warn": "警告(默认)",
"options.logLevel.error": "错误(最精简)",

"options.languageCard": "语言",
"options.language": "界面语言",
Expand Down Expand Up @@ -336,7 +343,7 @@ export default {
"dataNotice.gotIt": "知道了",

"error.title": "出错了",
"error.summary": "展开查看将要发送的内容(日志里可能含窗口标题与程序路径,请先检查)",
"error.summary": "展开查看将要发送的内容(日志不含窗口标题,用户目录已替换为 %USERPROFILE%)",
"error.hint": "日志仅在点击上报后发送,不会自动上报。",
"error.dontReport": "不上报",
"error.reporting": "上报中…",
Expand All @@ -346,7 +353,7 @@ export default {
"error.reportFailed": "上报失败:{err}",
"error.payloadError": "错误:{message}",
"error.payloadDetail": "详情:{detail}",
"error.payloadLog": "最近日志:",
"error.payloadLog": "核心本次运行日志:",

"state.suspendFailed": "暂停热键监控失败:{err}",
"state.pssuspendFound": "已检测到 pssuspend64.exe",
Expand Down
11 changes: 9 additions & 2 deletions apps/config/ui/src/locales/zh-TW.js
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,13 @@ export default {
"核心記錄檔會按日儲存在程式資料夾的 logs 資料夾內,啟動時自動清除更早的記錄檔;選擇「關閉」則不記錄。",
"options.logOff": "關閉",
"options.logDays": "{n} 天",
"options.logLevel": "記錄輸出等級",
"options.logLevelDesc":
"只記錄所選等級及以上的內容。預設「警告」,即僅記錄警告與錯誤;日常隱藏、還原等流水不寫入。回報問題前可暫時調低以取得更詳細的記錄。",
"options.logLevel.debug": "偵錯(最詳細)",
"options.logLevel.info": "資訊",
"options.logLevel.warn": "警告(預設)",
"options.logLevel.error": "錯誤(最精簡)",

"options.languageCard": "語言",
"options.language": "介面語言",
Expand Down Expand Up @@ -337,7 +344,7 @@ export default {
"dataNotice.gotIt": "知道了",

"error.title": "發生錯誤",
"error.summary": "展開檢視將要傳送的內容(記錄檔裡可能含視窗標題與程式路徑,請先檢查)",
"error.summary": "展開檢視將要傳送的內容(記錄檔不含視窗標題,使用者目錄已替換為 %USERPROFILE%)",
"error.hint": "記錄檔僅在點按回報後才傳送,不會自動回報。",
"error.dontReport": "不回報",
"error.reporting": "回報中…",
Expand All @@ -347,7 +354,7 @@ export default {
"error.reportFailed": "回報失敗:{err}",
"error.payloadError": "錯誤:{message}",
"error.payloadDetail": "詳細資料:{detail}",
"error.payloadLog": "最近的記錄:",
"error.payloadLog": "核心本次執行的記錄:",

"state.suspendFailed": "暫停快速鍵監聽失敗:{err}",
"state.pssuspendFound": "已偵測到 pssuspend64.exe",
Expand Down
66 changes: 66 additions & 0 deletions crates/common/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,20 @@ pub const DEFAULT_CLOSE_HOTKEY: &str = "Win+Esc";
pub const DEFAULT_AUTO_HIDE_TIME: u32 = 5;
/// 日志默认保留天数(`0` 表示关闭日志)。
pub const DEFAULT_LOG_RETENTION_DAYS: u32 = 7;
/// 日志输出等级的取值。低于所选等级的日志不写入文件。
pub const LOG_LEVEL_DEBUG: &str = "debug";
pub const LOG_LEVEL_INFO: &str = "info";
pub const LOG_LEVEL_WARN: &str = "warn";
pub const LOG_LEVEL_ERROR: &str = "error";
/// 由低到高的全部合法等级。
pub const LOG_LEVELS: [&str; 4] = [
LOG_LEVEL_DEBUG,
LOG_LEVEL_INFO,
LOG_LEVEL_WARN,
LOG_LEVEL_ERROR,
];
/// 默认输出等级:只记录警告及以上。
pub const DEFAULT_LOG_LEVEL: &str = LOG_LEVEL_WARN;
/// 连击判定窗口默认值(毫秒):两次点击间隔不超过它才算连击。
pub const DEFAULT_MULTI_CLICK_MS: u32 = 350;
pub const MIN_MULTI_CLICK_MS: u32 = 150;
Expand All @@ -36,6 +50,24 @@ fn default_auto_hide_time() -> u32 {
fn default_log_retention_days() -> u32 {
DEFAULT_LOG_RETENTION_DAYS
}
fn default_log_level() -> String {
DEFAULT_LOG_LEVEL.to_string()
}

/// 归一日志等级:忽略大小写与首尾空白,兼容 `warning`;无法识别时回落默认值。
pub fn normalize_log_level(value: &str) -> String {
let v = value.trim().to_ascii_lowercase();
let v = if v == "warning" {
LOG_LEVEL_WARN.to_string()
} else {
v
};
if LOG_LEVELS.contains(&v.as_str()) {
v
} else {
DEFAULT_LOG_LEVEL.to_string()
}
}
fn default_clicks() -> u8 {
1
}
Expand Down Expand Up @@ -356,6 +388,9 @@ pub struct Setting {
/// 日志保留天数;`0` 表示关闭日志。
#[serde(default = "default_log_retention_days")]
pub log_retention_days: u32,
/// 日志输出等级:`debug`/`info`/`warn`/`error`,低于它的日志不写入文件。
#[serde(default = "default_log_level")]
pub log_level: String,
/// 开机自启是否以管理员身份启动:`true` 注册最高权限计划任务,`false` 用普通权限。
/// 仅影响计划任务方式;注册表回退始终以普通权限运行。
#[serde(default)]
Expand Down Expand Up @@ -392,6 +427,7 @@ impl Default for Setting {
allow_move_restore: false,
corner_fast_only: true,
log_retention_days: DEFAULT_LOG_RETENTION_DAYS,
log_level: default_log_level(),
autostart_admin: false,
language: default_language(),
}
Expand All @@ -412,6 +448,7 @@ impl Setting {
self.tray_badges.normalize();
self.mouse.normalize();
self.language = crate::i18n::normalize_pref(&self.language);
self.log_level = normalize_log_level(&self.log_level);
}
}

Expand Down Expand Up @@ -784,9 +821,38 @@ mod tests {
assert!(!c.setting.freeze_whole_tree);
assert_eq!(c.setting.auto_hide_time, 5);
assert_eq!(c.setting.log_retention_days, 7, "日志保留天数默认 7");
assert_eq!(c.setting.log_level, "warn", "日志等级默认只记警告及以上");
assert!(!c.setting.autostart_admin, "自启默认普通权限");
}

#[test]
fn log_level_round_trips_and_normalizes() {
assert_eq!(Setting::default().log_level, LOG_LEVEL_WARN);

let c = Config::from_json(r#"{"setting": {"log_level": "debug"}}"#).unwrap();
assert_eq!(c.setting.log_level, LOG_LEVEL_DEBUG);
let back = Config::from_json(&c.to_json().unwrap()).unwrap();
assert_eq!(back.setting.log_level, LOG_LEVEL_DEBUG, "写回后应保留");

assert_eq!(
normalize_log_level(" INFO "),
LOG_LEVEL_INFO,
"忽略大小写与空白"
);
assert_eq!(
normalize_log_level("warning"),
LOG_LEVEL_WARN,
"兼容 warning"
);
assert_eq!(
normalize_log_level("verbose"),
DEFAULT_LOG_LEVEL,
"未知等级回落默认值"
);
let c = Config::from_json(r#"{"setting": {"log_level": "verbose"}}"#).unwrap();
assert_eq!(c.setting.log_level, DEFAULT_LOG_LEVEL);
}

#[test]
fn tray_badges_default_bindings() {
let d = TrayBadges::default();
Expand Down
Loading
Loading