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
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion npm/main/scripts/postinstall.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ try {
const platform = process.platform;
const arch = process.arch;
const homeDir = os.homedir();
const claudeDir = path.join(homeDir, '.claude', '88code');
const claudeDir = path.join(homeDir, '.claude', 'byebyecode');

// Create directory
fs.mkdirSync(claudeDir, { recursive: true });
Expand Down
2 changes: 1 addition & 1 deletion src/api/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const CACHE_FRESH_SECONDS: u64 = 300;
/// 获取缓存文件路径
fn get_cache_file(cache_type: &str) -> Option<PathBuf> {
let home = dirs::home_dir()?;
let cache_dir = home.join(".claude").join("88code").join("cache");
let cache_dir = home.join(".claude").join("byebyecode").join("cache");

// 确保缓存目录存在
fs::create_dir_all(&cache_dir).ok()?;
Expand Down
37 changes: 28 additions & 9 deletions src/api/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,38 @@ impl ApiClient {
}

pub fn get_usage(&self) -> Result<UsageData, Box<dyn std::error::Error>> {
let response = self
.client
.post(&self.config.usage_url)
.header("Authorization", format!("Bearer {}", self.config.api_key))
.header("Content-Type", "application/json")
.send()?;
let is_packyapi = self.config.is_packyapi();

let response = if is_packyapi {
self.client
.get(&self.config.usage_url)
.header("Authorization", format!("Bearer {}", self.config.api_key))
.send()?
} else {
self.client
.post(&self.config.usage_url)
.header("Authorization", format!("Bearer {}", self.config.api_key))
.header("Content-Type", "application/json")
.send()?
};

if !response.status().is_success() {
return Err(format!("Usage API request failed: {}", response.status()).into());
}

let mut usage: UsageData = response.json()?;
usage.calculate(); // 计算使用情况
let response_text = response.text()?;

let mut usage: UsageData = if is_packyapi {
let resp: super::PackyUsageResponse = serde_json::from_str(&response_text)
.map_err(|e| format!("Packyapi JSON parse error: {} | Response: {}", e, response_text))?;
UsageData::Packy(resp.data)
} else {
let data: super::Code88UsageData = serde_json::from_str(&response_text)
.map_err(|e| format!("API JSON parse error: {} | Response: {}", e, response_text))?;
UsageData::Code88(data)
};

usage.calculate();
Ok(usage)
}

Expand Down Expand Up @@ -59,6 +78,6 @@ impl ApiClient {

pub fn check_token_limit(&self) -> Result<bool, Box<dyn std::error::Error>> {
let usage = self.get_usage()?;
Ok(usage.remaining_tokens == 0)
Ok(usage.get_remaining_tokens() == 0)
}
}
145 changes: 128 additions & 17 deletions src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,28 @@ impl Default for ApiConfig {
}
}

impl ApiConfig {
pub fn is_packyapi(&self) -> bool {
self.usage_url.contains("packyapi.com")
}
}

#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum UsageData {
Code88(Code88UsageData),
Packy(PackyUsageData),
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct UsageData {
pub struct Code88UsageData {
#[serde(rename = "totalTokens")]
pub total_tokens: u64,
#[serde(rename = "creditLimit")]
pub credit_limit: f64,
#[serde(rename = "currentCredits")]
pub current_credits: f64,

// 计算字段(序列化时保存,从缓存读取时也能用)
#[serde(default)]
pub used_tokens: u64,
#[serde(default)]
Expand All @@ -41,41 +53,115 @@ pub struct UsageData {
pub percentage_used: f64,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PackyUsageResponse {
pub code: bool,
pub data: PackyUsageData,
pub message: String,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PackyUsageData {
pub expires_at: i64,
pub name: String,
pub object: String,
pub total_available: u64,
pub total_granted: u64,
pub total_used: u64,
pub unlimited_quota: bool,

#[serde(default)]
pub used_tokens: u64,
#[serde(default)]
pub remaining_tokens: u64,
#[serde(default)]
pub percentage_used: f64,
#[serde(default)]
pub credit_limit: f64,
#[serde(default)]
pub current_credits: f64,
}

impl UsageData {
/// 计算已使用和剩余的积分
pub fn calculate(&mut self) {
// 使用积分而不是token
// creditLimit = 20.0刀, currentCredits = 15.35刀
// 已使用积分 = 20 - 15.35 = 4.65刀
// 使用百分比 = (20 - 15.35) / 20 * 100 = 23.25%
match self {
UsageData::Code88(data) => data.calculate(),
UsageData::Packy(data) => data.calculate(),
}
}

pub fn is_exhausted(&self) -> bool {
match self {
UsageData::Code88(data) => data.is_exhausted(),
UsageData::Packy(data) => data.is_exhausted(),
}
}

pub fn get_used_tokens(&self) -> u64 {
match self {
UsageData::Code88(data) => data.used_tokens,
UsageData::Packy(data) => data.used_tokens,
}
}

pub fn get_remaining_tokens(&self) -> u64 {
match self {
UsageData::Code88(data) => data.remaining_tokens,
UsageData::Packy(data) => data.remaining_tokens,
}
}

pub fn get_credit_limit(&self) -> f64 {
match self {
UsageData::Code88(data) => data.credit_limit,
UsageData::Packy(data) => data.credit_limit,
}
}
}

impl Code88UsageData {
pub fn calculate(&mut self) {
let used_credits = self.credit_limit - self.current_credits;
self.percentage_used = if self.credit_limit > 0.0 {
(used_credits / self.credit_limit * 100.0).clamp(0.0, 100.0)
} else {
0.0
};

// 直接使用积分(美元)进行显示
// used_tokens 和 remaining_tokens 现在表示积分(以美分为单位,便于整数显示)
// 注意:currentCredits可能是负数(超额使用),需要正确处理
self.used_tokens = (used_credits * 100.0).max(0.0) as u64; // 转换为美分
self.used_tokens = (used_credits * 100.0).max(0.0) as u64;

// remaining_tokens 需要处理负数情况
// 如果 current_credits < 0,说明超额,remaining 应该是 0
if self.current_credits < 0.0 {
self.remaining_tokens = 0;
} else {
self.remaining_tokens = (self.current_credits * 100.0) as u64;
}
}

/// 检查额度是否已用完(包括超额使用)
pub fn is_exhausted(&self) -> bool {
self.current_credits <= 0.0
}
}

impl PackyUsageData {
pub fn calculate(&mut self) {
self.used_tokens = self.total_used;
self.remaining_tokens = self.total_available.saturating_sub(self.total_used);

self.percentage_used = if self.total_granted > 0 {
(self.total_used as f64 / self.total_granted as f64 * 100.0).clamp(0.0, 100.0)
} else {
0.0
};

self.credit_limit = (self.total_granted as f64) / 100.0;
self.current_credits = (self.remaining_tokens as f64) / 100.0;
}

pub fn is_exhausted(&self) -> bool {
!self.unlimited_quota && self.remaining_tokens == 0
}
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SubscriptionData {
#[serde(rename = "subscriptionPlanName")]
Expand Down Expand Up @@ -125,7 +211,7 @@ fn get_claude_settings_path() -> Option<PathBuf> {
dirs::home_dir().map(|home| home.join(".claude").join("settings.json"))
}

/// Read API key from Claude settings.json if base URL is 88code.org
/// Read API key from Claude settings.json if base URL is 88code.org or packyapi.com
pub fn get_api_key_from_claude_settings() -> Option<String> {
let settings_path = get_claude_settings_path()?;

Expand All @@ -138,12 +224,37 @@ pub fn get_api_key_from_claude_settings() -> Option<String> {

let env = settings.env?;

// Only use the API key if base URL is 88code.org
// Support both 88code.org and packyapi.com
if let Some(base_url) = env.base_url {
if base_url.contains("88code.org") {
if base_url.contains("88code.org") || base_url.contains("packyapi.com") {
return env.auth_token;
}
}

None
}

/// Get usage_url from Claude settings.json based on ANTHROPIC_BASE_URL
pub fn get_usage_url_from_claude_settings() -> Option<String> {
let settings_path = get_claude_settings_path()?;

if !settings_path.exists() {
return None;
}

let content = std::fs::read_to_string(settings_path).ok()?;
let settings = serde_json::from_str::<serde_json::Value>(&content).ok()?;

let base_url = settings
.get("env")?
.get("ANTHROPIC_BASE_URL")?
.as_str()?;

if base_url.contains("packyapi.com") {
Some("https://www.packyapi.com/api/usage/token/".to_string())
} else if base_url.contains("88code.org") {
Some("https://www.88code.org/api/usage".to_string())
} else {
None
}
}
2 changes: 1 addition & 1 deletion src/auto_config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ impl AutoConfigurator {
pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
let home = dirs::home_dir().ok_or("Could not find home directory")?;

let config_dir = home.join(".claude/88code");
let config_dir = home.join(".claude/byebyecode");
Ok(Self { config_dir })
}

Expand Down
14 changes: 7 additions & 7 deletions src/config/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,12 @@ impl ConfigLoader {
Ok(())
}

/// Get the themes directory path (~/.claude/88code/themes/)
/// Get the themes directory path (~/.claude/byebyecode/themes/)
pub fn get_themes_path() -> PathBuf {
if let Some(home) = dirs::home_dir() {
home.join(".claude").join("88code").join("themes")
home.join(".claude").join("byebyecode").join("themes")
} else {
PathBuf::from(".claude/88code/themes")
PathBuf::from(".claude/byebyecode/themes")
}
}

Expand Down Expand Up @@ -133,20 +133,20 @@ impl Config {
Ok(())
}

/// Get the default config file path (~/.claude/88code/config.toml)
/// Get the default config file path (~/.claude/byebyecode/config.toml)
fn get_config_path() -> PathBuf {
if let Some(home) = dirs::home_dir() {
home.join(".claude").join("88code").join("config.toml")
home.join(".claude").join("byebyecode").join("config.toml")
} else {
PathBuf::from(".claude/88code/config.toml")
PathBuf::from(".claude/byebyecode/config.toml")
}
}

/// Initialize config directory and create default config
pub fn init() -> Result<(), Box<dyn std::error::Error>> {
let config_path = Self::get_config_path();

// Delete existing 88code directory if it exists
// Delete existing byebyecode directory if it exists
if let Some(parent) = config_path.parent() {
if parent.exists() {
println!("Removing existing directory: {}", parent.display());
Expand Down
6 changes: 3 additions & 3 deletions src/config/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,15 @@ impl ModelConfig {

// First, try to create default models.toml if it doesn't exist
if let Some(home_dir) = dirs::home_dir() {
let user_models_path = home_dir.join(".claude/88code").join("models.toml");
let user_models_path = home_dir.join(".claude/byebyecode").join("models.toml");
if !user_models_path.exists() {
let _ = Self::create_default_file(&user_models_path);
}
}

// Try loading from user config directory first, then local
let config_paths = [
dirs::home_dir().map(|d| d.join(".claude/88code").join("models.toml")),
dirs::home_dir().map(|d| d.join(".claude/byebyecode").join("models.toml")),
Some(Path::new("models.toml").to_path_buf()),
];

Expand Down Expand Up @@ -101,7 +101,7 @@ impl ModelConfig {
let template_content = format!(
"# CCometixLine Model Configuration\n\
# This file defines model display names and context limits for different LLM models\n\
# File location: ~/.claude/88code/models.toml\n\
# File location: ~/.claude/byebyecode/models.toml\n\
\n\
{}\n\
\n\
Expand Down
Loading
Loading