mirror of
https://github.com/wisdgod/cursor-api.git
synced 2025-09-27 02:56:01 +08:00
1955 lines
55 KiB
HTML
1955 lines
55 KiB
HTML
<!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> |