diff --git a/.dockerignore b/.dockerignore
deleted file mode 100644
index 311d8d5c9225f59b701f82c422de896b12ee33a9..0000000000000000000000000000000000000000
--- a/.dockerignore
+++ /dev/null
@@ -1,54 +0,0 @@
-# Rust build artifacts
-target/
-Cargo.lock
-
-# Node.js dependencies and build artifacts
-admin-ui/node_modules/
-admin-ui/dist/
-admin-ui/pnpm-lock.yaml
-admin-ui/tsconfig.tsbuildinfo
-admin-ui/.vite/
-
-# Version control
-.git/
-.gitignore
-
-# IDE and editor files
-.idea/
-.vscode/
-*.swp
-*.swo
-*~
-
-# Claude/AI
-.claude/
-CLAUDE.md
-AGENTS.md
-
-# CI/CD
-.github/
-
-# Documentation and examples
-*.md
-README.md
-config.example.json
-credentials.example.*.json
-
-# Development and test files
-src/test.rs
-src/debug.rs
-test.json
-tools/
-
-# OS-specific files
-.DS_Store
-Thumbs.db
-
-# Local configuration (keep templates only)
-config.json
-credentials.json
-
-# Docker files
-Dockerfile
-.dockerignore
-docker-compose*.yml
\ No newline at end of file
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
deleted file mode 100644
index 95c7538542d3a04ed83d00c16fabae4e0e0ce733..0000000000000000000000000000000000000000
--- a/.github/workflows/build.yaml
+++ /dev/null
@@ -1,85 +0,0 @@
-name: Build Artifacts
-
-on:
- push:
- tags:
- - 'v*'
- workflow_dispatch:
- inputs:
- version:
- description: 'Version label for artifacts (e.g., 2025.12.1)'
- required: true
- default: '2026.1.1'
-
-permissions:
- contents: read
-
-jobs:
- build:
- strategy:
- fail-fast: false
- matrix:
- include:
- - platform: macos-latest
- target: aarch64-apple-darwin
- name: macOS-arm64
- - platform: macos-latest
- target: x86_64-apple-darwin
- name: macOS-x64
- - platform: windows-latest
- target: x86_64-pc-windows-msvc
- name: Windows-x64
- - platform: ubuntu-22.04
- target: x86_64-unknown-linux-gnu
- name: Linux-x64
- - platform: ubuntu-22.04-arm
- target: aarch64-unknown-linux-gnu
- name: Linux-arm64
-
- runs-on: ${{ matrix.platform }}
-
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Setup Node.js
- uses: actions/setup-node@v4
- with:
- node-version: '20'
-
- - name: Setup pnpm
- uses: pnpm/action-setup@v4
- with:
- version: 9
-
- - name: Install admin-ui dependencies
- working-directory: admin-ui
- run: pnpm install
-
- - name: Build admin-ui
- working-directory: admin-ui
- run: pnpm build
-
- - name: Setup Rust
- uses: dtolnay/rust-toolchain@stable
- with:
- targets: ${{ matrix.target }}
-
- - name: Setup Rust cache
- uses: Swatinem/rust-cache@v2
- with:
- shared-key: "rust-cache-${{ matrix.target }}"
- cache-on-failure: true
-
- - name: Build app
- run: cargo build --release --target ${{ matrix.target }}
-
- - name: Upload build artifacts
- uses: actions/upload-artifact@v4
- with:
- name: kiro-rs-${{ github.event.inputs.version || github.ref_name }}-${{ matrix.name }}
- if-no-files-found: error
- compression-level: 6
- path: |
- target/${{ matrix.target }}/release/kiro-rs
- target/${{ matrix.target }}/release/kiro-rs.exe
\ No newline at end of file
diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml
deleted file mode 100644
index 6d917a1d5be70dd919056e6a9f07e3b47b8d3e46..0000000000000000000000000000000000000000
--- a/.github/workflows/docker-build.yaml
+++ /dev/null
@@ -1,87 +0,0 @@
-name: Build and Push Docker Images
-
-on:
- push:
- tags:
- - 'v*'
- workflow_dispatch:
- inputs:
- version:
- description: 'Version tag for Docker images (e.g., 2025.12.1)'
- required: true
- default: '2026.1.1'
-
-permissions:
- contents: read
- packages: write
-
-jobs:
- build:
- runs-on: ${{ matrix.runner }}
-
- strategy:
- fail-fast: false
- matrix:
- include:
- - platform: linux/amd64
- runner: ubuntu-latest
- arch: amd64
- - platform: linux/arm64
- runner: ubuntu-22.04-arm
- arch: arm64
-
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
-
- - name: Log in to GitHub Container Registry
- uses: docker/login-action@v3
- with:
- registry: ghcr.io
- username: ${{ github.repository_owner }}
- password: ${{ github.token }}
-
- - name: Build and push
- uses: docker/build-push-action@v6
- with:
- context: .
- platforms: ${{ matrix.platform }}
- cache-from: type=gha
- cache-to: type=gha,mode=max
- push: true
- provenance: false
- tags: ghcr.io/${{ github.repository_owner }}/kiro-rs:${{ github.event.inputs.version || github.ref_name }}-${{ matrix.arch }}
- labels: |
- org.opencontainers.image.source=https://github.com/${{ github.repository }}
- org.opencontainers.image.description=Kiro.rs Docker Image
-
- manifest:
- needs: build
- runs-on: ubuntu-latest
- steps:
- - name: Log in to GitHub Container Registry
- uses: docker/login-action@v3
- with:
- registry: ghcr.io
- username: ${{ github.repository_owner }}
- password: ${{ github.token }}
-
- - name: Create and push multi-arch manifest
- run: |
- VERSION="${{ github.event.inputs.version || github.ref_name }}"
- IMAGE="ghcr.io/${{ github.repository_owner }}/kiro-rs"
-
- # Create manifest for version tag
- docker manifest create ${IMAGE}:${VERSION} \
- ${IMAGE}:${VERSION}-amd64 \
- ${IMAGE}:${VERSION}-arm64
- docker manifest push ${IMAGE}:${VERSION}
-
- # Create manifest for latest tag
- docker manifest create ${IMAGE}:latest \
- ${IMAGE}:${VERSION}-amd64 \
- ${IMAGE}:${VERSION}-arm64
- docker manifest push ${IMAGE}:latest
diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml
deleted file mode 100644
index 6df0b7e6fa3802c2c564e1766cf709ddd0a9b241..0000000000000000000000000000000000000000
--- a/.github/workflows/docker-build.yml
+++ /dev/null
@@ -1,55 +0,0 @@
-name: Build and Push Docker Image
-
-on:
- push:
- branches:
- - master
- paths-ignore:
- - '**.md'
- - '.gitignore'
- workflow_dispatch:
-
-env:
- REGISTRY: ghcr.io
- IMAGE_NAME: ${{ github.repository }}
-
-jobs:
- build-and-push:
- runs-on: ubuntu-latest
- permissions:
- contents: read
- packages: write
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v4
-
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
-
- - name: Log in to GitHub Container Registry
- uses: docker/login-action@v3
- with:
- registry: ${{ env.REGISTRY }}
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Extract metadata
- id: meta
- uses: docker/metadata-action@v5
- with:
- images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- tags: |
- type=raw,value=latest,enable={{is_default_branch}}
- type=sha,prefix={{branch}}-
-
- - name: Build and push Docker image
- uses: docker/build-push-action@v5
- with:
- context: .
- push: true
- tags: ${{ steps.meta.outputs.tags }}
- labels: ${{ steps.meta.outputs.labels }}
- cache-from: type=gha
- cache-to: type=gha,mode=max
- platforms: linux/amd64
diff --git a/.gitignore b/.gitignore
deleted file mode 100644
index 07b2905153645ab142c4b9374e23a449c9f38cb3..0000000000000000000000000000000000000000
--- a/.gitignore
+++ /dev/null
@@ -1,12 +0,0 @@
-/target
-/CLAUDE.md
-/AGENTS.md
-/config.json
-/credentials.json
-/.idea
-/test.json
-/Cargo.lock
-/admin-ui/node_modules/
-/admin-ui/dist/
-/admin-ui/pnpm-lock.yaml
-/admin-ui/tsconfig.tsbuildinfo
diff --git a/Cargo.toml b/Cargo.toml
deleted file mode 100644
index 053eca8f56ca48c69d7cea12fc2df29d6d2341fb..0000000000000000000000000000000000000000
--- a/Cargo.toml
+++ /dev/null
@@ -1,34 +0,0 @@
-[package]
-name = "kiro-rs"
-version = "2026.1.5"
-edition = "2024"
-
-[profile.release]
-lto = true
-strip = true
-
-[dependencies]
-axum = "0.8"
-tokio = { version = "1.0", features = ["full"] }
-reqwest = { version = "0.12", features = ["stream", "json", "socks"] }
-serde = { version = "1.0", features = ["derive"] }
-serde_json = "1.0"
-tracing = "0.1"
-tracing-subscriber = { version = "0.3", features = ["env-filter"] }
-anyhow = "1.0"
-http = "1.0"
-futures = "0.3"
-chrono = { version = "0.4", features = ["serde"] }
-uuid = { version = "1.10", features = ["v1", "v4", "fast-rng"] }
-fastrand = "2"
-sha2 = "0.10"
-hex = "0.4"
-crc = "3" # CRC32C 计算
-bytes = "1" # 高效的字节缓冲区
-tower-http = { version = "0.6", features = ["cors"] }
-clap = { version = "4.5", features = ["derive"] }
-urlencoding = "2"
-parking_lot = "0.12" # 高性能同步原语
-subtle = "2.6" # 常量时间比较(防止时序攻击)
-rust-embed = "8" # 嵌入静态文件
-mime_guess = "2" # MIME 类型推断
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index 8ee54c3f80be743a188ef3b8a8a2847b2b7c00df..0000000000000000000000000000000000000000
--- a/Dockerfile
+++ /dev/null
@@ -1,42 +0,0 @@
-FROM node:22-alpine AS frontend-builder
-
-WORKDIR /app/admin-ui
-COPY admin-ui/package.json ./
-RUN npm install -g pnpm && pnpm install
-COPY admin-ui ./
-RUN pnpm build
-
-FROM rust:1.85-alpine AS builder
-
-RUN apk add --no-cache musl-dev openssl-dev openssl-libs-static
-
-WORKDIR /app
-COPY Cargo.toml Cargo.lock* ./
-COPY src ./src
-COPY --from=frontend-builder /app/admin-ui/dist /app/admin-ui/dist
-
-RUN cargo build --release
-
-FROM alpine:3.21
-
-RUN apk add --no-cache ca-certificates
-
-# 创建非 root 用户 (HuggingFace Spaces 要求)
-RUN adduser -D -u 1000 appuser
-
-WORKDIR /app
-
-COPY --from=builder /app/target/release/kiro-rs /app/kiro-rs
-COPY entrypoint.sh /app/entrypoint.sh
-
-# 创建配置目录并设置权限
-RUN mkdir -p /app/config && \
- chown -R appuser:appuser /app && \
- chmod +x /app/entrypoint.sh
-
-USER appuser
-
-# HuggingFace Spaces 只支持端口 7860
-EXPOSE 7860
-
-ENTRYPOINT ["/app/entrypoint.sh"]
diff --git a/admin-ui/index.html b/admin-ui/index.html
deleted file mode 100644
index fa27c3ef561d10044930666513ad7ffe26671cd2..0000000000000000000000000000000000000000
--- a/admin-ui/index.html
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
- Kiro Admin
-
-
-
-
-
-
diff --git a/admin-ui/package.json b/admin-ui/package.json
deleted file mode 100644
index 8e14a46f35e7e0b8e30bb0471ff96a631596b79c..0000000000000000000000000000000000000000
--- a/admin-ui/package.json
+++ /dev/null
@@ -1,37 +0,0 @@
-{
- "name": "kiro-admin-ui",
- "version": "1.0.0",
- "type": "module",
- "scripts": {
- "dev": "vite",
- "build": "tsc -b && vite build",
- "preview": "vite preview"
- },
- "dependencies": {
- "react": "^18.3.1",
- "react-dom": "^18.3.1",
- "@tanstack/react-query": "^5.60.0",
- "axios": "^1.7.0",
- "clsx": "^2.1.1",
- "tailwind-merge": "^2.5.0",
- "class-variance-authority": "^0.7.0",
- "@radix-ui/react-slot": "^1.1.0",
- "@radix-ui/react-switch": "^1.1.0",
- "@radix-ui/react-dialog": "^1.1.0",
- "@radix-ui/react-dropdown-menu": "^2.1.0",
- "@radix-ui/react-toast": "^1.2.0",
- "@radix-ui/react-tooltip": "^1.1.0",
- "lucide-react": "^0.460.0",
- "sonner": "^1.7.0"
- },
- "devDependencies": {
- "@types/react": "^18.3.12",
- "@types/react-dom": "^18.3.1",
- "@vitejs/plugin-react-swc": "^3.7.0",
- "autoprefixer": "^10.4.20",
- "postcss": "^8.4.47",
- "tailwindcss": "^3.4.14",
- "typescript": "^5.6.3",
- "vite": "^5.4.0"
- }
-}
diff --git a/admin-ui/postcss.config.js b/admin-ui/postcss.config.js
deleted file mode 100644
index 2e7af2b7f1a6f391da1631d93968a9d487ba977d..0000000000000000000000000000000000000000
--- a/admin-ui/postcss.config.js
+++ /dev/null
@@ -1,6 +0,0 @@
-export default {
- plugins: {
- tailwindcss: {},
- autoprefixer: {},
- },
-}
diff --git a/admin-ui/public/vite.svg b/admin-ui/public/vite.svg
deleted file mode 100644
index 6a4109910f7762f0470eacc43118d1d613951d13..0000000000000000000000000000000000000000
--- a/admin-ui/public/vite.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/admin-ui/src/App.tsx b/admin-ui/src/App.tsx
deleted file mode 100644
index f2fab45510cb4b5b73fb4492bca1c6d1871cbd50..0000000000000000000000000000000000000000
--- a/admin-ui/src/App.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import { useState, useEffect } from 'react'
-import { storage } from '@/lib/storage'
-import { LoginPage } from '@/components/login-page'
-import { Dashboard } from '@/components/dashboard'
-import { Toaster } from '@/components/ui/sonner'
-
-function App() {
- const [isLoggedIn, setIsLoggedIn] = useState(false)
-
- useEffect(() => {
- // 检查是否已经有保存的 API Key
- if (storage.getApiKey()) {
- setIsLoggedIn(true)
- }
- }, [])
-
- const handleLogin = () => {
- setIsLoggedIn(true)
- }
-
- const handleLogout = () => {
- setIsLoggedIn(false)
- }
-
- return (
- <>
- {isLoggedIn ? (
-
- ) : (
-
- )}
-
- >
- )
-}
-
-export default App
diff --git a/admin-ui/src/api/credentials.ts b/admin-ui/src/api/credentials.ts
deleted file mode 100644
index 14591029bcbdcbd2ea4cc45f92ca84268eb85533..0000000000000000000000000000000000000000
--- a/admin-ui/src/api/credentials.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-import axios from 'axios'
-import { storage } from '@/lib/storage'
-import type {
- CredentialsStatusResponse,
- BalanceResponse,
- SuccessResponse,
- SetDisabledRequest,
- SetPriorityRequest,
- AddCredentialRequest,
- AddCredentialResponse,
-} from '@/types/api'
-
-// 创建 axios 实例
-const api = axios.create({
- baseURL: '/api/admin',
- headers: {
- 'Content-Type': 'application/json',
- },
-})
-
-// 请求拦截器添加 API Key
-api.interceptors.request.use((config) => {
- const apiKey = storage.getApiKey()
- if (apiKey) {
- config.headers['x-api-key'] = apiKey
- }
- return config
-})
-
-// 获取所有凭据状态
-export async function getCredentials(): Promise {
- const { data } = await api.get('/credentials')
- return data
-}
-
-// 设置凭据禁用状态
-export async function setCredentialDisabled(
- id: number,
- disabled: boolean
-): Promise {
- const { data } = await api.post(
- `/credentials/${id}/disabled`,
- { disabled } as SetDisabledRequest
- )
- return data
-}
-
-// 设置凭据优先级
-export async function setCredentialPriority(
- id: number,
- priority: number
-): Promise {
- const { data } = await api.post(
- `/credentials/${id}/priority`,
- { priority } as SetPriorityRequest
- )
- return data
-}
-
-// 重置失败计数
-export async function resetCredentialFailure(
- id: number
-): Promise {
- const { data } = await api.post(`/credentials/${id}/reset`)
- return data
-}
-
-// 获取凭据余额
-export async function getCredentialBalance(id: number): Promise {
- const { data } = await api.get(`/credentials/${id}/balance`)
- return data
-}
-
-// 添加新凭据
-export async function addCredential(
- req: AddCredentialRequest
-): Promise {
- const { data } = await api.post('/credentials', req)
- return data
-}
-
-// 删除凭据
-export async function deleteCredential(id: number): Promise {
- const { data } = await api.delete(`/credentials/${id}`)
- return data
-}
diff --git a/admin-ui/src/components/add-credential-dialog.tsx b/admin-ui/src/components/add-credential-dialog.tsx
deleted file mode 100644
index 1ea498f39f5e63cbb426cc278be8a189aa0d5285..0000000000000000000000000000000000000000
--- a/admin-ui/src/components/add-credential-dialog.tsx
+++ /dev/null
@@ -1,186 +0,0 @@
-import { useState } from 'react'
-import { toast } from 'sonner'
-import {
- Dialog,
- DialogContent,
- DialogHeader,
- DialogTitle,
- DialogFooter,
-} from '@/components/ui/dialog'
-import { Button } from '@/components/ui/button'
-import { Input } from '@/components/ui/input'
-import { useAddCredential } from '@/hooks/use-credentials'
-import { extractErrorMessage } from '@/lib/utils'
-
-interface AddCredentialDialogProps {
- open: boolean
- onOpenChange: (open: boolean) => void
-}
-
-type AuthMethod = 'social' | 'idc' | 'builder-id'
-
-export function AddCredentialDialog({ open, onOpenChange }: AddCredentialDialogProps) {
- const [refreshToken, setRefreshToken] = useState('')
- const [authMethod, setAuthMethod] = useState('social')
- const [clientId, setClientId] = useState('')
- const [clientSecret, setClientSecret] = useState('')
- const [priority, setPriority] = useState('0')
-
- const { mutate, isPending } = useAddCredential()
-
- const resetForm = () => {
- setRefreshToken('')
- setAuthMethod('social')
- setClientId('')
- setClientSecret('')
- setPriority('0')
- }
-
- const handleSubmit = (e: React.FormEvent) => {
- e.preventDefault()
-
- // 验证必填字段
- if (!refreshToken.trim()) {
- toast.error('请输入 Refresh Token')
- return
- }
-
- // IdC/Builder-ID 需要额外字段
- if ((authMethod === 'idc' || authMethod === 'builder-id') &&
- (!clientId.trim() || !clientSecret.trim())) {
- toast.error('IdC/Builder-ID 认证需要填写 Client ID 和 Client Secret')
- return
- }
-
- mutate(
- {
- refreshToken: refreshToken.trim(),
- authMethod,
- clientId: clientId.trim() || undefined,
- clientSecret: clientSecret.trim() || undefined,
- priority: parseInt(priority) || 0,
- },
- {
- onSuccess: (data) => {
- toast.success(data.message)
- onOpenChange(false)
- resetForm()
- },
- onError: (error: unknown) => {
- toast.error(`添加失败: ${extractErrorMessage(error)}`)
- },
- }
- )
- }
-
- return (
-
- )
-}
diff --git a/admin-ui/src/components/balance-dialog.tsx b/admin-ui/src/components/balance-dialog.tsx
deleted file mode 100644
index a7f9ec905dd9a2fde735629cabf44d52e2520f6b..0000000000000000000000000000000000000000
--- a/admin-ui/src/components/balance-dialog.tsx
+++ /dev/null
@@ -1,104 +0,0 @@
-import {
- Dialog,
- DialogContent,
- DialogHeader,
- DialogTitle,
-} from '@/components/ui/dialog'
-import { Progress } from '@/components/ui/progress'
-import { useCredentialBalance } from '@/hooks/use-credentials'
-import { parseError } from '@/lib/utils'
-
-interface BalanceDialogProps {
- credentialId: number | null
- open: boolean
- onOpenChange: (open: boolean) => void
-}
-
-export function BalanceDialog({ credentialId, open, onOpenChange }: BalanceDialogProps) {
- const { data: balance, isLoading, error } = useCredentialBalance(credentialId)
-
- const formatDate = (timestamp: number | null) => {
- if (!timestamp) return '未知'
- return new Date(timestamp * 1000).toLocaleString('zh-CN')
- }
-
- const formatNumber = (num: number) => {
- return num.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
- }
-
- return (
-
- )
-}
diff --git a/admin-ui/src/components/credential-card.tsx b/admin-ui/src/components/credential-card.tsx
deleted file mode 100644
index a3e4ea04e8cd655da8b05b7b811ad10dff6346f4..0000000000000000000000000000000000000000
--- a/admin-ui/src/components/credential-card.tsx
+++ /dev/null
@@ -1,298 +0,0 @@
-import { useState } from 'react'
-import { toast } from 'sonner'
-import { RefreshCw, ChevronUp, ChevronDown, Wallet, Trash2 } from 'lucide-react'
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
-import { Button } from '@/components/ui/button'
-import { Badge } from '@/components/ui/badge'
-import { Switch } from '@/components/ui/switch'
-import { Input } from '@/components/ui/input'
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from '@/components/ui/dialog'
-import type { CredentialStatusItem } from '@/types/api'
-import {
- useSetDisabled,
- useSetPriority,
- useResetFailure,
- useDeleteCredential,
-} from '@/hooks/use-credentials'
-
-interface CredentialCardProps {
- credential: CredentialStatusItem
- onViewBalance: (id: number) => void
-}
-
-export function CredentialCard({ credential, onViewBalance }: CredentialCardProps) {
- const [editingPriority, setEditingPriority] = useState(false)
- const [priorityValue, setPriorityValue] = useState(String(credential.priority))
- const [showDeleteDialog, setShowDeleteDialog] = useState(false)
-
- const setDisabled = useSetDisabled()
- const setPriority = useSetPriority()
- const resetFailure = useResetFailure()
- const deleteCredential = useDeleteCredential()
-
- const handleToggleDisabled = () => {
- setDisabled.mutate(
- { id: credential.id, disabled: !credential.disabled },
- {
- onSuccess: (res) => {
- toast.success(res.message)
- },
- onError: (err) => {
- toast.error('操作失败: ' + (err as Error).message)
- },
- }
- )
- }
-
- const handlePriorityChange = () => {
- const newPriority = parseInt(priorityValue, 10)
- if (isNaN(newPriority) || newPriority < 0) {
- toast.error('优先级必须是非负整数')
- return
- }
- setPriority.mutate(
- { id: credential.id, priority: newPriority },
- {
- onSuccess: (res) => {
- toast.success(res.message)
- setEditingPriority(false)
- },
- onError: (err) => {
- toast.error('操作失败: ' + (err as Error).message)
- },
- }
- )
- }
-
- const handleReset = () => {
- resetFailure.mutate(credential.id, {
- onSuccess: (res) => {
- toast.success(res.message)
- },
- onError: (err) => {
- toast.error('操作失败: ' + (err as Error).message)
- },
- })
- }
-
- const handleDelete = () => {
- deleteCredential.mutate(credential.id, {
- onSuccess: (res) => {
- toast.success(res.message)
- setShowDeleteDialog(false)
- },
- onError: (err) => {
- toast.error('删除失败: ' + (err as Error).message)
- },
- })
- }
-
- const formatExpiry = (expiresAt: string | null) => {
- if (!expiresAt) return '未知'
- const date = new Date(expiresAt)
- const now = new Date()
- const diff = date.getTime() - now.getTime()
- if (diff < 0) return '已过期'
- const minutes = Math.floor(diff / 60000)
- if (minutes < 60) return `${minutes} 分钟`
- const hours = Math.floor(minutes / 60)
- if (hours < 24) return `${hours} 小时`
- return `${Math.floor(hours / 24)} 天`
- }
-
- return (
- <>
-
-
-
-
- 凭据 #{credential.id}
- {credential.isCurrent && (
- 当前
- )}
- {credential.disabled && (
- 已禁用
- )}
-
-
- 启用
-
-
-
-
-
- {/* 信息网格 */}
-
-
-
优先级:
- {editingPriority ? (
-
- setPriorityValue(e.target.value)}
- className="w-16 h-7 text-sm"
- min="0"
- />
-
-
-
- ) : (
-
setEditingPriority(true)}
- >
- {credential.priority}
- (点击编辑)
-
- )}
-
-
- 失败次数:
- 0 ? 'text-red-500 font-medium' : ''}>
- {credential.failureCount}
-
-
-
- 认证方式:
- {credential.authMethod || '未知'}
-
-
- Token 有效期:
- {formatExpiry(credential.expiresAt)}
-
- {credential.hasProfileArn && (
-
- 有 Profile ARN
-
- )}
-
-
- {/* 操作按钮 */}
-
-
-
-
-
-
-
-
-
-
- {/* 删除确认对话框 */}
-
- >
- )
-}
diff --git a/admin-ui/src/components/dashboard.tsx b/admin-ui/src/components/dashboard.tsx
deleted file mode 100644
index 0898b5e799faa82a7db1c0433ac6c4b20a1ecd66..0000000000000000000000000000000000000000
--- a/admin-ui/src/components/dashboard.tsx
+++ /dev/null
@@ -1,186 +0,0 @@
-import { useState } from 'react'
-import { RefreshCw, LogOut, Moon, Sun, Server, Plus } from 'lucide-react'
-import { useQueryClient } from '@tanstack/react-query'
-import { toast } from 'sonner'
-import { storage } from '@/lib/storage'
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
-import { Button } from '@/components/ui/button'
-import { Badge } from '@/components/ui/badge'
-import { CredentialCard } from '@/components/credential-card'
-import { BalanceDialog } from '@/components/balance-dialog'
-import { AddCredentialDialog } from '@/components/add-credential-dialog'
-import { useCredentials } from '@/hooks/use-credentials'
-
-interface DashboardProps {
- onLogout: () => void
-}
-
-export function Dashboard({ onLogout }: DashboardProps) {
- const [selectedCredentialId, setSelectedCredentialId] = useState(null)
- const [balanceDialogOpen, setBalanceDialogOpen] = useState(false)
- const [addDialogOpen, setAddDialogOpen] = useState(false)
- const [darkMode, setDarkMode] = useState(() => {
- if (typeof window !== 'undefined') {
- return document.documentElement.classList.contains('dark')
- }
- return false
- })
-
- const queryClient = useQueryClient()
- const { data, isLoading, error, refetch } = useCredentials()
-
- const toggleDarkMode = () => {
- setDarkMode(!darkMode)
- document.documentElement.classList.toggle('dark')
- }
-
- const handleViewBalance = (id: number) => {
- setSelectedCredentialId(id)
- setBalanceDialogOpen(true)
- }
-
- const handleRefresh = () => {
- refetch()
- toast.success('已刷新凭据列表')
- }
-
- const handleLogout = () => {
- storage.removeApiKey()
- queryClient.clear()
- onLogout()
- }
-
- if (isLoading) {
- return (
-
- )
- }
-
- if (error) {
- return (
-
-
-
- 加载失败
- {(error as Error).message}
-
-
-
-
-
-
-
- )
- }
-
- return (
-
- {/* 顶部导航 */}
-
-
- {/* 主内容 */}
-
- {/* 统计卡片 */}
-
-
-
-
- 凭据总数
-
-
-
- {data?.total || 0}
-
-
-
-
-
- 可用凭据
-
-
-
- {data?.available || 0}
-
-
-
-
-
- 当前活跃
-
-
-
-
- #{data?.currentId || '-'}
- 活跃
-
-
-
-
-
- {/* 凭据列表 */}
-
-
-
凭据管理
-
-
- {data?.credentials.length === 0 ? (
-
-
- 暂无凭据
-
-
- ) : (
-
- {data?.credentials.map((credential) => (
-
- ))}
-
- )}
-
-
-
- {/* 余额对话框 */}
-
-
- {/* 添加凭据对话框 */}
-
-
- )
-}
diff --git a/admin-ui/src/components/login-page.tsx b/admin-ui/src/components/login-page.tsx
deleted file mode 100644
index c89f65c39835ac981aba1258e28e77e152695581..0000000000000000000000000000000000000000
--- a/admin-ui/src/components/login-page.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import { useState, useEffect } from 'react'
-import { KeyRound } from 'lucide-react'
-import { storage } from '@/lib/storage'
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
-import { Input } from '@/components/ui/input'
-import { Button } from '@/components/ui/button'
-
-interface LoginPageProps {
- onLogin: (apiKey: string) => void
-}
-
-export function LoginPage({ onLogin }: LoginPageProps) {
- const [apiKey, setApiKey] = useState('')
-
- useEffect(() => {
- // 从 storage 读取保存的 API Key
- const savedKey = storage.getApiKey()
- if (savedKey) {
- setApiKey(savedKey)
- }
- }, [])
-
- const handleSubmit = (e: React.FormEvent) => {
- e.preventDefault()
- if (apiKey.trim()) {
- storage.setApiKey(apiKey.trim())
- onLogin(apiKey.trim())
- }
- }
-
- return (
-
-
-
-
-
-
- Kiro Admin
-
- 请输入 Admin API Key 以访问管理面板
-
-
-
-
-
-
-
- )
-}
diff --git a/admin-ui/src/components/ui/badge.tsx b/admin-ui/src/components/ui/badge.tsx
deleted file mode 100644
index baa444c11155c9ea3562fb900b3c67d62e7b147f..0000000000000000000000000000000000000000
--- a/admin-ui/src/components/ui/badge.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import * as React from 'react'
-import { cva, type VariantProps } from 'class-variance-authority'
-import { cn } from '@/lib/utils'
-
-const badgeVariants = cva(
- 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
- {
- variants: {
- variant: {
- default:
- 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
- secondary:
- 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
- destructive:
- 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
- outline: 'text-foreground',
- success:
- 'border-transparent bg-green-500 text-white hover:bg-green-500/80',
- warning:
- 'border-transparent bg-yellow-500 text-white hover:bg-yellow-500/80',
- },
- },
- defaultVariants: {
- variant: 'default',
- },
- }
-)
-
-export interface BadgeProps
- extends React.HTMLAttributes,
- VariantProps {}
-
-function Badge({ className, variant, ...props }: BadgeProps) {
- return (
-
- )
-}
-
-export { Badge, badgeVariants }
diff --git a/admin-ui/src/components/ui/button.tsx b/admin-ui/src/components/ui/button.tsx
deleted file mode 100644
index 640e0f6ec37b5d09449cd24cb22eb107819574ad..0000000000000000000000000000000000000000
--- a/admin-ui/src/components/ui/button.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import * as React from 'react'
-import { Slot } from '@radix-ui/react-slot'
-import { cva, type VariantProps } from 'class-variance-authority'
-import { cn } from '@/lib/utils'
-
-const buttonVariants = cva(
- 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
- {
- variants: {
- variant: {
- default: 'bg-primary text-primary-foreground hover:bg-primary/90',
- destructive:
- 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
- outline:
- 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
- secondary:
- 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
- ghost: 'hover:bg-accent hover:text-accent-foreground',
- link: 'text-primary underline-offset-4 hover:underline',
- },
- size: {
- default: 'h-10 px-4 py-2',
- sm: 'h-9 rounded-md px-3',
- lg: 'h-11 rounded-md px-8',
- icon: 'h-10 w-10',
- },
- },
- defaultVariants: {
- variant: 'default',
- size: 'default',
- },
- }
-)
-
-export interface ButtonProps
- extends React.ButtonHTMLAttributes,
- VariantProps {
- asChild?: boolean
-}
-
-const Button = React.forwardRef(
- ({ className, variant, size, asChild = false, ...props }, ref) => {
- const Comp = asChild ? Slot : 'button'
- return (
-
- )
- }
-)
-Button.displayName = 'Button'
-
-export { Button, buttonVariants }
diff --git a/admin-ui/src/components/ui/card.tsx b/admin-ui/src/components/ui/card.tsx
deleted file mode 100644
index 18e786a24b5c6130963b1724ebdd8d44f7b0567f..0000000000000000000000000000000000000000
--- a/admin-ui/src/components/ui/card.tsx
+++ /dev/null
@@ -1,78 +0,0 @@
-import * as React from 'react'
-import { cn } from '@/lib/utils'
-
-const Card = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => (
-
-))
-Card.displayName = 'Card'
-
-const CardHeader = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => (
-
-))
-CardHeader.displayName = 'CardHeader'
-
-const CardTitle = React.forwardRef<
- HTMLParagraphElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => (
-
-))
-CardTitle.displayName = 'CardTitle'
-
-const CardDescription = React.forwardRef<
- HTMLParagraphElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => (
-
-))
-CardDescription.displayName = 'CardDescription'
-
-const CardContent = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => (
-
-))
-CardContent.displayName = 'CardContent'
-
-const CardFooter = React.forwardRef<
- HTMLDivElement,
- React.HTMLAttributes
->(({ className, ...props }, ref) => (
-
-))
-CardFooter.displayName = 'CardFooter'
-
-export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
diff --git a/admin-ui/src/components/ui/dialog.tsx b/admin-ui/src/components/ui/dialog.tsx
deleted file mode 100644
index ba67aa862159add7275c9b9100ba24b02fcda186..0000000000000000000000000000000000000000
--- a/admin-ui/src/components/ui/dialog.tsx
+++ /dev/null
@@ -1,119 +0,0 @@
-import * as React from 'react'
-import * as DialogPrimitive from '@radix-ui/react-dialog'
-import { X } from 'lucide-react'
-import { cn } from '@/lib/utils'
-
-const Dialog = DialogPrimitive.Root
-
-const DialogTrigger = DialogPrimitive.Trigger
-
-const DialogPortal = DialogPrimitive.Portal
-
-const DialogClose = DialogPrimitive.Close
-
-const DialogOverlay = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
-
-const DialogContent = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, children, ...props }, ref) => (
-
-
-
- {children}
-
-
- 关闭
-
-
-
-))
-DialogContent.displayName = DialogPrimitive.Content.displayName
-
-const DialogHeader = ({
- className,
- ...props
-}: React.HTMLAttributes) => (
-
-)
-DialogHeader.displayName = 'DialogHeader'
-
-const DialogFooter = ({
- className,
- ...props
-}: React.HTMLAttributes) => (
-
-)
-DialogFooter.displayName = 'DialogFooter'
-
-const DialogTitle = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-DialogTitle.displayName = DialogPrimitive.Title.displayName
-
-const DialogDescription = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-))
-DialogDescription.displayName = DialogPrimitive.Description.displayName
-
-export {
- Dialog,
- DialogPortal,
- DialogOverlay,
- DialogClose,
- DialogTrigger,
- DialogContent,
- DialogHeader,
- DialogFooter,
- DialogTitle,
- DialogDescription,
-}
diff --git a/admin-ui/src/components/ui/input.tsx b/admin-ui/src/components/ui/input.tsx
deleted file mode 100644
index d32ad81fb1d1ac66263698220ff19528cdc03d60..0000000000000000000000000000000000000000
--- a/admin-ui/src/components/ui/input.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import * as React from 'react'
-import { cn } from '@/lib/utils'
-
-export interface InputProps
- extends React.InputHTMLAttributes {}
-
-const Input = React.forwardRef(
- ({ className, type, ...props }, ref) => {
- return (
-
- )
- }
-)
-Input.displayName = 'Input'
-
-export { Input }
diff --git a/admin-ui/src/components/ui/progress.tsx b/admin-ui/src/components/ui/progress.tsx
deleted file mode 100644
index b400823c409b248dd7433546dd1b8fd6542c3fac..0000000000000000000000000000000000000000
--- a/admin-ui/src/components/ui/progress.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import * as React from 'react'
-import { cn } from '@/lib/utils'
-
-interface ProgressProps extends React.HTMLAttributes {
- value?: number
- max?: number
-}
-
-const Progress = React.forwardRef(
- ({ className, value = 0, max = 100, ...props }, ref) => {
- const percentage = Math.min(Math.max((value / max) * 100, 0), 100)
-
- return (
-
-
80 ? 'bg-red-500' : percentage > 60 ? 'bg-yellow-500' : 'bg-green-500'
- )}
- style={{ width: `${percentage}%` }}
- />
-
- )
- }
-)
-Progress.displayName = 'Progress'
-
-export { Progress }
diff --git a/admin-ui/src/components/ui/sonner.tsx b/admin-ui/src/components/ui/sonner.tsx
deleted file mode 100644
index f5d591c7902618bbbb6de455ea2113187b55515f..0000000000000000000000000000000000000000
--- a/admin-ui/src/components/ui/sonner.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { Toaster as Sonner } from 'sonner'
-
-type ToasterProps = React.ComponentProps
-
-const Toaster = ({ ...props }: ToasterProps) => {
- return (
-
- )
-}
-
-export { Toaster }
diff --git a/admin-ui/src/components/ui/switch.tsx b/admin-ui/src/components/ui/switch.tsx
deleted file mode 100644
index f2d6513519d95e611b2b0fe01627abfeeeb6a795..0000000000000000000000000000000000000000
--- a/admin-ui/src/components/ui/switch.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import * as React from 'react'
-import * as SwitchPrimitives from '@radix-ui/react-switch'
-import { cn } from '@/lib/utils'
-
-const Switch = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-
-
-))
-Switch.displayName = SwitchPrimitives.Root.displayName
-
-export { Switch }
diff --git a/admin-ui/src/hooks/use-credentials.ts b/admin-ui/src/hooks/use-credentials.ts
deleted file mode 100644
index 8c8e9afc4c71ec7306fb8ee4bbedbb89644bbc24..0000000000000000000000000000000000000000
--- a/admin-ui/src/hooks/use-credentials.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
-import {
- getCredentials,
- setCredentialDisabled,
- setCredentialPriority,
- resetCredentialFailure,
- getCredentialBalance,
- addCredential,
- deleteCredential,
-} from '@/api/credentials'
-import type { AddCredentialRequest } from '@/types/api'
-
-// 查询凭据列表
-export function useCredentials() {
- return useQuery({
- queryKey: ['credentials'],
- queryFn: getCredentials,
- refetchInterval: 30000, // 每 30 秒刷新一次
- })
-}
-
-// 查询凭据余额
-export function useCredentialBalance(id: number | null) {
- return useQuery({
- queryKey: ['credential-balance', id],
- queryFn: () => getCredentialBalance(id!),
- enabled: id !== null,
- retry: false, // 余额查询失败时不重试(避免重复请求被封禁的账号)
- })
-}
-
-// 设置禁用状态
-export function useSetDisabled() {
- const queryClient = useQueryClient()
- return useMutation({
- mutationFn: ({ id, disabled }: { id: number; disabled: boolean }) =>
- setCredentialDisabled(id, disabled),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['credentials'] })
- },
- })
-}
-
-// 设置优先级
-export function useSetPriority() {
- const queryClient = useQueryClient()
- return useMutation({
- mutationFn: ({ id, priority }: { id: number; priority: number }) =>
- setCredentialPriority(id, priority),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['credentials'] })
- },
- })
-}
-
-// 重置失败计数
-export function useResetFailure() {
- const queryClient = useQueryClient()
- return useMutation({
- mutationFn: (id: number) => resetCredentialFailure(id),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['credentials'] })
- },
- })
-}
-
-// 添加新凭据
-export function useAddCredential() {
- const queryClient = useQueryClient()
- return useMutation({
- mutationFn: (req: AddCredentialRequest) => addCredential(req),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['credentials'] })
- },
- })
-}
-
-// 删除凭据
-export function useDeleteCredential() {
- const queryClient = useQueryClient()
- return useMutation({
- mutationFn: (id: number) => deleteCredential(id),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['credentials'] })
- },
- })
-}
diff --git a/admin-ui/src/index.css b/admin-ui/src/index.css
deleted file mode 100644
index 01678720f31557bfe2b34127911f6836f925eaac..0000000000000000000000000000000000000000
--- a/admin-ui/src/index.css
+++ /dev/null
@@ -1,60 +0,0 @@
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
-
-@layer base {
- :root {
- --background: 0 0% 100%;
- --foreground: 222.2 84% 4.9%;
- --card: 0 0% 100%;
- --card-foreground: 222.2 84% 4.9%;
- --popover: 0 0% 100%;
- --popover-foreground: 222.2 84% 4.9%;
- --primary: 222.2 47.4% 11.2%;
- --primary-foreground: 210 40% 98%;
- --secondary: 210 40% 96.1%;
- --secondary-foreground: 222.2 47.4% 11.2%;
- --muted: 210 40% 96.1%;
- --muted-foreground: 215.4 16.3% 46.9%;
- --accent: 210 40% 96.1%;
- --accent-foreground: 222.2 47.4% 11.2%;
- --destructive: 0 84.2% 60.2%;
- --destructive-foreground: 210 40% 98%;
- --border: 214.3 31.8% 91.4%;
- --input: 214.3 31.8% 91.4%;
- --ring: 222.2 84% 4.9%;
- --radius: 0.5rem;
- }
-
- .dark {
- --background: 222.2 84% 4.9%;
- --foreground: 210 40% 98%;
- --card: 222.2 84% 4.9%;
- --card-foreground: 210 40% 98%;
- --popover: 222.2 84% 4.9%;
- --popover-foreground: 210 40% 98%;
- --primary: 210 40% 98%;
- --primary-foreground: 222.2 47.4% 11.2%;
- --secondary: 217.2 32.6% 17.5%;
- --secondary-foreground: 210 40% 98%;
- --muted: 217.2 32.6% 17.5%;
- --muted-foreground: 215 20.2% 65.1%;
- --accent: 217.2 32.6% 17.5%;
- --accent-foreground: 210 40% 98%;
- --destructive: 0 62.8% 30.6%;
- --destructive-foreground: 210 40% 98%;
- --border: 217.2 32.6% 17.5%;
- --input: 217.2 32.6% 17.5%;
- --ring: 212.7 26.8% 83.9%;
- }
-}
-
-@layer base {
- * {
- @apply border-border;
- }
- body {
- @apply bg-background text-foreground;
- font-feature-settings: "rlig" 1, "calt" 1;
- }
-}
diff --git a/admin-ui/src/lib/storage.ts b/admin-ui/src/lib/storage.ts
deleted file mode 100644
index b61b741c46b126977e79a1c39e03b79833759986..0000000000000000000000000000000000000000
--- a/admin-ui/src/lib/storage.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-const API_KEY_STORAGE_KEY = 'adminApiKey'
-
-export const storage = {
- getApiKey: () => localStorage.getItem(API_KEY_STORAGE_KEY),
- setApiKey: (key: string) => localStorage.setItem(API_KEY_STORAGE_KEY, key),
- removeApiKey: () => localStorage.removeItem(API_KEY_STORAGE_KEY),
-}
diff --git a/admin-ui/src/lib/utils.ts b/admin-ui/src/lib/utils.ts
deleted file mode 100644
index 0be83dd12f2f2a59a26043f78985096b821fd8c9..0000000000000000000000000000000000000000
--- a/admin-ui/src/lib/utils.ts
+++ /dev/null
@@ -1,106 +0,0 @@
-import { clsx, type ClassValue } from 'clsx'
-import { twMerge } from 'tailwind-merge'
-
-export function cn(...inputs: ClassValue[]) {
- return twMerge(clsx(inputs))
-}
-
-/**
- * 解析后端错误响应,提取用户友好的错误信息
- */
-export interface ParsedError {
- /** 简短的错误标题 */
- title: string
- /** 详细的错误描述 */
- detail?: string
- /** 错误类型 */
- type?: string
-}
-
-/**
- * 从错误对象中提取错误消息
- * 支持 Axios 错误和普通 Error 对象
- */
-export function extractErrorMessage(error: unknown): string {
- const parsed = parseError(error)
- return parsed.title
-}
-
-/**
- * 解析错误,返回结构化的错误信息
- */
-export function parseError(error: unknown): ParsedError {
- if (!error || typeof error !== 'object') {
- return { title: '未知错误' }
- }
-
- const axiosError = error as Record
- const response = axiosError.response as Record | undefined
- const data = response?.data as Record | undefined
- const errorObj = data?.error as Record | undefined
-
- // 尝试从后端错误响应中提取信息
- if (errorObj && typeof errorObj.message === 'string') {
- const message = errorObj.message
- const type = typeof errorObj.type === 'string' ? errorObj.type : undefined
-
- // 解析嵌套的错误信息(如:上游服务错误: 权限不足: 403 {...})
- const parsed = parseNestedErrorMessage(message)
-
- return {
- title: parsed.title,
- detail: parsed.detail,
- type,
- }
- }
-
- // 回退到 Error.message
- if ('message' in axiosError && typeof axiosError.message === 'string') {
- return { title: axiosError.message }
- }
-
- return { title: '未知错误' }
-}
-
-/**
- * 解析嵌套的错误消息
- * 例如:"上游服务错误: 权限不足,无法获取使用额度: 403 Forbidden {...}"
- */
-function parseNestedErrorMessage(message: string): { title: string; detail?: string } {
- // 尝试提取 HTTP 状态码(如 403、502 等)
- const statusMatch = message.match(/(\d{3})\s+\w+/)
- const statusCode = statusMatch ? statusMatch[1] : null
-
- // 尝试提取 JSON 中的 message 字段
- const jsonMatch = message.match(/\{[^{}]*"message"\s*:\s*"([^"]+)"[^{}]*\}/)
- if (jsonMatch) {
- const innerMessage = jsonMatch[1]
- // 提取主要错误原因(去掉前缀)
- const parts = message.split(':').map(s => s.trim())
- const mainReason = parts.length > 1 ? parts[1].split(':')[0] : parts[0]
-
- // 在 title 中包含状态码
- const title = statusCode
- ? `${mainReason || '服务错误'} (${statusCode})`
- : (mainReason || '服务错误')
-
- return {
- title,
- detail: innerMessage,
- }
- }
-
- // 尝试按冒号分割,提取主要信息
- const colonParts = message.split(':')
- if (colonParts.length >= 2) {
- const mainPart = colonParts[1].trim().split(':')[0].trim()
- const title = statusCode ? `${mainPart} (${statusCode})` : mainPart
-
- return {
- title,
- detail: colonParts.slice(2).join(':').trim() || undefined,
- }
- }
-
- return { title: message }
-}
diff --git a/admin-ui/src/main.tsx b/admin-ui/src/main.tsx
deleted file mode 100644
index 17ef62af4b05e9859405893d25919d3d6cc2ecf3..0000000000000000000000000000000000000000
--- a/admin-ui/src/main.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import React from 'react'
-import ReactDOM from 'react-dom/client'
-import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
-import App from './App'
-import './index.css'
-
-const queryClient = new QueryClient({
- defaultOptions: {
- queries: {
- staleTime: 5000,
- refetchOnWindowFocus: false,
- },
- },
-})
-
-ReactDOM.createRoot(document.getElementById('root')!).render(
-
-
-
-
- ,
-)
diff --git a/admin-ui/src/types/api.ts b/admin-ui/src/types/api.ts
deleted file mode 100644
index 05a77346544586fe569a78e7ba8d4aba90516bbd..0000000000000000000000000000000000000000
--- a/admin-ui/src/types/api.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-// 凭据状态响应
-export interface CredentialsStatusResponse {
- total: number
- available: number
- currentId: number
- credentials: CredentialStatusItem[]
-}
-
-// 单个凭据状态
-export interface CredentialStatusItem {
- id: number
- priority: number
- disabled: boolean
- failureCount: number
- isCurrent: boolean
- expiresAt: string | null
- authMethod: string | null
- hasProfileArn: boolean
-}
-
-// 余额响应
-export interface BalanceResponse {
- id: number
- subscriptionTitle: string | null
- currentUsage: number
- usageLimit: number
- remaining: number
- usagePercentage: number
- nextResetAt: number | null
-}
-
-// 成功响应
-export interface SuccessResponse {
- success: boolean
- message: string
-}
-
-// 错误响应
-export interface AdminErrorResponse {
- error: {
- type: string
- message: string
- }
-}
-
-// 请求类型
-export interface SetDisabledRequest {
- disabled: boolean
-}
-
-export interface SetPriorityRequest {
- priority: number
-}
-
-// 添加凭据请求
-export interface AddCredentialRequest {
- refreshToken: string
- authMethod?: 'social' | 'idc' | 'builder-id'
- clientId?: string
- clientSecret?: string
- priority?: number
-}
-
-// 添加凭据响应
-export interface AddCredentialResponse {
- success: boolean
- message: string
- credentialId: number
-}
diff --git a/admin-ui/tailwind.config.js b/admin-ui/tailwind.config.js
deleted file mode 100644
index 2e36b3bd0a1fd448a2c746716c76baf3bdb8edbc..0000000000000000000000000000000000000000
--- a/admin-ui/tailwind.config.js
+++ /dev/null
@@ -1,53 +0,0 @@
-/** @type {import('tailwindcss').Config} */
-export default {
- darkMode: 'class',
- content: [
- './index.html',
- './src/**/*.{js,ts,jsx,tsx}',
- ],
- theme: {
- extend: {
- colors: {
- border: 'hsl(var(--border))',
- input: 'hsl(var(--input))',
- ring: 'hsl(var(--ring))',
- background: 'hsl(var(--background))',
- foreground: 'hsl(var(--foreground))',
- primary: {
- DEFAULT: 'hsl(var(--primary))',
- foreground: 'hsl(var(--primary-foreground))',
- },
- secondary: {
- DEFAULT: 'hsl(var(--secondary))',
- foreground: 'hsl(var(--secondary-foreground))',
- },
- destructive: {
- DEFAULT: 'hsl(var(--destructive))',
- foreground: 'hsl(var(--destructive-foreground))',
- },
- muted: {
- DEFAULT: 'hsl(var(--muted))',
- foreground: 'hsl(var(--muted-foreground))',
- },
- accent: {
- DEFAULT: 'hsl(var(--accent))',
- foreground: 'hsl(var(--accent-foreground))',
- },
- popover: {
- DEFAULT: 'hsl(var(--popover))',
- foreground: 'hsl(var(--popover-foreground))',
- },
- card: {
- DEFAULT: 'hsl(var(--card))',
- foreground: 'hsl(var(--card-foreground))',
- },
- },
- borderRadius: {
- lg: 'var(--radius)',
- md: 'calc(var(--radius) - 2px)',
- sm: 'calc(var(--radius) - 4px)',
- },
- },
- },
- plugins: [],
-}
diff --git a/admin-ui/tsconfig.json b/admin-ui/tsconfig.json
deleted file mode 100644
index 5e1feb437651831f728f7188d3b8ce2c0e3eb8f7..0000000000000000000000000000000000000000
--- a/admin-ui/tsconfig.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "compilerOptions": {
- "target": "ES2020",
- "useDefineForClassFields": true,
- "lib": ["ES2020", "DOM", "DOM.Iterable"],
- "module": "ESNext",
- "skipLibCheck": true,
- "moduleResolution": "bundler",
- "allowImportingTsExtensions": true,
- "isolatedModules": true,
- "moduleDetection": "force",
- "noEmit": true,
- "jsx": "react-jsx",
- "strict": true,
- "noUnusedLocals": true,
- "noUnusedParameters": true,
- "noFallthroughCasesInSwitch": true,
- "baseUrl": ".",
- "paths": {
- "@/*": ["./src/*"]
- }
- },
- "include": ["src"]
-}
diff --git a/admin-ui/vite.config.ts b/admin-ui/vite.config.ts
deleted file mode 100644
index 5c8e864871a88c8ded8cb0af1198be8eb5fae61c..0000000000000000000000000000000000000000
--- a/admin-ui/vite.config.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { defineConfig } from 'vite'
-import react from '@vitejs/plugin-react-swc'
-import path from 'path'
-
-export default defineConfig({
- plugins: [react()],
- base: '/admin/',
- resolve: {
- alias: {
- '@': path.resolve(__dirname, './src'),
- },
- },
- server: {
- proxy: {
- '/api': {
- target: 'http://localhost:8080',
- changeOrigin: true,
- },
- },
- },
- build: {
- outDir: 'dist',
- emptyOutDir: true,
- },
-})
diff --git a/config.example.json b/config.example.json
deleted file mode 100644
index ab3544a9294f893db5a11ea2a090d1fce0bc0750..0000000000000000000000000000000000000000
--- a/config.example.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "host": "127.0.0.1",
- "port": 8990,
- "apiKey": "sk-kiro-rs-qazWSXedcRFV123456",
- "region": "us-east-1",
- "adminApiKey": "sk-admin-your-secret-key"
-}
\ No newline at end of file
diff --git a/credentials.example.idc.json b/credentials.example.idc.json
deleted file mode 100644
index 09f7cf135b37778be45fdd4d8d46407ac1babdf1..0000000000000000000000000000000000000000
--- a/credentials.example.idc.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "refreshToken": "xxxxxxxxxxxxxxxxxxxx",
- "expiresAt": "2025-12-31T02:32:45.144Z",
- "authMethod": "idc",
- "clientId": "xxxxxxxxx",
- "clientSecret": "xxxxxxxxx",
- "region": "us-east-2",
- "machineId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
-}
diff --git a/credentials.example.multiple.json b/credentials.example.multiple.json
deleted file mode 100644
index 0a57a002a4b48726cbfe8ef00f4a966af6868e60..0000000000000000000000000000000000000000
--- a/credentials.example.multiple.json
+++ /dev/null
@@ -1,19 +0,0 @@
-[
- {
- "refreshToken": "xxxxxxxxxxxxxxxxxxxx",
- "expiresAt": "2025-12-31T02:32:45.144Z",
- "authMethod": "social",
- "machineId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- "priority": 0
- },
- {
- "refreshToken": "yyyyyyyyyyyyyyyyyyyy",
- "expiresAt": "2025-12-31T02:32:45.144Z",
- "authMethod": "idc",
- "clientId": "xxxxxxxxx",
- "clientSecret": "xxxxxxxxx",
- "region": "us-east-2",
- "machineId": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
- "priority": 1
- }
-]
diff --git a/credentials.example.social.json b/credentials.example.social.json
deleted file mode 100644
index 259897f2c7f5bb734a45bbfc754dbaea97a1834c..0000000000000000000000000000000000000000
--- a/credentials.example.social.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "refreshToken": "xxxxxxxxxxxxxxxxxxxx",
- "expiresAt": "2025-12-31T02:32:45.144Z",
- "authMethod": "social",
- "machineId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
-}
diff --git a/entrypoint.sh b/entrypoint.sh
deleted file mode 100644
index cc806f0ffecb596a804a36616c9c0978aeec7207..0000000000000000000000000000000000000000
--- a/entrypoint.sh
+++ /dev/null
@@ -1,61 +0,0 @@
-#!/bin/sh
-
-# 从环境变量生成 config.json
-if [ -n "${ADMIN_API_KEY}" ]; then
- # 如果设置了 ADMIN_API_KEY,包含在配置中
- cat > /app/config/config.json << EOF
-{
- "host": "0.0.0.0",
- "port": 7860,
- "apiKey": "${API_KEY:-sk-kiro-rs-default}",
- "region": "${REGION:-us-east-1}",
- "adminApiKey": "${ADMIN_API_KEY}"
-}
-EOF
-else
- # 否则不包含 adminApiKey
- cat > /app/config/config.json << EOF
-{
- "host": "0.0.0.0",
- "port": 7860,
- "apiKey": "${API_KEY:-sk-kiro-rs-default}",
- "region": "${REGION:-us-east-1}"
-}
-EOF
-fi
-
-# 从环境变量生成 credentials.json
-# 支持两种模式:
-# 1. 多凭据模式:通过 CREDENTIALS_JSON 环境变量传入完整的 JSON 数组
-# 2. 单凭据模式:通过单独的环境变量(向后兼容)
-
-if [ -n "${CREDENTIALS_JSON}" ]; then
- # 多凭据模式:直接使用 CREDENTIALS_JSON
- echo "${CREDENTIALS_JSON}" > /app/config/credentials.json
- echo "Using multi-credential mode from CREDENTIALS_JSON"
-else
- # 单凭据模式
- if [ "${AUTH_METHOD}" = "idc" ]; then
- cat > /app/config/credentials.json << EOF
-{
- "refreshToken": "${REFRESH_TOKEN}",
- "expiresAt": "${EXPIRES_AT:-2020-01-01T00:00:00.000Z}",
- "authMethod": "idc",
- "clientId": "${CLIENT_ID}",
- "clientSecret": "${CLIENT_SECRET}"
-}
-EOF
- else
- cat > /app/config/credentials.json << EOF
-{
- "refreshToken": "${REFRESH_TOKEN}",
- "expiresAt": "${EXPIRES_AT:-2020-01-01T00:00:00.000Z}",
- "authMethod": "${AUTH_METHOD:-social}"
-}
-EOF
- fi
- echo "Using single-credential mode"
-fi
-
-echo "Starting kiro-rs..."
-exec /app/kiro-rs -c /app/config/config.json --credentials /app/config/credentials.json
diff --git a/src/admin/error.rs b/src/admin/error.rs
deleted file mode 100644
index e1f921954ebc94a168d2ff8b00a23d23de369b64..0000000000000000000000000000000000000000
--- a/src/admin/error.rs
+++ /dev/null
@@ -1,64 +0,0 @@
-//! Admin API 错误类型定义
-
-use std::fmt;
-
-use axum::http::StatusCode;
-
-use super::types::AdminErrorResponse;
-
-/// Admin 服务错误类型
-#[derive(Debug)]
-pub enum AdminServiceError {
- /// 凭据不存在
- NotFound { id: u64 },
-
- /// 上游服务调用失败(网络、API 错误等)
- UpstreamError(String),
-
- /// 内部状态错误
- InternalError(String),
-
- /// 凭据无效(验证失败)
- InvalidCredential(String),
-}
-
-impl fmt::Display for AdminServiceError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- AdminServiceError::NotFound { id } => {
- write!(f, "凭据不存在: {}", id)
- }
- AdminServiceError::UpstreamError(msg) => write!(f, "上游服务错误: {}", msg),
- AdminServiceError::InternalError(msg) => write!(f, "内部错误: {}", msg),
- AdminServiceError::InvalidCredential(msg) => write!(f, "凭据无效: {}", msg),
- }
- }
-}
-
-impl std::error::Error for AdminServiceError {}
-
-impl AdminServiceError {
- /// 获取对应的 HTTP 状态码
- pub fn status_code(&self) -> StatusCode {
- match self {
- AdminServiceError::NotFound { .. } => StatusCode::NOT_FOUND,
- AdminServiceError::UpstreamError(_) => StatusCode::BAD_GATEWAY,
- AdminServiceError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR,
- AdminServiceError::InvalidCredential(_) => StatusCode::BAD_REQUEST,
- }
- }
-
- /// 转换为 API 错误响应
- pub fn into_response(self) -> AdminErrorResponse {
- match &self {
- AdminServiceError::NotFound { .. } => AdminErrorResponse::not_found(self.to_string()),
- AdminServiceError::UpstreamError(_) => AdminErrorResponse::api_error(self.to_string()),
- AdminServiceError::InternalError(_) => {
- AdminErrorResponse::internal_error(self.to_string())
- }
- AdminServiceError::InvalidCredential(_) => {
- AdminErrorResponse::invalid_request(self.to_string())
- }
- }
- }
-}
diff --git a/src/admin/handlers.rs b/src/admin/handlers.rs
deleted file mode 100644
index 39190b115042016aec39b799067a1e88bf504920..0000000000000000000000000000000000000000
--- a/src/admin/handlers.rs
+++ /dev/null
@@ -1,104 +0,0 @@
-//! Admin API HTTP 处理器
-
-use axum::{
- Json,
- extract::{Path, State},
- response::IntoResponse,
-};
-
-use super::{
- middleware::AdminState,
- types::{AddCredentialRequest, SetDisabledRequest, SetPriorityRequest, SuccessResponse},
-};
-
-/// GET /api/admin/credentials
-/// 获取所有凭据状态
-pub async fn get_all_credentials(State(state): State) -> impl IntoResponse {
- let response = state.service.get_all_credentials();
- Json(response)
-}
-
-/// POST /api/admin/credentials/:id/disabled
-/// 设置凭据禁用状态
-pub async fn set_credential_disabled(
- State(state): State,
- Path(id): Path,
- Json(payload): Json,
-) -> impl IntoResponse {
- match state.service.set_disabled(id, payload.disabled) {
- Ok(_) => {
- let action = if payload.disabled { "禁用" } else { "启用" };
- Json(SuccessResponse::new(format!("凭据 #{} 已{}", id, action))).into_response()
- }
- Err(e) => (e.status_code(), Json(e.into_response())).into_response(),
- }
-}
-
-/// POST /api/admin/credentials/:id/priority
-/// 设置凭据优先级
-pub async fn set_credential_priority(
- State(state): State,
- Path(id): Path,
- Json(payload): Json,
-) -> impl IntoResponse {
- match state.service.set_priority(id, payload.priority) {
- Ok(_) => Json(SuccessResponse::new(format!(
- "凭据 #{} 优先级已设置为 {}",
- id, payload.priority
- )))
- .into_response(),
- Err(e) => (e.status_code(), Json(e.into_response())).into_response(),
- }
-}
-
-/// POST /api/admin/credentials/:id/reset
-/// 重置失败计数并重新启用
-pub async fn reset_failure_count(
- State(state): State,
- Path(id): Path,
-) -> impl IntoResponse {
- match state.service.reset_and_enable(id) {
- Ok(_) => Json(SuccessResponse::new(format!(
- "凭据 #{} 失败计数已重置并重新启用",
- id
- )))
- .into_response(),
- Err(e) => (e.status_code(), Json(e.into_response())).into_response(),
- }
-}
-
-/// GET /api/admin/credentials/:id/balance
-/// 获取指定凭据的余额
-pub async fn get_credential_balance(
- State(state): State,
- Path(id): Path,
-) -> impl IntoResponse {
- match state.service.get_balance(id).await {
- Ok(response) => Json(response).into_response(),
- Err(e) => (e.status_code(), Json(e.into_response())).into_response(),
- }
-}
-
-/// POST /api/admin/credentials
-/// 添加新凭据
-pub async fn add_credential(
- State(state): State,
- Json(payload): Json,
-) -> impl IntoResponse {
- match state.service.add_credential(payload).await {
- Ok(response) => Json(response).into_response(),
- Err(e) => (e.status_code(), Json(e.into_response())).into_response(),
- }
-}
-
-/// DELETE /api/admin/credentials/:id
-/// 删除凭据
-pub async fn delete_credential(
- State(state): State,
- Path(id): Path,
-) -> impl IntoResponse {
- match state.service.delete_credential(id) {
- Ok(_) => Json(SuccessResponse::new(format!("凭据 #{} 已删除", id))).into_response(),
- Err(e) => (e.status_code(), Json(e.into_response())).into_response(),
- }
-}
diff --git a/src/admin/middleware.rs b/src/admin/middleware.rs
deleted file mode 100644
index af11af917580e5d748c00f265d860a14e74b02a2..0000000000000000000000000000000000000000
--- a/src/admin/middleware.rs
+++ /dev/null
@@ -1,50 +0,0 @@
-//! Admin API 中间件
-
-use std::sync::Arc;
-
-use axum::{
- body::Body,
- extract::State,
- http::{Request, StatusCode},
- middleware::Next,
- response::{IntoResponse, Json, Response},
-};
-
-use super::service::AdminService;
-use super::types::AdminErrorResponse;
-use crate::common::auth;
-
-/// Admin API 共享状态
-#[derive(Clone)]
-pub struct AdminState {
- /// Admin API 密钥
- pub admin_api_key: String,
- /// Admin 服务
- pub service: Arc,
-}
-
-impl AdminState {
- pub fn new(admin_api_key: impl Into, service: AdminService) -> Self {
- Self {
- admin_api_key: admin_api_key.into(),
- service: Arc::new(service),
- }
- }
-}
-
-/// Admin API 认证中间件
-pub async fn admin_auth_middleware(
- State(state): State,
- request: Request,
- next: Next,
-) -> Response {
- let api_key = auth::extract_api_key(&request);
-
- match api_key {
- Some(key) if auth::constant_time_eq(&key, &state.admin_api_key) => next.run(request).await,
- _ => {
- let error = AdminErrorResponse::authentication_error();
- (StatusCode::UNAUTHORIZED, Json(error)).into_response()
- }
- }
-}
diff --git a/src/admin/mod.rs b/src/admin/mod.rs
deleted file mode 100644
index 21321f85b4b82acf1eabf1e59a52951ac097f0a3..0000000000000000000000000000000000000000
--- a/src/admin/mod.rs
+++ /dev/null
@@ -1,28 +0,0 @@
-//! Admin API 模块
-//!
-//! 提供凭据管理和监控功能的 HTTP API
-//!
-//! # 功能
-//! - 查询所有凭据状态
-//! - 启用/禁用凭据
-//! - 修改凭据优先级
-//! - 重置失败计数
-//! - 查询凭据余额
-//!
-//! # 使用
-//! ```ignore
-//! let admin_service = AdminService::new(token_manager.clone());
-//! let admin_state = AdminState::new(admin_api_key, admin_service);
-//! let admin_router = create_admin_router(admin_state);
-//! ```
-
-mod error;
-mod handlers;
-mod middleware;
-mod router;
-mod service;
-pub mod types;
-
-pub use middleware::AdminState;
-pub use router::create_admin_router;
-pub use service::AdminService;
diff --git a/src/admin/router.rs b/src/admin/router.rs
deleted file mode 100644
index c833dc5260efad64e2bcefa36bd34da4e201d231..0000000000000000000000000000000000000000
--- a/src/admin/router.rs
+++ /dev/null
@@ -1,47 +0,0 @@
-//! Admin API 路由配置
-
-use axum::{
- Router, middleware,
- routing::{delete, get, post},
-};
-
-use super::{
- handlers::{
- add_credential, delete_credential, get_all_credentials, get_credential_balance,
- reset_failure_count, set_credential_disabled, set_credential_priority,
- },
- middleware::{AdminState, admin_auth_middleware},
-};
-
-/// 创建 Admin API 路由
-///
-/// # 端点
-/// - `GET /credentials` - 获取所有凭据状态
-/// - `POST /credentials` - 添加新凭据
-/// - `DELETE /credentials/:id` - 删除凭据
-/// - `POST /credentials/:id/disabled` - 设置凭据禁用状态
-/// - `POST /credentials/:id/priority` - 设置凭据优先级
-/// - `POST /credentials/:id/reset` - 重置失败计数
-/// - `GET /credentials/:id/balance` - 获取凭据余额
-///
-/// # 认证
-/// 需要 Admin API Key 认证,支持:
-/// - `x-api-key` header
-/// - `Authorization: Bearer ` header
-pub fn create_admin_router(state: AdminState) -> Router {
- Router::new()
- .route(
- "/credentials",
- get(get_all_credentials).post(add_credential),
- )
- .route("/credentials/{id}", delete(delete_credential))
- .route("/credentials/{id}/disabled", post(set_credential_disabled))
- .route("/credentials/{id}/priority", post(set_credential_priority))
- .route("/credentials/{id}/reset", post(reset_failure_count))
- .route("/credentials/{id}/balance", get(get_credential_balance))
- .layer(middleware::from_fn_with_state(
- state.clone(),
- admin_auth_middleware,
- ))
- .with_state(state)
-}
diff --git a/src/admin/service.rs b/src/admin/service.rs
deleted file mode 100644
index f6affa5a0b72b3fd66241321289bc076cfc30c43..0000000000000000000000000000000000000000
--- a/src/admin/service.rs
+++ /dev/null
@@ -1,234 +0,0 @@
-//! Admin API 业务逻辑服务
-
-use std::sync::Arc;
-
-use crate::kiro::model::credentials::KiroCredentials;
-use crate::kiro::token_manager::MultiTokenManager;
-
-use super::error::AdminServiceError;
-use super::types::{
- AddCredentialRequest, AddCredentialResponse, BalanceResponse, CredentialStatusItem,
- CredentialsStatusResponse,
-};
-
-/// Admin 服务
-///
-/// 封装所有 Admin API 的业务逻辑
-pub struct AdminService {
- token_manager: Arc,
-}
-
-impl AdminService {
- pub fn new(token_manager: Arc) -> Self {
- Self { token_manager }
- }
-
- /// 获取所有凭据状态
- pub fn get_all_credentials(&self) -> CredentialsStatusResponse {
- let snapshot = self.token_manager.snapshot();
-
- let mut credentials: Vec = snapshot
- .entries
- .into_iter()
- .map(|entry| CredentialStatusItem {
- id: entry.id,
- priority: entry.priority,
- disabled: entry.disabled,
- failure_count: entry.failure_count,
- is_current: entry.id == snapshot.current_id,
- expires_at: entry.expires_at,
- auth_method: entry.auth_method,
- has_profile_arn: entry.has_profile_arn,
- })
- .collect();
-
- // 按优先级排序(数字越小优先级越高)
- credentials.sort_by_key(|c| c.priority);
-
- CredentialsStatusResponse {
- total: snapshot.total,
- available: snapshot.available,
- current_id: snapshot.current_id,
- credentials,
- }
- }
-
- /// 设置凭据禁用状态
- pub fn set_disabled(&self, id: u64, disabled: bool) -> Result<(), AdminServiceError> {
- // 先获取当前凭据 ID,用于判断是否需要切换
- let snapshot = self.token_manager.snapshot();
- let current_id = snapshot.current_id;
-
- self.token_manager
- .set_disabled(id, disabled)
- .map_err(|e| self.classify_error(e, id))?;
-
- // 只有禁用的是当前凭据时才尝试切换到下一个
- if disabled && id == current_id {
- let _ = self.token_manager.switch_to_next();
- }
- Ok(())
- }
-
- /// 设置凭据优先级
- pub fn set_priority(&self, id: u64, priority: u32) -> Result<(), AdminServiceError> {
- self.token_manager
- .set_priority(id, priority)
- .map_err(|e| self.classify_error(e, id))
- }
-
- /// 重置失败计数并重新启用
- pub fn reset_and_enable(&self, id: u64) -> Result<(), AdminServiceError> {
- self.token_manager
- .reset_and_enable(id)
- .map_err(|e| self.classify_error(e, id))
- }
-
- /// 获取凭据余额
- pub async fn get_balance(&self, id: u64) -> Result {
- let usage = self
- .token_manager
- .get_usage_limits_for(id)
- .await
- .map_err(|e| self.classify_balance_error(e, id))?;
-
- let current_usage = usage.current_usage();
- let usage_limit = usage.usage_limit();
- let remaining = (usage_limit - current_usage).max(0.0);
- let usage_percentage = if usage_limit > 0.0 {
- (current_usage / usage_limit * 100.0).min(100.0)
- } else {
- 0.0
- };
-
- Ok(BalanceResponse {
- id,
- subscription_title: usage.subscription_title().map(|s| s.to_string()),
- current_usage,
- usage_limit,
- remaining,
- usage_percentage,
- next_reset_at: usage.next_date_reset,
- })
- }
-
- /// 添加新凭据
- pub async fn add_credential(
- &self,
- req: AddCredentialRequest,
- ) -> Result {
- // 构建凭据对象
- let new_cred = KiroCredentials {
- id: None,
- access_token: None,
- refresh_token: Some(req.refresh_token),
- profile_arn: None,
- expires_at: None,
- auth_method: Some(req.auth_method),
- client_id: req.client_id,
- client_secret: req.client_secret,
- priority: req.priority,
- region: req.region,
- machine_id: req.machine_id,
- };
-
- // 调用 token_manager 添加凭据
- let credential_id = self
- .token_manager
- .add_credential(new_cred)
- .await
- .map_err(|e| self.classify_add_error(e))?;
-
- Ok(AddCredentialResponse {
- success: true,
- message: format!("凭据添加成功,ID: {}", credential_id),
- credential_id,
- })
- }
-
- /// 删除凭据
- pub fn delete_credential(&self, id: u64) -> Result<(), AdminServiceError> {
- self.token_manager
- .delete_credential(id)
- .map_err(|e| self.classify_delete_error(e, id))
- }
-
- /// 分类简单操作错误(set_disabled, set_priority, reset_and_enable)
- fn classify_error(&self, e: anyhow::Error, id: u64) -> AdminServiceError {
- let msg = e.to_string();
- if msg.contains("不存在") {
- AdminServiceError::NotFound { id }
- } else {
- AdminServiceError::InternalError(msg)
- }
- }
-
- /// 分类余额查询错误(可能涉及上游 API 调用)
- fn classify_balance_error(&self, e: anyhow::Error, id: u64) -> AdminServiceError {
- let msg = e.to_string();
-
- // 1. 凭据不存在
- if msg.contains("不存在") {
- return AdminServiceError::NotFound { id };
- }
-
- // 2. 上游服务错误特征:HTTP 响应错误或网络错误
- let is_upstream_error =
- // HTTP 响应错误(来自 refresh_*_token 的错误消息)
- msg.contains("凭证已过期或无效") ||
- msg.contains("权限不足") ||
- msg.contains("已被限流") ||
- msg.contains("服务器错误") ||
- msg.contains("Token 刷新失败") ||
- msg.contains("暂时不可用") ||
- // 网络错误(reqwest 错误)
- msg.contains("error trying to connect") ||
- msg.contains("connection") ||
- msg.contains("timeout") ||
- msg.contains("timed out");
-
- if is_upstream_error {
- AdminServiceError::UpstreamError(msg)
- } else {
- // 3. 默认归类为内部错误(本地验证失败、配置错误等)
- // 包括:缺少 refreshToken、refreshToken 已被截断、无法生成 machineId 等
- AdminServiceError::InternalError(msg)
- }
- }
-
- /// 分类添加凭据错误
- fn classify_add_error(&self, e: anyhow::Error) -> AdminServiceError {
- let msg = e.to_string();
-
- // 凭据验证失败(refreshToken 无效、格式错误等)
- let is_invalid_credential = msg.contains("缺少 refreshToken")
- || msg.contains("refreshToken 为空")
- || msg.contains("refreshToken 已被截断")
- || msg.contains("凭证已过期或无效")
- || msg.contains("权限不足")
- || msg.contains("已被限流");
-
- if is_invalid_credential {
- AdminServiceError::InvalidCredential(msg)
- } else if msg.contains("error trying to connect")
- || msg.contains("connection")
- || msg.contains("timeout")
- {
- AdminServiceError::UpstreamError(msg)
- } else {
- AdminServiceError::InternalError(msg)
- }
- }
-
- /// 分类删除凭据错误
- fn classify_delete_error(&self, e: anyhow::Error, id: u64) -> AdminServiceError {
- let msg = e.to_string();
- if msg.contains("不存在") {
- AdminServiceError::NotFound { id }
- } else if msg.contains("只能删除已禁用的凭据") {
- AdminServiceError::InvalidCredential(msg)
- } else {
- AdminServiceError::InternalError(msg)
- }
- }
-}
diff --git a/src/admin/types.rs b/src/admin/types.rs
deleted file mode 100644
index 52cd593ea9723de0bcdd0eb1f05f6977390ec91d..0000000000000000000000000000000000000000
--- a/src/admin/types.rs
+++ /dev/null
@@ -1,187 +0,0 @@
-//! Admin API 类型定义
-
-use serde::{Deserialize, Serialize};
-
-// ============ 凭据状态 ============
-
-/// 所有凭据状态响应
-#[derive(Debug, Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct CredentialsStatusResponse {
- /// 凭据总数
- pub total: usize,
- /// 可用凭据数量(未禁用)
- pub available: usize,
- /// 当前活跃凭据 ID
- pub current_id: u64,
- /// 各凭据状态列表
- pub credentials: Vec,
-}
-
-/// 单个凭据的状态信息
-#[derive(Debug, Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct CredentialStatusItem {
- /// 凭据唯一 ID
- pub id: u64,
- /// 优先级(数字越小优先级越高)
- pub priority: u32,
- /// 是否被禁用
- pub disabled: bool,
- /// 连续失败次数
- pub failure_count: u32,
- /// 是否为当前活跃凭据
- pub is_current: bool,
- /// Token 过期时间(RFC3339 格式)
- pub expires_at: Option,
- /// 认证方式
- pub auth_method: Option,
- /// 是否有 Profile ARN
- pub has_profile_arn: bool,
-}
-
-// ============ 操作请求 ============
-
-/// 启用/禁用凭据请求
-#[derive(Debug, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct SetDisabledRequest {
- /// 是否禁用
- pub disabled: bool,
-}
-
-/// 修改优先级请求
-#[derive(Debug, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct SetPriorityRequest {
- /// 新优先级值
- pub priority: u32,
-}
-
-/// 添加凭据请求
-#[derive(Debug, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct AddCredentialRequest {
- /// 刷新令牌(必填)
- pub refresh_token: String,
-
- /// 认证方式(可选,默认 social)
- #[serde(default = "default_auth_method")]
- pub auth_method: String,
-
- /// OIDC Client ID(IdC 认证需要)
- pub client_id: Option,
-
- /// OIDC Client Secret(IdC 认证需要)
- pub client_secret: Option,
-
- /// 优先级(可选,默认 0)
- #[serde(default)]
- pub priority: u32,
-
- /// 凭据级 Region 配置(用于 OIDC token 刷新)
- /// 未配置时回退到 config.json 的全局 region
- pub region: Option,
-
- /// 凭据级 Machine ID(可选,64 位字符串)
- /// 未配置时回退到 config.json 的 machineId
- pub machine_id: Option,
-}
-
-fn default_auth_method() -> String {
- "social".to_string()
-}
-
-/// 添加凭据成功响应
-#[derive(Debug, Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct AddCredentialResponse {
- pub success: bool,
- pub message: String,
- /// 新添加的凭据 ID
- pub credential_id: u64,
-}
-
-// ============ 余额查询 ============
-
-/// 余额查询响应
-#[derive(Debug, Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct BalanceResponse {
- /// 凭据 ID
- pub id: u64,
- /// 订阅类型
- pub subscription_title: Option,
- /// 当前使用量
- pub current_usage: f64,
- /// 使用限额
- pub usage_limit: f64,
- /// 剩余额度
- pub remaining: f64,
- /// 使用百分比
- pub usage_percentage: f64,
- /// 下次重置时间(Unix 时间戳)
- pub next_reset_at: Option,
-}
-
-// ============ 通用响应 ============
-
-/// 操作成功响应
-#[derive(Debug, Serialize)]
-pub struct SuccessResponse {
- pub success: bool,
- pub message: String,
-}
-
-impl SuccessResponse {
- pub fn new(message: impl Into) -> Self {
- Self {
- success: true,
- message: message.into(),
- }
- }
-}
-
-/// 错误响应
-#[derive(Debug, Serialize)]
-pub struct AdminErrorResponse {
- pub error: AdminError,
-}
-
-#[derive(Debug, Serialize)]
-pub struct AdminError {
- #[serde(rename = "type")]
- pub error_type: String,
- pub message: String,
-}
-
-impl AdminErrorResponse {
- pub fn new(error_type: impl Into, message: impl Into) -> Self {
- Self {
- error: AdminError {
- error_type: error_type.into(),
- message: message.into(),
- },
- }
- }
-
- pub fn invalid_request(message: impl Into) -> Self {
- Self::new("invalid_request", message)
- }
-
- pub fn authentication_error() -> Self {
- Self::new("authentication_error", "Invalid or missing admin API key")
- }
-
- pub fn not_found(message: impl Into) -> Self {
- Self::new("not_found", message)
- }
-
- pub fn api_error(message: impl Into) -> Self {
- Self::new("api_error", message)
- }
-
- pub fn internal_error(message: impl Into) -> Self {
- Self::new("internal_error", message)
- }
-}
diff --git a/src/admin_ui/mod.rs b/src/admin_ui/mod.rs
deleted file mode 100644
index 9537d2827434bf97f9e2eee1d1869399a28249e1..0000000000000000000000000000000000000000
--- a/src/admin_ui/mod.rs
+++ /dev/null
@@ -1,7 +0,0 @@
-//! Admin UI 静态文件服务模块
-//!
-//! 使用 rust-embed 嵌入前端构建产物
-
-mod router;
-
-pub use router::create_admin_ui_router;
diff --git a/src/admin_ui/router.rs b/src/admin_ui/router.rs
deleted file mode 100644
index 36d8bc7a0e9d08d2b38e77f6f30e98f4ee38cb96..0000000000000000000000000000000000000000
--- a/src/admin_ui/router.rs
+++ /dev/null
@@ -1,109 +0,0 @@
-//! Admin UI 路由配置
-
-use axum::{
- Router,
- body::Body,
- http::{Response, StatusCode, Uri, header},
- response::IntoResponse,
- routing::get,
-};
-use rust_embed::Embed;
-
-/// 嵌入前端构建产物
-#[derive(Embed)]
-#[folder = "admin-ui/dist"]
-struct Asset;
-
-/// 创建 Admin UI 路由
-pub fn create_admin_ui_router() -> Router {
- Router::new()
- .route("/", get(index_handler))
- .route("/{*file}", get(static_handler))
-}
-
-/// 处理首页请求
-async fn index_handler() -> impl IntoResponse {
- serve_index()
-}
-
-/// 处理静态文件请求
-async fn static_handler(uri: Uri) -> impl IntoResponse {
- let path = uri.path().trim_start_matches('/');
-
- // 安全检查:拒绝包含 .. 的路径
- if path.contains("..") {
- return Response::builder()
- .status(StatusCode::BAD_REQUEST)
- .body(Body::from("Invalid path"))
- .expect("Failed to build response");
- }
-
- // 尝试获取请求的文件
- if let Some(content) = Asset::get(path) {
- let mime = mime_guess::from_path(path)
- .first_or_octet_stream()
- .to_string();
-
- // 根据文件类型设置不同的缓存策略
- let cache_control = get_cache_control(path);
-
- return Response::builder()
- .status(StatusCode::OK)
- .header(header::CONTENT_TYPE, mime)
- .header(header::CACHE_CONTROL, cache_control)
- .body(Body::from(content.data.into_owned()))
- .expect("Failed to build response");
- }
-
- // SPA fallback: 如果文件不存在且不是资源文件,返回 index.html
- if !is_asset_path(path) {
- return serve_index();
- }
-
- // 404
- Response::builder()
- .status(StatusCode::NOT_FOUND)
- .body(Body::from("Not found"))
- .expect("Failed to build response")
-}
-
-/// 提供 index.html
-fn serve_index() -> Response {
- match Asset::get("index.html") {
- Some(content) => Response::builder()
- .status(StatusCode::OK)
- .header(header::CONTENT_TYPE, "text/html; charset=utf-8")
- .header(header::CACHE_CONTROL, "no-cache")
- .body(Body::from(content.data.into_owned()))
- .expect("Failed to build response"),
- None => Response::builder()
- .status(StatusCode::NOT_FOUND)
- .body(Body::from(
- "Admin UI not built. Run 'pnpm build' in admin-ui directory.",
- ))
- .expect("Failed to build response"),
- }
-}
-
-/// 根据文件类型返回合适的缓存策略
-fn get_cache_control(path: &str) -> &'static str {
- if path.ends_with(".html") {
- // HTML 文件不缓存,确保用户获取最新版本
- "no-cache"
- } else if path.starts_with("assets/") {
- // assets/ 目录下的文件带有内容哈希,可以长期缓存
- "public, max-age=31536000, immutable"
- } else {
- // 其他文件(如 favicon)使用较短的缓存
- "public, max-age=3600"
- }
-}
-
-/// 判断是否为资源文件路径(有扩展名的文件)
-fn is_asset_path(path: &str) -> bool {
- // 检查最后一个路径段是否包含扩展名
- path.rsplit('/')
- .next()
- .map(|filename| filename.contains('.'))
- .unwrap_or(false)
-}
diff --git a/src/anthropic/converter.rs b/src/anthropic/converter.rs
deleted file mode 100644
index 874ff283b39f2656ed679af0d653f4ac1b018b67..0000000000000000000000000000000000000000
--- a/src/anthropic/converter.rs
+++ /dev/null
@@ -1,1118 +0,0 @@
-//! Anthropic → Kiro 协议转换器
-//!
-//! 负责将 Anthropic API 请求格式转换为 Kiro API 请求格式
-
-use uuid::Uuid;
-
-use crate::kiro::model::requests::conversation::{
- AssistantMessage, ConversationState, CurrentMessage, HistoryAssistantMessage,
- HistoryUserMessage, KiroImage, Message, UserInputMessage, UserInputMessageContext, UserMessage,
-};
-use crate::kiro::model::requests::tool::{
- InputSchema, Tool, ToolResult, ToolSpecification, ToolUseEntry,
-};
-
-use super::types::{ContentBlock, MessagesRequest, Thinking};
-
-/// 模型映射:将 Anthropic 模型名映射到 Kiro 模型 ID
-///
-/// 按照用户要求:
-/// - 所有 sonnet → claude-sonnet-4.5
-/// - 所有 opus → claude-opus-4.5
-/// - 所有 haiku → claude-haiku-4.5
-pub fn map_model(model: &str) -> Option {
- let model_lower = model.to_lowercase();
-
- if model_lower.contains("sonnet") {
- Some("claude-sonnet-4.5".to_string())
- } else if model_lower.contains("opus") {
- Some("claude-opus-4.5".to_string())
- } else if model_lower.contains("haiku") {
- Some("claude-haiku-4.5".to_string())
- } else {
- None
- }
-}
-
-/// 转换结果
-#[derive(Debug)]
-pub struct ConversionResult {
- /// 转换后的 Kiro 请求
- pub conversation_state: ConversationState,
-}
-
-/// 转换错误
-#[derive(Debug)]
-pub enum ConversionError {
- UnsupportedModel(String),
- EmptyMessages,
-}
-
-impl std::fmt::Display for ConversionError {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- ConversionError::UnsupportedModel(model) => write!(f, "模型不支持: {}", model),
- ConversionError::EmptyMessages => write!(f, "消息列表为空"),
- }
- }
-}
-
-impl std::error::Error for ConversionError {}
-
-/// 从 metadata.user_id 中提取 session UUID
-///
-/// user_id 格式: user_xxx_account__session_0b4445e1-f5be-49e1-87ce-62bbc28ad705
-/// 提取 session_ 后面的 UUID 作为 conversationId
-fn extract_session_id(user_id: &str) -> Option {
- // 查找 "session_" 后面的内容
- if let Some(pos) = user_id.find("session_") {
- let session_part = &user_id[pos + 8..]; // "session_" 长度为 8
- // session_part 应该是 UUID 格式: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
- // 验证是否是有效的 UUID 格式(36 字符,包含 4 个连字符)
- if session_part.len() >= 36 {
- let uuid_str = &session_part[..36];
- // 简单验证 UUID 格式
- if uuid_str.chars().filter(|c| *c == '-').count() == 4 {
- return Some(uuid_str.to_string());
- }
- }
- }
- None
-}
-
-/// 收集历史消息中使用的所有工具名称
-fn collect_history_tool_names(history: &[Message]) -> Vec {
- let mut tool_names = Vec::new();
-
- for msg in history {
- if let Message::Assistant(assistant_msg) = msg {
- if let Some(ref tool_uses) = assistant_msg.assistant_response_message.tool_uses {
- for tool_use in tool_uses {
- if !tool_names.contains(&tool_use.name) {
- tool_names.push(tool_use.name.clone());
- }
- }
- }
- }
- }
-
- tool_names
-}
-
-/// 为历史中使用但不在 tools 列表中的工具创建占位符定义
-/// Kiro API 要求:历史消息中引用的工具必须在 currentMessage.tools 中有定义
-fn create_placeholder_tool(name: &str) -> Tool {
- Tool {
- tool_specification: ToolSpecification {
- name: name.to_string(),
- description: "Tool used in conversation history".to_string(),
- input_schema: InputSchema::from_json(serde_json::json!({
- "$schema": "http://json-schema.org/draft-07/schema#",
- "type": "object",
- "properties": {},
- "required": [],
- "additionalProperties": true
- })),
- },
- }
-}
-
-/// 将 Anthropic 请求转换为 Kiro 请求
-pub fn convert_request(req: &MessagesRequest) -> Result {
- // 1. 映射模型
- let model_id = map_model(&req.model)
- .ok_or_else(|| ConversionError::UnsupportedModel(req.model.clone()))?;
-
- // 2. 检查消息列表
- if req.messages.is_empty() {
- return Err(ConversionError::EmptyMessages);
- }
-
- // 3. 生成会话 ID 和代理 ID
- // 优先从 metadata.user_id 中提取 session UUID 作为 conversationId
- let conversation_id = req
- .metadata
- .as_ref()
- .and_then(|m| m.user_id.as_ref())
- .and_then(|user_id| extract_session_id(user_id))
- .unwrap_or_else(|| Uuid::new_v4().to_string());
- let agent_continuation_id = Uuid::new_v4().to_string();
-
- // 4. 确定触发类型
- let chat_trigger_type = determine_chat_trigger_type(req);
-
- // 5. 处理最后一条消息作为 current_message
- let last_message = req.messages.last().unwrap();
- let (text_content, images, tool_results) = process_message_content(&last_message.content)?;
-
- // 6. 转换工具定义
- let mut tools = convert_tools(&req.tools);
-
- // 7. 构建历史消息(需要先构建,以便收集历史中使用的工具)
- let history = build_history(req, &model_id)?;
-
- // 8. 验证并过滤 tool_use/tool_result 配对
- // 移除孤立的 tool_result(没有对应的 tool_use)
- let validated_tool_results = validate_tool_pairing(&history, &tool_results);
-
- // 9. 收集历史中使用的工具名称,为缺失的工具生成占位符定义
- // Kiro API 要求:历史消息中引用的工具必须在 tools 列表中有定义
- // 注意:Kiro 匹配工具名称时忽略大小写,所以这里也需要忽略大小写比较
- let history_tool_names = collect_history_tool_names(&history);
- let existing_tool_names: std::collections::HashSet<_> = tools
- .iter()
- .map(|t| t.tool_specification.name.to_lowercase())
- .collect();
-
- for tool_name in history_tool_names {
- if !existing_tool_names.contains(&tool_name.to_lowercase()) {
- tools.push(create_placeholder_tool(&tool_name));
- }
- }
-
- // 10. 构建 UserInputMessageContext
- let mut context = UserInputMessageContext::new();
- if !tools.is_empty() {
- context = context.with_tools(tools);
- }
- if !validated_tool_results.is_empty() {
- context = context.with_tool_results(validated_tool_results);
- }
-
- // 11. 构建当前消息
- // 保留文本内容,即使有工具结果也不丢弃用户文本
- let content = text_content;
-
- let mut user_input = UserInputMessage::new(content, &model_id)
- .with_context(context)
- .with_origin("AI_EDITOR");
-
- if !images.is_empty() {
- user_input = user_input.with_images(images);
- }
-
- let current_message = CurrentMessage::new(user_input);
-
- // 12. 构建 ConversationState
- let conversation_state = ConversationState::new(conversation_id)
- .with_agent_continuation_id(agent_continuation_id)
- .with_agent_task_type("vibe")
- .with_chat_trigger_type(chat_trigger_type)
- .with_current_message(current_message)
- .with_history(history);
-
- Ok(ConversionResult { conversation_state })
-}
-
-/// 确定聊天触发类型
-/// "AUTO" 模式可能会导致 400 Bad Request 错误
-fn determine_chat_trigger_type(_req: &MessagesRequest) -> String {
- "MANUAL".to_string()
-}
-
-/// 处理消息内容,提取文本、图片和工具结果
-fn process_message_content(
- content: &serde_json::Value,
-) -> Result<(String, Vec, Vec), ConversionError> {
- let mut text_parts = Vec::new();
- let mut images = Vec::new();
- let mut tool_results = Vec::new();
-
- match content {
- serde_json::Value::String(s) => {
- text_parts.push(s.clone());
- }
- serde_json::Value::Array(arr) => {
- for item in arr {
- if let Ok(block) = serde_json::from_value::(item.clone()) {
- match block.block_type.as_str() {
- "text" => {
- if let Some(text) = block.text {
- text_parts.push(text);
- }
- }
- "image" => {
- if let Some(source) = block.source {
- if let Some(format) = get_image_format(&source.media_type) {
- images.push(KiroImage::from_base64(format, source.data));
- }
- }
- }
- "tool_result" => {
- if let Some(tool_use_id) = block.tool_use_id {
- let result_content = extract_tool_result_content(&block.content);
- let is_error = block.is_error.unwrap_or(false);
-
- let mut result = if is_error {
- ToolResult::error(&tool_use_id, result_content)
- } else {
- ToolResult::success(&tool_use_id, result_content)
- };
- result.status =
- Some(if is_error { "error" } else { "success" }.to_string());
-
- tool_results.push(result);
- }
- }
- "tool_use" => {
- // tool_use 在 assistant 消息中处理,这里忽略
- }
- _ => {}
- }
- }
- }
- }
- _ => {}
- }
-
- Ok((text_parts.join("\n"), images, tool_results))
-}
-
-/// 从 media_type 获取图片格式
-fn get_image_format(media_type: &str) -> Option {
- match media_type {
- "image/jpeg" => Some("jpeg".to_string()),
- "image/png" => Some("png".to_string()),
- "image/gif" => Some("gif".to_string()),
- "image/webp" => Some("webp".to_string()),
- _ => None,
- }
-}
-
-/// 提取工具结果内容
-fn extract_tool_result_content(content: &Option) -> String {
- match content {
- Some(serde_json::Value::String(s)) => s.clone(),
- Some(serde_json::Value::Array(arr)) => {
- let mut parts = Vec::new();
- for item in arr {
- if let Some(text) = item.get("text").and_then(|v| v.as_str()) {
- parts.push(text.to_string());
- }
- }
- parts.join("\n")
- }
- Some(v) => v.to_string(),
- None => String::new(),
- }
-}
-
-/// 验证并过滤 tool_use/tool_result 配对
-///
-/// 收集所有 tool_use_id,验证 tool_result 是否匹配
-/// 静默跳过孤立的 tool_use 和 tool_result,输出警告日志
-///
-/// # Arguments
-/// * `history` - 历史消息引用
-/// * `tool_results` - 当前消息中的 tool_result 列表
-///
-/// # Returns
-/// 经过验证和过滤后的 tool_result 列表
-fn validate_tool_pairing(history: &[Message], tool_results: &[ToolResult]) -> Vec {
- use std::collections::HashSet;
-
- // 1. 收集所有历史中的 tool_use_id
- let mut all_tool_use_ids: HashSet = HashSet::new();
- // 2. 收集历史中已经有 tool_result 的 tool_use_id
- let mut history_tool_result_ids: HashSet = HashSet::new();
-
- for msg in history {
- match msg {
- Message::Assistant(assistant_msg) => {
- if let Some(ref tool_uses) = assistant_msg.assistant_response_message.tool_uses {
- for tool_use in tool_uses {
- all_tool_use_ids.insert(tool_use.tool_use_id.clone());
- }
- }
- }
- Message::User(user_msg) => {
- // 收集历史 user 消息中的 tool_results
- for result in &user_msg.user_input_message.user_input_message_context.tool_results
- {
- history_tool_result_ids.insert(result.tool_use_id.clone());
- }
- }
- }
- }
-
- // 3. 计算真正未配对的 tool_use_ids(排除历史中已配对的)
- let mut unpaired_tool_use_ids: HashSet = all_tool_use_ids
- .difference(&history_tool_result_ids)
- .cloned()
- .collect();
-
- // 4. 过滤并验证当前消息的 tool_results
- let mut filtered_results = Vec::new();
-
- for result in tool_results {
- if unpaired_tool_use_ids.contains(&result.tool_use_id) {
- // 配对成功
- filtered_results.push(result.clone());
- unpaired_tool_use_ids.remove(&result.tool_use_id);
- } else if all_tool_use_ids.contains(&result.tool_use_id) {
- // tool_use 存在但已经在历史中配对过了,这是重复的 tool_result
- tracing::warn!(
- "跳过重复的 tool_result:该 tool_use 已在历史中配对,tool_use_id={}",
- result.tool_use_id
- );
- } else {
- // 孤立 tool_result - 找不到对应的 tool_use
- tracing::warn!(
- "跳过孤立的 tool_result:找不到对应的 tool_use,tool_use_id={}",
- result.tool_use_id
- );
- }
- }
-
- // 5. 检测真正孤立的 tool_use(有 tool_use 但在历史和当前消息中都没有 tool_result)
- for orphaned_id in &unpaired_tool_use_ids {
- tracing::warn!(
- "检测到孤立的 tool_use:找不到对应的 tool_result,tool_use_id={}",
- orphaned_id
- );
- }
-
- filtered_results
-}
-
-/// 转换工具定义
-fn convert_tools(tools: &Option>) -> Vec {
- let Some(tools) = tools else {
- return Vec::new();
- };
-
- tools
- .iter()
- .map(|t| {
- let description = t.description.clone();
- // 限制描述长度为 10000 字符(安全截断 UTF-8,单次遍历)
- let description = match description.char_indices().nth(10000) {
- Some((idx, _)) => description[..idx].to_string(),
- None => description,
- };
-
- Tool {
- tool_specification: ToolSpecification {
- name: t.name.clone(),
- description,
- input_schema: InputSchema::from_json(serde_json::json!(t.input_schema)),
- },
- }
- })
- .collect()
-}
-
-/// 生成thinking标签前缀
-fn generate_thinking_prefix(thinking: &Option) -> Option {
- if let Some(t) = thinking {
- if t.thinking_type == "enabled" {
- return Some(format!(
- "enabled{}",
- t.budget_tokens
- ));
- }
- }
- None
-}
-
-/// 检查内容是否已包含thinking标签
-fn has_thinking_tags(content: &str) -> bool {
- content.contains("") || content.contains("")
-}
-
-/// 构建历史消息
-fn build_history(req: &MessagesRequest, model_id: &str) -> Result, ConversionError> {
- let mut history = Vec::new();
-
- // 生成thinking前缀(如果需要)
- let thinking_prefix = generate_thinking_prefix(&req.thinking);
-
- // 1. 处理系统消息
- if let Some(ref system) = req.system {
- let system_content: String = system
- .iter()
- .map(|s| s.text.clone())
- .collect::>()
- .join("\n");
-
- if !system_content.is_empty() {
- // 注入thinking标签到系统消息最前面(如果需要且不存在)
- let final_content = if let Some(ref prefix) = thinking_prefix {
- if !has_thinking_tags(&system_content) {
- format!("{}\n{}", prefix, system_content)
- } else {
- system_content
- }
- } else {
- system_content
- };
-
- // 系统消息作为 user + assistant 配对
- let user_msg = HistoryUserMessage::new(final_content, model_id);
- history.push(Message::User(user_msg));
-
- let assistant_msg = HistoryAssistantMessage::new("I will follow these instructions.");
- history.push(Message::Assistant(assistant_msg));
- }
- } else if let Some(ref prefix) = thinking_prefix {
- // 没有系统消息但有thinking配置,插入新的系统消息
- let user_msg = HistoryUserMessage::new(prefix.clone(), model_id);
- history.push(Message::User(user_msg));
-
- let assistant_msg = HistoryAssistantMessage::new("I will follow these instructions.");
- history.push(Message::Assistant(assistant_msg));
- }
-
- // 2. 处理常规消息历史
- // 最后一条消息作为 currentMessage,不加入历史
- let history_end_index = req.messages.len().saturating_sub(1);
-
- // 如果最后一条是 assistant,则包含在历史中
- let last_is_assistant = req
- .messages
- .last()
- .map(|m| m.role == "assistant")
- .unwrap_or(false);
-
- let history_end_index = if last_is_assistant {
- req.messages.len()
- } else {
- history_end_index
- };
-
- // 收集并配对消息
- let mut user_buffer: Vec<&super::types::Message> = Vec::new();
-
- for i in 0..history_end_index {
- let msg = &req.messages[i];
-
- if msg.role == "user" {
- user_buffer.push(msg);
- } else if msg.role == "assistant" {
- // 遇到 assistant,处理累积的 user 消息
- if !user_buffer.is_empty() {
- let merged_user = merge_user_messages(&user_buffer, model_id)?;
- history.push(Message::User(merged_user));
- user_buffer.clear();
-
- // 添加 assistant 消息
- let assistant = convert_assistant_message(msg)?;
- history.push(Message::Assistant(assistant));
- }
- }
- }
-
- // 处理结尾的孤立 user 消息
- if !user_buffer.is_empty() {
- let merged_user = merge_user_messages(&user_buffer, model_id)?;
- history.push(Message::User(merged_user));
-
- // 自动配对一个 "OK" 的 assistant 响应
- let auto_assistant = HistoryAssistantMessage::new("OK");
- history.push(Message::Assistant(auto_assistant));
- }
-
- Ok(history)
-}
-
-/// 合并多个 user 消息
-fn merge_user_messages(
- messages: &[&super::types::Message],
- model_id: &str,
-) -> Result {
- let mut content_parts = Vec::new();
- let mut all_images = Vec::new();
- let mut all_tool_results = Vec::new();
-
- for msg in messages {
- let (text, images, tool_results) = process_message_content(&msg.content)?;
- if !text.is_empty() {
- content_parts.push(text);
- }
- all_images.extend(images);
- all_tool_results.extend(tool_results);
- }
-
- let content = content_parts.join("\n");
- // 保留文本内容,即使有工具结果也不丢弃用户文本
- let mut user_msg = UserMessage::new(&content, model_id);
-
- if !all_images.is_empty() {
- user_msg = user_msg.with_images(all_images);
- }
-
- if !all_tool_results.is_empty() {
- let mut ctx = UserInputMessageContext::new();
- ctx = ctx.with_tool_results(all_tool_results);
- user_msg = user_msg.with_context(ctx);
- }
-
- Ok(HistoryUserMessage {
- user_input_message: user_msg,
- })
-}
-
-/// 转换 assistant 消息
-fn convert_assistant_message(
- msg: &super::types::Message,
-) -> Result {
- let mut thinking_content = String::new();
- let mut text_content = String::new();
- let mut tool_uses = Vec::new();
-
- match &msg.content {
- serde_json::Value::String(s) => {
- text_content = s.clone();
- }
- serde_json::Value::Array(arr) => {
- for item in arr {
- if let Ok(block) = serde_json::from_value::(item.clone()) {
- match block.block_type.as_str() {
- "thinking" => {
- if let Some(thinking) = block.thinking {
- thinking_content.push_str(&thinking);
- }
- }
- "text" => {
- if let Some(text) = block.text {
- text_content.push_str(&text);
- }
- }
- "tool_use" => {
- if let (Some(id), Some(name)) = (block.id, block.name) {
- let input = block.input.unwrap_or(serde_json::json!({}));
- tool_uses.push(ToolUseEntry::new(id, name).with_input(input));
- }
- }
- _ => {}
- }
- }
- }
- }
- _ => {}
- }
-
- // 组合 thinking 和 text 内容
- // 格式: 思考内容\n\ntext内容
- // 注意: Kiro API 要求 content 字段不能为空,当只有 tool_use 时需要占位符
- let final_content = if !thinking_content.is_empty() {
- if !text_content.is_empty() {
- format!(
- "{}\n\n{}",
- thinking_content, text_content
- )
- } else {
- format!("{}", thinking_content)
- }
- } else if text_content.is_empty() && !tool_uses.is_empty() {
- "There is a tool use.".to_string()
- } else {
- text_content
- };
-
- let mut assistant = AssistantMessage::new(final_content);
- if !tool_uses.is_empty() {
- assistant = assistant.with_tool_uses(tool_uses);
- }
-
- Ok(HistoryAssistantMessage {
- assistant_response_message: assistant,
- })
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_map_model_sonnet() {
- assert!(
- map_model("claude-sonnet-4-20250514")
- .unwrap()
- .contains("sonnet")
- );
- assert!(
- map_model("claude-3-5-sonnet-20241022")
- .unwrap()
- .contains("sonnet")
- );
- }
-
- #[test]
- fn test_map_model_opus() {
- assert!(
- map_model("claude-opus-4-20250514")
- .unwrap()
- .contains("opus")
- );
- }
-
- #[test]
- fn test_map_model_haiku() {
- assert!(
- map_model("claude-haiku-4-20250514")
- .unwrap()
- .contains("haiku")
- );
- }
-
- #[test]
- fn test_map_model_unsupported() {
- assert!(map_model("gpt-4").is_none());
- }
-
- #[test]
- fn test_determine_chat_trigger_type() {
- // 无工具时返回 MANUAL
- let req = MessagesRequest {
- model: "claude-sonnet-4".to_string(),
- max_tokens: 1024,
- messages: vec![],
- stream: false,
- system: None,
- tools: None,
- tool_choice: None,
- thinking: None,
- metadata: None,
- };
- assert_eq!(determine_chat_trigger_type(&req), "MANUAL");
- }
-
- #[test]
- fn test_collect_history_tool_names() {
- use crate::kiro::model::requests::tool::ToolUseEntry;
-
- // 创建包含工具使用的历史消息
- let mut assistant_msg = AssistantMessage::new("I'll read the file.");
- assistant_msg = assistant_msg.with_tool_uses(vec![
- ToolUseEntry::new("tool-1", "read")
- .with_input(serde_json::json!({"path": "/test.txt"})),
- ToolUseEntry::new("tool-2", "write")
- .with_input(serde_json::json!({"path": "/out.txt"})),
- ]);
-
- let history = vec![
- Message::User(HistoryUserMessage::new(
- "Read the file",
- "claude-sonnet-4.5",
- )),
- Message::Assistant(HistoryAssistantMessage {
- assistant_response_message: assistant_msg,
- }),
- ];
-
- let tool_names = collect_history_tool_names(&history);
- assert_eq!(tool_names.len(), 2);
- assert!(tool_names.contains(&"read".to_string()));
- assert!(tool_names.contains(&"write".to_string()));
- }
-
- #[test]
- fn test_create_placeholder_tool() {
- let tool = create_placeholder_tool("my_custom_tool");
-
- assert_eq!(tool.tool_specification.name, "my_custom_tool");
- assert!(!tool.tool_specification.description.is_empty());
-
- // 验证 JSON 序列化正确
- let json = serde_json::to_string(&tool).unwrap();
- assert!(json.contains("\"name\":\"my_custom_tool\""));
- }
-
- #[test]
- fn test_history_tools_added_to_tools_list() {
- use super::super::types::Message as AnthropicMessage;
-
- // 创建一个请求,历史中有工具使用,但 tools 列表为空
- let req = MessagesRequest {
- model: "claude-sonnet-4".to_string(),
- max_tokens: 1024,
- messages: vec![
- AnthropicMessage {
- role: "user".to_string(),
- content: serde_json::json!("Read the file"),
- },
- AnthropicMessage {
- role: "assistant".to_string(),
- content: serde_json::json!([
- {"type": "text", "text": "I'll read the file."},
- {"type": "tool_use", "id": "tool-1", "name": "read", "input": {"path": "/test.txt"}}
- ]),
- },
- AnthropicMessage {
- role: "user".to_string(),
- content: serde_json::json!([
- {"type": "tool_result", "tool_use_id": "tool-1", "content": "file content"}
- ]),
- },
- ],
- stream: false,
- system: None,
- tools: None, // 没有提供工具定义
- tool_choice: None,
- thinking: None,
- metadata: None,
- };
-
- let result = convert_request(&req).unwrap();
-
- // 验证 tools 列表中包含了历史中使用的工具的占位符定义
- let tools = &result
- .conversation_state
- .current_message
- .user_input_message
- .user_input_message_context
- .tools;
-
- assert!(!tools.is_empty(), "tools 列表不应为空");
- assert!(
- tools.iter().any(|t| t.tool_specification.name == "read"),
- "tools 列表应包含 'read' 工具的占位符定义"
- );
- }
-
- #[test]
- fn test_extract_session_id_valid() {
- // 测试有效的 user_id 格式
- let user_id = "user_0dede55c6dcc4a11a30bbb5e7f22e6fdf86cdeba3820019cc27612af4e1243cd_account__session_8bb5523b-ec7c-4540-a9ca-beb6d79f1552";
- let session_id = extract_session_id(user_id);
- assert_eq!(
- session_id,
- Some("8bb5523b-ec7c-4540-a9ca-beb6d79f1552".to_string())
- );
- }
-
- #[test]
- fn test_extract_session_id_no_session() {
- // 测试没有 session 的 user_id
- let user_id = "user_0dede55c6dcc4a11a30bbb5e7f22e6fdf86cdeba3820019cc27612af4e1243cd";
- let session_id = extract_session_id(user_id);
- assert_eq!(session_id, None);
- }
-
- #[test]
- fn test_extract_session_id_invalid_uuid() {
- // 测试无效的 UUID 格式
- let user_id = "user_xxx_session_invalid-uuid";
- let session_id = extract_session_id(user_id);
- assert_eq!(session_id, None);
- }
-
- #[test]
- fn test_convert_request_with_session_metadata() {
- use super::super::types::{Message as AnthropicMessage, Metadata};
-
- // 测试带有 metadata 的请求,应该使用 session UUID 作为 conversationId
- let req = MessagesRequest {
- model: "claude-sonnet-4".to_string(),
- max_tokens: 1024,
- messages: vec![AnthropicMessage {
- role: "user".to_string(),
- content: serde_json::json!("Hello"),
- }],
- stream: false,
- system: None,
- tools: None,
- tool_choice: None,
- thinking: None,
- metadata: Some(Metadata {
- user_id: Some(
- "user_0dede55c6dcc4a11a30bbb5e7f22e6fdf86cdeba3820019cc27612af4e1243cd_account__session_a0662283-7fd3-4399-a7eb-52b9a717ae88".to_string(),
- ),
- }),
- };
-
- let result = convert_request(&req).unwrap();
- assert_eq!(
- result.conversation_state.conversation_id,
- "a0662283-7fd3-4399-a7eb-52b9a717ae88"
- );
- }
-
- #[test]
- fn test_convert_request_without_metadata() {
- use super::super::types::Message as AnthropicMessage;
-
- // 测试没有 metadata 的请求,应该生成新的 UUID
- let req = MessagesRequest {
- model: "claude-sonnet-4".to_string(),
- max_tokens: 1024,
- messages: vec![AnthropicMessage {
- role: "user".to_string(),
- content: serde_json::json!("Hello"),
- }],
- stream: false,
- system: None,
- tools: None,
- tool_choice: None,
- thinking: None,
- metadata: None,
- };
-
- let result = convert_request(&req).unwrap();
- // 验证生成的是有效的 UUID 格式
- assert_eq!(result.conversation_state.conversation_id.len(), 36);
- assert_eq!(
- result
- .conversation_state
- .conversation_id
- .chars()
- .filter(|c| *c == '-')
- .count(),
- 4
- );
- }
-
- #[test]
- fn test_validate_tool_pairing_orphaned_result() {
- // 测试孤立的 tool_result 被过滤
- // 历史中没有 tool_use,但 tool_results 中有 tool_result
- let history = vec![
- Message::User(HistoryUserMessage::new("Hello", "claude-sonnet-4.5")),
- Message::Assistant(HistoryAssistantMessage::new("Hi there!")),
- ];
-
- let tool_results = vec![ToolResult::success("orphan-123", "some result")];
-
- let filtered = validate_tool_pairing(&history, &tool_results);
-
- // 孤立的 tool_result 应该被过滤掉
- assert!(filtered.is_empty(), "孤立的 tool_result 应该被过滤");
- }
-
- #[test]
- fn test_validate_tool_pairing_orphaned_use() {
- use crate::kiro::model::requests::tool::ToolUseEntry;
-
- // 测试孤立的 tool_use(有 tool_use 但没有对应的 tool_result)
- let mut assistant_msg = AssistantMessage::new("I'll read the file.");
- assistant_msg = assistant_msg.with_tool_uses(vec![ToolUseEntry::new("tool-orphan", "read")
- .with_input(serde_json::json!({"path": "/test.txt"}))]);
-
- let history = vec![
- Message::User(HistoryUserMessage::new(
- "Read the file",
- "claude-sonnet-4.5",
- )),
- Message::Assistant(HistoryAssistantMessage {
- assistant_response_message: assistant_msg,
- }),
- ];
-
- // 没有 tool_result
- let tool_results: Vec = vec![];
-
- let filtered = validate_tool_pairing(&history, &tool_results);
-
- // 结果应该为空(因为没有 tool_result)
- // 同时应该输出警告日志(孤立的 tool_use)
- assert!(filtered.is_empty());
- }
-
- #[test]
- fn test_validate_tool_pairing_valid() {
- use crate::kiro::model::requests::tool::ToolUseEntry;
-
- // 测试正常配对的情况
- let mut assistant_msg = AssistantMessage::new("I'll read the file.");
- assistant_msg = assistant_msg.with_tool_uses(vec![ToolUseEntry::new("tool-1", "read")
- .with_input(serde_json::json!({"path": "/test.txt"}))]);
-
- let history = vec![
- Message::User(HistoryUserMessage::new(
- "Read the file",
- "claude-sonnet-4.5",
- )),
- Message::Assistant(HistoryAssistantMessage {
- assistant_response_message: assistant_msg,
- }),
- ];
-
- let tool_results = vec![ToolResult::success("tool-1", "file content")];
-
- let filtered = validate_tool_pairing(&history, &tool_results);
-
- // 配对成功,应该保留
- assert_eq!(filtered.len(), 1);
- assert_eq!(filtered[0].tool_use_id, "tool-1");
- }
-
- #[test]
- fn test_validate_tool_pairing_mixed() {
- use crate::kiro::model::requests::tool::ToolUseEntry;
-
- // 测试混合情况:部分配对成功,部分孤立
- let mut assistant_msg = AssistantMessage::new("I'll use two tools.");
- assistant_msg = assistant_msg.with_tool_uses(vec![
- ToolUseEntry::new("tool-1", "read").with_input(serde_json::json!({})),
- ToolUseEntry::new("tool-2", "write").with_input(serde_json::json!({})),
- ]);
-
- let history = vec![
- Message::User(HistoryUserMessage::new("Do something", "claude-sonnet-4.5")),
- Message::Assistant(HistoryAssistantMessage {
- assistant_response_message: assistant_msg,
- }),
- ];
-
- // tool_results: tool-1 配对,tool-3 孤立
- let tool_results = vec![
- ToolResult::success("tool-1", "result 1"),
- ToolResult::success("tool-3", "orphan result"), // 孤立
- ];
-
- let filtered = validate_tool_pairing(&history, &tool_results);
-
- // 只有 tool-1 应该保留
- assert_eq!(filtered.len(), 1);
- assert_eq!(filtered[0].tool_use_id, "tool-1");
- // tool-2 是孤立的 tool_use(无 result),tool-3 是孤立的 tool_result
- }
-
- #[test]
- fn test_validate_tool_pairing_history_already_paired() {
- use crate::kiro::model::requests::tool::ToolUseEntry;
-
- // 测试历史中已配对的 tool_use 不应该被报告为孤立
- // 场景:多轮对话中,之前的 tool_use 已经在历史中有对应的 tool_result
- let mut assistant_msg1 = AssistantMessage::new("I'll read the file.");
- assistant_msg1 = assistant_msg1.with_tool_uses(vec![ToolUseEntry::new("tool-1", "read")
- .with_input(serde_json::json!({"path": "/test.txt"}))]);
-
- // 构建历史中的 user 消息,包含 tool_result
- let mut user_msg_with_result = UserMessage::new("", "claude-sonnet-4.5");
- let mut ctx = UserInputMessageContext::new();
- ctx = ctx.with_tool_results(vec![ToolResult::success("tool-1", "file content")]);
- user_msg_with_result = user_msg_with_result.with_context(ctx);
-
- let history = vec![
- // 第一轮:用户请求
- Message::User(HistoryUserMessage::new(
- "Read the file",
- "claude-sonnet-4.5",
- )),
- // 第一轮:assistant 使用工具
- Message::Assistant(HistoryAssistantMessage {
- assistant_response_message: assistant_msg1,
- }),
- // 第二轮:用户返回工具结果(历史中已配对)
- Message::User(HistoryUserMessage {
- user_input_message: user_msg_with_result,
- }),
- // 第二轮:assistant 响应
- Message::Assistant(HistoryAssistantMessage::new("The file contains...")),
- ];
-
- // 当前消息没有 tool_results(用户只是继续对话)
- let tool_results: Vec = vec![];
-
- let filtered = validate_tool_pairing(&history, &tool_results);
-
- // 结果应该为空,且不应该有孤立 tool_use 的警告
- // 因为 tool-1 已经在历史中配对了
- assert!(filtered.is_empty());
- }
-
- #[test]
- fn test_validate_tool_pairing_duplicate_result() {
- use crate::kiro::model::requests::tool::ToolUseEntry;
-
- // 测试重复的 tool_result(历史中已配对,当前消息又发送了相同的 tool_result)
- let mut assistant_msg = AssistantMessage::new("I'll read the file.");
- assistant_msg = assistant_msg.with_tool_uses(vec![ToolUseEntry::new("tool-1", "read")
- .with_input(serde_json::json!({"path": "/test.txt"}))]);
-
- // 历史中已有 tool_result
- let mut user_msg_with_result = UserMessage::new("", "claude-sonnet-4.5");
- let mut ctx = UserInputMessageContext::new();
- ctx = ctx.with_tool_results(vec![ToolResult::success("tool-1", "file content")]);
- user_msg_with_result = user_msg_with_result.with_context(ctx);
-
- let history = vec![
- Message::User(HistoryUserMessage::new(
- "Read the file",
- "claude-sonnet-4.5",
- )),
- Message::Assistant(HistoryAssistantMessage {
- assistant_response_message: assistant_msg,
- }),
- Message::User(HistoryUserMessage {
- user_input_message: user_msg_with_result,
- }),
- Message::Assistant(HistoryAssistantMessage::new("Done")),
- ];
-
- // 当前消息又发送了相同的 tool_result(重复)
- let tool_results = vec![ToolResult::success("tool-1", "file content again")];
-
- let filtered = validate_tool_pairing(&history, &tool_results);
-
- // 重复的 tool_result 应该被过滤掉
- assert!(filtered.is_empty(), "重复的 tool_result 应该被过滤");
- }
-
- #[test]
- fn test_convert_assistant_message_tool_use_only() {
- use super::super::types::Message as AnthropicMessage;
-
- // 测试仅包含 tool_use 的 assistant 消息(无 text 块)
- // Kiro API 要求 content 字段不能为空
- let msg = AnthropicMessage {
- role: "assistant".to_string(),
- content: serde_json::json!([
- {"type": "tool_use", "id": "toolu_01ABC", "name": "read_file", "input": {"path": "/test.txt"}}
- ]),
- };
-
- let result = convert_assistant_message(&msg).expect("应该成功转换");
-
- // 验证 content 不为空(使用占位符)
- assert!(
- !result.assistant_response_message.content.is_empty(),
- "content 不应为空"
- );
- assert_eq!(
- result.assistant_response_message.content, "There is a tool use.",
- "仅 tool_use 时应使用 'There is a tool use.' 占位符"
- );
-
- // 验证 tool_uses 被正确保留
- let tool_uses = result
- .assistant_response_message
- .tool_uses
- .expect("应该有 tool_uses");
- assert_eq!(tool_uses.len(), 1);
- assert_eq!(tool_uses[0].tool_use_id, "toolu_01ABC");
- assert_eq!(tool_uses[0].name, "read_file");
- }
-
- #[test]
- fn test_convert_assistant_message_with_text_and_tool_use() {
- use super::super::types::Message as AnthropicMessage;
-
- // 测试同时包含 text 和 tool_use 的 assistant 消息
- let msg = AnthropicMessage {
- role: "assistant".to_string(),
- content: serde_json::json!([
- {"type": "text", "text": "Let me read that file for you."},
- {"type": "tool_use", "id": "toolu_02XYZ", "name": "read_file", "input": {"path": "/data.json"}}
- ]),
- };
-
- let result = convert_assistant_message(&msg).expect("应该成功转换");
-
- // 验证 content 使用原始文本(不是占位符)
- assert_eq!(
- result.assistant_response_message.content,
- "Let me read that file for you."
- );
-
- // 验证 tool_uses 被正确保留
- let tool_uses = result
- .assistant_response_message
- .tool_uses
- .expect("应该有 tool_uses");
- assert_eq!(tool_uses.len(), 1);
- assert_eq!(tool_uses[0].tool_use_id, "toolu_02XYZ");
- }
-}
diff --git a/src/anthropic/handlers.rs b/src/anthropic/handlers.rs
deleted file mode 100644
index 49861226acd6513eddee0ed334c5671427f678b7..0000000000000000000000000000000000000000
--- a/src/anthropic/handlers.rs
+++ /dev/null
@@ -1,523 +0,0 @@
-//! Anthropic API Handler 函数
-
-use std::convert::Infallible;
-
-use crate::kiro::model::events::Event;
-use crate::kiro::model::requests::kiro::KiroRequest;
-use crate::kiro::parser::decoder::EventStreamDecoder;
-use crate::token;
-use axum::{
- Json as JsonExtractor,
- body::Body,
- extract::State,
- http::{StatusCode, header},
- response::{IntoResponse, Json, Response},
-};
-use bytes::Bytes;
-use futures::{Stream, StreamExt, stream};
-use serde_json::json;
-use std::time::Duration;
-use tokio::time::interval;
-use uuid::Uuid;
-
-use super::converter::{ConversionError, convert_request};
-use super::middleware::AppState;
-use super::stream::{SseEvent, StreamContext};
-use super::types::{
- CountTokensRequest, CountTokensResponse, ErrorResponse, MessagesRequest, Model, ModelsResponse,
-};
-use super::websearch;
-
-/// GET /v1/models
-///
-/// 返回可用的模型列表
-pub async fn get_models() -> impl IntoResponse {
- tracing::info!("Received GET /v1/models request");
-
- let models = vec![
- Model {
- id: "claude-sonnet-4-5-20250929".to_string(),
- object: "model".to_string(),
- created: 1727568000,
- owned_by: "anthropic".to_string(),
- display_name: "Claude Sonnet 4.5".to_string(),
- model_type: "chat".to_string(),
- max_tokens: 32000,
- },
- Model {
- id: "claude-opus-4-5-20251101".to_string(),
- object: "model".to_string(),
- created: 1730419200,
- owned_by: "anthropic".to_string(),
- display_name: "Claude Opus 4.5".to_string(),
- model_type: "chat".to_string(),
- max_tokens: 32000,
- },
- Model {
- id: "claude-haiku-4-5-20251001".to_string(),
- object: "model".to_string(),
- created: 1727740800,
- owned_by: "anthropic".to_string(),
- display_name: "Claude Haiku 4.5".to_string(),
- model_type: "chat".to_string(),
- max_tokens: 32000,
- },
- ];
-
- Json(ModelsResponse {
- object: "list".to_string(),
- data: models,
- })
-}
-
-/// POST /v1/messages
-///
-/// 创建消息(对话)
-pub async fn post_messages(
- State(state): State,
- JsonExtractor(payload): JsonExtractor,
-) -> Response {
- tracing::info!(
- model = %payload.model,
- max_tokens = %payload.max_tokens,
- stream = %payload.stream,
- message_count = %payload.messages.len(),
- "Received POST /v1/messages request"
- );
- // 检查 KiroProvider 是否可用
- let provider = match &state.kiro_provider {
- Some(p) => p.clone(),
- None => {
- tracing::error!("KiroProvider 未配置");
- return (
- StatusCode::SERVICE_UNAVAILABLE,
- Json(ErrorResponse::new(
- "service_unavailable",
- "Kiro API provider not configured",
- )),
- )
- .into_response();
- }
- };
-
- // 检查是否为 WebSearch 请求
- if websearch::has_web_search_tool(&payload) {
- tracing::info!("检测到 WebSearch 工具,路由到 WebSearch 处理");
-
- // 估算输入 tokens
- let input_tokens = token::count_all_tokens(
- payload.model.clone(),
- payload.system.clone(),
- payload.messages.clone(),
- payload.tools.clone(),
- ) as i32;
-
- return websearch::handle_websearch_request(provider, &payload, input_tokens).await;
- }
-
- // 转换请求
- let conversion_result = match convert_request(&payload) {
- Ok(result) => result,
- Err(e) => {
- let (error_type, message) = match &e {
- ConversionError::UnsupportedModel(model) => {
- ("invalid_request_error", format!("模型不支持: {}", model))
- }
- ConversionError::EmptyMessages => {
- ("invalid_request_error", "消息列表为空".to_string())
- }
- };
- tracing::warn!("请求转换失败: {}", e);
- return (
- StatusCode::BAD_REQUEST,
- Json(ErrorResponse::new(error_type, message)),
- )
- .into_response();
- }
- };
-
- // 构建 Kiro 请求
- let kiro_request = KiroRequest {
- conversation_state: conversion_result.conversation_state,
- profile_arn: state.profile_arn.clone(),
- };
-
- let request_body = match serde_json::to_string(&kiro_request) {
- Ok(body) => body,
- Err(e) => {
- tracing::error!("序列化请求失败: {}", e);
- return (
- StatusCode::INTERNAL_SERVER_ERROR,
- Json(ErrorResponse::new(
- "internal_error",
- format!("序列化请求失败: {}", e),
- )),
- )
- .into_response();
- }
- };
-
- tracing::debug!("Kiro request body: {}", request_body);
-
- // 估算输入 tokens
- let input_tokens = token::count_all_tokens(
- payload.model.clone(),
- payload.system,
- payload.messages,
- payload.tools,
- ) as i32;
-
- // 检查是否启用了thinking
- let thinking_enabled = payload
- .thinking
- .as_ref()
- .map(|t| t.thinking_type == "enabled")
- .unwrap_or(false);
-
- if payload.stream {
- // 流式响应
- handle_stream_request(
- provider,
- &request_body,
- &payload.model,
- input_tokens,
- thinking_enabled,
- )
- .await
- } else {
- // 非流式响应
- handle_non_stream_request(provider, &request_body, &payload.model, input_tokens).await
- }
-}
-
-/// 处理流式请求
-async fn handle_stream_request(
- provider: std::sync::Arc,
- request_body: &str,
- model: &str,
- input_tokens: i32,
- thinking_enabled: bool,
-) -> Response {
- // 调用 Kiro API(支持多凭据故障转移)
- let response = match provider.call_api_stream(request_body).await {
- Ok(resp) => resp,
- Err(e) => {
- tracing::error!("Kiro API 调用失败: {}", e);
- return (
- StatusCode::BAD_GATEWAY,
- Json(ErrorResponse::new(
- "api_error",
- format!("上游 API 调用失败: {}", e),
- )),
- )
- .into_response();
- }
- };
-
- // 创建流处理上下文
- let mut ctx = StreamContext::new_with_thinking(model, input_tokens, thinking_enabled);
-
- // 生成初始事件
- let initial_events = ctx.generate_initial_events();
-
- // 创建 SSE 流
- let stream = create_sse_stream(response, ctx, initial_events);
-
- // 返回 SSE 响应
- Response::builder()
- .status(StatusCode::OK)
- .header(header::CONTENT_TYPE, "text/event-stream")
- .header(header::CACHE_CONTROL, "no-cache")
- .header(header::CONNECTION, "keep-alive")
- .body(Body::from_stream(stream))
- .unwrap()
-}
-
-/// Ping 事件间隔(25秒)
-const PING_INTERVAL_SECS: u64 = 25;
-
-/// 创建 ping 事件的 SSE 字符串
-fn create_ping_sse() -> Bytes {
- Bytes::from("event: ping\ndata: {\"type\": \"ping\"}\n\n")
-}
-
-/// 创建 SSE 事件流
-fn create_sse_stream(
- response: reqwest::Response,
- ctx: StreamContext,
- initial_events: Vec,
-) -> impl Stream- > {
- // 先发送初始事件
- let initial_stream = stream::iter(
- initial_events
- .into_iter()
- .map(|e| Ok(Bytes::from(e.to_sse_string()))),
- );
-
- // 然后处理 Kiro 响应流,同时每25秒发送 ping 保活
- let body_stream = response.bytes_stream();
-
- let processing_stream = stream::unfold(
- (body_stream, ctx, EventStreamDecoder::new(), false, interval(Duration::from_secs(PING_INTERVAL_SECS))),
- |(mut body_stream, mut ctx, mut decoder, finished, mut ping_interval)| async move {
- if finished {
- return None;
- }
-
- // 使用 select! 同时等待数据和 ping 定时器
- tokio::select! {
- // 处理数据流
- chunk_result = body_stream.next() => {
- match chunk_result {
- Some(Ok(chunk)) => {
- // 解码事件
- if let Err(e) = decoder.feed(&chunk) {
- tracing::warn!("缓冲区溢出: {}", e);
- }
-
- let mut events = Vec::new();
- for result in decoder.decode_iter() {
- match result {
- Ok(frame) => {
- if let Ok(event) = Event::from_frame(frame) {
- let sse_events = ctx.process_kiro_event(&event);
- events.extend(sse_events);
- }
- }
- Err(e) => {
- tracing::warn!("解码事件失败: {}", e);
- }
- }
- }
-
- // 转换为 SSE 字节流
- let bytes: Vec> = events
- .into_iter()
- .map(|e| Ok(Bytes::from(e.to_sse_string())))
- .collect();
-
- Some((stream::iter(bytes), (body_stream, ctx, decoder, false, ping_interval)))
- }
- Some(Err(e)) => {
- tracing::error!("读取响应流失败: {}", e);
- // 发送最终事件并结束
- let final_events = ctx.generate_final_events();
- let bytes: Vec> = final_events
- .into_iter()
- .map(|e| Ok(Bytes::from(e.to_sse_string())))
- .collect();
- Some((stream::iter(bytes), (body_stream, ctx, decoder, true, ping_interval)))
- }
- None => {
- // 流结束,发送最终事件
- let final_events = ctx.generate_final_events();
- let bytes: Vec> = final_events
- .into_iter()
- .map(|e| Ok(Bytes::from(e.to_sse_string())))
- .collect();
- Some((stream::iter(bytes), (body_stream, ctx, decoder, true, ping_interval)))
- }
- }
- }
- // 发送 ping 保活
- _ = ping_interval.tick() => {
- tracing::trace!("发送 ping 保活事件");
- let bytes: Vec> = vec![Ok(create_ping_sse())];
- Some((stream::iter(bytes), (body_stream, ctx, decoder, false, ping_interval)))
- }
- }
- },
- )
- .flatten();
-
- initial_stream.chain(processing_stream)
-}
-
-/// 上下文窗口大小(200k tokens)
-const CONTEXT_WINDOW_SIZE: i32 = 200_000;
-
-/// 处理非流式请求
-async fn handle_non_stream_request(
- provider: std::sync::Arc,
- request_body: &str,
- model: &str,
- input_tokens: i32,
-) -> Response {
- // 调用 Kiro API(支持多凭据故障转移)
- let response = match provider.call_api(request_body).await {
- Ok(resp) => resp,
- Err(e) => {
- tracing::error!("Kiro API 调用失败: {}", e);
- return (
- StatusCode::BAD_GATEWAY,
- Json(ErrorResponse::new(
- "api_error",
- format!("上游 API 调用失败: {}", e),
- )),
- )
- .into_response();
- }
- };
-
- // 读取响应体
- let body_bytes = match response.bytes().await {
- Ok(bytes) => bytes,
- Err(e) => {
- tracing::error!("读取响应体失败: {}", e);
- return (
- StatusCode::BAD_GATEWAY,
- Json(ErrorResponse::new(
- "api_error",
- format!("读取响应失败: {}", e),
- )),
- )
- .into_response();
- }
- };
-
- // 解析事件流
- let mut decoder = EventStreamDecoder::new();
- if let Err(e) = decoder.feed(&body_bytes) {
- tracing::warn!("缓冲区溢出: {}", e);
- }
-
- let mut text_content = String::new();
- let mut tool_uses: Vec = Vec::new();
- let mut has_tool_use = false;
- let mut stop_reason = "end_turn".to_string();
- // 从 contextUsageEvent 计算的实际输入 tokens
- let mut context_input_tokens: Option = None;
-
- // 收集工具调用的增量 JSON
- let mut tool_json_buffers: std::collections::HashMap =
- std::collections::HashMap::new();
-
- for result in decoder.decode_iter() {
- match result {
- Ok(frame) => {
- if let Ok(event) = Event::from_frame(frame) {
- match event {
- Event::AssistantResponse(resp) => {
- text_content.push_str(&resp.content);
- }
- Event::ToolUse(tool_use) => {
- has_tool_use = true;
-
- // 累积工具的 JSON 输入
- let buffer = tool_json_buffers
- .entry(tool_use.tool_use_id.clone())
- .or_insert_with(String::new);
- buffer.push_str(&tool_use.input);
-
- // 如果是完整的工具调用,添加到列表
- if tool_use.stop {
- let input: serde_json::Value = serde_json::from_str(buffer)
- .unwrap_or_else(|e| {
- tracing::warn!(
- "工具输入 JSON 解析失败: {}, tool_use_id: {}, 原始内容: {}",
- e, tool_use.tool_use_id, buffer
- );
- serde_json::json!({})
- });
-
- tool_uses.push(json!({
- "type": "tool_use",
- "id": tool_use.tool_use_id,
- "name": tool_use.name,
- "input": input
- }));
- }
- }
- Event::ContextUsage(context_usage) => {
- // 从上下文使用百分比计算实际的 input_tokens
- // 公式: percentage * 200000 / 100 = percentage * 2000
- let actual_input_tokens = (context_usage.context_usage_percentage
- * (CONTEXT_WINDOW_SIZE as f64)
- / 100.0)
- as i32;
- context_input_tokens = Some(actual_input_tokens);
- tracing::debug!(
- "收到 contextUsageEvent: {}%, 计算 input_tokens: {}",
- context_usage.context_usage_percentage,
- actual_input_tokens
- );
- }
- Event::Exception { exception_type, .. } => {
- if exception_type == "ContentLengthExceededException" {
- stop_reason = "max_tokens".to_string();
- }
- }
- _ => {}
- }
- }
- }
- Err(e) => {
- tracing::warn!("解码事件失败: {}", e);
- }
- }
- }
-
- // 确定 stop_reason
- if has_tool_use && stop_reason == "end_turn" {
- stop_reason = "tool_use".to_string();
- }
-
- // 构建响应内容
- let mut content: Vec = Vec::new();
-
- if !text_content.is_empty() {
- content.push(json!({
- "type": "text",
- "text": text_content
- }));
- }
-
- content.extend(tool_uses);
-
- // 估算输出 tokens
- let output_tokens = token::estimate_output_tokens(&content);
-
- // 使用从 contextUsageEvent 计算的 input_tokens,如果没有则使用估算值
- let final_input_tokens = context_input_tokens.unwrap_or(input_tokens);
-
- // 构建 Anthropic 响应
- let response_body = json!({
- "id": format!("msg_{}", Uuid::new_v4().to_string().replace('-', "")),
- "type": "message",
- "role": "assistant",
- "content": content,
- "model": model,
- "stop_reason": stop_reason,
- "stop_sequence": null,
- "usage": {
- "input_tokens": final_input_tokens,
- "output_tokens": output_tokens
- }
- });
-
- (StatusCode::OK, Json(response_body)).into_response()
-}
-
-/// POST /v1/messages/count_tokens
-///
-/// 计算消息的 token 数量
-pub async fn count_tokens(
- JsonExtractor(payload): JsonExtractor,
-) -> impl IntoResponse {
- tracing::info!(
- model = %payload.model,
- message_count = %payload.messages.len(),
- "Received POST /v1/messages/count_tokens request"
- );
-
- let total_tokens = token::count_all_tokens(
- payload.model,
- payload.system,
- payload.messages,
- payload.tools,
- ) as i32;
-
- Json(CountTokensResponse {
- input_tokens: total_tokens.max(1) as i32,
- })
-}
diff --git a/src/anthropic/middleware.rs b/src/anthropic/middleware.rs
deleted file mode 100644
index 731c3e0b4d116f6118567ff84d47ca2c2201fbd2..0000000000000000000000000000000000000000
--- a/src/anthropic/middleware.rs
+++ /dev/null
@@ -1,84 +0,0 @@
-//! Anthropic API 中间件
-
-use std::sync::Arc;
-
-use axum::{
- body::Body,
- extract::State,
- http::{Request, StatusCode},
- middleware::Next,
- response::{IntoResponse, Json, Response},
-};
-
-use crate::common::auth;
-use crate::kiro::provider::KiroProvider;
-
-use super::types::ErrorResponse;
-
-/// 应用共享状态
-#[derive(Clone)]
-pub struct AppState {
- /// API 密钥
- pub api_key: String,
- /// Kiro Provider(可选,用于实际 API 调用)
- /// 内部使用 MultiTokenManager,已支持线程安全的多凭据管理
- pub kiro_provider: Option>,
- /// Profile ARN(可选,用于请求)
- pub profile_arn: Option,
-}
-
-impl AppState {
- /// 创建新的应用状态
- pub fn new(api_key: impl Into) -> Self {
- Self {
- api_key: api_key.into(),
- kiro_provider: None,
- profile_arn: None,
- }
- }
-
- /// 设置 KiroProvider
- pub fn with_kiro_provider(mut self, provider: KiroProvider) -> Self {
- self.kiro_provider = Some(Arc::new(provider));
- self
- }
-
- /// 设置 Profile ARN
- pub fn with_profile_arn(mut self, arn: impl Into) -> Self {
- self.profile_arn = Some(arn.into());
- self
- }
-}
-
-/// API Key 认证中间件
-pub async fn auth_middleware(
- State(state): State,
- request: Request,
- next: Next,
-) -> Response {
- match auth::extract_api_key(&request) {
- Some(key) if auth::constant_time_eq(&key, &state.api_key) => next.run(request).await,
- _ => {
- let error = ErrorResponse::authentication_error();
- (StatusCode::UNAUTHORIZED, Json(error)).into_response()
- }
- }
-}
-
-/// CORS 中间件层
-///
-/// **安全说明**:当前配置允许所有来源(Any),这是为了支持公开 API 服务。
-/// 如果需要更严格的安全控制,请根据实际需求配置具体的允许来源、方法和头信息。
-///
-/// # 配置说明
-/// - `allow_origin(Any)`: 允许任何来源的请求
-/// - `allow_methods(Any)`: 允许任何 HTTP 方法
-/// - `allow_headers(Any)`: 允许任何请求头
-pub fn cors_layer() -> tower_http::cors::CorsLayer {
- use tower_http::cors::{Any, CorsLayer};
-
- CorsLayer::new()
- .allow_origin(Any)
- .allow_methods(Any)
- .allow_headers(Any)
-}
diff --git a/src/anthropic/mod.rs b/src/anthropic/mod.rs
deleted file mode 100644
index a5f3842a7a63e4ca7f90df5bdf1cd3e3f9d07e1f..0000000000000000000000000000000000000000
--- a/src/anthropic/mod.rs
+++ /dev/null
@@ -1,27 +0,0 @@
-//! Anthropic API 兼容服务模块
-//!
-//! 提供与 Anthropic Claude API 兼容的 HTTP 服务端点。
-//!
-//! # 支持的端点
-//! - `GET /v1/models` - 获取可用模型列表
-//! - `POST /v1/messages` - 创建消息(对话)
-//! - `POST /v1/messages/count_tokens` - 计算 token 数量
-//!
-//! # 使用示例
-//! ```rust,ignore
-//! use kiro_rs::anthropic;
-//!
-//! let app = anthropic::create_router("your-api-key");
-//! let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
-//! axum::serve(listener, app).await?;
-//! ```
-
-mod converter;
-mod handlers;
-mod middleware;
-mod router;
-mod stream;
-pub mod types;
-mod websearch;
-
-pub use router::create_router_with_provider;
diff --git a/src/anthropic/router.rs b/src/anthropic/router.rs
deleted file mode 100644
index c5aafc75a38c417511a3f79092e21c71e9ce70a7..0000000000000000000000000000000000000000
--- a/src/anthropic/router.rs
+++ /dev/null
@@ -1,65 +0,0 @@
-//! Anthropic API 路由配置
-
-use axum::{
- Router,
- extract::DefaultBodyLimit,
- middleware,
- routing::{get, post},
-};
-
-use crate::kiro::provider::KiroProvider;
-
-use super::{
- handlers::{count_tokens, get_models, post_messages},
- middleware::{AppState, auth_middleware, cors_layer},
-};
-
-/// 请求体最大大小限制 (50MB)
-const MAX_BODY_SIZE: usize = 50 * 1024 * 1024;
-
-/// 创建 Anthropic API 路由
-///
-/// # 端点
-/// - `GET /v1/models` - 获取可用模型列表
-/// - `POST /v1/messages` - 创建消息(对话)
-/// - `POST /v1/messages/count_tokens` - 计算 token 数量
-///
-/// # 认证
-/// 所有 `/v1` 路径需要 API Key 认证,支持:
-/// - `x-api-key` header
-/// - `Authorization: Bearer ` header
-///
-/// # 参数
-/// - `api_key`: API 密钥,用于验证客户端请求
-/// - `kiro_provider`: 可选的 KiroProvider,用于调用上游 API
-
-/// 创建带有 KiroProvider 的 Anthropic API 路由
-pub fn create_router_with_provider(
- api_key: impl Into,
- kiro_provider: Option,
- profile_arn: Option,
-) -> Router {
- let mut state = AppState::new(api_key);
- if let Some(provider) = kiro_provider {
- state = state.with_kiro_provider(provider);
- }
- if let Some(arn) = profile_arn {
- state = state.with_profile_arn(arn);
- }
-
- // 需要认证的 /v1 路由
- let v1_routes = Router::new()
- .route("/models", get(get_models))
- .route("/messages", post(post_messages))
- .route("/messages/count_tokens", post(count_tokens))
- .layer(middleware::from_fn_with_state(
- state.clone(),
- auth_middleware,
- ));
-
- Router::new()
- .nest("/v1", v1_routes)
- .layer(cors_layer())
- .layer(DefaultBodyLimit::max(MAX_BODY_SIZE))
- .with_state(state)
-}
diff --git a/src/anthropic/stream.rs b/src/anthropic/stream.rs
deleted file mode 100644
index 1d52388040d5d89ad49078b27901adfdd61bf375..0000000000000000000000000000000000000000
--- a/src/anthropic/stream.rs
+++ /dev/null
@@ -1,1423 +0,0 @@
-//! 流式响应处理模块
-//!
-//! 实现 Kiro → Anthropic 流式响应转换和 SSE 状态管理
-
-use std::collections::HashMap;
-
-use serde_json::json;
-use uuid::Uuid;
-
-use crate::kiro::model::events::Event;
-
-/// 找到小于等于目标位置的最近有效UTF-8字符边界
-///
-/// UTF-8字符可能占用1-4个字节,直接按字节位置切片可能会切在多字节字符中间导致panic。
-/// 这个函数从目标位置向前搜索,找到最近的有效字符边界。
-fn find_char_boundary(s: &str, target: usize) -> usize {
- if target >= s.len() {
- return s.len();
- }
- if target == 0 {
- return 0;
- }
- // 从目标位置向前搜索有效的字符边界
- let mut pos = target;
- while pos > 0 && !s.is_char_boundary(pos) {
- pos -= 1;
- }
- pos
-}
-
-/// 需要跳过的包裹字符
-///
-/// 当 thinking 标签被这些字符包裹时,认为是在引用标签而非真正的标签:
-/// - 反引号 (`):行内代码
-/// - 双引号 ("):字符串
-/// - 单引号 ('):字符串
-const QUOTE_CHARS: &[u8] = &[
- b'`', b'"', b'\'', b'\\', b'#', b'!', b'@', b'$', b'%', b'^', b'&', b'*', b'(', b')', b'-',
- b'_', b'=', b'+', b'[', b']', b'{', b'}', b';', b':', b'<', b'>', b',', b'.', b'?', b'/',
-];
-
-/// 检查指定位置的字符是否是引用字符
-fn is_quote_char(buffer: &str, pos: usize) -> bool {
- buffer
- .as_bytes()
- .get(pos)
- .map(|c| QUOTE_CHARS.contains(c))
- .unwrap_or(false)
-}
-
-/// 查找真正的 thinking 结束标签(不被引用字符包裹,且后面有双换行符)
-///
-/// 当模型在思考过程中提到 `
` 时,通常会用反引号、引号等包裹,
-/// 或者在同一行有其他内容(如"关于 标签")。
-/// 这个函数会跳过这些情况,只返回真正的结束标签位置。
-///
-/// 跳过的情况:
-/// - 被引用字符包裹(反引号、引号等)
-/// - 后面没有双换行符(真正的结束标签后面会有 `\n\n`)
-/// - 标签在缓冲区末尾(流式处理时需要等待更多内容)
-///
-/// # 参数
-/// - `buffer`: 要搜索的字符串
-///
-/// # 返回值
-/// - `Some(pos)`: 真正的结束标签的起始位置
-/// - `None`: 没有找到真正的结束标签
-fn find_real_thinking_end_tag(buffer: &str) -> Option {
- const TAG: &str = "";
- let mut search_start = 0;
-
- while let Some(pos) = buffer[search_start..].find(TAG) {
- let absolute_pos = search_start + pos;
-
- // 检查前面是否有引用字符
- let has_quote_before = absolute_pos > 0 && is_quote_char(buffer, absolute_pos - 1);
-
- // 检查后面是否有引用字符
- let after_pos = absolute_pos + TAG.len();
- let has_quote_after = is_quote_char(buffer, after_pos);
-
- // 如果被引用字符包裹,跳过
- if has_quote_before || has_quote_after {
- search_start = absolute_pos + 1;
- continue;
- }
-
- // 检查后面的内容
- let after_content = &buffer[after_pos..];
-
- // 如果标签后面内容不足以判断是否有双换行符,等待更多内容
- if after_content.len() < 2 {
- return None;
- }
-
- // 真正的 thinking 结束标签后面会有双换行符 `\n\n`
- if after_content.starts_with("\n\n") {
- return Some(absolute_pos);
- }
-
- // 不是双换行符,跳过继续搜索
- search_start = absolute_pos + 1;
- }
-
- None
-}
-
-/// 查找缓冲区末尾的 thinking 结束标签(允许末尾只有空白字符)
-///
-/// 用于“边界事件”场景:例如 thinking 结束后立刻进入 tool_use,或流结束,
-/// 此时 `` 后面可能没有 `\n\n`,但结束标签依然应被识别并过滤。
-///
-/// 约束:只有当 `` 之后全部都是空白字符时才认为是结束标签,
-/// 以避免在 thinking 内容中提到 ``(非结束标签)时误判。
-fn find_real_thinking_end_tag_at_buffer_end(buffer: &str) -> Option {
- const TAG: &str = "";
- let mut search_start = 0;
-
- while let Some(pos) = buffer[search_start..].find(TAG) {
- let absolute_pos = search_start + pos;
-
- // 检查前面是否有引用字符
- let has_quote_before = absolute_pos > 0 && is_quote_char(buffer, absolute_pos - 1);
-
- // 检查后面是否有引用字符
- let after_pos = absolute_pos + TAG.len();
- let has_quote_after = is_quote_char(buffer, after_pos);
-
- if has_quote_before || has_quote_after {
- search_start = absolute_pos + 1;
- continue;
- }
-
- // 只有当标签后面全部是空白字符时才认定为结束标签
- if buffer[after_pos..].trim().is_empty() {
- return Some(absolute_pos);
- }
-
- search_start = absolute_pos + 1;
- }
-
- None
-}
-
-/// 查找真正的 thinking 开始标签(不被引用字符包裹)
-///
-/// 与 `find_real_thinking_end_tag` 类似,跳过被引用字符包裹的开始标签。
-fn find_real_thinking_start_tag(buffer: &str) -> Option {
- const TAG: &str = "";
- let mut search_start = 0;
-
- while let Some(pos) = buffer[search_start..].find(TAG) {
- let absolute_pos = search_start + pos;
-
- // 检查前面是否有引用字符
- let has_quote_before = absolute_pos > 0 && is_quote_char(buffer, absolute_pos - 1);
-
- // 检查后面是否有引用字符
- let after_pos = absolute_pos + TAG.len();
- let has_quote_after = is_quote_char(buffer, after_pos);
-
- // 如果不被引用字符包裹,则是真正的开始标签
- if !has_quote_before && !has_quote_after {
- return Some(absolute_pos);
- }
-
- // 继续搜索下一个匹配
- search_start = absolute_pos + 1;
- }
-
- None
-}
-
-/// SSE 事件
-#[derive(Debug, Clone)]
-pub struct SseEvent {
- pub event: String,
- pub data: serde_json::Value,
-}
-
-impl SseEvent {
- pub fn new(event: impl Into, data: serde_json::Value) -> Self {
- Self {
- event: event.into(),
- data,
- }
- }
-
- /// 格式化为 SSE 字符串
- pub fn to_sse_string(&self) -> String {
- format!(
- "event: {}\ndata: {}\n\n",
- self.event,
- serde_json::to_string(&self.data).unwrap_or_default()
- )
- }
-}
-
-/// 内容块状态
-#[derive(Debug, Clone)]
-struct BlockState {
- block_type: String,
- started: bool,
- stopped: bool,
-}
-
-impl BlockState {
- fn new(block_type: impl Into) -> Self {
- Self {
- block_type: block_type.into(),
- started: false,
- stopped: false,
- }
- }
-}
-
-/// SSE 状态管理器
-///
-/// 确保 SSE 事件序列符合 Claude API 规范:
-/// 1. message_start 只能出现一次
-/// 2. content_block 必须先 start 再 delta 再 stop
-/// 3. message_delta 只能出现一次,且在所有 content_block_stop 之后
-/// 4. message_stop 在最后
-#[derive(Debug)]
-pub struct SseStateManager {
- /// message_start 是否已发送
- message_started: bool,
- /// message_delta 是否已发送
- message_delta_sent: bool,
- /// 活跃的内容块状态
- active_blocks: HashMap,
- /// 消息是否已结束
- message_ended: bool,
- /// 下一个块索引
- next_block_index: i32,
- /// 当前 stop_reason
- stop_reason: Option,
- /// 是否有工具调用
- has_tool_use: bool,
-}
-
-impl Default for SseStateManager {
- fn default() -> Self {
- Self::new()
- }
-}
-
-impl SseStateManager {
- pub fn new() -> Self {
- Self {
- message_started: false,
- message_delta_sent: false,
- active_blocks: HashMap::new(),
- message_ended: false,
- next_block_index: 0,
- stop_reason: None,
- has_tool_use: false,
- }
- }
-
- /// 判断指定块是否处于可接收 delta 的打开状态
- fn is_block_open_of_type(&self, index: i32, expected_type: &str) -> bool {
- self.active_blocks
- .get(&index)
- .is_some_and(|b| b.started && !b.stopped && b.block_type == expected_type)
- }
-
- /// 获取下一个块索引
- pub fn next_block_index(&mut self) -> i32 {
- let index = self.next_block_index;
- self.next_block_index += 1;
- index
- }
-
- /// 记录工具调用
- pub fn set_has_tool_use(&mut self, has: bool) {
- self.has_tool_use = has;
- }
-
- /// 设置 stop_reason
- pub fn set_stop_reason(&mut self, reason: impl Into) {
- self.stop_reason = Some(reason.into());
- }
-
- /// 获取最终的 stop_reason
- pub fn get_stop_reason(&self) -> String {
- if let Some(ref reason) = self.stop_reason {
- reason.clone()
- } else if self.has_tool_use {
- "tool_use".to_string()
- } else {
- "end_turn".to_string()
- }
- }
-
- /// 处理 message_start 事件
- pub fn handle_message_start(&mut self, event: serde_json::Value) -> Option {
- if self.message_started {
- tracing::debug!("跳过重复的 message_start 事件");
- return None;
- }
- self.message_started = true;
- Some(SseEvent::new("message_start", event))
- }
-
- /// 处理 content_block_start 事件
- pub fn handle_content_block_start(
- &mut self,
- index: i32,
- block_type: &str,
- data: serde_json::Value,
- ) -> Vec {
- let mut events = Vec::new();
-
- // 如果是 tool_use 块,先关闭之前的文本块
- if block_type == "tool_use" {
- self.has_tool_use = true;
- for (block_index, block) in self.active_blocks.iter_mut() {
- if block.block_type == "text" && block.started && !block.stopped {
- // 自动发送 content_block_stop 关闭文本块
- events.push(SseEvent::new(
- "content_block_stop",
- json!({
- "type": "content_block_stop",
- "index": block_index
- }),
- ));
- block.stopped = true;
- }
- }
- }
-
- // 检查块是否已存在
- if let Some(block) = self.active_blocks.get_mut(&index) {
- if block.started {
- tracing::debug!("块 {} 已启动,跳过重复的 content_block_start", index);
- return events;
- }
- block.started = true;
- } else {
- let mut block = BlockState::new(block_type);
- block.started = true;
- self.active_blocks.insert(index, block);
- }
-
- events.push(SseEvent::new("content_block_start", data));
- events
- }
-
- /// 处理 content_block_delta 事件
- pub fn handle_content_block_delta(
- &mut self,
- index: i32,
- data: serde_json::Value,
- ) -> Option {
- // 确保块已启动
- if let Some(block) = self.active_blocks.get(&index) {
- if !block.started || block.stopped {
- tracing::warn!(
- "块 {} 状态异常: started={}, stopped={}",
- index,
- block.started,
- block.stopped
- );
- return None;
- }
- } else {
- // 块不存在,可能需要先创建
- tracing::warn!("收到未知块 {} 的 delta 事件", index);
- return None;
- }
-
- Some(SseEvent::new("content_block_delta", data))
- }
-
- /// 处理 content_block_stop 事件
- pub fn handle_content_block_stop(&mut self, index: i32) -> Option {
- if let Some(block) = self.active_blocks.get_mut(&index) {
- if block.stopped {
- tracing::debug!("块 {} 已停止,跳过重复的 content_block_stop", index);
- return None;
- }
- block.stopped = true;
- return Some(SseEvent::new(
- "content_block_stop",
- json!({
- "type": "content_block_stop",
- "index": index
- }),
- ));
- }
- None
- }
-
- /// 生成最终事件序列
- pub fn generate_final_events(
- &mut self,
- input_tokens: i32,
- output_tokens: i32,
- ) -> Vec {
- let mut events = Vec::new();
-
- // 关闭所有未关闭的块
- for (index, block) in self.active_blocks.iter_mut() {
- if block.started && !block.stopped {
- events.push(SseEvent::new(
- "content_block_stop",
- json!({
- "type": "content_block_stop",
- "index": index
- }),
- ));
- block.stopped = true;
- }
- }
-
- // 发送 message_delta
- if !self.message_delta_sent {
- self.message_delta_sent = true;
- events.push(SseEvent::new(
- "message_delta",
- json!({
- "type": "message_delta",
- "delta": {
- "stop_reason": self.get_stop_reason(),
- "stop_sequence": null
- },
- "usage": {
- "input_tokens": input_tokens,
- "output_tokens": output_tokens
- }
- }),
- ));
- }
-
- // 发送 message_stop
- if !self.message_ended {
- self.message_ended = true;
- events.push(SseEvent::new(
- "message_stop",
- json!({ "type": "message_stop" }),
- ));
- }
-
- events
- }
-}
-
-/// 上下文窗口大小(200k tokens)
-const CONTEXT_WINDOW_SIZE: i32 = 200_000;
-
-/// 流处理上下文
-pub struct StreamContext {
- /// SSE 状态管理器
- pub state_manager: SseStateManager,
- /// 请求的模型名称
- pub model: String,
- /// 消息 ID
- pub message_id: String,
- /// 输入 tokens(估算值)
- pub input_tokens: i32,
- /// 从 contextUsageEvent 计算的实际输入 tokens
- pub context_input_tokens: Option,
- /// 输出 tokens 累计
- pub output_tokens: i32,
- /// 工具块索引映射 (tool_id -> block_index)
- pub tool_block_indices: HashMap,
- /// thinking 是否启用
- pub thinking_enabled: bool,
- /// thinking 内容缓冲区
- pub thinking_buffer: String,
- /// 是否在 thinking 块内
- pub in_thinking_block: bool,
- /// thinking 块是否已提取完成
- pub thinking_extracted: bool,
- /// thinking 块索引
- pub thinking_block_index: Option,
- /// 文本块索引(thinking 启用时动态分配)
- pub text_block_index: Option,
-}
-
-impl StreamContext {
- /// 创建启用thinking的StreamContext
- pub fn new_with_thinking(
- model: impl Into,
- input_tokens: i32,
- thinking_enabled: bool,
- ) -> Self {
- Self {
- state_manager: SseStateManager::new(),
- model: model.into(),
- message_id: format!("msg_{}", Uuid::new_v4().to_string().replace('-', "")),
- input_tokens,
- context_input_tokens: None,
- output_tokens: 0,
- tool_block_indices: HashMap::new(),
- thinking_enabled,
- thinking_buffer: String::new(),
- in_thinking_block: false,
- thinking_extracted: false,
- thinking_block_index: None,
- text_block_index: None,
- }
- }
-
- /// 生成 message_start 事件
- pub fn create_message_start_event(&self) -> serde_json::Value {
- json!({
- "type": "message_start",
- "message": {
- "id": self.message_id,
- "type": "message",
- "role": "assistant",
- "content": [],
- "model": self.model,
- "stop_reason": null,
- "stop_sequence": null,
- "usage": {
- "input_tokens": self.input_tokens,
- "output_tokens": 1
- }
- }
- })
- }
-
- /// 生成初始事件序列 (message_start + 文本块 start)
- ///
- /// 当 thinking 启用时,不在初始化时创建文本块,而是等到实际收到内容时再创建。
- /// 这样可以确保 thinking 块(索引 0)在文本块(索引 1)之前。
- pub fn generate_initial_events(&mut self) -> Vec {
- let mut events = Vec::new();
-
- // message_start
- let msg_start = self.create_message_start_event();
- if let Some(event) = self.state_manager.handle_message_start(msg_start) {
- events.push(event);
- }
-
- // 如果启用了 thinking,不在这里创建文本块
- // thinking 块和文本块会在 process_content_with_thinking 中按正确顺序创建
- if self.thinking_enabled {
- return events;
- }
-
- // 创建初始文本块(仅在未启用 thinking 时)
- let text_block_index = self.state_manager.next_block_index();
- self.text_block_index = Some(text_block_index);
- let text_block_events = self.state_manager.handle_content_block_start(
- text_block_index,
- "text",
- json!({
- "type": "content_block_start",
- "index": text_block_index,
- "content_block": {
- "type": "text",
- "text": ""
- }
- }),
- );
- events.extend(text_block_events);
-
- events
- }
-
- /// 处理 Kiro 事件并转换为 Anthropic SSE 事件
- pub fn process_kiro_event(&mut self, event: &Event) -> Vec {
- match event {
- Event::AssistantResponse(resp) => self.process_assistant_response(&resp.content),
- Event::ToolUse(tool_use) => self.process_tool_use(tool_use),
- Event::ContextUsage(context_usage) => {
- // 从上下文使用百分比计算实际的 input_tokens
- // 公式: percentage * 200000 / 100 = percentage * 2000
- let actual_input_tokens = (context_usage.context_usage_percentage
- * (CONTEXT_WINDOW_SIZE as f64)
- / 100.0) as i32;
- self.context_input_tokens = Some(actual_input_tokens);
- tracing::debug!(
- "收到 contextUsageEvent: {}%, 计算 input_tokens: {}",
- context_usage.context_usage_percentage,
- actual_input_tokens
- );
- Vec::new()
- }
- Event::Error {
- error_code,
- error_message,
- } => {
- tracing::error!("收到错误事件: {} - {}", error_code, error_message);
- Vec::new()
- }
- Event::Exception {
- exception_type,
- message,
- } => {
- // 处理 ContentLengthExceededException
- if exception_type == "ContentLengthExceededException" {
- self.state_manager.set_stop_reason("max_tokens");
- }
- tracing::warn!("收到异常事件: {} - {}", exception_type, message);
- Vec::new()
- }
- _ => Vec::new(),
- }
- }
-
- /// 处理助手响应事件
- fn process_assistant_response(&mut self, content: &str) -> Vec {
- if content.is_empty() {
- return Vec::new();
- }
-
- // 估算 tokens
- self.output_tokens += estimate_tokens(content);
-
- // 如果启用了thinking,需要处理thinking块
- if self.thinking_enabled {
- return self.process_content_with_thinking(content);
- }
-
- // 非 thinking 模式同样复用统一的 text_delta 发送逻辑,
- // 以便在 tool_use 自动关闭文本块后能够自愈重建新的文本块,避免“吞字”。
- self.create_text_delta_events(content)
- }
-
- /// 处理包含thinking块的内容
- fn process_content_with_thinking(&mut self, content: &str) -> Vec {
- let mut events = Vec::new();
-
- // 将内容添加到缓冲区进行处理
- self.thinking_buffer.push_str(content);
-
- loop {
- if !self.in_thinking_block && !self.thinking_extracted {
- // 查找 开始标签(跳过被反引号包裹的)
- if let Some(start_pos) = find_real_thinking_start_tag(&self.thinking_buffer) {
- // 发送 之前的内容作为 text_delta
- let before_thinking = self.thinking_buffer[..start_pos].to_string();
- if !before_thinking.is_empty() {
- events.extend(self.create_text_delta_events(&before_thinking));
- }
-
- // 进入 thinking 块
- self.in_thinking_block = true;
- self.thinking_buffer =
- self.thinking_buffer[start_pos + "".len()..].to_string();
-
- // 创建 thinking 块的 content_block_start 事件
- let thinking_index = self.state_manager.next_block_index();
- self.thinking_block_index = Some(thinking_index);
- let start_events = self.state_manager.handle_content_block_start(
- thinking_index,
- "thinking",
- json!({
- "type": "content_block_start",
- "index": thinking_index,
- "content_block": {
- "type": "thinking",
- "thinking": ""
- }
- }),
- );
- events.extend(start_events);
- } else {
- // 没有找到 ,检查是否可能是部分标签
- // 保留可能是部分标签的内容
- let target_len = self
- .thinking_buffer
- .len()
- .saturating_sub("".len());
- let safe_len = find_char_boundary(&self.thinking_buffer, target_len);
- if safe_len > 0 {
- let safe_content = self.thinking_buffer[..safe_len].to_string();
- if !safe_content.is_empty() {
- events.extend(self.create_text_delta_events(&safe_content));
- }
- self.thinking_buffer = self.thinking_buffer[safe_len..].to_string();
- }
- break;
- }
- } else if self.in_thinking_block {
- // 在 thinking 块内,查找 结束标签(跳过被反引号包裹的)
- if let Some(end_pos) = find_real_thinking_end_tag(&self.thinking_buffer) {
- // 提取 thinking 内容
- let thinking_content = self.thinking_buffer[..end_pos].to_string();
- if !thinking_content.is_empty() {
- if let Some(thinking_index) = self.thinking_block_index {
- events.push(
- self.create_thinking_delta_event(thinking_index, &thinking_content),
- );
- }
- }
-
- // 结束 thinking 块
- self.in_thinking_block = false;
- self.thinking_extracted = true;
-
- // 发送空的 thinking_delta 事件,然后发送 content_block_stop 事件
- if let Some(thinking_index) = self.thinking_block_index {
- // 先发送空的 thinking_delta
- events.push(self.create_thinking_delta_event(thinking_index, ""));
- // 再发送 content_block_stop
- if let Some(stop_event) =
- self.state_manager.handle_content_block_stop(thinking_index)
- {
- events.push(stop_event);
- }
- }
-
- self.thinking_buffer =
- self.thinking_buffer[end_pos + "".len()..].to_string();
- } else {
- // 没有找到结束标签,发送当前缓冲区内容作为 thinking_delta
- // 保留可能是部分标签的内容
- let target_len = self
- .thinking_buffer
- .len()
- .saturating_sub("".len());
- let safe_len = find_char_boundary(&self.thinking_buffer, target_len);
- if safe_len > 0 {
- let safe_content = self.thinking_buffer[..safe_len].to_string();
- if !safe_content.is_empty() {
- if let Some(thinking_index) = self.thinking_block_index {
- events.push(
- self.create_thinking_delta_event(thinking_index, &safe_content),
- );
- }
- }
- self.thinking_buffer = self.thinking_buffer[safe_len..].to_string();
- }
- break;
- }
- } else {
- // thinking 已提取完成,剩余内容作为 text_delta
- if !self.thinking_buffer.is_empty() {
- let remaining = self.thinking_buffer.clone();
- self.thinking_buffer.clear();
- events.extend(self.create_text_delta_events(&remaining));
- }
- break;
- }
- }
-
- events
- }
-
- /// 创建 text_delta 事件
- ///
- /// 如果文本块尚未创建,会先创建文本块。
- /// 当发生 tool_use 时,状态机会自动关闭当前文本块;后续文本会自动创建新的文本块继续输出。
- ///
- /// 返回值包含可能的 content_block_start 事件和 content_block_delta 事件。
- fn create_text_delta_events(&mut self, text: &str) -> Vec {
- let mut events = Vec::new();
-
- // 如果当前 text_block_index 指向的块已经被关闭(例如 tool_use 开始时自动 stop),
- // 则丢弃该索引并创建新的文本块继续输出,避免 delta 被状态机拒绝导致“吞字”。
- if let Some(idx) = self.text_block_index {
- if !self.state_manager.is_block_open_of_type(idx, "text") {
- self.text_block_index = None;
- }
- }
-
- // 获取或创建文本块索引
- let text_index = if let Some(idx) = self.text_block_index {
- idx
- } else {
- // 文本块尚未创建,需要先创建
- let idx = self.state_manager.next_block_index();
- self.text_block_index = Some(idx);
-
- // 发送 content_block_start 事件
- let start_events = self.state_manager.handle_content_block_start(
- idx,
- "text",
- json!({
- "type": "content_block_start",
- "index": idx,
- "content_block": {
- "type": "text",
- "text": ""
- }
- }),
- );
- events.extend(start_events);
- idx
- };
-
- // 发送 content_block_delta 事件
- if let Some(delta_event) = self.state_manager.handle_content_block_delta(
- text_index,
- json!({
- "type": "content_block_delta",
- "index": text_index,
- "delta": {
- "type": "text_delta",
- "text": text
- }
- }),
- ) {
- events.push(delta_event);
- }
-
- events
- }
-
- /// 创建 thinking_delta 事件
- fn create_thinking_delta_event(&self, index: i32, thinking: &str) -> SseEvent {
- SseEvent::new(
- "content_block_delta",
- json!({
- "type": "content_block_delta",
- "index": index,
- "delta": {
- "type": "thinking_delta",
- "thinking": thinking
- }
- }),
- )
- }
-
- /// 处理工具使用事件
- fn process_tool_use(
- &mut self,
- tool_use: &crate::kiro::model::events::ToolUseEvent,
- ) -> Vec {
- let mut events = Vec::new();
-
- self.state_manager.set_has_tool_use(true);
-
- // tool_use 必须发生在 thinking 结束之后。
- // 但当 `` 后面没有 `\n\n`(例如紧跟 tool_use 或流结束)时,
- // thinking 结束标签会滞留在 thinking_buffer,导致后续 flush 时把 `` 当作内容输出。
- // 这里在开始 tool_use block 前做一次“边界场景”的结束标签识别与过滤。
- if self.thinking_enabled && self.in_thinking_block {
- if let Some(end_pos) = find_real_thinking_end_tag_at_buffer_end(&self.thinking_buffer) {
- let thinking_content = self.thinking_buffer[..end_pos].to_string();
- if !thinking_content.is_empty() {
- if let Some(thinking_index) = self.thinking_block_index {
- events.push(
- self.create_thinking_delta_event(thinking_index, &thinking_content),
- );
- }
- }
-
- // 结束 thinking 块
- self.in_thinking_block = false;
- self.thinking_extracted = true;
-
- if let Some(thinking_index) = self.thinking_block_index {
- // 先发送空的 thinking_delta
- events.push(self.create_thinking_delta_event(thinking_index, ""));
- // 再发送 content_block_stop
- if let Some(stop_event) =
- self.state_manager.handle_content_block_stop(thinking_index)
- {
- events.push(stop_event);
- }
- }
-
- // 把结束标签后的内容当作普通文本(通常为空或空白)
- let after_pos = end_pos + "".len();
- let remaining = self.thinking_buffer[after_pos..].to_string();
- self.thinking_buffer.clear();
- if !remaining.is_empty() {
- events.extend(self.create_text_delta_events(&remaining));
- }
- }
- }
-
- // thinking 模式下,process_content_with_thinking 可能会为了探测 `` 而暂存一小段尾部文本。
- // 如果此时直接开始 tool_use,状态机会自动关闭 text block,导致这段“待输出文本”看起来被 tool_use 吞掉。
- // 约束:只在尚未进入 thinking block、且 thinking 尚未被提取时,将缓冲区当作普通文本 flush。
- if self.thinking_enabled
- && !self.in_thinking_block
- && !self.thinking_extracted
- && !self.thinking_buffer.is_empty()
- {
- let buffered = std::mem::take(&mut self.thinking_buffer);
- events.extend(self.create_text_delta_events(&buffered));
- }
-
- // 获取或分配块索引
- let block_index = if let Some(&idx) = self.tool_block_indices.get(&tool_use.tool_use_id) {
- idx
- } else {
- let idx = self.state_manager.next_block_index();
- self.tool_block_indices
- .insert(tool_use.tool_use_id.clone(), idx);
- idx
- };
-
- // 发送 content_block_start
- let start_events = self.state_manager.handle_content_block_start(
- block_index,
- "tool_use",
- json!({
- "type": "content_block_start",
- "index": block_index,
- "content_block": {
- "type": "tool_use",
- "id": tool_use.tool_use_id,
- "name": tool_use.name,
- "input": {}
- }
- }),
- );
- events.extend(start_events);
-
- // 发送参数增量 (ToolUseEvent.input 是 String 类型)
- if !tool_use.input.is_empty() {
- self.output_tokens += (tool_use.input.len() as i32 + 3) / 4; // 估算 token
-
- if let Some(delta_event) = self.state_manager.handle_content_block_delta(
- block_index,
- json!({
- "type": "content_block_delta",
- "index": block_index,
- "delta": {
- "type": "input_json_delta",
- "partial_json": tool_use.input
- }
- }),
- ) {
- events.push(delta_event);
- }
- }
-
- // 如果是完整的工具调用(stop=true),发送 content_block_stop
- if tool_use.stop {
- if let Some(stop_event) = self.state_manager.handle_content_block_stop(block_index) {
- events.push(stop_event);
- }
- }
-
- events
- }
-
- /// 生成最终事件序列
- pub fn generate_final_events(&mut self) -> Vec {
- let mut events = Vec::new();
-
- // Flush thinking_buffer 中的剩余内容
- if self.thinking_enabled && !self.thinking_buffer.is_empty() {
- if self.in_thinking_block {
- // 末尾可能残留 ``(例如紧跟 tool_use 或流结束),需要在 flush 时过滤掉结束标签。
- if let Some(end_pos) =
- find_real_thinking_end_tag_at_buffer_end(&self.thinking_buffer)
- {
- let thinking_content = self.thinking_buffer[..end_pos].to_string();
- if !thinking_content.is_empty() {
- if let Some(thinking_index) = self.thinking_block_index {
- events.push(
- self.create_thinking_delta_event(thinking_index, &thinking_content),
- );
- }
- }
-
- // 关闭 thinking 块:先发送空的 thinking_delta,再发送 content_block_stop
- if let Some(thinking_index) = self.thinking_block_index {
- events.push(self.create_thinking_delta_event(thinking_index, ""));
- if let Some(stop_event) =
- self.state_manager.handle_content_block_stop(thinking_index)
- {
- events.push(stop_event);
- }
- }
-
- // 把结束标签后的内容当作普通文本(通常为空或空白)
- let after_pos = end_pos + "".len();
- let remaining = self.thinking_buffer[after_pos..].to_string();
- self.thinking_buffer.clear();
- self.in_thinking_block = false;
- self.thinking_extracted = true;
- if !remaining.is_empty() {
- events.extend(self.create_text_delta_events(&remaining));
- }
- } else {
- // 如果还在 thinking 块内,发送剩余内容作为 thinking_delta
- if let Some(thinking_index) = self.thinking_block_index {
- events.push(
- self.create_thinking_delta_event(thinking_index, &self.thinking_buffer),
- );
- }
- // 关闭 thinking 块:先发送空的 thinking_delta,再发送 content_block_stop
- if let Some(thinking_index) = self.thinking_block_index {
- // 先发送空的 thinking_delta
- events.push(self.create_thinking_delta_event(thinking_index, ""));
- // 再发送 content_block_stop
- if let Some(stop_event) =
- self.state_manager.handle_content_block_stop(thinking_index)
- {
- events.push(stop_event);
- }
- }
- }
- } else {
- // 否则发送剩余内容作为 text_delta
- let buffer_content = self.thinking_buffer.clone();
- events.extend(self.create_text_delta_events(&buffer_content));
- }
- self.thinking_buffer.clear();
- }
-
- // 使用从 contextUsageEvent 计算的 input_tokens,如果没有则使用估算值
- let final_input_tokens = self.context_input_tokens.unwrap_or(self.input_tokens);
-
- // 生成最终事件
- events.extend(
- self.state_manager
- .generate_final_events(final_input_tokens, self.output_tokens),
- );
- events
- }
-}
-
-/// 简单的 token 估算
-fn estimate_tokens(text: &str) -> i32 {
- let chars: Vec = text.chars().collect();
- let mut chinese_count = 0;
- let mut other_count = 0;
-
- for c in &chars {
- if *c >= '\u{4E00}' && *c <= '\u{9FFF}' {
- chinese_count += 1;
- } else {
- other_count += 1;
- }
- }
-
- // 中文约 1.5 字符/token,英文约 4 字符/token
- let chinese_tokens = (chinese_count * 2 + 2) / 3;
- let other_tokens = (other_count + 3) / 4;
-
- (chinese_tokens + other_tokens).max(1)
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_sse_event_format() {
- let event = SseEvent::new("message_start", json!({"type": "message_start"}));
- let sse_str = event.to_sse_string();
-
- assert!(sse_str.starts_with("event: message_start\n"));
- assert!(sse_str.contains("data: "));
- assert!(sse_str.ends_with("\n\n"));
- }
-
- #[test]
- fn test_sse_state_manager_message_start() {
- let mut manager = SseStateManager::new();
-
- // 第一次应该成功
- let event = manager.handle_message_start(json!({"type": "message_start"}));
- assert!(event.is_some());
-
- // 第二次应该被跳过
- let event = manager.handle_message_start(json!({"type": "message_start"}));
- assert!(event.is_none());
- }
-
- #[test]
- fn test_sse_state_manager_block_lifecycle() {
- let mut manager = SseStateManager::new();
-
- // 创建块
- let events = manager.handle_content_block_start(0, "text", json!({}));
- assert_eq!(events.len(), 1);
-
- // delta
- let event = manager.handle_content_block_delta(0, json!({}));
- assert!(event.is_some());
-
- // stop
- let event = manager.handle_content_block_stop(0);
- assert!(event.is_some());
-
- // 重复 stop 应该被跳过
- let event = manager.handle_content_block_stop(0);
- assert!(event.is_none());
- }
-
- #[test]
- fn test_text_delta_after_tool_use_restarts_text_block() {
- let mut ctx = StreamContext::new_with_thinking("test-model", 1, false);
-
- let initial_events = ctx.generate_initial_events();
- assert!(
- initial_events
- .iter()
- .any(|e| e.event == "content_block_start"
- && e.data["content_block"]["type"] == "text")
- );
-
- let initial_text_index = ctx
- .text_block_index
- .expect("initial text block index should exist");
-
- // tool_use 开始会自动关闭现有 text block
- let tool_events = ctx.process_tool_use(&crate::kiro::model::events::ToolUseEvent {
- name: "test_tool".to_string(),
- tool_use_id: "tool_1".to_string(),
- input: "{}".to_string(),
- stop: false,
- });
- assert!(
- tool_events.iter().any(|e| {
- e.event == "content_block_stop"
- && e.data["index"].as_i64() == Some(initial_text_index as i64)
- }),
- "tool_use should stop the previous text block"
- );
-
- // 之后再来文本增量,应自动创建新的 text block 而不是往已 stop 的块里写 delta
- let text_events = ctx.process_assistant_response("hello");
- let new_text_start_index = text_events.iter().find_map(|e| {
- if e.event == "content_block_start" && e.data["content_block"]["type"] == "text" {
- e.data["index"].as_i64()
- } else {
- None
- }
- });
- assert!(
- new_text_start_index.is_some(),
- "should start a new text block"
- );
- assert_ne!(
- new_text_start_index.unwrap(),
- initial_text_index as i64,
- "new text block index should differ from the stopped one"
- );
- assert!(
- text_events.iter().any(|e| {
- e.event == "content_block_delta"
- && e.data["delta"]["type"] == "text_delta"
- && e.data["delta"]["text"] == "hello"
- }),
- "should emit text_delta after restarting text block"
- );
- }
-
- #[test]
- fn test_tool_use_flushes_pending_thinking_buffer_text_before_tool_block() {
- // thinking 模式下,短文本可能被暂存在 thinking_buffer 以等待 `` 的跨 chunk 匹配。
- // 当紧接着出现 tool_use 时,应先 flush 这段文本,再开始 tool_use block。
- let mut ctx = StreamContext::new_with_thinking("test-model", 1, true);
- let _initial_events = ctx.generate_initial_events();
-
- // 两段短文本(各 2 个中文字符),总长度仍可能不足以满足 safe_len>0 的输出条件,
- // 因而会留在 thinking_buffer 中等待后续 chunk。
- let ev1 = ctx.process_assistant_response("有修");
- assert!(
- ev1.iter().all(|e| e.event != "content_block_delta"),
- "short prefix should be buffered under thinking mode"
- );
- let ev2 = ctx.process_assistant_response("改:");
- assert!(
- ev2.iter().all(|e| e.event != "content_block_delta"),
- "short prefix should still be buffered under thinking mode"
- );
-
- let events = ctx.process_tool_use(&crate::kiro::model::events::ToolUseEvent {
- name: "Write".to_string(),
- tool_use_id: "tool_1".to_string(),
- input: "{}".to_string(),
- stop: false,
- });
-
- let text_start_index = events.iter().find_map(|e| {
- if e.event == "content_block_start" && e.data["content_block"]["type"] == "text" {
- e.data["index"].as_i64()
- } else {
- None
- }
- });
- let pos_text_delta = events.iter().position(|e| {
- e.event == "content_block_delta" && e.data["delta"]["type"] == "text_delta"
- });
- let pos_text_stop = text_start_index.and_then(|idx| {
- events.iter().position(|e| {
- e.event == "content_block_stop" && e.data["index"].as_i64() == Some(idx)
- })
- });
- let pos_tool_start = events.iter().position(|e| {
- e.event == "content_block_start" && e.data["content_block"]["type"] == "tool_use"
- });
-
- assert!(
- text_start_index.is_some(),
- "should start a text block to flush buffered text"
- );
- assert!(
- pos_text_delta.is_some(),
- "should flush buffered text as text_delta"
- );
- assert!(
- pos_text_stop.is_some(),
- "should stop text block before tool_use block starts"
- );
- assert!(pos_tool_start.is_some(), "should start tool_use block");
-
- let pos_text_delta = pos_text_delta.unwrap();
- let pos_text_stop = pos_text_stop.unwrap();
- let pos_tool_start = pos_tool_start.unwrap();
-
- assert!(
- pos_text_delta < pos_text_stop && pos_text_stop < pos_tool_start,
- "ordering should be: text_delta -> text_stop -> tool_use_start"
- );
-
- assert!(
- events.iter().any(|e| {
- e.event == "content_block_delta"
- && e.data["delta"]["type"] == "text_delta"
- && e.data["delta"]["text"] == "有修改:"
- }),
- "flushed text should equal the buffered prefix"
- );
- }
-
- #[test]
- fn test_estimate_tokens() {
- assert!(estimate_tokens("Hello") > 0);
- assert!(estimate_tokens("你好") > 0);
- assert!(estimate_tokens("Hello 你好") > 0);
- }
-
- #[test]
- fn test_find_real_thinking_start_tag_basic() {
- // 基本情况:正常的开始标签
- assert_eq!(find_real_thinking_start_tag(""), Some(0));
- assert_eq!(find_real_thinking_start_tag("prefix"), Some(6));
- }
-
- #[test]
- fn test_find_real_thinking_start_tag_with_backticks() {
- // 被反引号包裹的应该被跳过
- assert_eq!(find_real_thinking_start_tag("``"), None);
- assert_eq!(find_real_thinking_start_tag("use `` tag"), None);
-
- // 先有被包裹的,后有真正的开始标签
- assert_eq!(
- find_real_thinking_start_tag("about `` tagcontent"),
- Some(22)
- );
- }
-
- #[test]
- fn test_find_real_thinking_start_tag_with_quotes() {
- // 被双引号包裹的应该被跳过
- assert_eq!(find_real_thinking_start_tag("\"\""), None);
- assert_eq!(find_real_thinking_start_tag("the \"\" tag"), None);
-
- // 被单引号包裹的应该被跳过
- assert_eq!(find_real_thinking_start_tag("''"), None);
-
- // 混合情况
- assert_eq!(
- find_real_thinking_start_tag("about \"\" and '' then"),
- Some(40)
- );
- }
-
- #[test]
- fn test_find_real_thinking_end_tag_basic() {
- // 基本情况:正常的结束标签后面有双换行符
- assert_eq!(find_real_thinking_end_tag("\n\n"), Some(0));
- assert_eq!(
- find_real_thinking_end_tag("content\n\n"),
- Some(7)
- );
- assert_eq!(
- find_real_thinking_end_tag("some text\n\nmore text"),
- Some(9)
- );
-
- // 没有双换行符的情况
- assert_eq!(find_real_thinking_end_tag(""), None);
- assert_eq!(find_real_thinking_end_tag("\n"), None);
- assert_eq!(find_real_thinking_end_tag(" more"), None);
- }
-
- #[test]
- fn test_find_real_thinking_end_tag_with_backticks() {
- // 被反引号包裹的应该被跳过
- assert_eq!(find_real_thinking_end_tag("``\n\n"), None);
- assert_eq!(
- find_real_thinking_end_tag("mention `` in code\n\n"),
- None
- );
-
- // 只有前面有反引号
- assert_eq!(find_real_thinking_end_tag("`\n\n"), None);
-
- // 只有后面有反引号
- assert_eq!(find_real_thinking_end_tag("`\n\n"), None);
- }
-
- #[test]
- fn test_find_real_thinking_end_tag_with_quotes() {
- // 被双引号包裹的应该被跳过
- assert_eq!(find_real_thinking_end_tag("\"\"\n\n"), None);
- assert_eq!(
- find_real_thinking_end_tag("the string \"\" is a tag\n\n"),
- None
- );
-
- // 被单引号包裹的应该被跳过
- assert_eq!(find_real_thinking_end_tag("''\n\n"), None);
- assert_eq!(
- find_real_thinking_end_tag("use '' as marker\n\n"),
- None
- );
-
- // 混合情况:双引号包裹后有真正的标签
- assert_eq!(
- find_real_thinking_end_tag("about \"\" tag\n\n"),
- Some(23)
- );
-
- // 混合情况:单引号包裹后有真正的标签
- assert_eq!(
- find_real_thinking_end_tag("about '' tag\n\n"),
- Some(23)
- );
- }
-
- #[test]
- fn test_find_real_thinking_end_tag_mixed() {
- // 先有被包裹的,后有真正的结束标签
- assert_eq!(
- find_real_thinking_end_tag("discussing `` tag\n\n"),
- Some(28)
- );
-
- // 多个被包裹的,最后一个是真正的
- assert_eq!(
- find_real_thinking_end_tag("`` and `` done\n\n"),
- Some(36)
- );
-
- // 多种引用字符混合
- assert_eq!(
- find_real_thinking_end_tag(
- "`` and \"\" and '' done\n\n"
- ),
- Some(54)
- );
- }
-
- #[test]
- fn test_tool_use_immediately_after_thinking_filters_end_tag_and_closes_thinking_block() {
- let mut ctx = StreamContext::new_with_thinking("test-model", 1, true);
- let _initial_events = ctx.generate_initial_events();
-
- let mut all_events = Vec::new();
-
- // thinking 内容以 `` 结尾,但后面没有 `\n\n`(模拟紧跟 tool_use 的场景)
- all_events.extend(ctx.process_assistant_response("abc"));
-
- let tool_events = ctx.process_tool_use(&crate::kiro::model::events::ToolUseEvent {
- name: "Write".to_string(),
- tool_use_id: "tool_1".to_string(),
- input: "{}".to_string(),
- stop: false,
- });
- all_events.extend(tool_events);
-
- all_events.extend(ctx.generate_final_events());
-
- // 不应把 `` 当作 thinking 内容输出
- assert!(
- all_events.iter().all(|e| {
- !(e.event == "content_block_delta"
- && e.data["delta"]["type"] == "thinking_delta"
- && e.data["delta"]["thinking"] == "")
- }),
- "`` should be filtered from output"
- );
-
- // thinking block 必须在 tool_use block 之前关闭
- let thinking_index = ctx
- .thinking_block_index
- .expect("thinking block index should exist");
- let pos_thinking_stop = all_events.iter().position(|e| {
- e.event == "content_block_stop"
- && e.data["index"].as_i64() == Some(thinking_index as i64)
- });
- let pos_tool_start = all_events.iter().position(|e| {
- e.event == "content_block_start" && e.data["content_block"]["type"] == "tool_use"
- });
- assert!(
- pos_thinking_stop.is_some(),
- "thinking block should be stopped"
- );
- assert!(pos_tool_start.is_some(), "tool_use block should be started");
- assert!(
- pos_thinking_stop.unwrap() < pos_tool_start.unwrap(),
- "thinking block should stop before tool_use block starts"
- );
- }
-
- #[test]
- fn test_final_flush_filters_standalone_thinking_end_tag() {
- let mut ctx = StreamContext::new_with_thinking("test-model", 1, true);
- let _initial_events = ctx.generate_initial_events();
-
- let mut all_events = Vec::new();
- all_events.extend(ctx.process_assistant_response("abc"));
- all_events.extend(ctx.generate_final_events());
-
- assert!(
- all_events.iter().all(|e| {
- !(e.event == "content_block_delta"
- && e.data["delta"]["type"] == "thinking_delta"
- && e.data["delta"]["thinking"] == "")
- }),
- "`` should be filtered during final flush"
- );
- }
-}
diff --git a/src/anthropic/types.rs b/src/anthropic/types.rs
deleted file mode 100644
index 37f52ab6e85fbea3e54377bc3afd9d5953cb1a96..0000000000000000000000000000000000000000
--- a/src/anthropic/types.rs
+++ /dev/null
@@ -1,270 +0,0 @@
-//! Anthropic API 类型定义
-
-use serde::{Deserialize, Serialize};
-use std::collections::HashMap;
-
-// === 错误响应 ===
-
-/// API 错误响应
-#[derive(Debug, Serialize)]
-pub struct ErrorResponse {
- pub error: ErrorDetail,
-}
-
-/// 错误详情
-#[derive(Debug, Serialize)]
-pub struct ErrorDetail {
- #[serde(rename = "type")]
- pub error_type: String,
- pub message: String,
-}
-
-impl ErrorResponse {
- /// 创建新的错误响应
- pub fn new(error_type: impl Into, message: impl Into) -> Self {
- Self {
- error: ErrorDetail {
- error_type: error_type.into(),
- message: message.into(),
- },
- }
- }
-
- /// 创建认证错误响应
- pub fn authentication_error() -> Self {
- Self::new("authentication_error", "Invalid API key")
- }
-}
-
-// === Models 端点类型 ===
-
-/// 模型信息
-#[derive(Debug, Serialize)]
-pub struct Model {
- pub id: String,
- pub object: String,
- pub created: i64,
- pub owned_by: String,
- pub display_name: String,
- #[serde(rename = "type")]
- pub model_type: String,
- pub max_tokens: i32,
-}
-
-/// 模型列表响应
-#[derive(Debug, Serialize)]
-pub struct ModelsResponse {
- pub object: String,
- pub data: Vec,
-}
-
-// === Messages 端点类型 ===
-
-/// 最大思考预算 tokens
-const MAX_BUDGET_TOKENS: i32 = 24576;
-
-/// Thinking 配置
-#[derive(Debug, Deserialize, Clone)]
-pub struct Thinking {
- #[serde(rename = "type")]
- pub thinking_type: String,
- #[serde(
- default = "default_budget_tokens",
- deserialize_with = "deserialize_budget_tokens"
- )]
- pub budget_tokens: i32,
-}
-
-fn default_budget_tokens() -> i32 {
- 20000
-}
-fn deserialize_budget_tokens<'de, D>(deserializer: D) -> Result
-where
- D: serde::Deserializer<'de>,
-{
- let value = i32::deserialize(deserializer)?;
- Ok(value.min(MAX_BUDGET_TOKENS))
-}
-
-/// Claude Code 请求中的 metadata
-#[derive(Debug, Clone, Deserialize)]
-pub struct Metadata {
- /// 用户 ID,格式如: user_xxx_account__session_0b4445e1-f5be-49e1-87ce-62bbc28ad705
- pub user_id: Option,
-}
-
-/// Messages 请求体
-#[derive(Debug, Deserialize)]
-pub struct MessagesRequest {
- pub model: String,
- pub max_tokens: i32,
- pub messages: Vec,
- #[serde(default)]
- pub stream: bool,
- #[serde(default, deserialize_with = "deserialize_system")]
- pub system: Option>,
- pub tools: Option>,
- pub tool_choice: Option,
- pub thinking: Option,
- /// Claude Code 请求中的 metadata,包含 session 信息
- pub metadata: Option,
-}
-
-/// 反序列化 system 字段,支持字符串或数组格式
-fn deserialize_system<'de, D>(deserializer: D) -> Result