release: bump version to 0.70.0
Some checks failed
ci / backend (push) Has been cancelled
ci / frontend (push) Has been cancelled
release / images (push) Has been cancelled
ci / delivery (push) Has been cancelled

This commit is contained in:
linkong
2026-06-04 17:16:23 +08:00
parent acbbfdf9e2
commit 8c204717cd
78 changed files with 1762 additions and 703 deletions

View File

@@ -7,9 +7,14 @@ from time import perf_counter
from uuid import uuid4
from fastapi import HTTPException, status
from sqlalchemy import func, select, update
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.enums import (
PlaygroundMessageKind,
PlaygroundMessageRole,
PlaygroundMessageStatus,
)
from app.core.logging import get_logger
from app.db.session import async_session_factory
from app.models.playground_message import PlaygroundMessage
@@ -21,7 +26,6 @@ from app.schemas.ai import (
PlaygroundMessageRecord,
PlaygroundMessageResendRequest,
PlaygroundMessageStopRequest,
PlaygroundSessionResponse,
PlaygroundSessionState,
PlaygroundSessionUpsertRequest,
PlaygroundThreadResponse,
@@ -37,6 +41,13 @@ STREAM_CHUNK_SIZE = 24
STREAM_INTERVAL_SECONDS = 0.08
THINKING_PREVIEW_SECONDS = 2.6
ORPHANED_RUN_MESSAGE = "后台生成任务已中断,请点击上一条用户消息的重试按钮重新生成。"
ACTIVE_MESSAGE_STATUSES = frozenset(
{
PlaygroundMessageStatus.PENDING.value,
PlaygroundMessageStatus.THINKING.value,
PlaygroundMessageStatus.ANSWERING.value,
}
)
class _ActiveRun:
@@ -93,7 +104,11 @@ async def _require_visible_message(
result = await db.execute(select(PlaygroundMessage).where(*conditions))
message = result.scalar_one_or_none()
if message is None:
detail = "User message not found" if role == "user" else "Playground message not found"
detail = (
"User message not found"
if role == PlaygroundMessageRole.USER.value
else "Playground message not found"
)
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=detail)
return message
@@ -108,7 +123,7 @@ def _message_to_record(message: PlaygroundMessage, parent_public_id: str | None
content=message.content or "",
thinking_content=message.thinking_content or "",
meta=list(message.meta or []),
markdown=message.role != "system",
markdown=message.role != PlaygroundMessageRole.SYSTEM.value,
provider=message.provider,
model=message.model,
request_id=message.request_id,
@@ -197,11 +212,11 @@ async def _reconcile_orphaned_active_messages(
) -> list[PlaygroundMessage]:
changed = False
for item in messages:
if item.status not in {"pending", "thinking", "answering"}:
if item.status not in ACTIVE_MESSAGE_STATUSES:
continue
if item.public_id in _ACTIVE_RUNS:
continue
item.status = "error"
item.status = PlaygroundMessageStatus.ERROR.value
item.content = item.content or ORPHANED_RUN_MESSAGE
orphan_meta = "错误: 后台任务已中断"
if orphan_meta not in (item.meta or []):
@@ -330,9 +345,9 @@ async def create_turn(
public_id=uuid4().hex,
session_id=session.id,
user_id=user_id,
role="user",
kind="message",
status="done",
role=PlaygroundMessageRole.USER.value,
kind=PlaygroundMessageKind.MESSAGE.value,
status=PlaygroundMessageStatus.DONE.value,
title=payload.selected_preset_key,
content=payload.input,
meta=[payload.title],
@@ -343,9 +358,9 @@ async def create_turn(
session_id=session.id,
user_id=user_id,
parent_message_id=None,
role="assistant",
kind="thinking",
status="pending",
role=PlaygroundMessageRole.ASSISTANT.value,
kind=PlaygroundMessageKind.THINKING.value,
status=PlaygroundMessageStatus.PENDING.value,
title="AI 回应",
content="",
thinking_content="",
@@ -397,9 +412,9 @@ async def _create_assistant_retry_turn(
session_id=session.id,
user_id=user_id,
parent_message_id=user_message.id,
role="assistant",
kind="thinking",
status="pending",
role=PlaygroundMessageRole.ASSISTANT.value,
kind=PlaygroundMessageKind.THINKING.value,
status=PlaygroundMessageStatus.PENDING.value,
title="AI 回应",
content="",
thinking_content="",
@@ -439,7 +454,7 @@ async def stop_message(
session = await _require_session(db, user_id=user_id, session_key=payload.session_key)
message = await _require_visible_message(db, user_id=user_id, public_id=payload.message_id)
if message.status not in {"pending", "thinking", "answering"}:
if message.status not in ACTIVE_MESSAGE_STATUSES:
return await _build_action_response(db, session=session)
active_run = _ACTIVE_RUNS.get(message.public_id)
@@ -447,7 +462,7 @@ async def stop_message(
active_run.stop_requested.set()
active_run.task.cancel()
message.status = "stopped"
message.status = PlaygroundMessageStatus.STOPPED.value
if "已手动停止生成" not in (message.meta or []):
message.meta = [*(message.meta or []), "已手动停止生成"]
await db.flush()
@@ -469,7 +484,7 @@ async def resend_turn(
db,
user_id=user_id,
public_id=payload.user_message_id,
role="user",
role=PlaygroundMessageRole.USER.value,
)
later_messages = await db.execute(
@@ -481,7 +496,7 @@ async def resend_turn(
)
for item in later_messages.scalars().all():
item.is_visible = False
if item.status in {"pending", "thinking", "answering"}:
if item.status in ACTIVE_MESSAGE_STATUSES:
active_run = _ACTIVE_RUNS.get(item.public_id)
if active_run is not None:
active_run.stop_requested.set()
@@ -520,7 +535,7 @@ async def edit_user_message(
db,
user_id=user_id,
public_id=payload.user_message_id,
role="user",
role=PlaygroundMessageRole.USER.value,
)
user_message.content = payload.content.strip()
@@ -566,12 +581,12 @@ def _build_conversation_history(messages: Sequence[PlaygroundMessage], current_u
for item in messages:
if item.id >= current_user_message_id:
break
if item.role == "system":
if item.role == PlaygroundMessageRole.SYSTEM.value:
continue
history.append(
{
"role": item.role,
"kind": item.kind or "message",
"kind": item.kind or PlaygroundMessageKind.MESSAGE.value,
"title": item.title,
"content": item.content or "",
}
@@ -668,7 +683,11 @@ async def _run_assistant_message(
assistant_message = await _mark_message_state(
db,
message_id=assistant_message_id,
status="thinking" if analysis.thinking_blocks else "answering",
status=(
PlaygroundMessageStatus.THINKING.value
if analysis.thinking_blocks
else PlaygroundMessageStatus.ANSWERING.value
),
title=f"{analysis.provider} / {analysis.model}",
provider=analysis.provider,
model=analysis.model,
@@ -706,7 +725,7 @@ async def _run_assistant_message(
await _mark_message_state(
db,
message_id=assistant_message_id,
status="answering",
status=PlaygroundMessageStatus.ANSWERING.value,
content=content[:cursor],
)
await db.commit()
@@ -717,7 +736,7 @@ async def _run_assistant_message(
assistant_message = await _mark_message_state(
db,
message_id=assistant_message_id,
status="done",
status=PlaygroundMessageStatus.DONE.value,
content=content,
meta=[
f"Request ID: {request_id}",
@@ -762,8 +781,8 @@ async def _run_assistant_message(
async with async_session_factory() as db:
result = await db.execute(select(PlaygroundMessage).where(PlaygroundMessage.id == assistant_message_id))
message = result.scalar_one_or_none()
if message is not None and message.status in {"pending", "thinking", "answering"}:
message.status = "stopped"
if message is not None and message.status in ACTIVE_MESSAGE_STATUSES:
message.status = PlaygroundMessageStatus.STOPPED.value
if "已手动停止生成" not in (message.meta or []):
message.meta = [*(message.meta or []), "已手动停止生成"]
await db.flush()
@@ -795,7 +814,7 @@ async def _run_assistant_message(
result = await db.execute(select(PlaygroundMessage).where(PlaygroundMessage.id == assistant_message_id))
message = result.scalar_one_or_none()
if message is not None:
message.status = "error"
message.status = PlaygroundMessageStatus.ERROR.value
message.content = message.content or f"分析失败:{error_message}"
message.meta = [
*(message.meta or []),