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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,4 @@ node_modules/

# Logs
*.log
/cmd/server/ccnexus/
191 changes: 191 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## 项目概述

ccNexus 是一个智能 API 端点轮换代理,专为 Claude Code 和 Codex CLI 设计。

**核心功能:**
- 多端点轮换与自动故障转移
- API 格式转换(Claude ↔ OpenAI ↔ Gemini)
- Codex Token Pool 管理(自动轮换、刷新、失效隔离)
- 实时统计与监控
- WebDAV 云同步

**两种运行模式:**
- **桌面模式**:基于 Wails v2 的跨平台 GUI 应用(`cmd/desktop/`)
- **服务器模式**:无头 HTTP API 代理(`cmd/server/`)

## 常用开发命令

### 开发与构建
```bash
# 桌面应用开发模式(支持热重载)
cd cmd/desktop && wails dev

# 构建桌面应用(指定平台)
wails build -platform linux/amd64 # Linux
wails build -platform darwin/amd64 # macOS
wails build -platform windows/amd64 # Windows

# 构建服务器
cd cmd/server && go build -ldflags="-s -w" -o ccnexus-server .

# 运行服务器
cd cmd/server && go run main.go
```

### 测试
```bash
# 运行所有测试
go test ./... -count=1

# 运行特定目录的测试
cd internal/proxy && go test -v ./...
cd internal/transformer/convert && go test -v ./...
```

### Docker
```bash
# 构建镜像
docker build -f cmd/server/Dockerfile -t ccnexus .

# 使用 docker-compose
cd cmd/server && docker-compose up -d
```

### 代码质量
```bash
go fmt ./... # 格式化代码
go vet ./... # 静态分析
go mod tidy # 清理依赖
```

## 核心架构

### 目录结构
```
ccNexus/
├── cmd/
│ ├── desktop/ # 桌面应用入口(Wails)
│ │ ├── frontend/ # Vue.js 前端
│ │ └── main.go # 桌面应用入口
│ └── server/ # 服务器模式入口
│ └── main.go # 服务器入口
└── internal/
├── proxy/ # HTTP 代理核心
├── transformer/ # API 格式转换器
├── storage/ # SQLite 数据存储
├── config/ # 配置管理
├── webdav/ # WebDAV 同步
├── logger/ # 日志系统
└── tray/ # 系统托盘(桌面模式)
```

### 关键组件

**代理层** (`internal/proxy/proxy.go`)
- 管理多个 API 端点,自动故障转移
- 跟踪当前端点和活动请求
- 使用连接池优化的 HTTP 客户端
- 处理流式和非流式响应

**转换器** (`internal/transformer/`)
- 在不同 API 格式之间转换请求和响应
- 支持流式传输的增量转换
- 处理工具调用和函数调用
- 类型定义:`internal/transformer/types.go`

**存储层** (`internal/storage/sqlite.go`)
- SQLite WAL 模式数据库
- 管理端点、凭证、使用统计、应用配置
- 线程安全操作

### 关键文件路径
- 数据库:`~/.ccNexus/ccnexus.db`
- 配置常量:`internal/config/config.go`(第 13-20 行:认证模式和端点 URL)
- 代理路由:`internal/proxy/proxy.go`(第 108-114 行)

## 端点配置

### 认证模式(`internal/config/config.go`)
- `api_key`:标准 API 密钥认证
- `token_pool`:Token 池(自动轮换)
- `codex_token_pool`:Codex Token Pool(使用 ChatGPT 后端)

### 转换器类型
- `claude`:Claude API
- `openai`:OpenAI Chat API
- `openai2`:OpenAI Response API
- `gemini`:Google Gemini API

### 端点配置规则
在 `internal/config/config.go` 的 `ApplyEndpointAuthModeRules` 函数中定义:
- Codex Token Pool 自动设置 API URL 和转换器
- Token Pool 模式会清空 APIKey
- URL 标准化处理

## API 端点

