cror / static /api.html
xcolorday's picture
Upload 442 files
6b356c4 verified
Raw
History Blame Contribute Delete
56.7 kB
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<link rel="icon" type="image/x-icon" href="data:image/x-icon;,">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API 测试工具</title>
<link rel="stylesheet" href="/static/shared-styles.css">
<script src="/static/shared.js"></script>
<style>
/* 顶部工具栏样式 */
.header-toolbar {
display: flex;
align-items: center;
gap: 20px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.auth-section {
flex: 1;
min-width: 300px;
display: flex;
align-items: center;
gap: 10px;
}
.auth-section input {
flex: 1;
}
.server-status {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--text-secondary);
}
.status-indicator.healthy {
background: var(--success-color);
animation: pulse 2s infinite;
}
.status-indicator.error {
background: var(--error-color);
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.quick-tools {
display: flex;
gap: 8px;
}
.quick-tools button {
padding: 6px 12px;
font-size: 14px;
min-height: 32px;
}
/* Tab样式 */
.tabs {
display: flex;
border-bottom: 2px solid var(--border-color);
margin-bottom: 20px;
}
.tab {
padding: 12px 24px;
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
font-size: 16px;
transition: all var(--transition-fast);
position: relative;
}
.tab:hover {
color: var(--text-primary);
background: var(--primary-color-alpha);
}
.tab.active {
color: var(--text-primary);
font-weight: 600;
background: var(--primary-color-alpha);
}
.tab.active::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
right: 0;
height: 3px;
background: var(--primary-color);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
/* Chat Completions样式 */
.chat-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
height: 600px;
}
.chat-container.playground-mode {
grid-template-columns: 1fr;
}
.chat-container.playground-mode .chat-right {
display: none;
}
.chat-left,
.chat-right {
display: flex;
flex-direction: column;
background: var(--card-background);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
padding: 20px;
overflow: hidden;
}
.mode-switcher {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.mode-switcher button {
padding: 6px 16px;
font-size: 14px;
min-height: 32px;
}
/* Raw模式样式 */
.raw-request {
flex: 1;
display: flex;
flex-direction: column;
}
.model-selector {
position: relative;
margin-bottom: 16px;
}
.model-input {
width: 100%;
padding-right: 30px;
}
.model-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
max-height: 200px;
overflow-y: auto;
background: var(--card-background);
border: 1px solid var(--border-color);
border-top: none;
border-radius: 0 0 4px 4px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
z-index: 100;
display: none;
}
.model-dropdown.show {
display: block;
}
.model-option {
padding: 8px 12px;
cursor: pointer;
transition: background var(--transition-fast);
}
.model-option:hover {
background: var(--primary-color-alpha);
}
.model-option.selected {
background: var(--primary-color);
color: white;
}
.json-editor {
flex: 1;
font-family: 'Monaco', 'Menlo', 'Consolas', 'Courier New', monospace;
font-size: 14px;
line-height: 1.5;
tab-size: 2;
white-space: pre;
overflow: auto;
resize: none;
transition: border-color var(--transition-fast), background-color var(--transition-fast);
}
.json-editor.valid {
border-color: var(--success-color);
background-color: rgba(40, 167, 69, 0.05);
}
.json-editor.invalid {
border-color: var(--error-color);
background-color: rgba(220, 53, 69, 0.05);
}
.request-options {
display: flex;
align-items: center;
gap: 20px;
margin-top: 16px;
}
.stream-toggle {
display: flex;
align-items: center;
gap: 8px;
}
.add-response-btn {
padding: 4px 12px;
font-size: 12px;
min-height: 24px;
margin-left: auto;
}
/* Playground模式样式 */
.playground-container {
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.system-prompt {
margin-bottom: 16px;
flex-shrink: 0;
}
.system-prompt textarea {
min-height: 80px;
font-size: 14px;
}
.conversation-history {
flex: 1;
overflow-y: auto;
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 16px;
margin-bottom: 16px;
background: var(--background-color);
min-height: 0;
}
.message {
margin-bottom: 16px;
padding: 12px 16px;
border-radius: 8px;
animation: none;
width: fit-content;
max-width: 70%;
min-width: 80px;
word-wrap: break-word;
word-break: break-word;
overflow-wrap: break-word;
}
.message.user {
background: var(--primary-color-alpha);
margin-left: auto;
margin-right: 0;
}
.message.assistant {
background: var(--card-background);
border: 1px solid var(--border-color);
margin-left: 0;
margin-right: auto;
}
/* Playground模式下的消息样式调整 */
.chat-container.playground-mode .message {
max-width: 80%;
min-width: 60px;
}
.chat-container.playground-mode .message.user {
margin-left: auto;
margin-right: 0;
}
.chat-container.playground-mode .message.assistant {
margin-left: 0;
margin-right: auto;
}
.message-role {
font-weight: 500;
margin-bottom: 4px;
font-size: 14px;
color: var(--text-secondary);
}
.message-content {
line-height: 1.6;
white-space: pre-wrap;
word-wrap: break-word;
word-break: break-word;
overflow-wrap: break-word;
}
.user-input-area {
display: flex;
gap: 10px;
flex-shrink: 0;
}
.user-input {
flex: 1;
min-height: 60px;
max-height: 150px;
resize: vertical;
}
/* 响应区域样式 */
.response-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border-color);
}
.response-status {
display: flex;
align-items: center;
gap: 12px;
font-size: 14px;
}
.status-code {
padding: 2px 8px;
border-radius: 4px;
font-weight: 500;
}
.status-code.success {
background: var(--success-color);
color: white;
}
.status-code.error {
background: var(--error-color);
color: white;
}
.response-time {
color: var(--text-secondary);
}
.response-body {
flex: 1;
overflow-y: auto;
font-family: 'Monaco', 'Menlo', 'Consolas', 'Courier New', monospace;
font-size: 14px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-all;
padding: 16px;
background: var(--background-color);
border-radius: 4px;
}
.stream-response {
padding: 16px;
background: var(--background-color);
border-radius: 4px;
min-height: 100px;
}
/* Models Tab样式 */
.models-container {
max-width: 800px;
margin: 0 auto;
}
.models-params {
background: var(--card-background);
padding: 20px;
border-radius: var(--border-radius);
border: 1px solid var(--border-color);
margin-bottom: 20px;
}
.update-data-section {
display: flex;
align-items: center;
gap: 10px;
padding: 12px;
background: var(--background-color);
border-radius: 4px;
margin-bottom: 16px;
}
.update-data-section label {
font-weight: 500;
margin: 0;
}
.params-section {
opacity: 0.5;
pointer-events: none;
transition: opacity var(--transition-fast);
}
.params-section.enabled {
opacity: 1;
pointer-events: auto;
}
.params-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 16px;
margin-bottom: 20px;
}
.param-item {
display: flex;
align-items: center;
gap: 8px;
}
.additional-models {
margin-top: 16px;
}
.models-table {
width: 100%;
background: var(--card-background);
border-radius: var(--border-radius);
overflow: hidden;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.models-table th {
background: var(--primary-color);
color: white;
padding: 12px;
text-align: left;
font-weight: 500;
position: sticky;
top: 0;
}
.models-table td {
padding: 12px;
border-bottom: 1px solid var(--border-color);
}
.models-table tr:hover {
background: var(--primary-color-alpha);
}
.model-features {
display: flex;
gap: 8px;
}
.feature-badge {
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
background: var(--primary-color-alpha);
color: var(--primary-color);
}
/* 搜索框样式 */
.search-box {
margin-bottom: 16px;
}
.search-box input {
width: 100%;
padding: 10px 16px;
font-size: 14px;
}
/* 底部状态栏 */
.status-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: var(--card-background);
border-top: 1px solid var(--border-color);
padding: 8px 20px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: var(--text-secondary);
}
.status-info {
display: flex;
gap: 20px;
}
/* 响应式设计 */
@media (max-width: 968px) {
.chat-container {
grid-template-columns: 1fr;
height: auto;
}
.chat-left,
.chat-right {
min-height: 400px;
}
.message {
min-width: 50px;
max-width: 90%;
}
.message.user {
margin-left: auto;
margin-right: 10px;
}
.message.assistant {
margin-left: 10px;
margin-right: auto;
}
.header-toolbar {
flex-direction: column;
align-items: stretch;
}
.auth-section {
min-width: 100%;
}
.status-bar {
position: relative;
flex-direction: column;
gap: 8px;
text-align: center;
}
}
/* 加载动画 */
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid var(--border-color);
border-radius: 50%;
border-top-color: var(--primary-color);
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* 工具提示 */
.tooltip {
position: relative;
cursor: help;
}
.tooltip::after {
content: attr(data-tooltip);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
padding: 4px 8px;
background: var(--text-primary);
color: var(--card-background);
font-size: 12px;
border-radius: 4px;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity var(--transition-fast);
}
.tooltip:hover::after {
opacity: 1;
}
/* 复制按钮样式 */
.copy-btn {
position: absolute;
top: 8px;
right: 8px;
padding: 4px 8px;
font-size: 12px;
min-height: 24px;
opacity: 0;
transition: opacity var(--transition-fast);
}
.chat-right:hover .copy-btn,
.response-body:hover .copy-btn {
opacity: 1;
}
/* Token统计样式 */
.token-stats {
padding: 8px 12px;
background: var(--background-color);
border-radius: 4px;
font-size: 14px;
display: flex;
gap: 16px;
}
.token-stat {
display: flex;
align-items: center;
gap: 4px;
}
.token-stat span {
font-weight: 500;
color: var(--primary-color);
}
</style>
</head>
<body>
<!-- 顶部工具栏 -->
<div class="container">
<div class="header-toolbar">
<div class="auth-section">
<label for="authToken" style="margin: 0;">AUTH Token</label>
<input type="password" id="authToken" placeholder="请输入 AUTH Token">
</div>
<div class="server-status">
<span class="status-indicator" id="statusIndicator"></span>
<span id="statusText">检查中...</span>
</div>
<div class="quick-tools">
<button onclick="generateHash()" class="secondary">生成 Hash</button>
<button onclick="generateChecksum()" class="secondary">生成 Checksum</button>
<button onclick="generateTimestampHeader()" class="secondary">生成 TimestampHeader</button>
</div>
</div>
</div>
<!-- 主体内容 -->
<div class="container">
<!-- Tab导航 -->
<div class="tabs">
<button class="tab active" onclick="switchTab('chat')">Chat Completions</button>
<button class="tab" onclick="switchTab('models')">Models</button>
</div>
<!-- Chat Completions Tab -->
<div id="chatTab" class="tab-content active">
<div class="mode-switcher">
<button class="active" onclick="switchChatMode('raw')">Raw 模式</button>
<button onclick="switchChatMode('playground')">Playground 模式</button>
</div>
<div class="chat-container" id="chatContainer">
<!-- 左侧:请求配置 -->
<div class="chat-left">
<!-- Raw模式内容 -->
<div id="rawMode" class="raw-request">
<div class="model-selector">
<label for="modelInput">模型</label>
<input type="text" id="modelInput" class="model-input" placeholder="输入或选择模型..." autocomplete="off">
<div id="modelDropdown" class="model-dropdown"></div>
</div>
<label for="requestBody">请求体 (JSON)</label>
<textarea id="requestBody" class="json-editor" placeholder='{
"messages": [
{
"role": "user",
"content": "Hello!"
}
]
}'></textarea>
<div class="request-options">
<div class="stream-toggle">
<label for="streamMode">响应模式</label>
<select id="streamMode">
<option value="normal">普通响应</option>
<option value="stream">流式响应</option>
<option value="stream-usage">流式响应 + Usage</option>
</select>
</div>
<button onclick="sendRequest()" id="sendBtn">
发送请求
<span class="tooltip" data-tooltip="Ctrl + Enter"></span>
</button>
<button class="add-response-btn secondary" onclick="addResponseToRequest()" id="addResponseBtn"
style="display: none;">
添加响应到请求
</button>
</div>
</div>
<!-- Playground模式内容 -->
<div id="playgroundMode" class="playground-container" style="display: none;">
<div class="system-prompt">
<label for="systemPrompt">系统提示词</label>
<textarea id="systemPrompt" placeholder="你是一个有用的助手..."></textarea>
</div>
<div class="conversation-history" id="conversationHistory">
<div style="text-align: center; color: var(--text-secondary);">
对话历史将在这里显示
</div>
</div>
<div class="user-input-area">
<textarea id="userInput" class="user-input" placeholder="输入你的消息..."
onkeydown="handleUserInputKeydown(event)"></textarea>
<button onclick="sendPlaygroundMessage()" id="playgroundSendBtn">发送</button>
</div>
</div>
</div>
<!-- 右侧:响应显示 -->
<div class="chat-right">
<div class="response-header">
<div class="response-status">
<span id="responseStatus">等待请求...</span>
<span class="response-time" id="responseTime"></span>
</div>
<button class="copy-btn secondary" onclick="copyResponse()">复制</button>
</div>
<div id="responseBody" class="response-body">
<div style="text-align: center; color: var(--text-secondary); padding: 40px;">
响应内容将在这里显示
</div>
</div>
<div id="tokenStats" class="token-stats" style="display: none;">
<div class="token-stat">
提示词: <span id="promptTokens">0</span>
</div>
<div class="token-stat">
完成: <span id="completionTokens">0</span>
</div>
<div class="token-stat">
总计: <span id="totalTokens">0</span>
</div>
</div>
</div>
</div>
</div>
<!-- Models Tab -->
<div id="modelsTab" class="tab-content">
<div class="models-container">
<div class="models-params">
<div class="update-data-section">
<input type="checkbox" id="updateData" onchange="toggleUpdateData()">
<label for="updateData">更新数据(需要 AUTH Token)</label>
</div>
<div id="paramsSection" class="params-section">
<h3>请求参数</h3>
<div class="params-grid">
<div class="param-item">
<input type="checkbox" id="isNightly">
<label for="isNightly">包含 Nightly 版本</label>
</div>
<div class="param-item">
<input type="checkbox" id="includeLongContext">
<label for="includeLongContext">包含长上下文模型</label>
</div>
<div class="param-item">
<input type="checkbox" id="excludeMaxNamed" checked>
<label for="excludeMaxNamed">排除 Max 命名模型</label>
</div>
</div>
<div class="additional-models">
<label for="additionalModels">额外模型名称(逗号分隔)</label>
<input type="text" id="additionalModels" placeholder="model1, model2, model3">
</div>
</div>
<div class="button-group" style="margin-top: 20px;">
<button onclick="fetchModels()">获取模型列表</button>
</div>
</div>
<div class="search-box">
<input type="text" id="modelSearch" placeholder="搜索模型..." oninput="filterModels()">
</div>
<div id="modelsTableContainer"></div>
</div>
</div>
</div>
<!-- 底部状态栏 -->
<div class="status-bar">
<div class="status-info">
<span id="versionInfo">版本: -</span>
<span id="uptimeInfo">运行时间: -</span>
<span id="lastRequestTime">最后请求: -</span>
</div>
<div>
<span id="requestCount">总请求数: 0</span>
</div>
</div>
<script>
// 全局变量
let globalModels = [];
let currentMode = 'raw';
let currentTab = 'chat';
let conversationHistory = [];
let isStreaming = false;
let abortController = null;
let requestCount = 0;
let lastResponseData = null;
let responseAddedToRequest = false;
// 初始化
document.addEventListener('DOMContentLoaded', () => {
// 初始化Token处理
initializeTokenHandling('authToken');
// 检查服务器状态
checkServerHealth();
setInterval(checkServerHealth, 300000); // 每5分钟检查一次
// 初始化模型自动补全
initializeModelAutocomplete();
// 绑定快捷键
bindShortcuts();
// 从localStorage恢复数据
restoreSessionData();
// 初始化JSON编辑器验证
initializeJsonValidation();
});
// 检查服务器健康状态
async function checkServerHealth() {
const indicator = document.getElementById('statusIndicator');
const statusText = document.getElementById('statusText');
try {
const token = document.getElementById('authToken').value;
const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
// 添加超时控制
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
const response = await fetch('/health', {
headers,
signal: controller.signal
});
clearTimeout(timeoutId);
const data = await response.json();
if (data.status === 'success') {
indicator.classList.add('healthy');
indicator.classList.remove('error');
statusText.textContent = 'Healthy';
// 更新版本和运行时间,使用可选链(?.)和空值合并(??)来防止错误
const version = data.service?.version ?? '未知';
document.getElementById('versionInfo').textContent = `版本: ${version}`;
const uptimeSeconds = data.runtime?.uptime_seconds;
if (typeof uptimeSeconds === 'number') {
document.getElementById('uptimeInfo').textContent = formatUptime(uptimeSeconds);
} else {
document.getElementById('uptimeInfo').textContent = '运行时间: 未知';
}
// 更新全局模型列表,如果不存在则默认为空数组
globalModels = data.capabilities?.models || [];
updateModelDropdown();
// 更新请求统计,安全地访问嵌套属性
const totalRequests = data.runtime?.requests?.total;
if (typeof totalRequests === 'number') {
document.getElementById('requestCount').textContent =
`总请求数: ${totalRequests}`;
} else {
document.getElementById('requestCount').textContent = '总请求数: 未知';
}
} else {
throw new Error('Server not healthy');
}
} catch (error) {
indicator.classList.add('error');
indicator.classList.remove('healthy');
statusText.textContent = 'Error';
console.error('Health check failed:', error);
}
}
// 格式化运行时间
function formatUptime(seconds) {
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (days > 0) {
return `运行时间: ${days}${hours}${minutes}分`;
} else if (hours > 0) {
return `运行时间: ${hours}${minutes}分`;
} else {
return `运行时间: ${minutes}分`;
}
}
// Tab切换
function switchTab(tab) {
currentTab = tab;
// 更新Tab样式
document.querySelectorAll('.tab').forEach(t => {
t.classList.remove('active');
});
event.target.classList.add('active');
// 切换内容
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
document.getElementById(`${tab}Tab`).classList.add('active');
}
// Chat模式切换
function switchChatMode(mode) {
currentMode = mode;
// 更新按钮样式
document.querySelectorAll('.mode-switcher button').forEach(btn => {
btn.classList.remove('active');
});
event.target.classList.add('active');
// 切换内容
const chatContainer = document.getElementById('chatContainer');
if (mode === 'raw') {
document.getElementById('rawMode').style.display = 'flex';
document.getElementById('playgroundMode').style.display = 'none';
chatContainer.classList.remove('playground-mode');
} else {
document.getElementById('rawMode').style.display = 'none';
document.getElementById('playgroundMode').style.display = 'flex';
chatContainer.classList.add('playground-mode');
updateConversationDisplay();
}
// 同步数据
syncModeData();
}
// 初始化模型自动补全
function initializeModelAutocomplete() {
const modelInput = document.getElementById('modelInput');
const dropdown = document.getElementById('modelDropdown');
modelInput.addEventListener('input', (e) => {
const value = e.target.value.toLowerCase();
updateModelDropdown(value);
});
modelInput.addEventListener('focus', () => {
updateModelDropdown(modelInput.value.toLowerCase());
});
// 点击其他地方关闭下拉框
document.addEventListener('click', (e) => {
if (!e.target.closest('.model-selector')) {
dropdown.classList.remove('show');
}
});
}
// 更新模型下拉列表
function updateModelDropdown(filter = '') {
const dropdown = document.getElementById('modelDropdown');
dropdown.innerHTML = '';
const filteredModels = globalModels.filter(model =>
model.toLowerCase().includes(filter)
);
if (filteredModels.length === 0) {
dropdown.classList.remove('show');
return;
}
filteredModels.forEach(model => {
const option = document.createElement('div');
option.className = 'model-option';
option.textContent = model;
option.onclick = () => selectModel(model);
dropdown.appendChild(option);
});
dropdown.classList.add('show');
}
// 选择模型
function selectModel(model) {
document.getElementById('modelInput').value = model;
document.getElementById('modelDropdown').classList.remove('show');
// 如果在Raw模式,更新请求体中的模型
if (currentMode === 'raw') {
try {
const requestBody = document.getElementById('requestBody');
const json = JSON.parse(requestBody.value || '{}');
json.model = model;
requestBody.value = JSON.stringify(json, null, 2);
} catch (e) {
// 忽略JSON解析错误
}
}
}
// 发送请求(Raw模式)
async function sendRequest() {
const token = document.getElementById('authToken').value;
if (!token) {
showGlobalMessage('请输入 AUTH Token', true);
return;
}
const modelInput = document.getElementById('modelInput').value;
const requestBodyStr = document.getElementById('requestBody').value;
const streamMode = document.getElementById('streamMode').value;
try {
// 解析并合并请求体
let requestBody = JSON.parse(requestBodyStr || '{}');
if (modelInput) {
requestBody.model = modelInput;
}
// 设置流式选项
if (streamMode === 'stream') {
requestBody.stream = true;
} else if (streamMode === 'stream-usage') {
requestBody.stream = true;
requestBody.stream_options = {
include_usage: true
};
} else {
requestBody.stream = false;
}
// 更新UI
const sendBtn = document.getElementById('sendBtn');
sendBtn.disabled = true;
sendBtn.textContent = streamMode !== 'normal' ? '停止' : '发送中...';
document.getElementById('responseStatus').innerHTML =
'<span class="loading"></span> 请求中...';
document.getElementById('responseTime').textContent = '';
document.getElementById('responseBody').textContent = '';
document.getElementById('tokenStats').style.display = 'none';
const startTime = Date.now();
// 重置响应添加状态
responseAddedToRequest = false;
lastResponseData = null;
document.getElementById('addResponseBtn').style.display = 'none';
if (streamMode !== 'normal') {
await handleStreamRequest(requestBody, token);
} else {
await handleNormalRequest(requestBody, token);
}
const endTime = Date.now();
document.getElementById('responseTime').textContent =
`${endTime - startTime}ms`;
// 更新最后请求时间
updateLastRequestTime();
requestCount++;
} catch (error) {
console.error('Request error:', error);
document.getElementById('responseStatus').innerHTML =
'<span class="status-code error">错误</span>';
document.getElementById('responseBody').textContent =
error.message || '请求失败';
} finally {
const sendBtn = document.getElementById('sendBtn');
sendBtn.disabled = false;
sendBtn.textContent = '发送请求';
}
}
// 处理普通请求
async function handleNormalRequest(requestBody, token) {
const response = await fetch('/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
});
// 获取原始响应文本
const responseText = await response.text();
let data;
try {
data = JSON.parse(responseText);
} catch (e) {
data = { error: 'Failed to parse response' };
}
if (response.ok) {
document.getElementById('responseStatus').innerHTML =
'<span class="status-code success">200 OK</span>';
// 在Raw模式下显示原始响应文本,否则显示格式化的JSON
document.getElementById('responseBody').textContent = responseText;
// 保存响应数据
lastResponseData = data;
document.getElementById('addResponseBtn').style.display = 'inline-block';
// 显示Token统计
if (data.usage) {
document.getElementById('tokenStats').style.display = 'flex';
document.getElementById('promptTokens').textContent = data.usage.prompt_tokens || 0;
document.getElementById('completionTokens').textContent = data.usage.completion_tokens || 0;
document.getElementById('totalTokens').textContent = data.usage.total_tokens || 0;
}
// 同步到conversation history
if (requestBody.messages) {
conversationHistory = requestBody.messages;
if (data.choices && data.choices[0]) {
conversationHistory.push(data.choices[0].message);
}
}
} else {
document.getElementById('responseStatus').innerHTML =
`<span class="status-code error">${response.status} ${response.statusText}</span>`;
// 在Raw模式下显示原始响应文本,否则显示格式化的JSON
document.getElementById('responseBody').textContent = responseText;
}
}
// 处理流式请求
async function handleStreamRequest(requestBody, token) {
isStreaming = true;
abortController = new AbortController();
const response = await fetch('/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody),
signal: abortController.signal
});
if (!response.ok) {
const error = await response.json();
document.getElementById('responseStatus').innerHTML =
`<span class="status-code error">${response.status} ${response.statusText}</span>`;
document.getElementById('responseBody').textContent =
JSON.stringify(error, null, 2);
return;
}
document.getElementById('responseStatus').innerHTML =
'<span class="status-code success">200 OK</span> (流式)';
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let fullContent = '';
let rawData = ''; // 用于存储原始数据
let responseContainer = document.getElementById('responseBody');
responseContainer.textContent = '';
let streamedData = [];
let finalUsage = null;
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
// 在Raw模式下,直接显示原始数据
rawData += line + '\n';
responseContainer.textContent = rawData;
const data = line.slice(6);
if (data === '[DONE]') {
isStreaming = false;
return;
}
try {
const json = JSON.parse(data);
// 保存流式数据
streamedData.push(json);
if (json.choices && json.choices[0] && json.choices[0].delta) {
const content = json.choices[0].delta.content || '';
fullContent += content;
}
// 检查是否有usage信息
if (json.usage) {
finalUsage = json.usage;
document.getElementById('tokenStats').style.display = 'flex';
document.getElementById('promptTokens').textContent = json.usage.prompt_tokens || 0;
document.getElementById('completionTokens').textContent = json.usage.completion_tokens || 0;
document.getElementById('totalTokens').textContent = json.usage.total_tokens || 0;
}
} catch (e) {
console.error('Parse error:', e);
}
// 自动滚动到底部
responseContainer.scrollTop = responseContainer.scrollHeight;
}
}
}
} catch (error) {
if (error.name === 'AbortError') {
responseContainer.textContent += '\n\n[请求已取消]';
} else {
throw error;
}
} finally {
isStreaming = false;
// 构建完整的响应对象
if (streamedData.length > 0) {
const firstChunk = streamedData[0];
lastResponseData = {
id: firstChunk.id,
object: 'chat.completion',
created: firstChunk.created,
model: firstChunk.model,
choices: [{
index: 0,
message: {
role: 'assistant',
content: fullContent
},
finish_reason: 'stop'
}]
};
if (finalUsage) {
lastResponseData.usage = finalUsage;
}
document.getElementById('addResponseBtn').style.display = 'inline-block';
}
}
}
// Playground模式发送消息
async function sendPlaygroundMessage() {
const token = document.getElementById('authToken').value;
if (!token) {
showGlobalMessage('请输入 AUTH Token', true);
return;
}
const modelInput = document.getElementById('modelInput').value;
const systemPrompt = document.getElementById('systemPrompt').value;
const userInput = document.getElementById('userInput').value.trim();
if (!userInput) {
showGlobalMessage('请输入消息', true);
return;
}
// 构建消息历史
const messages = [];
if (systemPrompt) {
messages.push({ role: 'system', content: systemPrompt });
}
messages.push(...conversationHistory);
messages.push({ role: 'user', content: userInput });
// 添加用户消息到界面
conversationHistory.push({ role: 'user', content: userInput });
updateConversationDisplay();
// 清空输入框
document.getElementById('userInput').value = '';
// 准备请求
const streamMode = document.getElementById('streamMode').value;
const requestBody = {
model: modelInput || 'default',
messages: messages
};
// 设置流式选项
if (streamMode === 'stream') {
requestBody.stream = true;
} else if (streamMode === 'stream-usage') {
requestBody.stream = true;
requestBody.stream_options = {
include_usage: true
};
} else {
requestBody.stream = false;
}
// 更新按钮状态
const sendBtn = document.getElementById('playgroundSendBtn');
sendBtn.disabled = true;
sendBtn.textContent = '发送中...';
try {
const startTime = Date.now();
if (requestBody.stream) {
await handlePlaygroundStream(requestBody, token);
} else {
await handlePlaygroundNormal(requestBody, token);
}
const endTime = Date.now();
document.getElementById('responseTime').textContent =
`${endTime - startTime}ms`;
updateLastRequestTime();
requestCount++;
} catch (error) {
console.error('Playground error:', error);
showGlobalMessage('请求失败: ' + error.message, true);
} finally {
sendBtn.disabled = false;
sendBtn.textContent = '发送';
}
}
// 处理Playground普通请求
async function handlePlaygroundNormal(requestBody, token) {
const response = await fetch('/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
});
const data = await response.json();
if (response.ok && data.choices && data.choices[0]) {
const assistantMessage = data.choices[0].message;
conversationHistory.push(assistantMessage);
updateConversationDisplay();
// 在Playground模式下,不更新右侧响应面板
if (currentMode === 'raw') {
document.getElementById('responseStatus').innerHTML =
'<span class="status-code success">200 OK</span>';
document.getElementById('responseBody').textContent =
JSON.stringify(data, null, 2);
// 显示Token统计
if (data.usage) {
document.getElementById('tokenStats').style.display = 'flex';
document.getElementById('promptTokens').textContent = data.usage.prompt_tokens || 0;
document.getElementById('completionTokens').textContent = data.usage.completion_tokens || 0;
document.getElementById('totalTokens').textContent = data.usage.total_tokens || 0;
}
}
} else {
throw new Error(`${response.status} ${response.statusText}`);
}
}
// 处理Playground流式请求
async function handlePlaygroundStream(requestBody, token) {
// 先添加一个空的助手消息
const assistantMessageIndex = conversationHistory.length;
conversationHistory.push({ role: 'assistant', content: '' });
updateConversationDisplay();
isStreaming = true;
abortController = new AbortController();
const response = await fetch('/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody),
signal: abortController.signal
});
if (!response.ok) {
conversationHistory.pop(); // 移除空消息
throw new Error(`${response.status} ${response.statusText}`);
}
// 在Playground模式下,不更新右侧响应面板
if (currentMode === 'raw') {
document.getElementById('responseStatus').innerHTML =
'<span class="status-code success">200 OK</span> (流式)';
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let fullResponse = '';
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') {
isStreaming = false;
return;
}
try {
const json = JSON.parse(data);
if (json.choices && json.choices[0] && json.choices[0].delta) {
const content = json.choices[0].delta.content || '';
conversationHistory[assistantMessageIndex].content += content;
fullResponse += content;
updateConversationDisplay();
// 在Raw模式下更新响应面板
if (currentMode === 'raw') {
document.getElementById('responseBody').textContent = fullResponse;
}
}
// 在Raw模式下检查usage
if (currentMode === 'raw' && json.usage) {
document.getElementById('tokenStats').style.display = 'flex';
document.getElementById('promptTokens').textContent = json.usage.prompt_tokens || 0;
document.getElementById('completionTokens').textContent = json.usage.completion_tokens || 0;
document.getElementById('totalTokens').textContent = json.usage.total_tokens || 0;
}
} catch (e) {
console.error('Parse error:', e);
}
}
}
}
} catch (error) {
if (error.name === 'AbortError') {
conversationHistory[assistantMessageIndex].content += '\n[请求已取消]';
updateConversationDisplay();
} else {
throw error;
}
} finally {
isStreaming = false;
}
}
// 更新对话显示
function updateConversationDisplay() {
const container = document.getElementById('conversationHistory');
container.innerHTML = '';
if (conversationHistory.length === 0) {
container.innerHTML = `
<div style="text-align: center; color: var(--text-secondary);">
对话历史将在这里显示
</div>
`;
return;
}
conversationHistory.forEach(msg => {
if (msg.role === 'system') return; // 不显示系统消息
const messageDiv = document.createElement('div');
messageDiv.className = `message ${msg.role}`;
const roleDiv = document.createElement('div');
roleDiv.className = 'message-role';
roleDiv.textContent = msg.role === 'user' ? '用户' : '助手';
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
contentDiv.textContent = msg.content;
messageDiv.appendChild(roleDiv);
messageDiv.appendChild(contentDiv);
container.appendChild(messageDiv);
});
// 滚动到底部
container.scrollTop = container.scrollHeight;
}
// 同步模式数据
function syncModeData() {
if (currentMode === 'playground') {
// 从Raw模式同步到Playground
try {
const requestBodyStr = document.getElementById('requestBody').value;
const requestBody = JSON.parse(requestBodyStr || '{}');
if (requestBody.messages) {
conversationHistory = requestBody.messages.filter(m => m.role !== 'system');
const systemMsg = requestBody.messages.find(m => m.role === 'system');
if (systemMsg) {
document.getElementById('systemPrompt').value = systemMsg.content;
}
}
updateConversationDisplay();
} catch (e) {
// 忽略解析错误
}
} else {
// 从Playground同步到Raw模式
const systemPrompt = document.getElementById('systemPrompt').value;
const messages = [];
if (systemPrompt) {
messages.push({ role: 'system', content: systemPrompt });
}
messages.push(...conversationHistory);
const requestBody = {
model: document.getElementById('modelInput').value || 'gpt-4',
messages: messages
};
document.getElementById('requestBody').value = JSON.stringify(requestBody, null, 2);
}
}
// 快捷键处理
function bindShortcuts() {
// Ctrl+Enter 发送请求
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 'Enter') {
e.preventDefault();
if (currentTab === 'chat') {
if (currentMode === 'raw') {
sendRequest();
} else {
sendPlaygroundMessage();
}
}
}
});
}
// 用户输入框快捷键
function handleUserInputKeydown(event) {
if (event.ctrlKey && event.key === 'Enter') {
event.preventDefault();
sendPlaygroundMessage();
}
}
// 复制响应内容
function copyResponse() {
const responseBody = document.getElementById('responseBody').textContent;
copyToClipboard(responseBody, {
successMessage: '响应内容已复制',
errorMessage: '复制失败'
});
}
// 生成工具函数
async function generateHash() {
try {
const response = await fetch('/gen-hash');
const hash = await response.text();
copyToClipboard(hash, {
successMessage: 'Hash已复制到剪贴板'
});
showGlobalMessage(`生成的Hash: ${hash}`);
} catch (error) {
showGlobalMessage('生成Hash失败', true);
}
}
async function generateChecksum() {
try {
const response = await fetch('/gen-checksum');
const checksum = await response.text();
copyToClipboard(checksum, {
successMessage: 'Checksum已复制到剪贴板'
});
showGlobalMessage(`生成的Checksum: ${checksum}`);
} catch (error) {
showGlobalMessage('生成Checksum失败', true);
}
}
async function generateTimestampHeader() {
try {
const response = await fetch('/get-timestamp-header');
const timestampHeader = await response.text();
copyToClipboard(timestampHeader, {
successMessage: 'TimestampHeader已复制到剪贴板'
});
showGlobalMessage(`生成的TimestampHeader: ${timestampHeader}`);
} catch (error) {
showGlobalMessage('生成TimestampHeader失败', true);
}
}
// 切换更新数据选项
function toggleUpdateData() {
const updateData = document.getElementById('updateData').checked;
const paramsSection = document.getElementById('paramsSection');
if (updateData) {
paramsSection.classList.add('enabled');
} else {
paramsSection.classList.remove('enabled');
}
}
// 获取模型列表
async function fetchModels() {
const updateData = document.getElementById('updateData').checked;
const token = document.getElementById('authToken').value;
// 只有在更新数据时才需要token
if (updateData && !token) {
showGlobalMessage('更新数据需要输入 AUTH Token', true);
return;
}
try {
let url = '/v1/models';
const headers = {};
// 只有在更新数据时才添加认证和参数
if (updateData) {
headers['Authorization'] = `Bearer ${token}`;
// 构建查询参数
const queryParams = new URLSearchParams();
const isNightly = document.getElementById('isNightly').checked;
const includeLongContext = document.getElementById('includeLongContext').checked;
const excludeMaxNamed = document.getElementById('excludeMaxNamed').checked;
const additionalModels = document.getElementById('additionalModels').value
.split(',')
.map(s => s.trim())
.filter(s => s);
// 只添加非默认值的参数
if (isNightly) queryParams.append('is_nightly', 'true');
if (includeLongContext) queryParams.append('include_long_context_models', 'true');
if (excludeMaxNamed) queryParams.append('exclude_max_named_models', 'true');
if (additionalModels.length > 0) {
additionalModels.forEach(model => {
queryParams.append('additional_model_names', model);
});
}
const s = queryParams.toString();
if (s) {
url += '?' + s;
}
}
const response = await fetch(url, {
method: 'GET',
headers: headers
});
const data = await response.json();
if (response.ok) {
displayModelsTable(data.data || []);
showGlobalMessage(`获取到 ${data.data.length} 个模型`);
} else {
showGlobalMessage('获取模型列表失败', true);
}
} catch (error) {
showGlobalMessage('请求失败: ' + error.message, true);
}
}
// 显示模型表格
function displayModelsTable(models) {
const container = document.getElementById('modelsTableContainer');
if (models.length === 0) {
container.innerHTML = '<p style="text-align: center; color: var(--text-secondary);">没有找到模型</p>';
return;
}
const table = document.createElement('table');
table.className = 'models-table';
// 表头
const thead = document.createElement('thead');
thead.innerHTML = `
<tr>
<th>模型ID</th>
<th>显示名称</th>
<th>所有者</th>
<th>特性</th>
<th>创建时间</th>
</tr>
`;
table.appendChild(thead);
// 表体
const tbody = document.createElement('tbody');
models.forEach(model => {
const tr = document.createElement('tr');
// 特性徽章
const features = [];
if (model.supports_thinking) features.push('思考');
if (model.supports_images) features.push('图像');
if (model.supports_max_mode) features.push('Max模式');
if (model.supports_non_max_mode) features.push('非Max模式');
tr.innerHTML = `
<td>${model.id}</td>
<td>${model.display_name || model.id}</td>
<td>${model.owned_by || '-'}</td>
<td>
<div class="model-features">
${features.map(f => `<span class="feature-badge">${f}</span>`).join('')}
</div>
</td>
<td>${model.created_at || new Date(model.created * 1000).toLocaleDateString()}</td>
`;
tbody.appendChild(tr);
});
table.appendChild(tbody);
container.innerHTML = '';
container.appendChild(table);
// 保存到全局以供搜索
window.currentModelsData = models;
}
// 搜索模型
function filterModels() {
const searchTerm = document.getElementById('modelSearch').value.toLowerCase();
if (!window.currentModelsData) return;
const filtered = window.currentModelsData.filter(model =>
model.id.toLowerCase().includes(searchTerm) ||
(model.display_name && model.display_name.toLowerCase().includes(searchTerm))
);
displayModelsTable(filtered);
}
// 更新最后请求时间
function updateLastRequestTime() {
const now = new Date();
const timeStr = now.toLocaleTimeString('zh-CN');
document.getElementById('lastRequestTime').textContent = `最后请求: ${timeStr}`;
}
// 保存和恢复会话数据
function saveSessionData() {
const sessionData = {
model: document.getElementById('modelInput').value,
systemPrompt: document.getElementById('systemPrompt').value,
conversationHistory: conversationHistory,
requestBody: document.getElementById('requestBody').value,
streamMode: document.getElementById('streamMode').value
};
localStorage.setItem('apiTestSession', JSON.stringify(sessionData));
}
function restoreSessionData() {
try {
const sessionStr = localStorage.getItem('apiTestSession');
if (!sessionStr) return;
const session = JSON.parse(sessionStr);
if (session.model) {
document.getElementById('modelInput').value = session.model;
}
if (session.systemPrompt) {
document.getElementById('systemPrompt').value = session.systemPrompt;
}
if (session.conversationHistory) {
conversationHistory = session.conversationHistory;
}
if (session.requestBody) {
document.getElementById('requestBody').value = session.requestBody;
}
if (session.streamMode) {
document.getElementById('streamMode').value = session.streamMode;
}
} catch (e) {
console.error('Failed to restore session:', e);
}
}
// 自动保存会话
setInterval(saveSessionData, 30000); // 每30秒保存一次
// 页面卸载时保存
window.addEventListener('beforeunload', saveSessionData);
// 初始化JSON验证
function initializeJsonValidation() {
const jsonEditor = document.getElementById('requestBody');
jsonEditor.addEventListener('input', () => {
try {
JSON.parse(jsonEditor.value || '{}');
jsonEditor.classList.remove('invalid');
jsonEditor.classList.add('valid');
} catch (e) {
jsonEditor.classList.remove('valid');
jsonEditor.classList.add('invalid');
}
});
// 触发初始验证
jsonEditor.dispatchEvent(new Event('input'));
}
// 添加响应到请求
function addResponseToRequest() {
if (!lastResponseData || responseAddedToRequest) {
return;
}
try {
const requestBody = document.getElementById('requestBody');
const currentRequest = JSON.parse(requestBody.value || '{}');
// 添加响应数据到请求
if (!currentRequest.messages) {
currentRequest.messages = [];
}
if (lastResponseData.choices && lastResponseData.choices[0] && lastResponseData.choices[0].message) {
currentRequest.messages.push(lastResponseData.choices[0].message);
}
requestBody.value = JSON.stringify(currentRequest, null, 2);
// 触发验证
requestBody.dispatchEvent(new Event('input'));
// 标记已添加,禁用按钮
responseAddedToRequest = true;
document.getElementById('addResponseBtn').disabled = true;
document.getElementById('addResponseBtn').textContent = '已添加';
showGlobalMessage('响应已添加到请求');
} catch (e) {
showGlobalMessage('添加响应失败: ' + e.message, true);
}
}
</script>
</body>
</html>