feat(api): API 测试 增强为 操练场,支持更多参数配置与调试
- 添加了对多个 API 参数的支持,包括 temperature、top_p、frequency_penalty 等。 - 改进了请求体的构建逻辑,支持更复杂的请求参数。 - 增加了对频道的支持,确保 API 调用时的频道权限验证。 - 优化了消息处理和响应显示,支持流式响应和消息编辑功能。
This commit is contained in:
1 parent
cbe5d9d8f7
commit
fa5fed56d9
9 files changed
+1031
-313
No files matched your search
@@ -38,6 +38,19 @@ type RequestBehavior = {
|
||||
skipUnauthorizedHandler?: boolean
|
||||
}
|
||||
|
||||
type TestApiOptions = {
|
||||
channelKey?: string
|
||||
}
|
||||
|
||||
type TestApiStreamOptions = TestApiOptions & {
|
||||
signal?: AbortSignal
|
||||
onChunk: (chunk: string) => void
|
||||
}
|
||||
|
||||
type TestApiStreamResult = {
|
||||
status: number
|
||||
}
|
||||
|
||||
const isAdminEndpoint = (endpoint: string): boolean => endpoint.startsWith('/api/admin/')
|
||||
|
||||
export function setErrorHandlers(handlers: {
|
||||
@@ -290,15 +303,20 @@ export const apiClient = {
|
||||
),
|
||||
|
||||
// API Test - 使用自定义 token,不走通用拦截器
|
||||
testApi: async (endpoint: string, token: string, body: unknown): Promise<TestResponse> => {
|
||||
testApi: async (endpoint: string, token: string, body: unknown, options: TestApiOptions = {}): Promise<TestResponse> => {
|
||||
const url = `${BASE_URL}${endpoint}`
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
}
|
||||
if (options.channelKey) {
|
||||
headers['x-channel-key'] = options.channelKey
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
credentials: 'omit',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
@@ -320,6 +338,53 @@ export const apiClient = {
|
||||
return responseInterceptor(response)
|
||||
},
|
||||
|
||||
testApiStream: async (endpoint: string, token: string, body: unknown, options: TestApiStreamOptions): Promise<TestApiStreamResult> => {
|
||||
const url = `${BASE_URL}${endpoint}`
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
}
|
||||
if (options.channelKey) {
|
||||
headers['x-channel-key'] = options.channelKey
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
credentials: 'omit',
|
||||
signal: options.signal,
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
await responseInterceptor(response)
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
throw new ApiError('Response stream is not available', response.status)
|
||||
}
|
||||
|
||||
const reader = response.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
options.onChunk(decoder.decode(value, { stream: true }))
|
||||
}
|
||||
|
||||
const tail = decoder.decode()
|
||||
if (tail) {
|
||||
options.onChunk(tail)
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock()
|
||||
}
|
||||
|
||||
return { status: response.status }
|
||||
},
|
||||
|
||||
// Auth check
|
||||
checkAuth: () => request<ApiResponse>('/api/admin/channel', { method: 'GET' }),
|
||||
|
||||
|
||||
@@ -143,8 +143,8 @@ export const AutoCompleteInput = React.forwardRef<HTMLInputElement, AutoComplete
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
onClick={() => handleSelect(option.value)}
|
||||
>
|
||||
<span className="text-sm font-medium">{label}</span>
|
||||
{showDescription && <span className="text-xs text-muted-foreground">{option.description}</span>}
|
||||
<span className="w-full truncate text-sm font-medium">{label}</span>
|
||||
{showDescription && <span className="w-full truncate text-xs text-muted-foreground">{option.description}</span>}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"tokens": "Tokens",
|
||||
"pricing": "Pricing",
|
||||
"usageLogs": "Usage Logs",
|
||||
"apiTest": "API Test",
|
||||
"apiTest": "Playground",
|
||||
"settings": "Settings",
|
||||
"theme": "Theme",
|
||||
"language": "Language",
|
||||
@@ -381,25 +381,50 @@
|
||||
"detailChannel": "Channel"
|
||||
},
|
||||
"apiTest": {
|
||||
"title": "API Test",
|
||||
"description": "Test API connections and configurations",
|
||||
"title": "Playground",
|
||||
"description": "Debug multi-turn conversations with a selected token, model, and channel",
|
||||
"request": "Request",
|
||||
"response": "Response",
|
||||
"endpoint": "Endpoint",
|
||||
"apiToken": "API Token",
|
||||
"model": "Model",
|
||||
"modelPlaceholder": "Select or enter model name",
|
||||
"selectTokenFirst": "Select an API token first",
|
||||
"modelRequired": "Please select a model",
|
||||
"channel": "Channel",
|
||||
"autoChannel": "Auto-select channel",
|
||||
"noMatchingChannels": "No matching channels",
|
||||
"requestBody": "Request Body",
|
||||
"send": "Send Request",
|
||||
"playgroundParams": "Playground Parameters",
|
||||
"temperature": "Temperature",
|
||||
"topP": "Top P",
|
||||
"frequencyPenalty": "Frequency Penalty",
|
||||
"presencePenalty": "Presence Penalty",
|
||||
"stream": "Stream",
|
||||
"maxTokens": "Max Tokens",
|
||||
"seed": "Seed",
|
||||
"paramsUnsupported": "This endpoint does not support visual parameters. You can edit the custom request body directly.",
|
||||
"conversation": "Conversation",
|
||||
"conversationEmpty": "Select an API token and model, then send your first message.",
|
||||
"messagePlaceholder": "Type a message, Ctrl/⌘ + Enter to send",
|
||||
"user": "User",
|
||||
"assistant": "Assistant",
|
||||
"editMessage": "Edit",
|
||||
"retryMessage": "Retry",
|
||||
"viewRawResponse": "Response Body",
|
||||
"rawResponse": "Raw Response Body",
|
||||
"emptyReadableResponse": "The response has no readable text content. Open the raw response body for details.",
|
||||
"responseFailed": "Failed",
|
||||
"streaming": "Streaming",
|
||||
"stopGeneration": "Stop Generation",
|
||||
"generationStopped": "Generation stopped",
|
||||
"send": "Send",
|
||||
"sending": "Sending...",
|
||||
"noMatchingTokens": "No matching tokens",
|
||||
"noMatchingModels": "No matching models",
|
||||
"tokenRequired": "Please enter an API token",
|
||||
"bodyJsonError": "Invalid request body JSON",
|
||||
"copyResponse": "Copy response",
|
||||
"audioSuccess": "Audio response received",
|
||||
"downloadAudio": "Download Audio",
|
||||
"responseEmpty": "Send a request to see the response here"
|
||||
"copyResponse": "Copy response"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"tokens": "令牌管理",
|
||||
"pricing": "定价管理",
|
||||
"usageLogs": "使用日志",
|
||||
"apiTest": "API 测试",
|
||||
"apiTest": "操练场",
|
||||
"settings": "系统设置",
|
||||
"theme": "主题",
|
||||
"language": "语言",
|
||||
@@ -381,25 +381,50 @@
|
||||
"detailChannel": "渠道"
|
||||
},
|
||||
"apiTest": {
|
||||
"title": "API 测试",
|
||||
"description": "测试 API 连接和配置",
|
||||
"title": "操练场",
|
||||
"description": "用指定令牌、模型和渠道进行多轮会话调试",
|
||||
"request": "请求",
|
||||
"response": "响应",
|
||||
"endpoint": "端点",
|
||||
"apiToken": "API 令牌",
|
||||
"model": "模型",
|
||||
"modelPlaceholder": "选择或输入模型名称",
|
||||
"selectTokenFirst": "请先选择 API 令牌",
|
||||
"modelRequired": "请选择模型",
|
||||
"channel": "渠道",
|
||||
"autoChannel": "自动选择渠道",
|
||||
"noMatchingChannels": "没有匹配的渠道",
|
||||
"requestBody": "请求体",
|
||||
"send": "发送请求",
|
||||
"playgroundParams": "操练参数",
|
||||
"temperature": "Temperature",
|
||||
"topP": "Top P",
|
||||
"frequencyPenalty": "Frequency Penalty",
|
||||
"presencePenalty": "Presence Penalty",
|
||||
"stream": "流式输出",
|
||||
"maxTokens": "Max Tokens",
|
||||
"seed": "Seed",
|
||||
"paramsUnsupported": "当前端点不支持可视化调参,可直接编辑自定义请求体。",
|
||||
"conversation": "会话",
|
||||
"conversationEmpty": "选择 API 令牌和模型后,发送第一条消息开始操练。",
|
||||
"messagePlaceholder": "输入消息,Ctrl/⌘ + Enter 发送",
|
||||
"user": "用户",
|
||||
"assistant": "助手",
|
||||
"editMessage": "编辑",
|
||||
"retryMessage": "重发",
|
||||
"viewRawResponse": "响应体",
|
||||
"rawResponse": "真实响应体",
|
||||
"emptyReadableResponse": "响应中没有可直接展示的文字内容,请查看真实响应体。",
|
||||
"responseFailed": "失败",
|
||||
"streaming": "流式输出中",
|
||||
"stopGeneration": "停止生成",
|
||||
"generationStopped": "已停止生成",
|
||||
"send": "发送",
|
||||
"sending": "发送中...",
|
||||
"noMatchingTokens": "没有匹配的令牌",
|
||||
"noMatchingModels": "没有匹配的模型",
|
||||
"tokenRequired": "请输入 API 令牌",
|
||||
"bodyJsonError": "请求体 JSON 格式错误",
|
||||
"copyResponse": "复制响应",
|
||||
"audioSuccess": "音频返回成功",
|
||||
"downloadAudio": "下载音频",
|
||||
"responseEmpty": "发送请求后在此查看响应"
|
||||
"copyResponse": "复制响应"
|
||||
},
|
||||
"settings": {
|
||||
"title": "系统设置",
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"tokens": "令牌管理",
|
||||
"pricing": "定價管理",
|
||||
"usageLogs": "使用日誌",
|
||||
"apiTest": "API 測試",
|
||||
"apiTest": "操練場",
|
||||
"settings": "系統設定",
|
||||
"theme": "主題",
|
||||
"language": "語言",
|
||||
@@ -381,25 +381,50 @@
|
||||
"detailChannel": "渠道"
|
||||
},
|
||||
"apiTest": {
|
||||
"title": "API 測試",
|
||||
"description": "測試 API 連接和設定",
|
||||
"title": "操練場",
|
||||
"description": "用指定令牌、模型和渠道進行多輪會話調試",
|
||||
"request": "請求",
|
||||
"response": "回應",
|
||||
"endpoint": "端點",
|
||||
"apiToken": "API 令牌",
|
||||
"model": "模型",
|
||||
"modelPlaceholder": "選擇或輸入模型名稱",
|
||||
"selectTokenFirst": "請先選擇 API 令牌",
|
||||
"modelRequired": "請選擇模型",
|
||||
"channel": "渠道",
|
||||
"autoChannel": "自動選擇渠道",
|
||||
"noMatchingChannels": "沒有匹配的渠道",
|
||||
"requestBody": "請求體",
|
||||
"send": "傳送請求",
|
||||
"playgroundParams": "操練參數",
|
||||
"temperature": "Temperature",
|
||||
"topP": "Top P",
|
||||
"frequencyPenalty": "Frequency Penalty",
|
||||
"presencePenalty": "Presence Penalty",
|
||||
"stream": "串流輸出",
|
||||
"maxTokens": "Max Tokens",
|
||||
"seed": "Seed",
|
||||
"paramsUnsupported": "目前端點不支援視覺化調參,可直接編輯自訂請求體。",
|
||||
"conversation": "會話",
|
||||
"conversationEmpty": "選擇 API 令牌和模型後,傳送第一條訊息開始操練。",
|
||||
"messagePlaceholder": "輸入訊息,Ctrl/⌘ + Enter 傳送",
|
||||
"user": "使用者",
|
||||
"assistant": "助手",
|
||||
"editMessage": "編輯",
|
||||
"retryMessage": "重發",
|
||||
"viewRawResponse": "響應體",
|
||||
"rawResponse": "真實響應體",
|
||||
"emptyReadableResponse": "響應中沒有可直接展示的文字內容,請查看真實響應體。",
|
||||
"responseFailed": "失敗",
|
||||
"streaming": "串流輸出中",
|
||||
"stopGeneration": "停止生成",
|
||||
"generationStopped": "已停止生成",
|
||||
"send": "傳送",
|
||||
"sending": "傳送中...",
|
||||
"noMatchingTokens": "沒有匹配的令牌",
|
||||
"noMatchingModels": "沒有匹配的模型",
|
||||
"tokenRequired": "請輸入 API 令牌",
|
||||
"bodyJsonError": "請求體 JSON 格式錯誤",
|
||||
"copyResponse": "複製回應",
|
||||
"audioSuccess": "音訊返回成功",
|
||||
"downloadAudio": "下載音訊",
|
||||
"responseEmpty": "傳送請求後在此查看回應"
|
||||
"copyResponse": "複製回應"
|
||||
},
|
||||
"settings": {
|
||||
"title": "系統設定",
|
||||
|
||||
@@ -113,18 +113,35 @@ export const getUniqueModelNamesFromChannels = (channels: Channel[]): string[] =
|
||||
return Array.from(modelNames).sort()
|
||||
}
|
||||
|
||||
export const getModelNamesForToken = (tokenKey: string, tokens: Token[], channels: Channel[]): string[] => {
|
||||
export const getChannelsForToken = (tokenKey: string, tokens: Token[], channels: Channel[]): Channel[] => {
|
||||
const matchedToken = tokens.find((token) => token.key === tokenKey)
|
||||
|
||||
if (!matchedToken) {
|
||||
return getUniqueModelNamesFromChannels(channels)
|
||||
return []
|
||||
}
|
||||
|
||||
const tokenConfig = parseTokenConfig(matchedToken)
|
||||
const allowedChannelKeys = tokenConfig.channel_keys || []
|
||||
const targetChannels = allowedChannelKeys.length === 0
|
||||
|
||||
return allowedChannelKeys.length === 0
|
||||
? channels
|
||||
: channels.filter((channel) => allowedChannelKeys.includes(channel.key))
|
||||
}
|
||||
|
||||
export const channelSupportsModel = (channel: Channel, modelName: string): boolean => {
|
||||
if (!modelName) {
|
||||
return true
|
||||
}
|
||||
|
||||
const config = parseChannelConfig(channel)
|
||||
return getChannelModels(config)
|
||||
.filter((model) => isChannelModelEnabled(model))
|
||||
.some((model) => model.name === modelName)
|
||||
}
|
||||
|
||||
export const getModelNamesForChannels = (channels: Channel[]): string[] => getUniqueModelNamesFromChannels(channels)
|
||||
|
||||
export const getModelNamesForToken = (tokenKey: string, tokens: Token[], channels: Channel[]): string[] => {
|
||||
const targetChannels = getChannelsForToken(tokenKey, tokens, channels)
|
||||
return getUniqueModelNamesFromChannels(targetChannels)
|
||||
}
|
||||
+822
-279
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user