代理服务器提供以下端点(`internal/proxy/proxy.go` 第 108-114 行):
- `/` - 主代理路由(所有 API 请求)
- `/v1/messages/count_tokens` - Token 计数
- `/v1/models` - 模型列表(带缓存)
- `/health` - 健康检查
- `/stats` - 统计数据

## 环境变量

服务器模式支持以下环境变量(`cmd/server/main.go`):
- `CCNEXUS_PORT` - 覆盖默认端口
- `CCNEXUS_LOG_LEVEL` - 日志级别
- `CCNEXUS_DB_PATH` - 数据库路径
- `CCNEXUS_DATA_DIR` - 数据目录
- `CCNEXUS_BASIC_AUTH_USERNAME` - Basic Auth 用户名
- `CCNEXUS_BASIC_AUTH_PASSWORD` - Basic Auth 密码

## 依赖

- Go 1.24+
- Wails v2(桌面模式)
- Node.js 18+(前端开发)
- SQLite(modernc.org/sqlite,纯 Go 实现)

## 代码规范

**静态函数命名**:所有静态函数必须使用 `__` 前缀表示内部可见性

```c
// 符合规范
static int __internal_helper_function(int param) {
return param + 1;
}

// 不符合规范
static int internal_helper_function(int param) {
return param + 1;
}
```

**变量声明**:所有局部变量必须在函数体开头声明,并在声明时显式初始化

```c
// 符合规范
int function_name(void) {
int ret = 0;
int value = 0;
char buffer[256] = {0};
char *ptr = NULL;

/* 可执行语句 */
ret = do_something();
}

// 不符合规范
int function_name(void) {
int ret = 0;
ret = do_something();
int value = 0; /* 错误:在可执行语句后声明 */
}
```
9 changes: 7 additions & 2 deletions cmd/server/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@ WORKDIR /app
# Copy module definition first for caching
COPY go.mod ./

# 启用 Go Modules
# go env -w GO111MODULE=on
# 配置国内七牛云代理(最稳定)
# go env -w GOPROXY=https://goproxy.cn,direct

# Download dependencies (go.sum will be generated after tidy)
RUN go mod download
RUN go env -w GO111MODULE=on && go env -w GOPROXY=https://goproxy.cn,direct && go mod download

# Copy source code
COPY . ./
Expand All @@ -22,7 +27,7 @@ RUN go mod tidy
RUN CGO_ENABLED=1 GOOS=linux go build -ldflags="-s -w" -o ccnexus-server ./cmd/server

# Runtime stage
FROM alpine:3.19
FROM alpine:latest

# Install runtime dependencies
RUN apk add --no-cache ca-certificates sqlite-libs tzdata wget
Expand Down
2 changes: 1 addition & 1 deletion cmd/server/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ services:
ports:
- "3021:3000"
volumes:
- /data/ccnexus/:/data
- ./ccnexus/:/data
environment:
- CCNEXUS_PORT=3000
- CCNEXUS_DATA_DIR=/data
Expand Down
15 changes: 9 additions & 6 deletions cmd/server/webui/ui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,31 @@
<nav id="sidebar">
<div class="sidebar-header">
<h1>ccNexus</h1>
<p class="subtitle">AI Proxy Admin</p>
<p class="subtitle" id="sidebar-subtitle">AI Proxy Admin</p>
</div>
<ul class="nav-menu">
<li><a href="#" data-view="dashboard" class="nav-link active">
<span class="icon">📊</span>
<span>Dashboard</span>
<span class="nav-label" data-i18n="common.dashboard">Dashboard</span>
</a></li>
<li><a href="#" data-view="endpoints" class="nav-link">
<span class="icon">🔗</span>
<span>Endpoints</span>
<span class="nav-label" data-i18n="common.endpoints">Endpoints</span>
</a></li>
<li><a href="#" data-view="stats" class="nav-link">
<span class="icon">📈</span>
<span>Statistics</span>
<span class="nav-label" data-i18n="common.statistics">Statistics</span>
</a></li>
<li><a href="#" data-view="testing" class="nav-link">
<span class="icon">🧪</span>
<span>Testing</span>
<span class="nav-label" data-i18n="common.testing">Testing</span>
</a></li>
</ul>
<div class="sidebar-footer">
<button id="theme-toggle" class="btn-icon" title="Toggle theme">
<button id="lang-toggle" class="btn-icon" title="切换语言 / Switch Language">
<span class="icon">🌐</span>
</button>
<button id="theme-toggle" class="btn-icon" title="切换主题 / Toggle theme">
<span class="icon">🌙</span>
</button>
</div>
Expand Down
33 changes: 20 additions & 13 deletions cmd/server/webui/ui/js/components/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,46 @@ import { api } from '../api.js';
import { state } from '../state.js';
import { notifications } from '../utils/notifications.js';
import { formatNumber, formatTokens } from '../utils/formatters.js';
import { t } from '../utils/i18n.js';

