Skip to content
Open
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 .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ DB_NAME=claude_code_hub

# 应用配置
APP_PORT=23000
NODE_OPTIONS=--max-old-space-size=1536
APP_URL= # 应用访问地址(留空自动检测,生产环境建议显式配置)
# 示例:https://your-domain.com 或 http://192.168.1.100:23000

Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,5 @@ tmp/
.omx/
.opencode/
bunx-*/
# Local runtime artifacts
.runtime/
4 changes: 4 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000

RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*

# 关键:确保复制了所有必要的文件,特别是 drizzle 文件夹
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
Expand Down
1 change: 1 addition & 0 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ Once started:
- **Admin Dashboard**: `http://localhost:23000` (login with `ADMIN_TOKEN` from `.env`)
- **API Docs (Scalar UI)**: `http://localhost:23000/api/actions/scalar`
- **API Docs (Swagger UI)**: `http://localhost:23000/api/actions/docs`
- **Temporary key group API examples**: [docs/examples/temporary-key-groups.md](docs/examples/temporary-key-groups.md)

> 💡 **Tip**: To change the port, edit the `ports` section in `docker-compose.yml`.

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process -Force
- **API 文档(Swagger UI)**:`http://localhost:23000/api/actions/docs`
- **公开状态 API**:[docs/public-status-api.md](docs/public-status-api.md)
- **API 认证指南**:[docs/api-authentication-guide.md](docs/api-authentication-guide.md)
- **临时 Key 分组 API 示例**:[docs/examples/temporary-key-groups.md](docs/examples/temporary-key-groups.md)

