feat(chat): 前端 chat_ws.js 实时渲染进度/错误(保留重载兜底)
This commit is contained in:
@@ -119,6 +119,13 @@ def create_app(
|
|||||||
raise _error(404, "NOT_FOUND", "chat_state.js 不存在")
|
raise _error(404, "NOT_FOUND", "chat_state.js 不存在")
|
||||||
return FileResponse(p, media_type="application/javascript")
|
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")
|
@app.post("/api/projects")
|
||||||
|
|||||||
@@ -454,8 +454,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/chat_state.js"></script>
|
<script src="/chat_state.js"></script>
|
||||||
|
<script src="/chat_ws.js"></script>
|
||||||
<script>
|
<script>
|
||||||
let sid = null;
|
let sid = null;
|
||||||
|
let progressWs = null;
|
||||||
let draftProject = null; // 下一空白会话将绑定/已绑的项目(原 currentProject)
|
let draftProject = null; // 下一空白会话将绑定/已绑的项目(原 currentProject)
|
||||||
let activeProject = null; // 当前已落库会话绑定的项目(来自后端)
|
let activeProject = null; // 当前已落库会话绑定的项目(来自后端)
|
||||||
let projects = [];
|
let projects = [];
|
||||||
@@ -765,6 +767,7 @@ function renderEmptyOrWelcome() {
|
|||||||
|
|
||||||
// ---------- 会话创建/加载 ----------
|
// ---------- 会话创建/加载 ----------
|
||||||
async function newSession() {
|
async function newSession() {
|
||||||
|
if (progressWs) { try { progressWs.close(); } catch (e) {} } progressWs = null;
|
||||||
sid = null;
|
sid = null;
|
||||||
activeProject = null;
|
activeProject = null;
|
||||||
// 保留 draftProject(当前顶栏已选项目),使新会话继承项目绑定(§4.2)
|
// 保留 draftProject(当前顶栏已选项目),使新会话继承项目绑定(§4.2)
|
||||||
@@ -784,6 +787,11 @@ async function loadSession(id) {
|
|||||||
draftProject = rec.project || null; // 载入既有会话时,顶栏与侧边栏过滤对齐该会话项目
|
draftProject = rec.project || null; // 载入既有会话时,顶栏与侧边栏过滤对齐该会话项目
|
||||||
applyProjectContext();
|
applyProjectContext();
|
||||||
badge.textContent = '会话: ' + (rec.name || '新会话');
|
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');
|
const msgs = await api('GET', '/api/chat/' + id + '/messages');
|
||||||
chatEl.innerHTML = '';
|
chatEl.innerHTML = '';
|
||||||
for (const m of msgs) {
|
for (const m of msgs) {
|
||||||
@@ -816,6 +824,11 @@ async function send() {
|
|||||||
return;
|
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 = '';
|
inputEl.value = '';
|
||||||
addMsg('user', esc(text));
|
addMsg('user', esc(text));
|
||||||
const typing = showTyping();
|
const typing = showTyping();
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
});
|
||||||
@@ -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('炸了'));
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user