class Dashboard {
constructor() {
this.container = document.getElementById('view-container');
// 监听语言切换
window.addEventListener('languageChanged', () => {
if (state.get('currentView') === 'dashboard') {
this.render();
}
});
}

async render() {
this.container.innerHTML = `
<div class="dashboard">
<h1>Dashboard</h1>
<h1>${t('dashboard.title')}</h1>
<div id="stats-cards" class="grid grid-cols-4 mt-3">
<div class="stat-card">
<div class="stat-label">Total Requests</div>
<div class="stat-label">${t('dashboard.totalRequests')}</div>
<div class="stat-value" id="stat-requests">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Success Rate</div>
<div class="stat-label">${t('dashboard.successRate')}</div>
<div class="stat-value" id="stat-success">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Input Tokens</div>
<div class="stat-label">${t('dashboard.inputTokens')}</div>
<div class="stat-value" id="stat-input-tokens">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Output Tokens</div>
<div class="stat-label">${t('dashboard.outputTokens')}</div>
<div class="stat-value" id="stat-output-tokens">-</div>
</div>
</div>

<div class="grid grid-cols-2 mt-4">
<div class="card">
<div class="card-header">
<h3 class="card-title">Active Endpoints</h3>
<h3 class="card-title">${t('dashboard.activeEndpoints')}</h3>
</div>
<div class="card-body">
<div id="endpoints-list"></div>
Expand All @@ -43,7 +50,7 @@ class Dashboard {

<div class="card">
<div class="card-header">
<h3 class="card-title">Recent Activity</h3>
<h3 class="card-title">${t('dashboard.recentActivity')}</h3>
</div>
<div class="card-body">
<canvas id="activity-chart"></canvas>
Expand Down Expand Up @@ -91,14 +98,14 @@ class Dashboard {
const container = document.getElementById('endpoints-list');

if (!endpoints || endpoints.length === 0) {
container.innerHTML = '<div class="empty-state"><p>No endpoints configured</p></div>';
container.innerHTML = `<div class="empty-state"><p>${t('dashboard.noEndpoints')}</p></div>`;
return;
}

const enabledEndpoints = endpoints.filter(ep => ep.enabled);

if (enabledEndpoints.length === 0) {
container.innerHTML = '<div class="empty-state"><p>No enabled endpoints</p></div>';
container.innerHTML = `<div class="empty-state"><p>${t('dashboard.noEnabledEndpoints')}</p></div>`;
return;
}

Expand All @@ -107,9 +114,9 @@ class Dashboard {
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Status</th>
<th>${t('common.name')}</th>
<th>${t('endpoints.transformer')}</th>
<th>${t('common.status')}</th>
</tr>
</thead>
<tbody>
Expand All @@ -119,7 +126,7 @@ class Dashboard {
<td>${this.escapeHtml(ep.transformer)}</td>
<td>
<span class="status-indicator online"></span>
<span class="badge badge-success">Active</span>
<span class="badge badge-success">${t('common.active')}</span>
</td>
</tr>
`).join('')}
Expand Down
Loading
Loading