feat(chat): 前端 chat_ws.js 实时渲染进度/错误(保留重载兜底)

This commit is contained in:
lhl
2026-08-29 11:58:38 +08:00
parent 61611b6b8a
commit 14a7dc3501
4 changed files with 68 additions and 0 deletions
+7
View File
@@ -119,6 +119,13 @@ def create_app(
raise _error(404, "NOT_FOUND", "chat_state.js 不存在")
return FileResponse(p, media_type="application/javascript")
@app.get("/chat_ws.js")
def chat_ws_js():
p = static_dir / "chat_ws.js"
if not p.exists():
raise _error(404, "NOT_FOUND", "chat_ws.js 不存在")
return FileResponse(p, media_type="application/javascript")
# ---------- 项目配置 ----------
@app.post("/api/projects")
+13
View File
@@ -454,8 +454,10 @@
</div>
<script src="/chat_state.js"></script>
<script src="/chat_ws.js"></script>
<script>
let sid = null;
let progressWs = null;
let draftProject = null; // 下一空白会话将绑定/已绑的项目(原 currentProject
let activeProject = null; // 当前已落库会话绑定的项目(来自后端)
let projects = [];
@@ -765,6 +767,7 @@ function renderEmptyOrWelcome() {
// ---------- 会话创建/加载 ----------
async function newSession() {
if (progressWs) { try { progressWs.close(); } catch (e) {} } progressWs = null;
sid = null;
activeProject = null;
// 保留 draftProject(当前顶栏已选项目),使新会话继承项目绑定(§4.2)
@@ -784,6 +787,11 @@ async function loadSession(id) {
draftProject = rec.project || null; // 载入既有会话时,顶栏与侧边栏过滤对齐该会话项目
applyProjectContext();
badge.textContent = '会话: ' + (rec.name || '新会话');
if (progressWs) { try { progressWs.close(); } catch (e) {} }
progressWs = GenesisWS.connectProgressWs(id, {
onProgress: (e) => GenesisWS.applyProgressEvent(e, (role, text) => addMsg(role, text)),
onClose: () => {},
});
const msgs = await api('GET', '/api/chat/' + id + '/messages');
chatEl.innerHTML = '';
for (const m of msgs) {
@@ -816,6 +824,11 @@ async function send() {
return;
}
}
if (progressWs) { try { progressWs.close(); } catch (e) {} }
progressWs = GenesisWS.connectProgressWs(sid, {
onProgress: (e) => GenesisWS.applyProgressEvent(e, (role, text) => addMsg(role, text)),
onClose: () => {},
});
inputEl.value = '';
addMsg('user', esc(text));
const typing = showTyping();
+30
View File
@@ -0,0 +1,30 @@
(function (root, factory) {
const api = factory();
if (typeof module !== 'undefined' && module.exports) module.exports = api;
else root.GenesisWS = api;
})(typeof self !== 'undefined' ? self : this, function () {
function connectProgressWs(sid, handlers) {
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
let ws;
try {
ws = new WebSocket(`${proto}://${location.host}/api/sessions/${encodeURIComponent(sid)}/ws`);
} catch (err) {
// 连接失败(如无 WS 依赖 / 代理拦截)静默降级;现有持久化 + 重载兜底仍可见进度
if (handlers.onClose) handlers.onClose();
return null;
}
ws.onmessage = (ev) => {
let e;
try { e = JSON.parse(ev.data); } catch { return; }
if (handlers.onProgress) handlers.onProgress(e);
};
ws.onerror = () => { try { ws.close(); } catch {} };
ws.onclose = () => handlers.onClose && handlers.onClose();
return ws;
}
function applyProgressEvent(e, render) {
if (e.type === 'progress') render('progress', `${e.step}: ${e.detail || ''}`);
else if (e.type === 'error') render('error', e.detail || '错误');
}
return { connectProgressWs, applyProgressEvent };
});
+18
View File
@@ -0,0 +1,18 @@
const test = require('node:test');
const assert = require('node:assert');
const { applyProgressEvent } = require('../src/genesis/server/static/chat_ws.js');
test('applyProgressEvent 渲染 progress 角色', () => {
const got = [];
applyProgressEvent({ type: 'progress', step: 'parse', detail: '完成' }, (role, text) => got.push([role, text]));
assert.strictEqual(got.length, 1);
assert.strictEqual(got[0][0], 'progress');
assert.ok(got[0][1].includes('parse'));
});
test('applyProgressEvent 渲染 error 角色', () => {
const got = [];
applyProgressEvent({ type: 'error', detail: '炸了' }, (role, text) => got.push([role, text]));
assert.strictEqual(got[0][0], 'error');
assert.ok(got[0][1].includes('炸了'));
});