feat: 模型推理异步加载与对话链路修复,同步基线

模型推理全异步化改造:
- 计算节点 InferenceSession 改为后台线程异步加载模型,load 立即返回,
  加载期间事件循环保持响应(/inference/status 与 /health 不阻塞)
- 后端模型加载改为异步派发 + 轮询对账器(reconcile_inference_loads),
  任务状态由 starting 自动推进到 ready/error,解决多节点启动超时
  (timeout of 120000ms exceeded)
- 推理删除/卸载改为任务感知 + 短超时,删除先删记录再 best-effort 卸载,
  不再被不可达节点阻塞;同节点新模型替换旧任务标记失效
- 流式对话透传 task_id/node_id 路由到真正加载模型的算力节点,
  useStreamChat 解析 SSE 错误帧以干净文案展示
- 对话历史按任务 id 本地持久化,退出重进可恢复;移除页脚提示文本
- 新增后端推理异步加载与计算节点异步状态机单元测试

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
wuyongtao
2026-08-04 16:59:34 +08:00
parent 250e060271
commit 0271942ba5
21 changed files with 1272 additions and 245 deletions

View File

@@ -44,6 +44,24 @@ export function useStreamChat() {
})
const loading = ref(false)
/** 从 SSE 帧中提取错误信息(后端/计算节点错误以 data: {"error": "..."} 形式下发) */
function extractSseError(buffer: string): string | null {
const trimmed = buffer.trim()
if (!trimmed.startsWith('data: ')) return null
const lines = trimmed.split(/\r?\n/)
for (let i = lines.length - 1; i >= 0; i--) {
const line = lines[i].trim()
if (!line.startsWith('data: ')) continue
try {
const obj = JSON.parse(line.slice(6))
if (obj && typeof obj.error === 'string' && obj.error) return obj.error
} catch {
/* 非 JSON 的 data 行忽略 */
}
}
return trimmed
}
/** 从内容中解析 think 标签 */
function parseContent(content: string) {
const thinkRegex = /<think>([\s\S]*?)(<\/think>)?/g
@@ -121,6 +139,16 @@ export function useStreamChat() {
}
// 最终更新
// 若整段响应是 SSE 错误帧,提取 error 字段以干净文案展示
const sseError = extractSseError(buffer)
if (sseError) {
message.value.isThinking = false
message.value.isStreaming = false
message.value.done = true
message.value.error = sseError
message.value.displayContent = sseError
return
}
const parsed = parseContent(buffer)
message.value.thinkContent = parsed.think
message.value.displayContent = parsed.display