> 💡 **提示**:
> - 如需修改端口,请编辑 `docker-compose.yml` 中的 `ports` 配置。
Expand Down
10 changes: 9 additions & 1 deletion dev/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ services:
condition: service_healthy
environment:
NODE_ENV: production
HOST: 0.0.0.0
HOSTNAME: 0.0.0.0
DSN: postgresql://${DB_USER:-postgres}:${DB_PASSWORD:-postgres}@postgres:5432/${DB_NAME:-claude_code_hub}
REDIS_URL: redis://redis:6379
AUTO_MIGRATE: ${AUTO_MIGRATE:-true}
Expand All @@ -62,7 +64,13 @@ services:
ports:
- "${APP_PORT:-23000}:3000"
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:3000/api/actions/health || exit 1"]
test:
[
"CMD",
"node",
"-e",
"fetch('http://127.0.0.1:3000/api/actions/health').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))",
]
interval: 15s
timeout: 5s
retries: 20
Expand Down
5 changes: 4 additions & 1 deletion docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ services:
- ./.env
environment:
NODE_ENV: production
NODE_OPTIONS: ${NODE_OPTIONS:---max-old-space-size=1536}
HOST: 0.0.0.0
HOSTNAME: 0.0.0.0
# 容器内使用 Dockerfile 默认端口 3000,对外通过 APP_PORT 暴露(默认 23000)
DSN: postgresql://${DB_USER:-postgres}:${DB_PASSWORD:-postgres}@postgres:5432/${DB_NAME:-claude_code_hub}
REDIS_URL: redis://redis:6379
Expand All @@ -77,7 +80,7 @@ services:
"CMD",
"node",
"-e",
"fetch('http://' + (process.env.HOSTNAME || '127.0.0.1') + ':3000/api/actions/health').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))",
"fetch('http://127.0.0.1:3000/api/actions/health').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))",
]
interval: 30s
timeout: 5s
Expand Down
90 changes: 90 additions & 0 deletions docs/examples/api-key-quota-extractor-compatible.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
({
request: {
url: "{{baseUrl}}/api/actions/my-usage/getMyQuota",
method: "POST",
headers: {
"Authorization": "Bearer {{apiKey}}",
"Content-Type": "application/json",
"User-Agent": "cc-switch/1.0"
},
body: "{}"
},

extractor: function(response) {
const data = response && response.ok === true && response.data && typeof response.data === "object"
? response.data
: {};

const toNumber = function(value, fallback) {
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
};

const toBoolean = function(value, fallback) {
return typeof value === "boolean" ? value : fallback;
};

const round2 = function(value) {
return Math.round(value * 100) / 100;
};

const percent = function(used, total) {
return total > 0 ? round2((used / total) * 100) : null;
};

const quotaWindows = data.quotaWindows && typeof data.quotaWindows === "object"
? data.quotaWindows
: {};
const fiveHour = quotaWindows.fiveHour || {};
const daily = quotaWindows.daily || {};
const weekly = quotaWindows.weekly || {};
const monthly = quotaWindows.monthly || {};
const total = quotaWindows.total || {};

const limitMonthlyUsd = toNumber(data.limitMonthlyUsd, null);
const limitTotalUsd = toNumber(data.limitTotalUsd, limitMonthlyUsd);
const usedTotalUsd = toNumber(data.usedTotalUsd, toNumber(data.usedMonthlyUsd, 0));
const remainingTotalUsd = limitTotalUsd === null ? null : round2(Math.max(limitTotalUsd - usedTotalUsd, 0));

const limitDailyUsd = toNumber(data.limitDailyUsd, null);
const usedDailyUsd = toNumber(data.usedDailyUsd, 0);
const remainingDailyUsd = limitDailyUsd === null ? null : round2(Math.max(limitDailyUsd - usedDailyUsd, 0));

const limit5hUsd = toNumber(data.limit5hUsd, null);
const used5hUsd = toNumber(data.used5hUsd, 0);
const remaining5hUsd = limit5hUsd === null ? null : round2(Math.max(limit5hUsd - used5hUsd, 0));

const isValid =
response &&
response.ok === true &&
toBoolean(data.keyIsEnabled, true) &&
toBoolean(data.userIsEnabled, true);

return {
isValid: !!isValid,
invalidMessage: isValid ? undefined : "套餐不可用",
planName: "Total Quota",
unit: typeof data.unit === "string" ? data.unit : "USD",
remaining: toNumber(total.remainingUsd, remainingTotalUsd),
total: toNumber(total.limitUsd, limitTotalUsd),
used: toNumber(total.usedUsd, usedTotalUsd),
todayUsed: toNumber(data.todayUsedUsd, toNumber(daily.usedUsd, usedDailyUsd)),
todayRemaining: toNumber(data.todayRemainingUsd, toNumber(daily.remainingUsd, remainingDailyUsd)),
todayUsedPercent: toNumber(data.todayUsedPercent, toNumber(daily.usedPercent, percent(usedDailyUsd, limitDailyUsd))),
todayRemainingPercent: toNumber(
data.todayRemainingPercent,
toNumber(daily.remainingPercent, percent(remainingDailyUsd || 0, limitDailyUsd))
),
remaining5h: toNumber(fiveHour.remainingUsd, remaining5hUsd),
remainingDaily: toNumber(daily.remainingUsd, remainingDailyUsd),
remainingWeekly: toNumber(weekly.remainingUsd, toNumber(data.remainingWeeklyUsd, null)),
remainingMonthly: toNumber(monthly.remainingUsd, toNumber(data.remainingMonthlyUsd, null)),
remainingTotal: toNumber(total.remainingUsd, remainingTotalUsd),
remainingPercent: toNumber(total.remainingPercent, data.remainingPercent),
extra: "5H剩余:" + toNumber(fiveHour.remainingPercent, percent(remaining5hUsd || 0, limit5hUsd)) + "%"
+ "/日剩余:" + toNumber(daily.remainingPercent, percent(remainingDailyUsd || 0, limitDailyUsd)) + "%"
+ "/周剩余:" + toNumber(weekly.remainingPercent, null) + "%"
+ "/月剩余:" + toNumber(monthly.remainingPercent, null) + "%"
+ "/总剩余:" + toNumber(total.remainingPercent, data.remainingPercent) + "%"
};
}
})
48 changes: 48 additions & 0 deletions docs/examples/api-key-quota-extractor-daily.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
({
request: {
url: "{{baseUrl}}/api/actions/my-usage/getMyQuota",
method: "POST",
headers: {
"Authorization": "Bearer {{apiKey}}",
"Content-Type": "application/json",
"User-Agent": "cc-switch/1.0"
},
body: "{}"
},

extractor: function(response) {
const data = response && response.ok === true && response.data && typeof response.data === "object"
? response.data
: {};

const toNumber = function(value, fallback) {
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
};

const toBoolean = function(value, fallback) {
return typeof value === "boolean" ? value : fallback;
};

const quotaWindows = data.quotaWindows && typeof data.quotaWindows === "object"
? data.quotaWindows
: {};
const daily = quotaWindows.daily || {};

return {
ok: response && response.ok === true,
isValid: toBoolean(data.keyIsEnabled, true) && toBoolean(data.userIsEnabled, true),
planName: "Daily Quota",
remaining: toNumber(daily.remainingUsd, toNumber(data.remainingDailyUsd, null)),
total: toNumber(daily.limitUsd, toNumber(data.limitDailyUsd, null)),
used: toNumber(daily.usedUsd, toNumber(data.usedDailyUsd, 0)),
usedPercent: toNumber(daily.usedPercent, data.todayUsedPercent),
remainingPercent: toNumber(daily.remainingPercent, data.todayRemainingPercent),
unit: typeof data.unit === "string" ? data.unit : "USD",
keyName: typeof data.keyName === "string" ? data.keyName : null,
userName: typeof data.userName === "string" ? data.userName : null,
providerGroup: typeof data.providerGroup === "string" ? data.providerGroup : null,
resetMode: typeof data.resetMode === "string" ? data.resetMode : null,
resetTime: typeof data.resetTime === "string" ? data.resetTime : null
};
}
})
61 changes: 61 additions & 0 deletions docs/examples/api-key-quota-extractor-direct.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
({
request: {
url: "{{baseUrl}}/api/actions/my-usage/getMyQuota",
method: "POST",
headers: {
"Authorization": "Bearer {{apiKey}}",
"Content-Type": "application/json",
"User-Agent": "cc-switch/1.0"
},
body: "{}"
},

extractor: function(response) {
const data = response && response.ok === true && response.data && typeof response.data === "object"
? response.data
: {};
const quotaWindows = data.quotaWindows && typeof data.quotaWindows === "object"
? data.quotaWindows
: {};
const fiveHour = quotaWindows.fiveHour || {};
const daily = quotaWindows.daily || {};
const weekly = quotaWindows.weekly || {};
const monthly = quotaWindows.monthly || {};
const total = quotaWindows.total || {};

const toBoolean = function(value, fallback) {
return typeof value === "boolean" ? value : fallback;
};

const isValid =
response &&
response.ok === true &&
toBoolean(data.keyIsEnabled, true) &&
toBoolean(data.userIsEnabled, true);

return {
isValid: !!isValid,
invalidMessage: isValid ? undefined : "套餐不可用",
planName: "Total Quota",
unit: typeof data.unit === "string" ? data.unit : "USD",
remaining: total.remainingUsd,
total: total.limitUsd,
used: total.usedUsd,
todayUsed: data.todayUsedUsd,
todayRemaining: data.todayRemainingUsd,
todayUsedPercent: data.todayUsedPercent,
todayRemainingPercent: data.todayRemainingPercent,
remaining5h: fiveHour.remainingUsd,
remainingDaily: daily.remainingUsd,
remainingWeekly: weekly.remainingUsd,
remainingMonthly: monthly.remainingUsd,
remainingTotal: total.remainingUsd,
remainingPercent: data.remainingPercent,
extra: "5H剩余:" + fiveHour.remainingPercent + "%"
+ "/日剩余:" + daily.remainingPercent + "%"
+ "/周剩余:" + weekly.remainingPercent + "%"
+ "/月剩余:" + monthly.remainingPercent + "%"
+ "/总剩余:" + total.remainingPercent + "%"
};
}
})
48 changes: 48 additions & 0 deletions docs/examples/api-key-quota-extractor-total.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
({
request: {
url: "{{baseUrl}}/api/actions/my-usage/getMyQuota",
method: "POST",
headers: {
"Authorization": "Bearer {{apiKey}}",
"Content-Type": "application/json",
"User-Agent": "cc-switch/1.0"
},
body: "{}"
},

extractor: function(response) {
const data = response && response.ok === true && response.data && typeof response.data === "object"
? response.data
: {};

const toNumber = function(value, fallback) {
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
};

const toBoolean = function(value, fallback) {
return typeof value === "boolean" ? value : fallback;
};

const quotaWindows = data.quotaWindows && typeof data.quotaWindows === "object"
? data.quotaWindows
: {};
const total = quotaWindows.total || {};

return {
ok: response && response.ok === true,
isValid: toBoolean(data.keyIsEnabled, true) && toBoolean(data.userIsEnabled, true),
planName: "Total Quota",
remaining: toNumber(total.remainingUsd, toNumber(data.remainingTotalUsd, null)),
total: toNumber(total.limitUsd, toNumber(data.limitTotalUsd, null)),
used: toNumber(total.usedUsd, toNumber(data.usedTotalUsd, 0)),
usedPercent: toNumber(total.usedPercent, null),
remainingPercent: toNumber(total.remainingPercent, data.remainingPercent),
unit: typeof data.unit === "string" ? data.unit : "USD",
keyName: typeof data.keyName === "string" ? data.keyName : null,
userName: typeof data.userName === "string" ? data.userName : null,
providerGroup: typeof data.providerGroup === "string" ? data.providerGroup : null,
resetMode: typeof data.resetMode === "string" ? data.resetMode : null,
resetTime: typeof data.resetTime === "string" ? data.resetTime : null
};
}
})
48 changes: 48 additions & 0 deletions docs/examples/api-key-quota-extractor-weekly.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
({
request: {
url: "{{baseUrl}}/api/actions/my-usage/getMyQuota",
method: "POST",
headers: {
"Authorization": "Bearer {{apiKey}}",
"Content-Type": "application/json",
"User-Agent": "cc-switch/1.0"
},
body: "{}"
},

extractor: function(response) {
const data = response && response.ok === true && response.data && typeof response.data === "object"
? response.data
: {};

const toNumber = function(value, fallback) {
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
};

const toBoolean = function(value, fallback) {
return typeof value === "boolean" ? value : fallback;
};

const quotaWindows = data.quotaWindows && typeof data.quotaWindows === "object"
? data.quotaWindows
: {};
const weekly = quotaWindows.weekly || {};

return {
ok: response && response.ok === true,
isValid: toBoolean(data.keyIsEnabled, true) && toBoolean(data.userIsEnabled, true),
planName: "Weekly Quota",
remaining: toNumber(weekly.remainingUsd, toNumber(data.remainingWeeklyUsd, null)),
total: toNumber(weekly.limitUsd, toNumber(data.limitWeeklyUsd, null)),
used: toNumber(weekly.usedUsd, toNumber(data.usedWeeklyUsd, 0)),
usedPercent: toNumber(weekly.usedPercent, null),
remainingPercent: toNumber(weekly.remainingPercent, null),
unit: typeof data.unit === "string" ? data.unit : "USD",
keyName: typeof data.keyName === "string" ? data.keyName : null,
userName: typeof data.userName === "string" ? data.userName : null,
providerGroup: typeof data.providerGroup === "string" ? data.providerGroup : null,
resetMode: typeof data.resetMode === "string" ? data.resetMode : null,
resetTime: typeof data.resetTime === "string" ? data.resetTime : null
};
}
})
Loading
Loading