v0.1.3-rc.3.3

限制 `TOKEN_DELIMITER` 为 ASCII 避免问题
This commit is contained in:
wisdgod
2025-01-18 04:19:55 +08:00
parent 742c2e1c5c
commit 3e304f53d4
10 changed files with 43 additions and 112 deletions

View File

@@ -2,9 +2,9 @@ name: Build
on:
workflow_dispatch:
push:
tags:
- 'v*'
# push:
# tags:
# - 'v*'
jobs:
build:

View File

@@ -49,7 +49,7 @@ jobs:
tags: |
type=raw,value=latest,enable=${{ github.event_name == 'workflow_dispatch' && inputs.update_latest }}
type=raw,value=${{ steps.cargo_version.outputs.version }},enable=${{ github.event_name == 'workflow_dispatch' }}
type=ref,event=tag,enable=${{ github.event_name == 'push' }}
type=raw,value=${{ github.ref_name }},enable=${{ github.event_name == 'push' }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.8.0
@@ -70,17 +70,24 @@ jobs:
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
outputs: type=local,dest=./dist,enable=${{ github.event_name == 'workflow_dispatch' && inputs.upload_artifacts }}
outputs: type=local,dest=./dist,enable=${{ (github.event_name == 'workflow_dispatch' && inputs.upload_artifacts) || github.event_name == 'push'}}
- name: Prepare artifacts
if: github.event_name == 'workflow_dispatch' && inputs.upload_artifacts
run: |
mkdir -p artifacts/amd64 artifacts/arm64
cp dist/linux_amd64/app/cursor-api artifacts/amd64/
cp dist/linux_arm64/app/cursor-api artifacts/arm64/
mkdir -p artifacts
cp dist/linux_amd64/app/cursor-api artifacts/cursor-api-x86_64-${{ steps.cargo_version.outputs.version }}
cp dist/linux_arm64/app/cursor-api artifacts/cursor-api-aarch64-${{ steps.cargo_version.outputs.version }}
- name: Prepare artifacts
if: github.event_name == 'push'
run: |
mkdir -p artifacts
cp dist/linux_amd64/app/cursor-api artifacts/cursor-api-x86_64-${{ github.event_name }}
cp dist/linux_arm64/app/cursor-api artifacts/cursor-api-aarch64-${{ github.event_name }}
- name: Upload artifacts
if: github.event_name == 'workflow_dispatch' && inputs.upload_artifacts
if: (github.event_name == 'workflow_dispatch' && inputs.upload_artifacts) || github.event_name == 'push'
uses: actions/upload-artifact@v4.6.0
with:
name: cursor-api-binaries

2
Cargo.lock generated
View File

@@ -327,7 +327,7 @@ dependencies = [
[[package]]
name = "cursor-api"
version = "0.1.3-rc.3.2"
version = "0.1.3-rc.3.3"
dependencies = [
"axum",
"base64",

View File

@@ -1,6 +1,6 @@
[package]
name = "cursor-api"
version = "0.1.3-rc.3.2"
version = "0.1.3-rc.3.3"
edition = "2021"
authors = ["wisdgod <nav@wisdgod.com>"]
description = "OpenAI format compatibility layer for the Cursor API"

View File

@@ -1,5 +1,5 @@
# AMD64 构建阶段
FROM --platform=linux/amd64 rust:1.83.0-slim-bookworm as builder-amd64
FROM --platform=linux/amd64 rust:1.84.0-slim-bookworm as builder-amd64
WORKDIR /app
RUN apt-get update && \
apt-get install -y --no-install-recommends \
@@ -11,7 +11,7 @@ RUN cargo build --release && \
cp target/release/cursor-api /app/cursor-api
# ARM64 构建阶段
FROM --platform=linux/arm64 rust:1.83.0-slim-bookworm as builder-arm64
FROM --platform=linux/arm64 rust:1.84.0-slim-bookworm as builder-arm64
WORKDIR /app
RUN apt-get update && \
apt-get install -y --no-install-recommends \

View File

@@ -3,7 +3,7 @@ use crate::{
CURSOR_API2_HOST, CURSOR_HOST, DEFAULT_TOKEN_FILE_NAME, DEFAULT_TOKEN_LIST_FILE_NAME,
EMPTY_STRING,
},
common::utils::{parse_char_from_env, parse_string_from_env},
common::utils::{parse_ascii_char_from_env, parse_string_from_env},
};
use std::sync::LazyLock;
@@ -55,8 +55,18 @@ def_pub_static!(SHARED_AUTH_TOKEN, env: "SHARED_AUTH_TOKEN", default: EMPTY_STRI
pub static USE_SHARE: LazyLock<bool> = LazyLock::new(|| !SHARED_AUTH_TOKEN.is_empty());
pub static TOKEN_DELIMITER: LazyLock<char> =
LazyLock::new(|| parse_char_from_env("TOKEN_DELIMITER", ','));
pub static TOKEN_DELIMITER: LazyLock<char> = LazyLock::new(|| {
let delimiter = parse_ascii_char_from_env("TOKEN_DELIMITER", ',');
if delimiter.is_ascii_alphabetic()
|| delimiter.is_ascii_digit()
|| delimiter == '+'
|| delimiter == '/'
{
','
} else {
delimiter
}
});
pub static TOKEN_DELIMITER_LEN: LazyLock<usize> = LazyLock::new(|| TOKEN_DELIMITER.len_utf8());

View File

@@ -24,12 +24,12 @@ pub fn parse_string_from_env(key: &str, default: &str) -> String {
std::env::var(key).unwrap_or_else(|_| default.to_string())
}
pub fn parse_char_from_env(key: &str, default: char) -> char {
pub fn parse_ascii_char_from_env(key: &str, default: char) -> char {
std::env::var(key)
.ok()
.and_then(|v| {
let chars: Vec<char> = v.chars().collect();
if chars.len() == 1 {
if chars.len() == 1 && chars[0].is_ascii() {
Some(chars[0])
} else {
None

View File

@@ -307,24 +307,13 @@
// 添加用户基本信息
if (tokenInfo.user || calibInfo) {
const user = tokenInfo.user || {};
userDetails.innerHTML += `
<p>用户ID: ${calibInfo ? calibInfo.user_id : user.id}</p>
<p>邮箱: ${user.email || ''}</p>
<p>用户名: ${user.name || ''}</p>
${user.updated_at ? `<p>更新时间: ${new Date(user.updated_at).toLocaleString()}</p>` : ''}
${calibInfo ? `<p>令牌创建时间: ${new Date(calibInfo.create_at).toLocaleString()}</p>` : ''}
${calibInfo && calibInfo.checksum_time ? `<p>校验和时间区间: ${new Date(calibInfo.checksum_time * 1e6).toLocaleString()} - ${new Date((calibInfo.checksum_time + 1) * 1e6 - 1).toLocaleString()}</p>` : ''}
`;
userDetails.innerHTML += `<p>用户ID: ${calibInfo ? calibInfo.user_id : user.id}</p><p>邮箱: ${user.email || ''}</p><p>用户名: ${user.name || ''}</p>${user.updated_at ? `<p>更新时间: ${new Date(user.updated_at).toLocaleString()}</p>` : ''}${calibInfo ? `<p>令牌创建时间: ${new Date(calibInfo.create_at).toLocaleString()}</p>` : ''}${calibInfo && calibInfo.checksum_time ? `<p>校验和时间区间: ${new Date(calibInfo.checksum_time * 1e6).toLocaleString()} - ${new Date((calibInfo.checksum_time + 1) * 1e6 - 1).toLocaleString()}</p>` : ''}`;
}
// 添加 Stripe 会员信息
if (tokenInfo.stripe) {
const stripe = tokenInfo.stripe;
userDetails.innerHTML += `
<p>会员类型: ${stripe.membership_type}</p>
${stripe.payment_id ? `<p>付款 ID: ${stripe.payment_id}</p>` : ''}
<p>试用剩余: ${stripe.days_remaining_on_trial} 天</p>
`;
userDetails.innerHTML += `<p>会员类型: ${stripe.membership_type}</p>${stripe.payment_id ? `<p>付款 ID: ${stripe.payment_id}</p>` : ''}<p>试用剩余: ${stripe.days_remaining_on_trial} 天</p>`;
}
// 添加使用情况进度条
@@ -342,15 +331,7 @@
const percentage = isUnlimited ? 100 : (data.requests / data.max_requests * 100).toFixed(1);
const progressClass = isUnlimited ? 'unlimited' : getProgressBarClass(parseFloat(percentage));
progressContainer.innerHTML += `
<div>
<p>${modelName}: ${data.requests}/${isUnlimited ? '∞' : data.max_requests} 请求
${isUnlimited ? '' : `(${percentage}%)`}, ${data.tokens} tokens</p>
<div class="usage-progress-container">
<div class="usage-progress-bar ${progressClass}" style="width: ${percentage}%"></div>
</div>
</div>
`;
progressContainer.innerHTML += `<div><p>${modelName}: ${data.requests}/${isUnlimited ? '∞' : data.max_requests} 请求 ${isUnlimited ? '' : `(${percentage}%)`}, ${data.tokens} tokens</p><div class="usage-progress-container"><div class="usage-progress-bar ${progressClass}" style="width: ${percentage}%"></div></div></div>`;
}
});
}

View File

@@ -584,9 +584,7 @@
const progressDiv = document.createElement('div');
progressDiv.className = 'usage-progress-container';
const colorClass = getProgressBarClass(parseFloat(percentage));
progressDiv.innerHTML = `
<div class="usage-progress-bar ${colorClass}" style="width: ${percentage}%"></div>
`;
progressDiv.innerHTML = `<div class="usage-progress-bar ${colorClass}" style="width: ${percentage}%"></div>`;
container.appendChild(progressDiv);
} else {
element.textContent = `${requests} requests, ${tokens} tokens`;
@@ -629,54 +627,14 @@
['Premium', premiumUsage]
];
return rows.map(([label, value]) => `
<div class="tooltip-info-row">
<span class="label">${label}:</span>
<span class="value">${value}</span>
</div>
`).join('');
return rows.map(([label, value]) => `<div class="tooltip-info-row"><span class="label">${label}:</span><span class="value">${value}</span></div>`).join('');
}
function updateTable(data) {
const tbody = document.getElementById('logsBody');
updateStats(data);
tbody.innerHTML = data.logs.map(log => `
<tr>
<td>${log.id}</td>
<td>${new Date(log.timestamp).toLocaleString()}</td>
<td>${log.model}</td>
<td>
<div class="token-info-tooltip">
<button class="info-button" onclick='showTokenModal(${JSON.stringify(log.token_info)})'>
查看详情
<div class="tooltip-content">
${formatSimpleTokenInfo(log.token_info)}
</div>
</button>
</div>
</td>
<td>
${log.prompt ?
`<div class="token-info-tooltip prompt-preview">
<button class="info-button" onclick="showPromptModal(decodeURIComponent('${encodeURIComponent(log.prompt).replace(/'/g, "\\'")}'))">
查看对话
<div class="tooltip-content">
${formatPromptPreview(log.prompt)}
</div>
</button>
</div>` :
'-'
}
</td>
<td>
${formatTiming(log.timing.total, log.timing.first)}
</td>
<td>${log.stream ? '是' : '否'}</td>
<td>${log.status}</td>
<td>${log.error || '-'}</td>
</tr>
`).join('');
tbody.innerHTML = data.logs.map(log => `<tr><td>${log.id}</td><td>${new Date(log.timestamp).toLocaleString()}</td><td>${log.model}</td><td><div class="token-info-tooltip"><button class="info-button" onclick='showTokenModal(${JSON.stringify(log.token_info)})'>查看详情<div class="tooltip-content">${formatSimpleTokenInfo(log.token_info)}</div></button></div></td><td>${log.prompt ?`<div class="token-info-tooltip prompt-preview"><button class="info-button" onclick="showPromptModal(decodeURIComponent('${encodeURIComponent(log.prompt).replace(/'/g, "\\'")}'))">查看对话<div class="tooltip-content">${formatPromptPreview(log.prompt)}</div></button></div>` :'-'}</td><td>${formatTiming(log.timing.total, log.timing.first)}</td><td>${log.stream ? '是' : '否'}</td><td>${log.status}</td><td>${log.error || '-'}</td></tr>`).join('');
}
function formatTiming(total, first) {
@@ -700,15 +658,7 @@
'assistant': '助手'
};
return `
<div class="message-meta">最后一条消息 (${roleLabels[lastMessage.role] || lastMessage.role}):</div>
<div class="last-message">${lastMessage.content
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\n/g, '<br>')
}</div>
`;
return `<div class="message-meta">最后一条消息 (${roleLabels[lastMessage.role] || lastMessage.role}):</div><div class="last-message">${lastMessage.content.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, '<br>')}</div>`;
} catch (e) {
console.error('预览对话内容失败:', e);
return '无法解析对话内容';

View File

@@ -214,24 +214,7 @@ function formatPromptToTable(messages) {
return escaped;
}
return `
<table class="message-table">
<thead>
<tr>
<th>角色</th>
<th>内容</th>
</tr>
</thead>
<tbody>
${messages.map(msg => `
<tr>
<td>${roleLabels[msg.role] || msg.role}</td>
<td>${escapeHtml(msg.content).replace(/\n/g, '<br>')}</td>
</tr>
`).join('')}
</tbody>
</table>
`;
return `<table class="message-table"><thead><tr><th>角色</th><th>内容</th></tr></thead><tbody>${messages.map(msg => `<tr><td>${roleLabels[msg.role] || msg.role}</td><td>${escapeHtml(msg.content).replace(/\n/g, '<br>')}</td></tr>`).join('')}</tbody></table>`;
}
/**