deploy: 2026-07-15 14:48 部署更新
This commit is contained in:
+35
-7
@@ -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")
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user