From 32f2df31eee59f2a8c42011ea784c3fe6ee04565 Mon Sep 17 00:00:00 2001 From: TRIP <1933142963@qq.com> Date: Thu, 7 May 2026 10:46:53 +0800 Subject: [PATCH 1/2] Add Claude Code GitHub Workflow (#308) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * "Claude PR Assistant workflow" * "Claude Code Review workflow" * ci(claude): 收紧 review 触发:只 OWNER @claude 才跑,默认 Sonnet 4.6 - 删除 claude-code-review.yml:原本 PR opened/sync 自动触发会无差别烧 OAuth 额度 - claude.yml if 加 author_association == 'OWNER' 门禁:只有仓库 owner 评论 @claude 才触发 - 加 Pick model step:默认 claude-sonnet-4-6;评论包含 --opus / claude-opus 时切 claude-opus-4-7 - 用 env 注入 body / title 而不是直接 inline expression,避免外部内容引号注入 --------- Co-authored-by: baiqing --- .github/workflows/claude.yml | 68 ++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 .github/workflows/claude.yml diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 00000000..cc1eff5a --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,68 @@ +name: Claude Code + +# 触发策略: +# - 默认不主动 review;任何 PR 打开 / 同步都不会自动调用模型,避免烧 OAuth token 额度。 +# - 评论 / review / issue 中出现 `@claude` 才触发,且发起者必须是仓库 OWNER(appergb)。 +# - 模型默认 claude-sonnet-4-6(量大、成本低);评论里写 `--opus` 或包含 `claude-opus` +# 字样会切到 claude-opus-4-7(用于需要更深推理的任务)。 + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + +jobs: + claude: + # 双重门禁:(1) 内容含 @claude (2) 触发者是 OWNER。 + # author_association == 'OWNER' 是 GitHub 内建字段,免一次 API 调用。 + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude') && github.event.comment.author_association == 'OWNER') || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude') && github.event.comment.author_association == 'OWNER') || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude') && github.event.review.author_association == 'OWNER') || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')) && github.event.issue.author_association == 'OWNER') + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + actions: read # Required for Claude to read CI results on PRs + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Pick model (default sonnet, opt-in opus) + id: pick_model + env: + COMMENT_BODY: ${{ github.event.comment.body }} + REVIEW_BODY: ${{ github.event.review.body }} + ISSUE_BODY: ${{ github.event.issue.body }} + ISSUE_TITLE: ${{ github.event.issue.title }} + run: | + set -eu + body="${COMMENT_BODY}${REVIEW_BODY}${ISSUE_BODY}${ISSUE_TITLE}" + if [[ "$body" == *"--opus"* ]] || [[ "$body" == *"claude-opus"* ]]; then + echo "model=claude-opus-4-7" >> "$GITHUB_OUTPUT" + else + echo "model=claude-sonnet-4-6" >> "$GITHUB_OUTPUT" + fi + + - name: Run Claude Code + id: claude + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + + # 让 Claude 能看 CI 结果 + additional_permissions: | + actions: read + + # 模型策略:默认 sonnet,评论里 --opus 才升级到 opus + claude_args: '--model ${{ steps.pick_model.outputs.model }}' From 275a11e3e5345932d9f27e9a4351c4fe5eaf73d8 Mon Sep 17 00:00:00 2001 From: H-Chris233 Date: Thu, 7 May 2026 10:38:44 +0800 Subject: [PATCH 2/2] Prevent startup logs from growing without bounds Long-running tray installs can accumulate an unbounded openless.log. Startup now checks the current log after single-instance arbitration but before simplelog opens the file, then rotates files over 10 MiB to openless.log.1. Normal append logging resumes with a fresh current log and log export continues to copy only openless.log. Rotation remains best-effort: startup continues if archive cleanup or rename fails, but stderr receives an explicit warning so the failure is diagnosable even before the file logger is initialized. Boundary tests cover oversized, small, and missing log files. Constraint: Issue #303 asks for a minimal startup size check with openless.log.1 archive. Constraint: Duplicate macOS/Linux launches must exit before rotation so they cannot rename the owning instance's active log file. Rejected: Add tracing-appender rolling support | new dependency and wider logging rewrite for a low-risk ops fix. Rejected: Add platform-specific rotation-failure tests | brittle across Linux/macOS/Windows file locking semantics. Confidence: high Scope-risk: narrow Directive: Keep export_error_log focused on openless.log; do not include rotated archives unless product explicitly asks for history export. Directive: Keep log rotation best-effort; startup must continue even if archive cleanup or rename fails. Tested: cargo test --manifest-path src-tauri/Cargo.toml log_ Tested: cargo check --manifest-path src-tauri/Cargo.toml Tested: git diff --check Not-tested: Manual startup rotation on Windows/macOS. Related: #303 --- openless-all/app/src-tauri/src/lib.rs | 92 +++++++++++++++++++++++++-- 1 file changed, 88 insertions(+), 4 deletions(-) diff --git a/openless-all/app/src-tauri/src/lib.rs b/openless-all/app/src-tauri/src/lib.rs index f8900a44..4a6579a1 100644 --- a/openless-all/app/src-tauri/src/lib.rs +++ b/openless-all/app/src-tauri/src/lib.rs @@ -38,6 +38,8 @@ use std::sync::Arc; #[cfg(target_os = "macos")] use std::time::Duration; +const LOG_ROTATE_LIMIT_BYTES: u64 = 10 * 1024 * 1024; + /// 第一次 show 时把 QA 浮窗摆到屏幕底部居中;之后的 show 不再 reposition, /// 让用户拖动后的位置在 hide → show 之间得以保持。详见 issue #118 v2。 static QA_WINDOW_POSITIONED: AtomicBool = AtomicBool::new(false); @@ -47,9 +49,6 @@ use tauri::{AppHandle, Emitter, LogicalPosition, LogicalSize, Manager, RunEvent, #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { - init_file_logger(); - log::info!("=== OpenLess 启动 ==="); - let coordinator = Arc::new(coordinator::Coordinator::new()); let local_asr_download_manager = Arc::new(asr::local::DownloadManager::new()); @@ -76,6 +75,9 @@ pub fn run() { .manage(coordinator.clone()) .manage(local_asr_download_manager.clone()) .setup(move |app| { + init_file_logger(); + log::info!("=== OpenLess 启动 ==="); + // Capsule 启动时定位到屏幕底部居中并隐藏;coordinator 按需显示。 // 与 Swift `CapsuleWindowController.repositionToBottomCenter` 同语义。 if let Some(capsule) = app.get_webview_window("capsule") { @@ -430,6 +432,9 @@ fn init_file_logger() { let log_dir = log_dir_path(); let _ = std::fs::create_dir_all(&log_dir); let log_file = log_dir.join("openless.log"); + if let Err(e) = rotate_log_if_too_large(&log_file) { + eprintln!("[logger] WARN 日志轮转失败: {e}"); + } let config = ConfigBuilder::new().set_time_format_rfc3339().build(); let mut loggers: Vec> = vec![TermLogger::new( LevelFilter::Info, @@ -447,6 +452,23 @@ fn init_file_logger() { let _ = CombinedLogger::init(loggers); } +fn rotate_log_if_too_large(path: &std::path::Path) -> std::io::Result<()> { + let Ok(metadata) = std::fs::metadata(path) else { + return Ok(()); + }; + if metadata.len() <= LOG_ROTATE_LIMIT_BYTES { + return Ok(()); + } + + let archive = path.with_file_name("openless.log.1"); + match std::fs::remove_file(&archive) { + Ok(()) => {} + Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} + Err(e) => return Err(e), + } + std::fs::rename(path, archive) +} + pub fn log_dir_path() -> std::path::PathBuf { #[cfg(target_os = "macos")] { @@ -837,7 +859,11 @@ fn capsule_height_for_qa() -> f64 { #[cfg(test)] mod tests { - use super::{capsule_height_for_qa, capsule_visual_height, capsule_window_bounds}; + use super::{ + capsule_height_for_qa, capsule_visual_height, capsule_window_bounds, + rotate_log_if_too_large, LOG_ROTATE_LIMIT_BYTES, + }; + use std::io::Write; #[test] fn capsule_window_bounds_leave_room_for_windows_shadow() { @@ -888,4 +914,62 @@ mod tests { #[cfg(not(target_os = "windows"))] assert_eq!(capsule_height_for_qa(), 96.0); } + + #[test] + fn oversized_log_rotates_to_single_archive() { + let dir = std::env::temp_dir().join(format!("openless-log-rotate-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let log = dir.join("openless.log"); + let archive = dir.join("openless.log.1"); + + { + let mut file = std::fs::File::create(&log).unwrap(); + file.set_len(LOG_ROTATE_LIMIT_BYTES + 1).unwrap(); + file.write_all(b"x").unwrap(); + } + std::fs::write(&archive, b"old").unwrap(); + + rotate_log_if_too_large(&log).unwrap(); + + assert!(!log.exists()); + assert!(archive.exists()); + assert!(std::fs::metadata(&archive).unwrap().len() > LOG_ROTATE_LIMIT_BYTES); + + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn small_log_does_not_rotate() { + let dir = std::env::temp_dir().join(format!("openless-log-small-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let log = dir.join("openless.log"); + let archive = dir.join("openless.log.1"); + std::fs::write(&log, b"small").unwrap(); + + rotate_log_if_too_large(&log).unwrap(); + + assert!(log.exists()); + assert!(!archive.exists()); + assert_eq!(std::fs::read(&log).unwrap(), b"small"); + + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn missing_log_does_not_rotate() { + let dir = std::env::temp_dir().join(format!("openless-log-missing-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let log = dir.join("openless.log"); + let archive = dir.join("openless.log.1"); + + rotate_log_if_too_large(&log).unwrap(); + + assert!(!log.exists()); + assert!(!archive.exists()); + + let _ = std::fs::remove_dir_all(&dir); + } }