diff --git a/README.md b/README.md
index 0841f67e..b2a318e9 100644
--- a/README.md
+++ b/README.md
@@ -490,12 +490,12 @@ Droid CLI 读取 `~/.factory/config.json`。可以在该文件中添加自定义
{
"custom_models": [
{
- "model_display_name": "Sonnet 4.5 [crs]",
- "model": "claude-sonnet-4-5-20250929",
+ "model_display_name": "Opus 4.5 [crs]",
+ "model": "claude-opus-4-5-20251101",
"base_url": "http://127.0.0.1:3000/droid/claude",
"api_key": "后台创建的API密钥",
"provider": "anthropic",
- "max_tokens": 8192
+ "max_tokens": 64000
},
{
"model_display_name": "GPT5-Codex [crs]",
@@ -511,7 +511,7 @@ Droid CLI 读取 `~/.factory/config.json`。可以在该文件中添加自定义
"base_url": "http://127.0.0.1:3000/droid/comm/v1/",
"api_key": "后台创建的API密钥",
"provider": "generic-chat-completion-api",
- "max_tokens": 32000
+ "max_tokens": 65535
},
{
"model_display_name": "GLM-4.6 [crs]",
@@ -519,7 +519,7 @@ Droid CLI 读取 `~/.factory/config.json`。可以在该文件中添加自定义
"base_url": "http://127.0.0.1:3000/droid/comm/v1/",
"api_key": "后台创建的API密钥",
"provider": "generic-chat-completion-api",
- "max_tokens": 32000
+ "max_tokens": 202800
}
]
}
diff --git a/src/routes/apiStats.js b/src/routes/apiStats.js
index d2cb2b39..808d48e5 100644
--- a/src/routes/apiStats.js
+++ b/src/routes/apiStats.js
@@ -790,6 +790,283 @@ router.post('/api/batch-model-stats', async (req, res) => {
}
})
+// 🧪 API Key 端点测试接口 - 测试API Key是否能正常访问服务
+router.post('/api-key/test', async (req, res) => {
+ const axios = require('axios')
+ const config = require('../../config/config')
+
+ try {
+ const { apiKey, model = 'claude-sonnet-4-5-20250929' } = req.body
+
+ if (!apiKey) {
+ return res.status(400).json({
+ error: 'API Key is required',
+ message: 'Please provide your API Key'
+ })
+ }
+
+ // 基本格式验证
+ if (typeof apiKey !== 'string' || apiKey.length < 10 || apiKey.length > 512) {
+ return res.status(400).json({
+ error: 'Invalid API key format',
+ message: 'API key format is invalid'
+ })
+ }
+
+ // 首先验证API Key是否有效(不触发激活)
+ const validation = await apiKeyService.validateApiKeyForStats(apiKey)
+ if (!validation.valid) {
+ return res.status(401).json({
+ error: 'Invalid API key',
+ message: validation.error
+ })
+ }
+
+ logger.api(`🧪 API Key test started for: ${validation.keyData.name} (${validation.keyData.id})`)
+
+ // 设置SSE响应头
+ res.setHeader('Content-Type', 'text/event-stream')
+ res.setHeader('Cache-Control', 'no-cache')
+ res.setHeader('Connection', 'keep-alive')
+ res.setHeader('X-Accel-Buffering', 'no')
+
+ // 发送测试开始事件
+ res.write(`data: ${JSON.stringify({ type: 'test_start', message: 'Test started' })}\n\n`)
+
+ // 构建测试请求,模拟 Claude CLI 客户端
+ const port = config.server.port || 3000
+ const baseURL = `http://127.0.0.1:${port}`
+
+ const testPayload = {
+ model,
+ messages: [
+ {
+ role: 'user',
+ content: 'hi'
+ }
+ ],
+ system: [
+ {
+ type: 'text',
+ text: "You are Claude Code, Anthropic's official CLI for Claude."
+ }
+ ],
+ max_tokens: 32000,
+ temperature: 1,
+ stream: true
+ }
+
+ const headers = {
+ 'Content-Type': 'application/json',
+ 'User-Agent': 'claude-cli/2.0.52 (external, cli)',
+ 'x-api-key': apiKey,
+ 'anthropic-version': config.claude.apiVersion || '2023-06-01'
+ }
+
+ // 向自身服务发起测试请求
+ // 使用 validateStatus 允许所有状态码通过,以便我们可以处理流式错误响应
+ const response = await axios.post(`${baseURL}/api/v1/messages`, testPayload, {
+ headers,
+ responseType: 'stream',
+ timeout: 60000, // 60秒超时
+ validateStatus: () => true // 接受所有状态码,自行处理错误
+ })
+
+ // 检查响应状态码,如果不是2xx,尝试读取错误信息
+ if (response.status >= 400) {
+ logger.error(
+ `🧪 API Key test received error status ${response.status} for: ${validation.keyData.name}`
+ )
+
+ // 尝试从流中读取错误信息
+ let errorBody = ''
+ for await (const chunk of response.data) {
+ errorBody += chunk.toString()
+ }
+
+ let errorMessage = `HTTP ${response.status}`
+ try {
+ // 尝试解析SSE格式的错误
+ const lines = errorBody.split('\n')
+ for (const line of lines) {
+ if (line.startsWith('data: ')) {
+ const dataStr = line.substring(6).trim()
+ if (dataStr && dataStr !== '[DONE]') {
+ const data = JSON.parse(dataStr)
+ if (data.error?.message) {
+ errorMessage = data.error.message
+ break
+ } else if (data.message) {
+ errorMessage = data.message
+ break
+ } else if (typeof data.error === 'string') {
+ errorMessage = data.error
+ break
+ }
+ }
+ }
+ }
+ // 如果不是SSE格式,尝试直接解析JSON
+ if (errorMessage === `HTTP ${response.status}`) {
+ const jsonError = JSON.parse(errorBody)
+ errorMessage =
+ jsonError.error?.message || jsonError.message || jsonError.error || errorMessage
+ }
+ } catch {
+ // 解析失败,使用原始错误体或默认消息
+ if (errorBody && errorBody.length < 500) {
+ errorMessage = errorBody
+ }
+ }
+
+ res.write(`data: ${JSON.stringify({ type: 'error', error: errorMessage })}\n\n`)
+ res.write(
+ `data: ${JSON.stringify({ type: 'test_complete', success: false, error: errorMessage })}\n\n`
+ )
+ res.end()
+ return
+ }
+
+ let receivedContent = ''
+ let testSuccess = false
+ let upstreamError = null
+
+ // 处理流式响应
+ response.data.on('data', (chunk) => {
+ const lines = chunk.toString().split('\n')
+
+ for (const line of lines) {
+ if (line.startsWith('data: ')) {
+ const dataStr = line.substring(6).trim()
+ if (dataStr === '[DONE]') {
+ continue
+ }
+
+ try {
+ const data = JSON.parse(dataStr)
+
+ // 检查上游返回的错误事件
+ if (data.type === 'error' || data.error) {
+ let errorMsg = 'Unknown upstream error'
+
+ // 优先从 data.error 提取(如果是对象,获取其 message)
+ if (typeof data.error === 'object' && data.error?.message) {
+ errorMsg = data.error.message
+ } else if (typeof data.error === 'string' && data.error !== 'Claude API error') {
+ // 如果 error 是字符串且不是通用错误,直接使用
+ errorMsg = data.error
+ } else if (data.details) {
+ // 尝试从 details 字段解析详细错误(claudeRelayService 格式)
+ try {
+ const details =
+ typeof data.details === 'string' ? JSON.parse(data.details) : data.details
+ if (details.error?.message) {
+ errorMsg = details.error.message
+ } else if (details.message) {
+ errorMsg = details.message
+ }
+ } catch {
+ // details 不是有效 JSON,尝试直接使用
+ if (typeof data.details === 'string' && data.details.length < 500) {
+ errorMsg = data.details
+ }
+ }
+ } else if (data.message) {
+ errorMsg = data.message
+ }
+
+ // 添加状态码信息(如果有)
+ if (data.status && errorMsg !== 'Unknown upstream error') {
+ errorMsg = `[${data.status}] ${errorMsg}`
+ }
+
+ upstreamError = errorMsg
+ logger.error(`🧪 Upstream error in test for: ${validation.keyData.name}:`, errorMsg)
+ res.write(`data: ${JSON.stringify({ type: 'error', error: errorMsg })}\n\n`)
+ continue
+ }
+
+ // 提取文本内容
+ if (data.type === 'content_block_delta' && data.delta?.text) {
+ receivedContent += data.delta.text
+ res.write(`data: ${JSON.stringify({ type: 'content', text: data.delta.text })}\n\n`)
+ }
+
+ // 消息结束
+ if (data.type === 'message_stop') {
+ testSuccess = true
+ res.write(`data: ${JSON.stringify({ type: 'message_stop' })}\n\n`)
+ }
+ } catch {
+ // 忽略解析错误
+ }
+ }
+ }
+ })
+
+ response.data.on('end', () => {
+ // 如果有上游错误,标记为失败
+ if (upstreamError) {
+ testSuccess = false
+ }
+
+ logger.api(
+ `🧪 API Key test completed for: ${validation.keyData.name}, success: ${testSuccess}, content length: ${receivedContent.length}${upstreamError ? `, error: ${upstreamError}` : ''}`
+ )
+ res.write(
+ `data: ${JSON.stringify({
+ type: 'test_complete',
+ success: testSuccess,
+ contentLength: receivedContent.length,
+ error: upstreamError || undefined
+ })}\n\n`
+ )
+ res.end()
+ })
+
+ response.data.on('error', (err) => {
+ logger.error(`🧪 API Key test stream error for: ${validation.keyData.name}`, err)
+
+ // 如果已经捕获了上游错误,优先使用那个
+ let errorMsg = upstreamError || err.message || 'Stream error'
+
+ // 如果错误消息是通用的 "Claude API error: xxx",提供更友好的提示
+ if (errorMsg.startsWith('Claude API error:') && upstreamError) {
+ errorMsg = upstreamError
+ }
+
+ res.write(`data: ${JSON.stringify({ type: 'error', error: errorMsg })}\n\n`)
+ res.write(
+ `data: ${JSON.stringify({ type: 'test_complete', success: false, error: errorMsg })}\n\n`
+ )
+ res.end()
+ })
+
+ // 处理客户端断开连接
+ req.on('close', () => {
+ if (!res.writableEnded) {
+ response.data.destroy()
+ }
+ })
+ } catch (error) {
+ logger.error('❌ API Key test failed:', error)
+
+ // 如果还未发送响应头,返回JSON错误
+ if (!res.headersSent) {
+ return res.status(500).json({
+ error: 'Test failed',
+ message: error.response?.data?.error?.message || error.message || 'Internal server error'
+ })
+ }
+
+ // 如果已经是SSE流,发送错误事件
+ res.write(
+ `data: ${JSON.stringify({ type: 'error', error: error.response?.data?.error?.message || error.message || 'Test failed' })}\n\n`
+ )
+ res.end()
+ }
+})
+
// 📊 用户模型统计查询接口 - 安全的自查询接口
router.post('/api/user-model-stats', async (req, res) => {
try {
diff --git a/web/admin-spa/src/components/apikeys/ApiKeyTestModal.vue b/web/admin-spa/src/components/apikeys/ApiKeyTestModal.vue
new file mode 100644
index 00000000..281283ab
--- /dev/null
+++ b/web/admin-spa/src/components/apikeys/ApiKeyTestModal.vue
@@ -0,0 +1,496 @@
+
+
+ {{ displayName }}
+
+ 测试将使用此 API Key 调用当前服务的 /api 端点
+ {{ statusTitle }} {{ statusDescription }}
+ {{ responseText }}
+
+
+
+ 等待响应中...
+
+ {{ errorMessage }}
+
+ API Key 端点测试
+
+