diff --git a/.claude/settings.local.json b/.claude/settings.local.json index bc967eb..c260414 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -2,7 +2,9 @@ "permissions": { "allow": [ "WebSearch", - "Bash(find:*)" + "Bash(find:*)", + "mcp__exa__web_search_exa", + "mcp__ace-tool__search_context" ], "deny": [], "ask": [] diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 41139f7..f1da945 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,8 +19,10 @@ jobs: - uses: actions/checkout@v4 - name: Update version from tag + id: version run: | VERSION=${GITHUB_REF#refs/tags/v} + echo "VERSION=$VERSION" >> $GITHUB_OUTPUT echo "Updating version to: $VERSION" sed -i '' "s/const AppVersion = \"v[^\"]*\"/const AppVersion = \"v$VERSION\"/" version_service.go echo "Updated version_service.go:" @@ -64,13 +66,13 @@ jobs: - name: Archive macOS app run: | cd bin - ditto -c -k --sequesterRsrc --keepParent "$(basename ${{ steps.find-app.outputs.app_path }})" codeswitch-macos-${{ matrix.arch }}.zip + ditto -c -k --sequesterRsrc --keepParent "$(basename ${{ steps.find-app.outputs.app_path }})" CodeSwitch-v${{ steps.version.outputs.VERSION }}-macos-${{ matrix.arch }}.zip - name: Upload artifact uses: actions/upload-artifact@v4 with: name: macos-${{ matrix.arch }} - path: bin/codeswitch-macos-${{ matrix.arch }}.zip + path: bin/CodeSwitch-v${{ steps.version.outputs.VERSION }}-macos-${{ matrix.arch }}.zip build-windows: name: Build Windows @@ -79,9 +81,11 @@ jobs: - uses: actions/checkout@v4 - name: Update version from tag + id: version shell: pwsh run: | $VERSION = "${{ github.ref_name }}".TrimStart('v') + "VERSION=$VERSION" | Out-File -FilePath $env:GITHUB_OUTPUT -Append Write-Host "Updating version to: $VERSION" # Update version_service.go @@ -161,11 +165,30 @@ jobs: go build -ldflags="-w -s -H windowsgui" -o bin/updater.exe ./cmd/updater Remove-Item cmd/updater/*.syso -ErrorAction SilentlyContinue + - name: Rename files with version + shell: pwsh + run: | + $VERSION = "${{ steps.version.outputs.VERSION }}" + Push-Location bin + Rename-Item "CodeSwitch.exe" "CodeSwitch-v$VERSION.exe" + Rename-Item "updater.exe" "updater-v$VERSION.exe" + Pop-Location + # Rename installer (generated by NSIS) + if (Test-Path "build/windows/nsis/CodeSwitch-amd64-installer.exe") { + Move-Item "build/windows/nsis/CodeSwitch-amd64-installer.exe" "bin/CodeSwitch-v$VERSION-amd64-installer.exe" + } elseif (Test-Path "bin/CodeSwitch-amd64-installer.exe") { + Rename-Item "bin/CodeSwitch-amd64-installer.exe" "CodeSwitch-v$VERSION-amd64-installer.exe" + } + Write-Host "Files renamed:" + Get-ChildItem bin + - name: Generate SHA256 Checksums + shell: pwsh run: | + $VERSION = "${{ steps.version.outputs.VERSION }}" Push-Location bin - Get-FileHash -Algorithm SHA256 CodeSwitch.exe | ForEach-Object { "$($_.Hash.ToLower()) CodeSwitch.exe" } | Out-File -Encoding ascii CodeSwitch.exe.sha256 - Get-FileHash -Algorithm SHA256 updater.exe | ForEach-Object { "$($_.Hash.ToLower()) updater.exe" } | Out-File -Encoding ascii updater.exe.sha256 + Get-FileHash -Algorithm SHA256 "CodeSwitch-v$VERSION.exe" | ForEach-Object { "$($_.Hash.ToLower()) CodeSwitch-v$VERSION.exe" } | Out-File -Encoding ascii "CodeSwitch-v$VERSION.exe.sha256" + Get-FileHash -Algorithm SHA256 "updater-v$VERSION.exe" | ForEach-Object { "$($_.Hash.ToLower()) updater-v$VERSION.exe" } | Out-File -Encoding ascii "updater-v$VERSION.exe.sha256" Write-Host "SHA256 checksums generated:" Get-Content *.sha256 Pop-Location @@ -175,11 +198,11 @@ jobs: with: name: windows-amd64 path: | - bin/CodeSwitch-amd64-installer.exe - bin/CodeSwitch.exe - bin/CodeSwitch.exe.sha256 - bin/updater.exe - bin/updater.exe.sha256 + bin/CodeSwitch-v${{ steps.version.outputs.VERSION }}-amd64-installer.exe + bin/CodeSwitch-v${{ steps.version.outputs.VERSION }}.exe + bin/CodeSwitch-v${{ steps.version.outputs.VERSION }}.exe.sha256 + bin/updater-v${{ steps.version.outputs.VERSION }}.exe + bin/updater-v${{ steps.version.outputs.VERSION }}.exe.sha256 build-linux: name: Build Linux @@ -188,8 +211,10 @@ jobs: - uses: actions/checkout@v4 - name: Update version from tag + id: version run: | VERSION=${GITHUB_REF#refs/tags/v} + echo "VERSION=$VERSION" >> $GITHUB_OUTPUT echo "Updating version to: $VERSION" sed -i "s/const AppVersion = \"v[^\"]*\"/const AppVersion = \"v$VERSION\"/" version_service.go echo "Updated version_service.go:" @@ -237,18 +262,19 @@ jobs: - name: Rename AppImage run: | + VERSION=${{ steps.version.outputs.VERSION }} cd bin echo "Files in bin/:" ls -la # linuxdeploy creates AppImage with lowercase name and arch suffix for f in *-x86_64.AppImage *-aarch64.AppImage; do if [ -f "$f" ]; then - mv "$f" CodeSwitch.AppImage - echo "Renamed $f -> CodeSwitch.AppImage" + mv "$f" "CodeSwitch-v${VERSION}.AppImage" + echo "Renamed $f -> CodeSwitch-v${VERSION}.AppImage" break fi done - ls -la CodeSwitch.AppImage + ls -la "CodeSwitch-v${VERSION}.AppImage" - name: Set nfpm script permissions run: | @@ -269,8 +295,9 @@ jobs: - name: Generate SHA256 Checksums run: | + VERSION=${{ steps.version.outputs.VERSION }} cd bin - sha256sum CodeSwitch.AppImage > CodeSwitch.AppImage.sha256 + sha256sum "CodeSwitch-v${VERSION}.AppImage" > "CodeSwitch-v${VERSION}.AppImage.sha256" for f in codeswitch_*.deb; do [ -f "$f" ] && sha256sum "$f" > "${f}.sha256"; done for f in codeswitch-*.rpm; do [ -f "$f" ] && sha256sum "$f" > "${f}.sha256"; done echo "SHA256 checksums:" @@ -281,8 +308,8 @@ jobs: with: name: linux-amd64 path: | - bin/CodeSwitch.AppImage - bin/CodeSwitch.AppImage.sha256 + bin/CodeSwitch-v${{ steps.version.outputs.VERSION }}.AppImage + bin/CodeSwitch-v${{ steps.version.outputs.VERSION }}.AppImage.sha256 bin/codeswitch_*.deb bin/codeswitch_*.deb.sha256 bin/codeswitch-*.rpm @@ -305,22 +332,23 @@ jobs: - name: Prepare release assets run: | + VERSION=${GITHUB_REF#refs/tags/v} mkdir -p release-assets # macOS - cp artifacts/macos-arm64/codeswitch-macos-arm64.zip release-assets/ - cp artifacts/macos-amd64/codeswitch-macos-amd64.zip release-assets/ + cp artifacts/macos-arm64/CodeSwitch-v${VERSION}-macos-arm64.zip release-assets/ + cp artifacts/macos-amd64/CodeSwitch-v${VERSION}-macos-amd64.zip release-assets/ # Windows - cp artifacts/windows-amd64/CodeSwitch-amd64-installer.exe release-assets/ - cp artifacts/windows-amd64/CodeSwitch.exe release-assets/ - cp artifacts/windows-amd64/CodeSwitch.exe.sha256 release-assets/ - cp artifacts/windows-amd64/updater.exe release-assets/ - cp artifacts/windows-amd64/updater.exe.sha256 release-assets/ + cp artifacts/windows-amd64/CodeSwitch-v${VERSION}-amd64-installer.exe release-assets/ + cp artifacts/windows-amd64/CodeSwitch-v${VERSION}.exe release-assets/ + cp artifacts/windows-amd64/CodeSwitch-v${VERSION}.exe.sha256 release-assets/ + cp artifacts/windows-amd64/updater-v${VERSION}.exe release-assets/ + cp artifacts/windows-amd64/updater-v${VERSION}.exe.sha256 release-assets/ # Linux - cp artifacts/linux-amd64/CodeSwitch.AppImage release-assets/ - cp artifacts/linux-amd64/CodeSwitch.AppImage.sha256 release-assets/ + cp artifacts/linux-amd64/CodeSwitch-v${VERSION}.AppImage release-assets/ + cp artifacts/linux-amd64/CodeSwitch-v${VERSION}.AppImage.sha256 release-assets/ cp artifacts/linux-amd64/codeswitch_*.deb release-assets/ 2>/dev/null || true cp artifacts/linux-amd64/codeswitch_*.deb.sha256 release-assets/ 2>/dev/null || true cp artifacts/linux-amd64/codeswitch-*.rpm release-assets/ 2>/dev/null || true @@ -330,12 +358,13 @@ jobs: - name: Generate latest.json run: | VERSION=${GITHUB_REF#refs/tags/} + VERSION_NUM=${GITHUB_REF#refs/tags/v} REPO="${{ github.repository }}" BASE_URL="https://github.com/${REPO}/releases/download/${VERSION}" # Read SHA256 checksums from existing .sha256 files - WIN_SHA=$(cut -d' ' -f1 release-assets/CodeSwitch.exe.sha256) - LINUX_SHA=$(cut -d' ' -f1 release-assets/CodeSwitch.AppImage.sha256) + WIN_SHA=$(cut -d' ' -f1 release-assets/CodeSwitch-${VERSION}.exe.sha256) + LINUX_SHA=$(cut -d' ' -f1 release-assets/CodeSwitch-${VERSION}.AppImage.sha256) cat > release-assets/latest.json << EOF { @@ -343,21 +372,21 @@ jobs: "release_date": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", "files": { "windows": { - "name": "CodeSwitch.exe", - "url": "${BASE_URL}/CodeSwitch.exe", + "name": "CodeSwitch-${VERSION}.exe", + "url": "${BASE_URL}/CodeSwitch-${VERSION}.exe", "sha256": "${WIN_SHA}" }, "darwin-arm64": { - "name": "codeswitch-macos-arm64.zip", - "url": "${BASE_URL}/codeswitch-macos-arm64.zip" + "name": "CodeSwitch-${VERSION}-macos-arm64.zip", + "url": "${BASE_URL}/CodeSwitch-${VERSION}-macos-arm64.zip" }, "darwin-amd64": { - "name": "codeswitch-macos-amd64.zip", - "url": "${BASE_URL}/codeswitch-macos-amd64.zip" + "name": "CodeSwitch-${VERSION}-macos-amd64.zip", + "url": "${BASE_URL}/CodeSwitch-${VERSION}-macos-amd64.zip" }, "linux": { - "name": "CodeSwitch.AppImage", - "url": "${BASE_URL}/CodeSwitch.AppImage", + "name": "CodeSwitch-${VERSION}.AppImage", + "url": "${BASE_URL}/CodeSwitch-${VERSION}.AppImage", "sha256": "${LINUX_SHA}" } } @@ -386,7 +415,7 @@ jobs: # 组合完整的 release body cat /tmp/version_notes.md > /tmp/release_body.md - cat >> /tmp/release_body.md << 'EOF' + cat >> /tmp/release_body.md << EOF --- @@ -394,46 +423,46 @@ jobs: | 平台 | 文件 | 说明 | |------|------|------| - | **Windows (首次)** | `CodeSwitch-amd64-installer.exe` | NSIS 安装器 | - | **Windows (便携)** | `CodeSwitch.exe` | 直接运行 | - | **Windows (更新)** | `updater.exe` | 静默更新辅助 | - | **macOS (ARM)** | `codeswitch-macos-arm64.zip` | Apple Silicon | - | **macOS (Intel)** | `codeswitch-macos-amd64.zip` | Intel 芯片 | - | **Linux (通用)** | `CodeSwitch.AppImage` | 跨发行版便携 | - | **Linux (Debian/Ubuntu)** | `codeswitch_*.deb` | apt 安装 | - | **Linux (RHEL/Fedora)** | `codeswitch-*.rpm` | dnf/yum 安装 | + | **Windows (首次)** | \`CodeSwitch-v${VERSION}-amd64-installer.exe\` | NSIS 安装器 | + | **Windows (便携)** | \`CodeSwitch-v${VERSION}.exe\` | 直接运行 | + | **Windows (更新)** | \`updater-v${VERSION}.exe\` | 静默更新辅助 | + | **macOS (ARM)** | \`CodeSwitch-v${VERSION}-macos-arm64.zip\` | Apple Silicon | + | **macOS (Intel)** | \`CodeSwitch-v${VERSION}-macos-amd64.zip\` | Intel 芯片 | + | **Linux (通用)** | \`CodeSwitch-v${VERSION}.AppImage\` | 跨发行版便携 | + | **Linux (Debian/Ubuntu)** | \`codeswitch_*.deb\` | apt 安装 | + | **Linux (RHEL/Fedora)** | \`codeswitch-*.rpm\` | dnf/yum 安装 | ## Linux 安装 ### AppImage (推荐) - ```bash - chmod +x CodeSwitch.AppImage - ./CodeSwitch.AppImage - ``` - 如遇 FUSE 问题:`./CodeSwitch.AppImage --appimage-extract-and-run` + \`\`\`bash + chmod +x CodeSwitch-v${VERSION}.AppImage + ./CodeSwitch-v${VERSION}.AppImage + \`\`\` + 如遇 FUSE 问题:\`./CodeSwitch-v${VERSION}.AppImage --appimage-extract-and-run\` ### Debian/Ubuntu - ```bash + \`\`\`bash sudo dpkg -i codeswitch_*.deb sudo apt-get install -f # 安装依赖 - ``` + \`\`\` ### RHEL/Fedora - ```bash + \`\`\`bash sudo rpm -i codeswitch-*.rpm # 或 sudo dnf install codeswitch-*.rpm - ``` + \`\`\` ## 文件校验 - 所有平台均提供 SHA256 校验文件(`.sha256`),下载后可验证完整性: - ```bash + 所有平台均提供 SHA256 校验文件(\`.sha256\`),下载后可验证完整性: + \`\`\`bash # Linux/macOS - sha256sum -c CodeSwitch.AppImage.sha256 + sha256sum -c CodeSwitch-v${VERSION}.AppImage.sha256 # Windows PowerShell - Get-FileHash CodeSwitch.exe | Format-List - ``` + Get-FileHash CodeSwitch-v${VERSION}.exe | Format-List + \`\`\` EOF echo "Generated release body:" diff --git a/.gitignore b/.gitignore index 3444dd2..040cb4d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ frontend/dist .task bin CLAUDE.md +.ace-tool/ diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index f2be55a..8542662 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,14 @@ +# Code Switch v2.6.14 + +## 新功能 +- **自适应热力图**:新增供应商使用热力图功能,直观展示各供应商的请求分布情况,支持按时间范围筛选 +- **图标搜索**:新增供应商图标搜索功能,方便快速查找和选择合适的图标 + +## 修复 +- 修复控制台日志递归爆炸问题 + +--- + # Code Switch v2.0.0 ## 新功能 diff --git a/codeswitch.exe b/codeswitch.exe deleted file mode 100644 index 417cecd..0000000 Binary files a/codeswitch.exe and /dev/null differ diff --git a/codeswitch_test.exe b/codeswitch_test.exe deleted file mode 100644 index f0ac46c..0000000 Binary files a/codeswitch_test.exe and /dev/null differ diff --git a/frontend/src/App.vue b/frontend/src/App.vue index b2d10d9..e4a7e3c 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,6 +1,6 @@ diff --git a/frontend/src/components/Logs/Index.vue b/frontend/src/components/Logs/Index.vue index 4412c48..a265155 100644 --- a/frontend/src/components/Logs/Index.vue +++ b/frontend/src/components/Logs/Index.vue @@ -16,11 +16,14 @@
{{ card.label }}
-
{{ card.value }}
+
+ {{ card.value }} + ({{ card.subValue }}) +
{{ card.hint }}
@@ -68,6 +71,7 @@ {{ t('components.logs.table.httpCode') }} {{ t('components.logs.table.stream') }} {{ t('components.logs.table.duration') }} + {{ t('components.logs.table.cost') }} {{ t('components.logs.table.tokens') }} @@ -80,31 +84,32 @@ {{ item.http_code }} {{ formatStream(item.is_stream) }} {{ formatDuration(item.duration_sec) }} + {{ formatCurrency(item.total_cost) }}
{{ t('components.logs.tokenLabels.input') }} - {{ formatNumber(item.input_tokens) }} + {{ formatTokenNumber(item.input_tokens) }}
{{ t('components.logs.tokenLabels.output') }} - {{ formatNumber(item.output_tokens) }} + {{ formatTokenNumber(item.output_tokens) }}
{{ t('components.logs.tokenLabels.reasoning') }} - {{ formatNumber(item.reasoning_tokens) }} + {{ formatTokenNumber(item.reasoning_tokens) }}
{{ t('components.logs.tokenLabels.cacheWrite') }} - {{ formatNumber(item.cache_create_tokens) }} + {{ formatTokenNumber(item.cache_create_tokens) }}
{{ t('components.logs.tokenLabels.cacheRead') }} - {{ formatNumber(item.cache_read_tokens) }} + {{ formatTokenNumber(item.cache_read_tokens) }}
- {{ t('components.logs.empty') }} + {{ t('components.logs.empty') }} @@ -144,6 +149,26 @@ + + + +
+
+
+ {{ t('components.logs.tokenLabels.input') }} + {{ formatTokenNumber(stats?.input_tokens) }} +
+
+ {{ t('components.logs.tokenLabels.output') }} + {{ formatTokenNumber(stats?.output_tokens) }} +
+
+
+
@@ -201,6 +226,13 @@ const costDetailModal = reactive<{ data: [], }) +// Token 明细弹窗状态 +const tokenDetailModal = reactive<{ + open: boolean +}>({ + open: false, +}) + // 打开金额明细弹窗 const openCostDetailModal = async () => { costDetailModal.open = true @@ -225,6 +257,25 @@ const closeCostDetailModal = () => { costDetailModal.open = false } +// 处理卡片点击 +const handleCardClick = (key: string) => { + if (key === 'cost') { + openCostDetailModal() + } else if (key === 'tokens') { + openTokenDetailModal() + } +} + +// 打开 Token 明细弹窗 +const openTokenDetailModal = () => { + tokenDetailModal.open = true +} + +// 关闭 Token 明细弹窗 +const closeTokenDetailModal = () => { + tokenDetailModal.open = false +} + const parseLogDate = (value?: string) => { if (!value) return null const normalize = value.replace(' ', 'T') @@ -512,6 +563,44 @@ const formatNumber = (value?: number) => { return value.toLocaleString() } +/** + * 格式化 token 数值,支持 k/M/B 单位换算 + * @author sm + */ +const formatTokenNumber = (value?: number) => { + if (value === undefined || value === null) return '—' + + if (value >= 1_000_000_000) { + return `${(value / 1_000_000_000).toFixed(2)}B` + } + if (value >= 1_000_000) { + return `${(value / 1_000_000).toFixed(2)}M` + } + if (value >= 1_000) { + return `${(value / 1_000).toFixed(2)}k` + } + + return value.toLocaleString() +} + +/** + * 计算缓存命中率 + * @param cacheRead 缓存读取 token 数 + * @param inputTokens 输入 token 数 + * @returns 命中率百分比字符串 + * @author sm + */ +const formatCacheHitRate = (cacheRead?: number, inputTokens?: number) => { + const read = cacheRead ?? 0 + const input = inputTokens ?? 0 + const total = read + input + + if (total === 0) return '0%' + + const rate = (read / total) * 100 + return `${rate.toFixed(1)}%` +} + const formatCurrency = (value?: number) => { if (value === undefined || value === null || Number.isNaN(value)) { return '$0.0000' @@ -547,13 +636,14 @@ const statsCards = computed(() => { key: 'tokens', label: t('components.logs.summary.tokens'), hint: t('components.logs.summary.tokenHint'), - value: data ? formatNumber(totalTokens) : '—', + value: data ? formatTokenNumber(totalTokens) : '—', }, { key: 'cacheReads', label: t('components.logs.summary.cache'), hint: t('components.logs.summary.cacheHint'), - value: data ? formatNumber(data.cache_read_tokens) : '—', + value: data ? formatTokenNumber(data.cache_read_tokens) : '—', + subValue: data ? formatCacheHitRate(data.cache_read_tokens, data.input_tokens) : '', }, { key: 'cost', @@ -646,6 +736,13 @@ onUnmounted(() => { color: #94a3b8; } +.summary-card__sub-value { + font-size: 0.65em; + font-weight: 400; + color: #64748b; + margin-left: 0.25rem; +} + html.dark .summary-card { border-color: rgba(255, 255, 255, 0.12); background: radial-gradient(circle at top, rgba(148, 163, 184, 0.2), rgba(15, 23, 42, 0.35)); @@ -663,6 +760,10 @@ html.dark .summary-card__hint { color: rgba(186, 194, 210, 0.8); } +html.dark .summary-card__sub-value { + color: #94a3b8; +} + @media (max-width: 768px) { .logs-summary { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); @@ -737,4 +838,54 @@ html.dark .cost-detail-item__name { color: #f97316; font-variant-numeric: tabular-nums; } + +/* Token 弹窗 */ +.token-detail-modal { + min-height: 80px; +} +.token-detail-list { + display: flex; + flex-direction: column; + gap: 0.75rem; +} +.token-detail-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem 1rem; + background: rgba(148, 163, 184, 0.08); + border-radius: 8px; + transition: background 0.15s ease; +} +.token-detail-item:hover { + background: rgba(148, 163, 184, 0.12); +} +html.dark .token-detail-item { + background: rgba(148, 163, 184, 0.12); +} +html.dark .token-detail-item:hover { + background: rgba(148, 163, 184, 0.18); +} +.token-detail-item__name { + font-weight: 500; + color: #1e293b; +} +html.dark .token-detail-item__name { + color: #f1f5f9; +} +.token-detail-item__value { + font-weight: 600; + color: #34d399; + font-variant-numeric: tabular-nums; +} + +/* 金额列 */ +.col-cost { + width: 80px; +} +.cost-cell { + color: #f97316; + font-weight: 500; + font-variant-numeric: tabular-nums; +} diff --git a/frontend/src/components/Main/Index.vue b/frontend/src/components/Main/Index.vue index a52abeb..0ae9273 100644 --- a/frontend/src/components/Main/Index.vue +++ b/frontend/src/components/Main/Index.vue @@ -498,7 +498,7 @@ class="ghost-icon direct-apply-btn" :class="{ 'is-active': isDirectApplied(card) && !activeProxyState }" :disabled="activeProxyState" - :title="activeProxyState ? t('components.main.directApply.proxyEnabled') : (isDirectApplied(card) ? t('components.main.directApply.inUse') : t('components.main.directApply.title'))" + :data-tooltip="activeProxyState ? t('components.main.directApply.proxyEnabled') : (isDirectApplied(card) ? t('components.main.directApply.inUse') : t('components.main.directApply.title'))" @click.stop="!isDirectApplied(card) && handleDirectApply(card)" > {{ t('components.main.directApply.inUse') }} @@ -506,7 +506,7 @@ - -