v0.2.1 修复对话体验:1)按钮兜底显示(配置加载失败也默认全开) 2)重新生成先删除旧回答再流式输出 3)推荐短语点击填入输入框 4)思考模型reasoning流式输出+自动折叠 5)语音输入改长按录音松开提交 6)新建对话直接用通用助手不弹窗

This commit is contained in:
2026-08-15 02:01:54 +08:00
parent e26605adf8
commit dd0a0e93c0
8 changed files with 265 additions and 160 deletions
+8 -1
View File
@@ -34,6 +34,7 @@ def _message_out(m: ChatMessage) -> dict:
"id": m.id, "session_id": m.session_id, "role": m.role, "content": m.content,
"model": m.model, "file_ids": file_ids, "feedback": m.feedback or "",
"suggestions": suggestions, "edited": bool(m.edited), "regenerated": m.regenerated or 0,
"reasoning": m.reasoning_content or "",
"created_at": m.created_at.isoformat(), "updated_at": m.updated_at.isoformat(),
}
@@ -142,7 +143,7 @@ async def regenerate(session_id: int,
"""REST 非流式重新生成最后一条 AI 回答(备用,前端主要走 WS 流式)。"""
s = _get_owned_session(db, user, session_id)
try:
async for delta, message_id in chat_service.regenerate_stream(db, s):
async for _k, _d, message_id in _regenerate_rest(chat_service.regenerate_stream(db, s)):
pass # 流式丢弃,最终内容已入库
except LookupError as e:
raise HTTPException(status_code=404, detail=str(e))
@@ -150,3 +151,9 @@ async def regenerate(session_id: int,
raise HTTPException(status_code=502, detail=f"模型调用失败:{e}")
m = db.get(ChatMessage, message_id)
return ok(_message_out(m))
async def _regenerate_rest(agen):
"""适配 regenerate_stream 的 (kind, delta), id 产出结构。"""
async for (kind, delta), message_id in agen:
yield kind, delta, message_id
+5 -5
View File
@@ -1,7 +1,7 @@
"""WebSocket 路由:流式对话 + 重新生成。协议:
客户端 → {"type":"chat","content":"...","agent_id":null,"model":"","file_ids":[]}
{"type":"regenerate"}
服务端 → {"type":"delta","message_id":1,"content":"增量文本"}
服务端 → {"type":"delta","message_id":1,"kind":"reasoning"|"content","content":"增量文本"}
{"type":"done","message_id":1}
{"type":"suggestions","message_id":1,"items":["..",".."]}
{"type":"title","title":"..."}
@@ -70,20 +70,20 @@ async def chat_ws(websocket: WebSocket):
await websocket.send_json({"type": "error", "message": "消息不能为空"})
continue
try:
async for delta, message_id in chat_service.chat_stream(
async for (kind, delta), message_id in chat_service.chat_stream(
db, user, session, content,
agent_id=data.get("agent_id"), model=data.get("model", ""),
file_ids=file_ids,
):
await websocket.send_json({"type": "delta", "message_id": message_id, "content": delta})
await websocket.send_json({"type": "delta", "message_id": message_id, "kind": kind, "content": delta})
await websocket.send_json({"type": "done", "message_id": message_id})
except Exception as e:
await websocket.send_json({"type": "error", "message": f"模型调用失败:{e}"})
elif msg_type == "regenerate":
try:
async for delta, message_id in chat_service.regenerate_stream(db, session):
await websocket.send_json({"type": "delta", "message_id": message_id, "content": delta})
async for (kind, delta), message_id in chat_service.regenerate_stream(db, session):
await websocket.send_json({"type": "delta", "message_id": message_id, "kind": kind, "content": delta})
await websocket.send_json({"type": "done", "message_id": message_id})
except LookupError as e:
await websocket.send_json({"type": "error", "message": str(e)})
+34 -4
View File
@@ -41,8 +41,10 @@ async def chat_completion_stream(
model: Optional[str] = None,
temperature: float = 0.7,
max_tokens: int = 4096,
) -> AsyncIterator[str]:
"""流式对话补全:逐段产出增量文本"""
):
"""流式对话补全:逐段产出增量。
产出 (kind, text)kind="reasoning" 思考内容 / kind="content" 回答内容。
"""
client = _client_for(resolve_model(model))
stream = await client.chat.completions.create(
model=resolve_model(model),
@@ -52,8 +54,36 @@ async def chat_completion_stream(
stream=True,
)
async for chunk in stream:
if chunk.choices and chunk.choices[0].delta and chunk.choices[0].delta.content:
yield chunk.choices[0].delta.content
if not chunk.choices or not chunk.choices[0].delta:
continue
delta = chunk.choices[0].delta
# 思考内容:deepseek-reasoner 用 reasoning_content,部分模型用 reasoning/thinking
reasoning = getattr(delta, "reasoning_content", None) or getattr(delta, "reasoning", None)
if reasoning:
yield ("reasoning", reasoning)
continue
if delta.content:
yield ("content", delta.content)
async def chat_completion_full(
messages: list[dict],
model: Optional[str] = None,
temperature: float = 0.7,
max_tokens: int = 4096,
) -> tuple[str, str]:
"""非流式对话补全:返回 (回答内容, 思考内容)。"""
client = _client_for(resolve_model(model))
resp = await client.chat.completions.create(
model=resolve_model(model),
messages=messages,
temperature=temperature,
max_tokens=max_tokens,
stream=False,
)
msg = resp.choices[0].message
reasoning = getattr(msg, "reasoning_content", None) or getattr(msg, "reasoning", None) or ""
return (msg.content or ""), reasoning
async def vision_analysis(prompt: str, image_urls: list[str], model: Optional[str] = None) -> str:
+1
View File
@@ -12,6 +12,7 @@ def migrate(db):
"suggestions": "TEXT DEFAULT '[]'",
"edited": "BOOLEAN DEFAULT 0",
"regenerated": "INTEGER DEFAULT 0",
"reasoning_content": "TEXT DEFAULT ''",
"updated_at": "DATETIME",
}
for name, ddl in additions.items():
+2
View File
@@ -39,6 +39,8 @@ class ChatMessage(Base):
feedback: Mapped[str] = mapped_column(String(8), default="")
# 推荐短语(JSON 数组,AI 回答后生成 1-3 条)
suggestions: Mapped[str] = mapped_column(Text, default="[]")
# 思考内容(思考模型的 reasoning 流式输出)
reasoning_content: Mapped[str] = mapped_column(Text, default="")
# 用户消息是否被编辑过
edited: Mapped[bool] = mapped_column(Boolean, default=False)
# 重新生成次数
+32 -14
View File
@@ -220,9 +220,11 @@ async def chat_once(db: Session, user: User, session: ChatSession, content: str,
db.commit()
messages = build_messages(db, session, content, file_ids)
reply_text = await llm.chat_completion(messages, model=session.model, temperature=agent.temperature if agent else 0.7)
reply_text, reply_reasoning = await llm.chat_completion_full(
messages, model=session.model, temperature=agent.temperature if agent else 0.7)
reply = ChatMessage(session_id=session.id, role="assistant", content=reply_text, model=session.model)
reply = ChatMessage(session_id=session.id, role="assistant", content=reply_text,
reasoning_content=reply_reasoning, model=session.model)
db.add(reply)
session.updated_at = datetime.utcnow()
db.commit()
@@ -245,25 +247,32 @@ async def chat_stream(db: Session, user: User, session: ChatSession, content: st
db.commit()
messages = build_messages(db, session, content, file_ids)
reply = ChatMessage(session_id=session.id, role="assistant", content="", model=session.model)
reply = ChatMessage(session_id=session.id, role="assistant", content="", reasoning_content="", model=session.model)
db.add(reply)
db.commit()
db.refresh(reply)
parts: list[str] = []
reasoning_parts: list[str] = []
try:
async for delta in llm.chat_completion_stream(
async for kind, delta in llm.chat_completion_stream(
messages, model=session.model, temperature=agent.temperature if agent else 0.7
):
parts.append(delta)
yield delta, reply.id
if kind == "reasoning":
reasoning_parts.append(delta)
yield ("reasoning", delta), reply.id
else:
parts.append(delta)
yield ("content", delta), reply.id
except Exception as e:
reply.content = "".join(parts) or f"(调用失败:{e}"
reply.reasoning_content = "".join(reasoning_parts)
session.updated_at = datetime.utcnow()
db.commit()
raise
else:
reply.content = "".join(parts)
reply.reasoning_content = "".join(reasoning_parts)
session.updated_at = datetime.utcnow()
db.commit()
_fire_background_jobs(db, session, content, reply.id, reply.content)
@@ -276,7 +285,7 @@ def _fire_background_jobs(db: Session, session: ChatSession, first_content: str,
async def regenerate_stream(db: Session, session: ChatSession):
"""重新生成最后一条 AI 回答(流式)。返回 (message_id, async_iter)"""
"""重新生成最后一条 AI 回答(流式):先删除旧回答内容,再流式输出"""
last = (
db.query(ChatMessage)
.filter(ChatMessage.session_id == session.id, ChatMessage.role == "assistant")
@@ -286,27 +295,36 @@ async def regenerate_stream(db: Session, session: ChatSession):
if not last:
raise LookupError("没有可重新生成的消息")
agent = db.get(Agent, session.agent_id) if session.agent_id else None
messages = build_messages(db, session, "", exclude_last=True)
# 重新生成后旧建议清空,等新建议
# 先删除旧回答(内容清空,前端同步清空显示)
last.content = ""
last.reasoning_content = ""
last.suggestions = "[]"
db.commit()
agent = db.get(Agent, session.agent_id) if session.agent_id else None
messages = build_messages(db, session, "", exclude_last=True)
parts: list[str] = []
reasoning_parts: list[str] = []
try:
async for delta in llm.chat_completion_stream(
async for kind, delta in llm.chat_completion_stream(
messages, model=session.model, temperature=agent.temperature if agent else 0.7
):
parts.append(delta)
yield delta, last.id
if kind == "reasoning":
reasoning_parts.append(delta)
yield ("reasoning", delta), last.id
else:
parts.append(delta)
yield ("content", delta), last.id
except Exception as e:
last.content = "".join(parts) or f"(调用失败:{e}"
last.reasoning_content = "".join(reasoning_parts)
last.regenerated += 1
db.commit()
raise
else:
last.content = "".join(parts)
last.reasoning_content = "".join(reasoning_parts)
last.regenerated += 1
session.updated_at = datetime.utcnow()
db.commit()