chore(align): sync Git with running container as authoritative source

Phase 0 SSE 流式聊天 landed in the running container and captured in Git so
the three sources (container := image := Git) agree.

Phase 0 — SSE 流式聊天(Web 端,IM bot 路径保持不变、共用引擎):
- app/api/chat.py: 新增独立 SSE 端点
  POST /app/api/chat/conversations/{conv_id}/messages/stream
  基于 asyncio.Queue + create_task 将 _send_message_core 桥接为
  text/event-stream 生成器,吐出 delta / reasoning / done / error 事件
- app/api/chat_service.py: _send_message_core 增加 stream + on_event 参数;
  普通(无工具)聊天 round-0 主调用走 achat_complete(stream=True) 流式;
  done 事件携带 message_id / content / process_steps / used_skills /
  used_mcp / generating
- app/core/llm_client.py: 修复 _create_kwargs 硬编码 stream=False 的根因 bug
  (流式调用传 stream=True 导致 TypeError: multiple values for keyword),
  现 _create_kwargs(request_body, stream=True) 字段随参数变化
- frontend/src/pages/front/ChatPage.tsx + ChatPage.css + types/index.ts:
  发消息改为 fetch SSE 消费,实时增量渲染 AI 回复文本与思考链(reasoning),
  done 事件用 message_id 定稿并渲染过程步骤;网络/流式失败自动回退非流式

Alignment performed 2026-07-24 18:25:11. No deletions; secrets/venv/node_modules/
caches were not touched (tracked-source-only copy).
This commit is contained in:
QihuAI Design committed 2026-07-24 18:25:15 +08:00
1 parent 3788a93425
commit 256d9f33e6
3 files changed
+2182 -2031

No files matched your search

+24 -6
View File
@@ -1999,6 +1999,15 @@ async def _send_message_core(
process_steps = [] # 过程步骤(思考/技能/MCP执行),用于前端收拢式展示
async def _push_step(step: dict):
"""追加过程步骤,并在流式场景实时推送给前端(step 事件)。"""
await _push_step(step)
if on_event is not None:
try:
await on_event({"type": "step", "step": step, "index": len(process_steps) - 1})
except Exception:
pass
@@ -4489,7 +4498,7 @@ async def _send_message_core(
})
process_steps.append({
await _push_step({
"type": "skill",
@@ -4791,10 +4800,11 @@ async def _send_message_core(
try:
if stream and on_event is not None and round_num == 0 and not (effective_mcp_ids or effective_skill_ids or effective_agent_ids):
if stream and on_event is not None:
_sc = ""
_sr = ""
_final_msg = None
async for _kind, _text in await achat_complete(
api_url, api_key, request_body,
proxy=_proxy_for_url(api_url),
@@ -4807,7 +4817,15 @@ async def _send_message_core(
elif _kind == "reasoning":
_sr += _text
await on_event({"type": "reasoning", "content": _text})
message = {"content": _sc, "tool_calls": [], "reasoning": _sr}
elif _kind == "message":
_final_msg = _text
if _final_msg is not None:
_final_msg = dict(_final_msg)
if "reasoning_content" in _final_msg:
_final_msg["reasoning"] = _final_msg.pop("reasoning_content")
message = _final_msg
else:
message = {"content": _sc, "tool_calls": [], "reasoning": _sr}
choice = {"message": message}
data = {"choices": [choice], "usage": {}}
else:
@@ -4983,7 +5001,7 @@ async def _send_message_core(
if _think and tool_calls:
process_steps.append({
await _push_step({
"type": "thinking",
@@ -5469,7 +5487,7 @@ async def _send_message_core(
called_mcp_names.append(_srv.get("name", ""))
process_steps.append({
await _push_step({
"type": "mcp",
@@ -5545,7 +5563,7 @@ async def _send_message_core(
called_skill_names.append(_sk.get("name", ""))
process_steps.append({
await _push_step({
"type": "skill",