fix(assistant): 模型测试按钮改用流式累积 + 增强诊断信息

- 测试请求切换到 stream: true + SSE 累积,绕开部分兼容网关
  non-streaming 分支对某些模型返回 200 + 空 body 的已知 bug,
  行为与真实对话路径一致
- 后端 test_model_verbose 显式设置 Accept-Encoding: identity,
  避免压缩协商带来的解码风险
- 用 resp.bytes() + 严格 UTF-8 decode,失败时 fallback 到
  lossy 字符串 + 前 200 字节 hex dump,方便定位非 UTF-8 响应
- 展开 reqwest error source 链,响应头与字节数原样返回前端
- 前端结果面板突出显示完整模型回复、固定 prompt 标注、
  响应头与 raw bytes hex,方便用户自查上游问题
- scripts/dev-api.js 同步 Rust 后端行为,保证 Web/桌面两侧诊断一致
This commit is contained in:
晴天
2026-04-20 15:36:09 +08:00
parent b1902baa13
commit 12cdc72d2b
3 changed files with 342 additions and 85 deletions

View File

@@ -2936,7 +2936,7 @@ function renderMessages() {
function _linkify(str) { return str.replace(/(https?:\/\/[^\s,,。;))'"]+)/g, '<a href="$1" target="_blank" style="color:var(--primary)">$1</a>') }
function buildTestResult({ success, elapsed, usedApi, reqUrl, reqBody, respStatus, respBody, reply, error }) {
function buildTestResult({ success, elapsed, usedApi, reqUrl, reqBody, respStatus, respHeaders, respBody, respRawHex, respByteCount, reply, error }) {
let html = ''
// 尝试解析 API 返回的错误信息
let apiErrMsg = ''
@@ -2958,6 +2958,17 @@ function buildTestResult({ success, elapsed, usedApi, reqUrl, reqBody, respStatu
if (apiErrMsg) {
html += `<div style="margin-top:6px;padding:8px 10px;background:var(--bg-tertiary);border-left:3px solid var(--warning);border-radius:4px;font-size:12px;color:var(--text-secondary);line-height:1.6;word-break:break-all">${_linkify(escHtml(apiErrMsg))}</div>`
}
// 解码失败时显眼展示关键诊断信息Content-Encoding 和字节数
if (error && respHeaders) {
const contentEncoding = respHeaders['content-encoding'] || respHeaders['Content-Encoding'] || '(未声明)'
const contentType = respHeaders['content-type'] || respHeaders['Content-Type'] || '(未知)'
html += `<div style="margin-top:6px;padding:8px 10px;background:var(--bg-tertiary);border-left:3px solid var(--error);border-radius:4px;font-size:11px;color:var(--text-secondary);line-height:1.7;font-family:var(--font-mono)">` +
`<div style="color:var(--text-primary);font-weight:600;margin-bottom:4px;font-family:var(--font-sans)">🔍 诊断信息</div>` +
`Content-Encoding: <strong style="color:var(--warning)">${escHtml(contentEncoding)}</strong><br>` +
`Content-Type: ${escHtml(contentType)}<br>` +
(respByteCount ? `响应字节数: ${respByteCount}` : '') +
`</div>`
}
// 模型回复(完整展示,不截断;长回复给最大高度 + scroll
if (reply) {
html += `<div style="margin-top:6px;padding:8px 10px;background:var(--bg-tertiary);border-left:3px solid var(--success);border-radius:4px;font-size:13px;color:var(--text-primary);line-height:1.6;white-space:pre-wrap;word-break:break-word;max-height:180px;overflow:auto">` +
@@ -2977,7 +2988,21 @@ function buildTestResult({ success, elapsed, usedApi, reqUrl, reqBody, respStatu
html += `<strong>POST</strong> ${escHtml(reqUrl)}\n\n`
html += `<strong>Request Body:</strong>\n${escHtml(JSON.stringify(reqBody, null, 2))}\n\n`
html += `<strong>Response Status:</strong> ${respStatus}\n\n`
html += `<strong>Response Body:</strong>\n`
// Response Headers完整列出每行一个
if (respHeaders && typeof respHeaders === 'object') {
html += `<strong>Response Headers:</strong>\n`
const entries = Object.entries(respHeaders)
if (entries.length === 0) {
html += `<span style="color:var(--text-tertiary);font-style:italic">(无)</span>\n\n`
} else {
html += entries.map(([k, v]) => ` ${escHtml(k)}: ${escHtml(String(v))}`).join('\n') + '\n\n'
}
}
html += `<strong>Response Body:</strong>`
if (respByteCount !== undefined && respByteCount !== null) {
html += ` <span style="color:var(--text-tertiary);font-weight:normal">(${respByteCount} bytes)</span>`
}
html += `\n`
// 美化 JSON空串单独提示避免误导为"empty"字面量)
if (!respBody) {
html += `<span style="color:var(--text-tertiary);font-style:italic">${escHtml(t('assistant.testRespBodyEmptyDetail'))}</span>`
@@ -2988,6 +3013,10 @@ function buildTestResult({ success, elapsed, usedApi, reqUrl, reqBody, respStatu
html += escHtml(respBody.slice(0, 4000))
}
}
// Raw Bytes (hex)UTF-8 解码失败时最关键的诊断信息
if (respRawHex) {
html += `\n\n<strong>Raw Bytes (前 200 字节 hex):</strong>\n<span style="color:var(--text-tertiary);font-size:10px">${escHtml(respRawHex)}</span>`
}
html += `</div></details>`
return html
}
@@ -3906,7 +3935,10 @@ function showSettings() {
reqUrl: r.reqUrl || baseUrl,
reqBody: r.reqBody || {},
respStatus: r.status ?? 0,
respHeaders: r.respHeaders || null,
respBody: r.respBody || '',
respRawHex: r.respRawHex || '',
respByteCount: r.respByteCount || 0,
reply: r.reply || '',
error: r.error || null,
})
@@ -3919,7 +3951,10 @@ function showSettings() {
reqUrl: baseUrl,
reqBody: {},
respStatus: 0,
respHeaders: null,
respBody: '',
respRawHex: '',
respByteCount: 0,
error: err?.message || String(err),
})
}