Files
cursor-api/static/api.html
2025-08-14 18:21:36 +08:00

1955 lines
55 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>