Download static/api.html from xcolorday/cror: direct link, hf CLI and curl.
- Browser
- Download file 56.7 kB
-
https://huggingface.co/spaces/xcolorday/cror/resolve/main/static/api.html
- Command line
-
hf download hf://spaces/xcolorday/cror/static/api.html
-
curl -L -o api.html https://huggingface.co/spaces/xcolorday/cror/resolve/main/static/api.html
56.7 kB
| <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> |