diff --git a/CLAUDE.md b/CLAUDE.md index 2fb0c2b..c2a51a8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,8 +27,9 @@ ByeByeCode 是一个 Rust 编写的 Claude Code 状态栏增强工具,用于 | `fix/issue-9-subscription-usage-display` | Issue #9 修复 | PR #10 | ✅ 已合并 | | `feature/progress-bar-usage-display` | 进度条功能 | PR #11 | ✅ 已合并 | | `fix/skip-free-subscription` | 跳过 FREE 套餐 | PR #12 | ✅ 已合并 | -| `feature/simplify-subscription-display` | 精简订阅显示格式 | PR #15 | 🔄 待审核 | -| `feature/support-new-88code-domains` | 支持新域名 88code.ai | PR #16 | 🔄 待审核 | +| `feature/simplify-subscription-display` | 精简订阅显示格式 | PR #15 | ✅ 已合并 | +| `feature/support-new-88code-domains` | 支持新域名 88code.ai | PR #16 | ✅ 已合并 | +| `feature/sort-subscriptions-by-remaining-days` | 按剩余天数排序 | PR #18 | ✅ 已合并 | ### 分支工作流 @@ -331,3 +332,334 @@ Git 的 `link.exe` 干扰了 MSVC 的 `link.exe`。解决方案: ### 状态栏显示 FREE 套餐用量 确保代码包含跳过 FREE 的逻辑(PR #12)。 + +--- + +## 项目审查报告(2025-01-13) + +本章节包含对 byebyecode 项目的全面审查,涵盖 UI/UX 优化建议、已发现的潜在 Bug、关键文件清单及实施建议。 + +### 📊 审查概览 + +- **审查日期**: 2025-01-13 +- **审查范围**: UI/UX、架构设计、Bug 排查 +- **发现数量**: 9 项 UI 优化建议、9 个潜在 Bug +- **关键文件**: 8 个核心文件需重点关注 + +--- + +## 🎨 UI 优化与高级感提升建议 + +### 🔴 P0 - 必须优化(严重影响用户体验) + +#### 1. 额度用完:视觉警示强化 + +**当前问题**: +\`\`\` +❌ "已用完 提示:你有其他套餐可用" +\`\`\` +- 纯文字提示,无颜色/图标 +- 缺少行动指引("手动重置" vs "切换套餐") + +**优化方案**: +\`\`\`rust +// src/core/segments/byebyecode_usage.rs:107-125 + +// 额度用完时 +primary: format!("⚠️ 已用完 ${}/${}", used, total) // 红色背景 + 感叹号图标 +secondary: match has_reset_times { + true => format!("→ 可重置×{} 点击重置", reset_count), // 行动指引 + false => "→ 切换至其他套餐".to_string(), +} + +// 应用危险色 +metadata.insert("danger_mode".to_string(), "true".to_string()); +\`\`\` + +--- + +### 🟡 P1 - 应该优化(用户体验改进) + +#### 4. 错误提示改进 + +**当前**:API 失败显示 "未配置密钥"(可能是网络错误) + +**改进**: +\`\`\`rust +match fetch_usage_sync(...) { + Ok(usage) => usage, + Err(e) => { + let error_msg = if e.to_string().contains("timeout") { + "⏱️ 网络超时" + } else if e.to_string().contains("401") { + "🔑 密钥无效" + } else { + format!("❌ API错误: {}", e) + }; + return Some(SegmentData { + primary: error_msg, + secondary: String::new(), + }); + } +} +\`\`\` + +--- + +### 🟢 P2 - 可以优化(锦上添花) + +#### 5. 响应式布局 + +\`\`\`rust +// 根据终端宽度自动切换精简模式 +let terminal_width = terminal::size().map(|(w, _)| w).unwrap_or(80); +let compact_mode = terminal_width < 80; + +if compact_mode { + // 只显示当前扣费套餐 + // 缩短文字格式 +} +\`\`\` + +#### 6. 快过期警示 + +\`\`\`rust +// 订阅段:剩余天数 < 7 天时高亮显示 +let days_color = if sub.remaining_days <= 7 { + AnsiColor::Color16 { c16: 9 } // 红色 +} else if sub.remaining_days <= 30 { + AnsiColor::Color16 { c16: 11 } // 黄色 +} else { + AnsiColor::Color16 { c16: 7 } // 白色 +}; +\`\`\` + +#### 7. 配置项扩展 + +\`\`\`toml +[byebyecode_usage.options] +show_progress_bar = true +show_percentage = true +compact_mode = false +warning_threshold = 80 # 百分比超过 80% 显示黄色 + +[byebyecode_subscription.options] +show_reset_times = true +show_days_threshold = 30 # 只在剩余天数<30天时显示 +compact_mode = false +\`\`\` + +--- + +## 🐛 已发现的潜在 Bug + +### 🔴 高严重性(可能导致 panic 或崩溃) + +#### Bug #1: 货币计算可能溢出 + +**位置**:\`src/api/mod.rs:173\` + +**问题**:浮点数乘 100 后转 u64,超过 u64::MAX 会 panic。 + +\`\`\`rust +// 当前代码 +self.used_tokens = (used_credits * 100.0).max(0.0) as u64; + +// 修复方案 +self.used_tokens = (used_credits * 100.0) + .max(0.0) + .min(u64::MAX as f64) as u64; +\`\`\` + +#### Bug #2: unwrap() 导致 panic + +**位置**:\`src/core/segments/byebyecode_usage.rs\` 多处 + +**问题**:如果 model 为 None 会 panic。 + +\`\`\`rust +// 当前代码 +let model_id = &input.model.id; + +// 修复方案 +let model_id = input.model.as_ref().map(|m| m.id.as_str()); +\`\`\` + +#### Bug #3: API 响应状态未验证 + +**位置**:\`src/api/client.rs:43-44\` + +**问题**:只检查 HTTP 状态码,未检查业务状态码(\`code\` 字段)。 + +\`\`\`rust +// 当前代码 +if !response.status().is_success() { + return Err(format!("API request failed: {}", response.status()).into()); +} + +// 修复方案 +let resp: ResponseDTO = serde_json::from_str(&response_text)?; +if resp.code != 0 { // 假设 0 表示成功 + return Err(format!("API error: {}", resp.message).into()); +} +\`\`\` + +--- + +### 🟡 中等严重性(数据不一致或逻辑错误) + +#### Bug #4: 浮点数精度问题 + +**位置**:\`src/api/mod.rs:167-168\` + +**问题**:连续浮点运算可能累积误差。 + +\`\`\`rust +// 当前代码 +self.percentage_used = (used_credits / credit_limit * 100.0).clamp(0.0, 100.0); + +// 修复方案 +self.percentage_used = ((used_credits / credit_limit) * 10000.0).round() / 100.0; +// 保留两位小数 +\`\`\` + +#### Bug #5: 订阅过滤边界错误 + +**位置**:\`src/core/segments/byebyecode_subscription.rs:120\` + +**问题**:\`remaining_days == 0\` 当天仍然有效,不应过滤。 + +\`\`\`rust +// 当前代码 +.filter(|sub| sub.is_active && sub.remaining_days > 0) + +// 修复方案 +.filter(|sub| sub.is_active && sub.remaining_days >= 0) +\`\`\` + +#### Bug #6: 缓存 URL 硬编码 + +**位置**:\`src/api/cache.rs:120-142\` + +**问题**:与实际使用的 88code API 不一致,缓存机制无法工作。 + +\`\`\`rust +// 当前代码(硬编码) +subscription_url: "https://api.cometix.cn/v1/billing/subscription/list" + +// 修复方案:从配置或 Claude settings 读取 +\`\`\` + +--- + +### 🟢 低严重性(改进机会) + +#### Bug #7: 线程安全隐患 + +**位置**:\`src/api/cache.rs:113-152\` + +**问题**:多个并发刷新可能竞争写入缓存文件。 + +**建议**:使用文件锁或原子操作。 + +#### Bug #8: 配置错误提示不清晰 + +**位置**:\`src/core/segments/byebyecode_usage.rs:48-52\` + +**当前**: +\`\`\`rust +primary: "未配置密钥".to_string(), +\`\`\` + +**建议**: +\`\`\`rust +primary: "未配置密钥 (检查 ~/.claude/settings.json)".to_string(), +\`\`\` + +#### Bug #9: URL 判断逻辑可能误判 + +**位置**:\`src/api/mod.rs:302-310\` + +**问题**:\`rainapp.top\` 应该使用其原始域名,而非重定向到 \`88code.ai\`。 + +\`\`\`rust +// 当前代码 +if base_url.contains("88code.ai") || base_url.contains("rainapp.top") { + Some("https://www.88code.ai/api/usage".to_string()) +} + +// 修复方案 +if base_url.contains("rainapp.top") { + Some(format!("{}/api/usage", base_url)) // 保持原域名 +} else if base_url.contains("88code.ai") { + Some("https://www.88code.ai/api/usage".to_string()) +} +\`\`\` + +--- + +## 📁 关键文件清单(按优先级) + +### 🎨 UI 优化相关 + +1. **\`src/core/segments/byebyecode_usage.rs\`** (178 行) + - 用量段完整逻辑:进度条、百分比计算、状态色 + +2. **\`src/core/segments/byebyecode_subscription.rs\`** (182 行) + - 订阅段实现:颜色生成、格式化、排序 + +3. **\`src/core/statusline.rs\`** (522 行) + - 渲染引擎:ANSI 颜色、Powerline 箭头 + +### 🐛 Bug 修复相关 + +4. **\`src/api/mod.rs\`** (312 行) + - 货币计算溢出、浮点精度问题 + +5. **\`src/api/client.rs\`** (121 行) + - API 状态码验证、错误处理 + +6. **\`src/api/cache.rs\`** (152 行) + - 缓存 URL 硬编码、线程安全 + +### ⚙️ 配置与架构 + +7. **\`src/config/types.rs\`** (420 行) + - 配置结构定义、颜色类型 + +8. **\`src/ui/themes/theme_default.rs\`** (233 行) + - 默认主题配置、图标语义化 + +--- + +## 💡 实施建议与优先级 + +### 📊 优化收益评估 + +| 优化项 | 实施难度 | 用户体验提升 | 建议优先级 | +|--------|---------|-------------|-----------| +| 百分比优先 + 状态色 | ⭐⭐ | ⭐⭐⭐⭐⭐ | 🔴 P0 | +| 订阅段精简格式 | ⭐⭐⭐ | ⭐⭐⭐⭐ | 🔴 P0 | +| 额度用完视觉警示 | ⭐⭐ | ⭐⭐⭐⭐ | 🔴 P0 | +| 货币计算溢出修复 | ⭐ | ⭐⭐⭐⭐⭐ | 🔴 P0 | +| 加载状态可视化 | ⭐ | ⭐⭐⭐ | 🟡 P1 | +| 错误提示改进 | ⭐ | ⭐⭐⭐ | 🟡 P1 | +| 响应式布局 | ⭐⭐⭐⭐ | ⭐⭐ | 🟢 P2 | + +### 🎯 推荐实施路径 + +**第一阶段(优先修复)**: +1. 修复货币计算溢出 bug(5分钟) +2. 修复订阅过滤边界错误(2分钟) +3. 实现状态色系统(30分钟) +4. 百分比优先显示(15分钟) + +**第二阶段(用户体验提升)**: +5. 精简订阅段格式(1小时) +6. 额度用完视觉警示(30分钟) +7. 加载状态可视化(20分钟) + +**第三阶段(按需优化)**: +8. 响应式布局 +9. 配置项扩展 +10. 其他低优先级优化 diff --git a/src/core/segments/byebyecode_subscription.rs b/src/core/segments/byebyecode_subscription.rs index 09952d3..8b27701 100644 --- a/src/core/segments/byebyecode_subscription.rs +++ b/src/core/segments/byebyecode_subscription.rs @@ -2,32 +2,20 @@ use crate::api::{client::ApiClient, ApiConfig}; use crate::config::Config; use crate::config::InputData; use crate::core::segments::SegmentData; -use std::collections::hash_map::DefaultHasher; use std::collections::HashMap; -use std::hash::{Hash, Hasher}; - -/// 生成柔和的随机颜色(基于字符串哈希) -fn get_soft_color(text: &str) -> String { - let mut hasher = DefaultHasher::new(); - text.hash(&mut hasher); - let hash = hasher.finish(); - - // 定义一组柔和的颜色(RGB格式) - let soft_colors = [ - (150, 180, 220), // 柔和蓝 - (180, 150, 200), // 柔和紫 - (200, 170, 150), // 柔和橙 - (150, 200, 180), // 柔和青 - (220, 180, 150), // 柔和棕 - (180, 200, 150), // 柔和绿 - (200, 150, 180), // 柔和粉 - (170, 190, 200), // 柔和灰蓝 - ]; - - let idx = (hash % soft_colors.len() as u64) as usize; - let (r, g, b) = soft_colors[idx]; - - format!("\x1b[38;2;{};{};{}m", r, g, b) + +/// 根据套餐类型获取语义化颜色 +/// - PLUS/PRO/MAX: 橙色(高级套餐) +/// - PAYGO: 蓝色(按需套餐) +/// - FREE: 灰色(免费套餐) +/// - 其他: 白色 +fn get_plan_color(plan_name: &str) -> &'static str { + match plan_name.to_uppercase().as_str() { + "PLUS" | "PRO" | "MAX" => "\x1b[38;5;214m", // 橙色 + "PAYGO" => "\x1b[38;5;39m", // 蓝色 + "FREE" => "\x1b[38;5;245m", // 灰色 + _ => "\x1b[38;5;255m", // 白色 + } } /// ANSI 重置代码 @@ -120,8 +108,16 @@ pub fn collect(config: &Config, input: &InputData) -> Option { .filter(|sub| sub.is_active && sub.remaining_days > 0) .collect(); - // 按剩余天数升序排序(快过期的排在前面) - active_subscriptions.sort_by(|a, b| a.remaining_days.cmp(&b.remaining_days)); + // 排序:FREE 优先,然后按剩余天数升序 + active_subscriptions.sort_by(|a, b| { + let a_is_free = a.plan_name.to_uppercase() == "FREE"; + let b_is_free = b.plan_name.to_uppercase() == "FREE"; + match (a_is_free, b_is_free) { + (true, false) => std::cmp::Ordering::Less, // FREE 排前面 + (false, true) => std::cmp::Ordering::Greater, // 非FREE 排后面 + _ => a.remaining_days.cmp(&b.remaining_days), // 同类型按天数排 + } + }); if active_subscriptions.is_empty() { return Some(SegmentData { @@ -131,31 +127,21 @@ pub fn collect(config: &Config, input: &InputData) -> Option { }); } - // 组合所有订阅信息 + // 组合所有订阅信息(精简格式) let mut subscription_texts = Vec::new(); let mut metadata = HashMap::new(); for (idx, sub) in active_subscriptions.iter().enumerate() { - // 为每个订阅生成基于其计划名的柔和颜色 - let color = get_soft_color(&sub.plan_name); + // 语义化颜色 + let color = get_plan_color(&sub.plan_name); - // 精简价格显示:去掉"付"字(月付→月,年付→年) + // 精简格式:PLUS ¥198/月 53天 + // 去掉重置次数,只保留套餐名、价格、剩余天数 let short_price = sub.plan_price.replace("付", ""); - - // 精简格式:PLUS ¥198/月 可重置2次 53天 | PAYGO ¥66/年 989天 - let subscription_text = if sub.plan_name == "PAYGO" { - // PAYGO 不显示重置次数 - format!( - "{}{} {} {}天{}", - color, sub.plan_name, short_price, sub.remaining_days, RESET - ) - } else { - // 其他套餐显示重置次数 - format!( - "{}{} {} 可重置{}次 {}天{}", - color, sub.plan_name, short_price, sub.reset_times, sub.remaining_days, RESET - ) - }; + let subscription_text = format!( + "{}{} {} {}天{}", + color, sub.plan_name, short_price, sub.remaining_days, RESET + ); subscription_texts.push(subscription_text); // 保存元数据 diff --git a/src/core/segments/byebyecode_usage.rs b/src/core/segments/byebyecode_usage.rs index eeef087..934af5e 100644 --- a/src/core/segments/byebyecode_usage.rs +++ b/src/core/segments/byebyecode_usage.rs @@ -1,9 +1,26 @@ -use crate::api::{client::ApiClient, ApiConfig}; +use crate::api::{cache, client::ApiClient, ApiConfig}; use crate::config::Config; use crate::config::InputData; use crate::core::segments::SegmentData; use std::collections::HashMap; +/// ANSI 重置代码 +const RESET: &str = "\x1b[0m"; + +/// 根据百分比获取状态色(柔和色调) +/// - 0-50%: 柔和绿 (充足) +/// - 50-80%: 柔和黄 (注意) +/// - 80%+: 柔和红 (紧急) +fn get_status_color(percentage: f64) -> &'static str { + if percentage <= 50.0 { + "\x1b[38;5;114m" // 柔和绿 (256色 #114) + } else if percentage <= 80.0 { + "\x1b[38;5;179m" // 柔和黄/橙 (256色 #179) + } else { + "\x1b[38;5;167m" // 柔和红 (256色 #167) + } +} + pub fn collect(config: &Config, input: &InputData) -> Option { // Get API config from segment options let segment = config @@ -62,12 +79,33 @@ pub fn collect(config: &Config, input: &InputData) -> Option { // 从输入数据获取当前使用的模型 let model_id = &input.model.id; - let usage = fetch_usage_sync(&api_key, &usage_url, Some(model_id))?; - fn fetch_usage_sync( + // 优先使用缓存,API 失败时降级 + let usage = fetch_usage_with_cache(&api_key, &usage_url, Some(model_id), service_name); + + let usage = match usage { + Some(u) => u, + None => { + // 完全没有数据,显示加载中 + let mut metadata = HashMap::new(); + metadata.insert("dynamic_icon".to_string(), service_name.to_string()); + return Some(SegmentData { + primary: "⏳ 获取中...".to_string(), + secondary: String::new(), + metadata, + }); + } + }; + + /// 带缓存的用量获取 + /// 1. 尝试从 API 获取最新数据 + /// 2. 成功则更新缓存并返回 + /// 3. 失败则尝试使用缓存降级 + fn fetch_usage_with_cache( api_key: &str, usage_url: &str, model: Option<&str>, + _service_name: &str, ) -> Option { let api_config = ApiConfig { enabled: true, @@ -76,9 +114,18 @@ pub fn collect(config: &Config, input: &InputData) -> Option { subscription_url: String::new(), }; - let client = ApiClient::new(api_config).ok()?; - let usage = client.get_usage(model).ok()?; - Some(usage) + // 尝试从 API 获取 + if let Ok(client) = ApiClient::new(api_config) { + if let Ok(usage) = client.get_usage(model) { + // API 成功,保存缓存 + let _ = cache::save_cached_usage(&usage); + return Some(usage); + } + } + + // API 失败,尝试使用缓存降级 + let (cached, _) = cache::get_cached_usage(); + cached } // 处理使用数据 @@ -143,11 +190,20 @@ pub fn collect(config: &Config, input: &InputData) -> Option { 0.0 }; - // 生成进度条(10格) + // 生成进度条(10格)+ 状态色 let bar_length = 10; let filled = ((percentage / 100.0) * bar_length as f64).round() as usize; let empty = bar_length - filled; - let progress_bar = format!("{}{}", "▓".repeat(filled), "░".repeat(empty)); + + // 根据百分比获取状态色 + let status_color = get_status_color(percentage); + let progress_bar = format!( + "{}{}{}{}", + status_color, + "▓".repeat(filled), + "░".repeat(empty), + RESET + ); Some(SegmentData { primary: format!(