Files
antiwear-gateway/test.html
2026-04-22 16:49:18 +08:00

453 lines
13 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Dify 中转服务 — 测试面板</title>
<style>
:root {
--bg: #0f1117;
--surface: #1a1d27;
--border: #2a2d3a;
--primary: #6366f1;
--primary-hover: #4f46e5;
--accent: #22d3ee;
--text: #e2e8f0;
--muted: #64748b;
--success: #22c55e;
--error: #ef4444;
--warning: #f59e0b;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Inter', 'PingFang SC', 'Microsoft YaHei', sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.container {
width: 100%;
max-width: 720px;
}
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 16px;
padding: 32px;
margin-bottom: 20px;
}
.header {
text-align: center;
margin-bottom: 32px;
}
.header h1 {
font-size: 1.5rem;
font-weight: 700;
background: linear-gradient(135deg, var(--primary), var(--accent));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 6px;
}
.header p {
font-size: 0.85rem;
color: var(--muted);
}
.field {
margin-bottom: 20px;
}
.field label {
display: block;
font-size: 0.8rem;
font-weight: 600;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 8px;
}
.field input, .field textarea {
width: 100%;
background: #111827;
border: 1px solid var(--border);
border-radius: 10px;
color: var(--text);
font-size: 0.9rem;
padding: 10px 14px;
transition: border-color 0.2s;
outline: none;
}
.field input:focus, .field textarea:focus {
border-color: var(--primary);
}
.field textarea {
resize: vertical;
min-height: 120px;
font-family: 'Fira Code', 'Consolas', monospace;
}
.field small {
display: block;
color: var(--muted);
font-size: 0.75rem;
margin-top: 6px;
}
.row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.btn-row {
display: flex;
gap: 12px;
margin-top: 24px;
}
.btn {
flex: 1;
padding: 11px 20px;
border: none;
border-radius: 10px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary {
background: var(--primary);
color: #fff;
}
.btn-primary:hover { background: var(--primary-hover); }
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-ghost {
background: transparent;
border: 1px solid var(--border);
color: var(--muted);
}
.btn-ghost:hover { border-color: var(--primary); color: var(--text); }
/* Status badge */
.status-bar {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
background: #111827;
border: 1px solid var(--border);
border-radius: 10px;
margin-bottom: 20px;
font-size: 0.85rem;
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.dot.ok { background: var(--success); box-shadow: 0 0 6px var(--success); }
.dot.err { background: var(--error); box-shadow: 0 0 6px var(--error); }
.dot.idle { background: var(--muted); }
/* Output */
#output {
background: #0a0d14;
border: 1px solid var(--border);
border-radius: 12px;
padding: 16px;
font-family: 'Fira Code', 'Consolas', monospace;
font-size: 0.8rem;
line-height: 1.7;
color: #a5b4fc;
white-space: pre-wrap;
word-break: break-all;
max-height: 400px;
overflow-y: auto;
min-height: 80px;
}
#output::-webkit-scrollbar { width: 6px; }
#output::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
.error-text { color: #fca5a5; }
.success-text { color: #86efac; }
.warn-text { color: #fcd34d; }
/* Streaming indicator */
.streaming-indicator {
display: none;
align-items: center;
gap: 8px;
color: var(--accent);
font-size: 0.85rem;
margin-bottom: 12px;
}
.spinner {
width: 14px;
height: 14px;
border: 2px solid var(--border);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.section-title {
font-size: 0.75rem;
font-weight: 700;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: 12px;
}
</style>
</head>
<body>
<div class="container">
<div class="card">
<div class="header">
<h1>数字人中转服务 — 测试面板</h1>
<p>快速验证 digital-human 与 Dify 的连通性</p>
</div>
<!-- Status -->
<div class="status-bar">
<div class="dot idle" id="statusDot"></div>
<span id="statusText">尚未测试</span>
</div>
<div class="section-title">中转服务配置</div>
<div class="field">
<label>中转服务地址</label>
<input type="text" id="relayUrl" placeholder="http://你的服务器IP:8000" value="http://你的服务器IP:8000" />
<small>填写 digital-human 中转服务的根地址,末尾不要加斜杠</small>
</div>
<div class="field">
<label>Session ID</label>
<input type="text" id="sessionId" placeholder="留空则自动生成" />
<small>用于多轮对话上下文,不填会自动生成</small>
</div>
<div class="row">
<div class="field">
<label>App ID</label>
<input type="text" id="appId" placeholder="app-xxxxxxxx" />
</div>
<div class="field">
<label>User</label>
<input type="text" id="user" placeholder="user-xxx" />
</div>
</div>
<div class="field">
<label>测试消息</label>
<textarea id="message" placeholder="请输入你想问的问题...">你好,请用一句话介绍自己。</textarea>
</div>
<div class="field">
<label>
<input type="checkbox" id="streamMode" checked style="width:auto;margin-right:6px;" />
流式传输(Streaming)
</label>
</div>
<div class="btn-row">
<button class="btn btn-primary" id="sendBtn" onclick="sendMessage()">发送请求</button>
</div>
</div>
<div class="card">
<div class="section-title">响应输出</div>
<div class="streaming-indicator" id="streamingIndicator">
<div class="spinner"></div>
<span>AI 正在生成回复…</span>
</div>
<div id="output">// 响应结果将在此显示</div>
</div>
</div>
<script>
// ── State ──────────────────────────────────────────────────
let abortController = null;
// ── Helpers ────────────────────────────────────────────────
const relayUrl = () => document.getElementById('relayUrl').value.replace(/\/$/, '');
const sessionId = () => document.getElementById('sessionId').value.trim() || crypto.randomUUID();
const appId = () => document.getElementById('appId').value.trim();
const user = () => document.getElementById('user').value.trim() || 'test-user';
const message = () => document.getElementById('message').value.trim();
const isStream = () => document.getElementById('streamMode').checked;
function setStatus(type, text) {
const dot = document.getElementById('statusDot');
const span = document.getElementById('statusText');
dot.className = `dot ${type}`;
span.textContent = text;
}
function append(text, cls = '') {
const el = document.getElementById('output');
if (cls) {
const span = document.createElement('span');
span.className = cls;
span.textContent = text;
el.appendChild(span);
} else {
el.appendChild(document.createTextNode(text));
}
el.scrollTop = el.scrollHeight;
}
function clearOutput() {
document.getElementById('output').innerHTML = '';
}
function setLoading(loading) {
const btn = document.getElementById('sendBtn');
btn.disabled = loading;
document.getElementById('streamingIndicator').style.display = loading ? 'flex' : 'none';
}
// ── Send Message ───────────────────────────────────────────
async function sendMessage() {
const url = relayUrl();
if (!url) { alert('请填写"中转服务地址"'); return; }
const msg = message();
if (!msg) { alert('请填写"测试消息"'); return; }
// abort previous
if (abortController) abortController.abort();
abortController = new AbortController();
clearOutput();
setLoading(true);
setStatus('idle', '请求中…');
const payload = {
messages: [{ content: msg }],
app_id: appId() || 'test-app',
user: user(),
session_id: sessionId(),
is_stream: isStream(),
};
append(`POST ${url}/digital-human\n\n`);
append(`Request:\n${JSON.stringify(payload, null, 2)}\n\n`);
append(`Response:\n`);
try {
if (isStream()) {
// ── Streaming ───────────────────────────────────────
const endpoint = `${url}/digital-human`;
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
signal: abortController.signal,
});
if (!res.ok) {
const errText = await res.text();
append(`[${res.status}] ERROR\n${errText}`, 'error-text');
setStatus('err', `HTTP ${res.status}`);
return;
}
const reader = res.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
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) {
const trimmed = line.trim();
if (!trimmed || trimmed === 'data: [DONE]' || trimmed === 'data:') continue;
if (trimmed.startsWith('data: ')) {
try {
const raw = trimmed.slice(6);
const chunk = JSON.parse(raw);
const content = chunk?.choices?.[0]?.message?.content || '';
if (content) append(content, 'success-text');
} catch { /* skip malformed */ }
}
}
}
append('\n\n[DONE]', 'warn-text');
setStatus('ok', '流式响应完成');
} else {
// ── Non-Streaming ────────────────────────────────────
const endpoint = `${url}/digital-human`;
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
signal: abortController.signal,
});
const raw = await res.text();
let data;
try { data = JSON.parse(raw); } catch { data = raw; }
append(`[${res.status}] `, res.ok ? 'success-text' : 'error-text');
append(`${res.ok ? 'OK' : 'ERROR'}\n\n`, res.ok ? 'success-text' : 'error-text');
if (res.ok && data.choices) {
const reply = data.choices[0]?.message?.content || '';
append(`回复内容:\n${reply}`, 'success-text');
setStatus('ok', '请求成功');
} else {
append(`原始响应:\n${typeof data === 'string' ? data : JSON.stringify(data, null, 2)}`, 'error-text');
setStatus('err', `HTTP ${res.status}`);
}
}
} catch (e) {
if (e.name === 'AbortError') {
append('请求已取消', 'warn-text');
setStatus('idle', '已取消');
} else {
append(`网络错误:${e.message}`, 'error-text');
setStatus('err', '网络错误');
}
} finally {
setLoading(false);
}
}
</script>
</body>
</html>