deploy: 2026-07-15 14:48 部署更新

This commit is contained in:
selfrelease
2026-07-15 14:48:10 +08:00
parent bb6eaff775
commit 8e40d2c9b6
2 changed files with 92 additions and 14 deletions
+35 -7
View File
@@ -18,7 +18,7 @@ import psycopg2
import psycopg2.pool import psycopg2.pool
from fastapi import FastAPI, HTTPException, Query from fastapi import FastAPI, HTTPException, Query
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.responses import RedirectResponse from fastapi.responses import RedirectResponse, StreamingResponse
from pydantic import BaseModel from pydantic import BaseModel
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -932,6 +932,8 @@ def generate_paradigm(
], ],
"response_format": {"type": "json_object"}, "response_format": {"type": "json_object"},
"temperature": 0.4, "temperature": 0.4,
"stream": True,
"stream_options": {"include_usage": False},
}).encode("utf-8") }).encode("utf-8")
req = _urllib.Request( req = _urllib.Request(
@@ -944,14 +946,40 @@ def generate_paradigm(
method="POST", method="POST",
) )
def stream_generator():
"""SSE 流式生成器,逐块推送 LLM content delta。"""
try: try:
with _urllib.urlopen(req, timeout=60) as resp: resp = _urllib.urlopen(req, timeout=120)
result = json.loads(resp.read().decode("utf-8")) buffer = b""
content = result["choices"][0]["message"]["content"] while True:
parsed = json.loads(content) chunk = resp.read(4096)
return parsed if not chunk:
break
buffer += chunk
while b"\n" in buffer:
line, buffer = buffer.split(b"\n", 1)
line = line.strip()
if not line:
continue
if line.startswith(b"data:"):
data_str = line[5:].strip()
if data_str == b"[DONE]":
yield "data: [DONE]\n\n"
return
try:
chunk_obj = json.loads(data_str)
delta = chunk_obj.get("choices", [{}])[0].get("delta", {})
content = delta.get("content", "")
if content:
yield f"data: {json.dumps({'content': content}, ensure_ascii=False)}\n\n"
except (json.JSONDecodeError, IndexError, KeyError):
continue
resp.close()
except Exception as exc: except Exception as exc:
raise HTTPException(status_code=502, detail=f"LLM 调用失败: {exc}") yield f"data: {json.dumps({'error': str(exc)}, ensure_ascii=False)}\n\n"
yield "data: [DONE]\n\n"
return StreamingResponse(stream_generator(), media_type="text/event-stream")
@app.get("/api/sync/status") @app.get("/api/sync/status")
+54 -4
View File
@@ -1132,13 +1132,63 @@ async function generateParadigm() {
const result = document.getElementById('paradigm-result'); const result = document.getElementById('paradigm-result');
btn.disabled = true; btn.disabled = true;
btn.textContent = '生成中...'; btn.textContent = '生成中...';
result.innerHTML = '<p class="text-sm text-gray-400">正在调用千问 LLM 生成标准范式对话,请稍候...</p>'; result.innerHTML = `
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center gap-2 mb-4">
<svg class="animate-spin w-5 h-5 text-green-600" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>
<span class="text-sm text-gray-500">正在调用千问 LLM 生成标准范式对话...</span>
</div>
<pre id="paradigm-stream-text" class="text-xs text-gray-600 bg-gray-50 rounded p-3 max-h-96 overflow-auto whitespace-pre-wrap break-all font-mono"></pre>
</div>
`;
const streamText = document.getElementById('paradigm-stream-text');
let fullContent = '';
try { try {
const productId = document.getElementById('paradigm-product-select').value; const productId = document.getElementById('paradigm-product-select').value;
let paradigmPath = `/conversations/${customerId}/paradigm`; let paradigmUrl = `${API}/conversations/${customerId}/paradigm`;
if (productId) paradigmPath += `?product_id=${productId}`; if (productId) paradigmUrl += `?product_id=${productId}`;
const data = await api(paradigmPath, { method: 'POST' }); const resp = await fetch(paradigmUrl, { method: 'POST' });
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const reader = resp.body.getReader();
const decoder = new TextDecoder();
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) {
if (!line.startsWith('data: ')) continue;
const dataStr = line.slice(6).trim();
if (dataStr === '[DONE]') continue;
try {
const obj = JSON.parse(dataStr);
if (obj.error) throw new Error(obj.error);
if (obj.content) {
fullContent += obj.content;
streamText.textContent = fullContent;
streamText.scrollTop = streamText.scrollHeight;
}
} catch (e) {
// 忽略解析错误的行
}
}
}
// 尝试解析完整 JSON 并渲染结构化结果
try {
const data = JSON.parse(fullContent);
renderParadigm(data); renderParadigm(data);
} catch (e) {
// JSON 解析失败,保留原始流式文本
result.innerHTML = `
<div class="bg-white rounded-lg shadow p-6">
<h3 class="text-lg font-bold mb-2">标准范式对话模板</h3>
<p class="text-sm text-orange-500 mb-3">LLM 返回内容无法解析为 JSON,以下为原始输出:</p>
<pre class="text-xs text-gray-600 bg-gray-50 rounded p-3 max-h-96 overflow-auto whitespace-pre-wrap break-all">${escapeHtml(fullContent)}</pre>
</div>
`;
}
} catch (e) { } catch (e) {
result.innerHTML = `<p class="text-sm text-red-500">生成失败: ${e.message}</p>`; result.innerHTML = `<p class="text-sm text-red-500">生成失败: ${e.message}</p>`;
} finally { } finally {