Compare commits

...

2 Commits

8 changed files with 88 additions and 41 deletions

View File

@@ -15,6 +15,9 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- name: Setup Node.js
uses: actions/setup-node@v4
@@ -36,27 +39,48 @@ jobs:
mv index.html management.html
ls -lh management.html
- name: Generate release notes
run: |
set -euo pipefail
current_tag="${GITHUB_REF_NAME}"
previous_tag="$(git tag --list 'v*' --sort=-v:refname | grep -v "^${current_tag}$" | head -n 1 || true)"
if [ -n "${previous_tag}" ]; then
range="${previous_tag}..${current_tag}"
title="Changes since ${previous_tag}"
else
range="${current_tag}"
title="Changes"
fi
{
echo "## CLI Proxy API Management Center - ${current_tag}"
echo
echo "### Download and Usage"
echo "1. Download the \`management.html\` file"
echo "2. Open it directly in your browser"
echo "3. All assets (CSS, JavaScript, images) are bundled into this single file"
echo
echo "### Features"
echo "- Single file, no external dependencies required"
echo "- Complete management interface for CLI Proxy API"
echo "- Support for local and remote connections"
echo "- Multi-language support (Chinese/English)"
echo "- Dark/Light theme support"
echo
echo "### ${title}"
echo
git log --pretty=format:"- %h %s" "${range}"
echo
echo
echo "---"
echo "🤖 Generated with GitHub Actions"
} > release-notes.md
- name: Create Release
uses: softprops/action-gh-release@v1
with:
files: dist/management.html
body: |
## CLI Proxy API Management Center - ${{ github.ref_name }}
### Download and Usage
1. Download the `management.html` file
2. Open it directly in your browser
3. All assets (CSS, JavaScript, images) are bundled into this single file
### Features
- Single file, no external dependencies required
- Complete management interface for CLI Proxy API
- Support for local and remote connections
- Multi-language support (Chinese/English)
- Dark/Light theme support
---
🤖 Generated with GitHub Actions
body_path: release-notes.md
draft: false
prerelease: false
env:

View File

@@ -1,7 +1,13 @@
export function LoadingSpinner({ size = 20 }: { size?: number }) {
export function LoadingSpinner({
size = 20,
className = ''
}: {
size?: number;
className?: string;
}) {
return (
<div
className="loading-spinner"
className={`loading-spinner${className ? ` ${className}` : ''}`}
style={{ width: size, height: size, borderWidth: size / 7 }}
role="status"
aria-live="polite"

View File

@@ -404,8 +404,17 @@
}
.excludedModelTag {
background: rgba(251, 191, 36, 0.2);
border-color: rgba(251, 191, 36, 0.4);
background: rgba(251, 191, 36, 0.22);
border-color: rgba(251, 191, 36, 0.55);
color: #fde68a;
.modelName {
color: #fde68a;
}
}
.excludedModelsLabel {
color: #fde68a;
}
.apiKeyEntryCard {

View File

@@ -1897,19 +1897,21 @@ export function AiProvidersPage() {
keyPlaceholder={t('common.custom_headers_key_placeholder')}
valuePlaceholder={t('common.custom_headers_value_placeholder')}
/>
<div className="form-group">
<label>{t('ai_providers.claude_models_label')}</label>
<ModelInputList
entries={providerForm.modelEntries}
onChange={(entries) =>
setProviderForm((prev) => ({ ...prev, modelEntries: entries }))
}
addLabel={t('ai_providers.claude_models_add_btn')}
namePlaceholder={t('common.model_name_placeholder')}
aliasPlaceholder={t('common.model_alias_placeholder')}
disabled={saving}
/>
</div>
{modal?.type === 'claude' && (
<div className="form-group">
<label>{t('ai_providers.claude_models_label')}</label>
<ModelInputList
entries={providerForm.modelEntries}
onChange={(entries) =>
setProviderForm((prev) => ({ ...prev, modelEntries: entries }))
}
addLabel={t('ai_providers.claude_models_add_btn')}
namePlaceholder={t('common.model_name_placeholder')}
aliasPlaceholder={t('common.model_alias_placeholder')}
disabled={saving}
/>
</div>
)}
<div className="form-group">
<label>{t('ai_providers.excluded_models_label')}</label>
<textarea

View File

@@ -66,11 +66,12 @@
border: 1px solid var(--border-color);
background: var(--bg-primary);
box-shadow: var(--shadow-lg);
}
:global(.loading-spinner) {
border-color: rgba(59, 130, 246, 0.25);
border-top-color: var(--primary-color);
}
.loadingOverlaySpinner {
border-color: rgba(59, 130, 246, 0.25);
border-top-color: var(--primary-color);
box-shadow: 0 0 10px rgba(59, 130, 246, 0.25);
}
.loadingOverlayText {

View File

@@ -520,7 +520,7 @@ export function UsagePage() {
{loading && !usage && (
<div className={styles.loadingOverlay} aria-busy="true">
<div className={styles.loadingOverlayContent}>
<LoadingSpinner size={28} />
<LoadingSpinner size={28} className={styles.loadingOverlaySpinner} />
<span className={styles.loadingOverlayText}>{t('common.loading')}</span>
</div>
</div>

View File

@@ -3,6 +3,7 @@
*/
import { apiClient } from './client';
import { LOGS_TIMEOUT_MS } from '@/utils/constants';
export interface LogsQuery {
after?: number;
@@ -25,14 +26,17 @@ export interface ErrorLogsResponse {
}
export const logsApi = {
fetchLogs: (params: LogsQuery = {}): Promise<LogsResponse> => apiClient.get('/logs', { params }),
fetchLogs: (params: LogsQuery = {}): Promise<LogsResponse> =>
apiClient.get('/logs', { params, timeout: LOGS_TIMEOUT_MS }),
clearLogs: () => apiClient.delete('/logs'),
fetchErrorLogs: (): Promise<ErrorLogsResponse> => apiClient.get('/request-error-logs'),
fetchErrorLogs: (): Promise<ErrorLogsResponse> =>
apiClient.get('/request-error-logs', { timeout: LOGS_TIMEOUT_MS }),
downloadErrorLog: (filename: string) =>
apiClient.getRaw(`/request-error-logs/${encodeURIComponent(filename)}`, {
responseType: 'blob',
timeout: LOGS_TIMEOUT_MS
}),
};

View File

@@ -18,6 +18,7 @@ export const LOG_REFRESH_DELAY_MS = 500;
// 日志相关
export const MAX_LOG_LINES = 2000;
export const LOG_FETCH_LIMIT = 2500;
export const LOGS_TIMEOUT_MS = 60 * 1000;
// 认证文件分页
export const DEFAULT_AUTH_FILES_PAGE_SIZE = 20;