M2b:删除旧 chat 体系——线性会话/叩心/情绪/纪念日模型与页面退役(迁移 003 双向验证),runs 去会话外键,测试全量修剪

This commit is contained in:
chenxin committed 2026-08-21 23:43:31 +08:00
1 parent 5f36e3785c
commit ad9c9e942a
22 files changed
+424 -427

No files matched your search

-4
View File
@@ -9,7 +9,6 @@ from app.api.v1.endpoints import (
avatar,
character_import,
characters,
chat,
drive,
events,
evolution,
@@ -30,7 +29,6 @@ from app.api.v1.endpoints import (
sandbox_files,
settings,
skills,
time,
tools,
world,
)
@@ -40,7 +38,6 @@ api_router.include_router(health.router)
api_router.include_router(auth.router)
api_router.include_router(characters.router)
api_router.include_router(character_import.router)
api_router.include_router(chat.router)
api_router.include_router(runs.router)
api_router.include_router(kernel_runs.router)
api_router.include_router(embryo_drafts.router)
@@ -66,4 +63,3 @@ api_router.include_router(goals.router)
api_router.include_router(admin.router)
api_router.include_router(audio.router)
api_router.include_router(avatar.router)
api_router.include_router(time.router)
-2
View File
@@ -38,7 +38,6 @@ router = APIRouter(prefix="/runs", tags=["runs"])
@router.get("", response_model=Page[RunResponse])
async def list_runs(
character_id: str | None = Query(default=None, description="按人物过滤。"),
session_id: str | None = Query(default=None, description="按会话过滤。"),
status: str | None = Query(default=None, description="按状态过滤。"),
page: int = Query(default=1, ge=1),
page_size: int = Query(default=20, ge=1, le=100),
@@ -46,7 +45,6 @@ async def list_runs(
"""Run 列表(按创建时间倒序);归档页运行时间河数据源。"""
return await list_runs_response(
character_id=character_id,
session_id=session_id,
status=status,
page=page,
page_size=page_size,
-10
View File
@@ -30,12 +30,10 @@ from app.core.logging import (
from app.db.init_data import seed_default_data
from app.db.session import AsyncSessionLocal, close_engine
from app.integrations.sandbox.client import SandboxClient
from app.modules.knocking.pivot import KnockingPivot
from app.modules.safety.confirmation import InMemoryConfirmationStore
from app.schemas.common import ErrorResponse
from app.services.dispatcher import Dispatcher
from app.services.event_store import DbEventStore
from app.services.personality_emotion import handle_time_event
from app.services.safety import load_governor_from_db
# 模块级初始化日志系统:import 本模块即完成 loguru 配置并接管标准 logging,
@@ -117,12 +115,7 @@ async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]:
# 一炁进程内订阅分发器:订阅 EventStore 全量事件,按 glob pattern 分发给注册的 handler。
# 各枢核心就绪后在 start() 前 register;lifespan 管理 start/stop。
_app.state.dispatcher = Dispatcher(_app.state.event_store)
# 灵核情绪状态机:订阅辰正 time.* 事件驱动 EmotionState(设计 §11,首个真实 handler)
_app.state.dispatcher.register("time.*", handle_time_event)
await _app.state.dispatcher.start()
# 叩心枢主动对话守护进程:后台常驻循环,在 dispatcher 就绪后启动
_app.state.knocking_pivot = KnockingPivot(_app.state.event_store)
await _app.state.knocking_pivot.start()
_app.state.confirmation_store = InMemoryConfirmationStore()
if settings.use_db_confirmation_store:
from app.modules.safety.confirmation import DbConfirmationStore
@@ -177,9 +170,6 @@ async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]:
yield
# 叩心枢先停,再停 dispatcher
if getattr(_app.state, "knocking_pivot", None) is not None:
await _app.state.knocking_pivot.stop()
# Dispatcher 先停(取消 pump/consumer task + unsubscribe all_queue),再关 http/engine
if getattr(_app.state, "dispatcher", None) is not None:
await _app.state.dispatcher.stop()
-7
View File
@@ -3,7 +3,6 @@
from app.models.big_cycle import BigCycle
from app.models.candidates import MemoryCandidate, ProfileCandidate
from app.models.character import Character
from app.models.chat import ChatMessage, ChatSession
from app.models.drive import CuriosityQuestion, ExploratorySubgoal
from app.models.evolution import EvolutionCandidate
from app.models.governance import (
@@ -28,12 +27,10 @@ from app.models.mirror_tree import (
)
from app.models.memory import Memory, MemoryLink
from app.models.personality import PersonalityProposal, PersonalityVersion
from app.models.personality_emotion import CharacterEmotionState
from app.models.rbac import Permission, Role, RolePermission, UserRole
from app.models.runtime import Event, Run
from app.models.safety import AuditEvent, PendingConfirmationRecord
from app.models.skills import Skill
from app.models.time_anniversary import TimeAnniversary
from app.models.user import User
from app.models.world import CharacterWorldEntity, WorldRelation
@@ -42,11 +39,8 @@ __all__ = [
"AuditEvent",
"BigCycle",
"Character",
"CharacterEmotionState",
"CharacterImportJob",
"CharacterWorldEntity",
"ChatMessage",
"ChatSession",
"CuriosityQuestion",
"Event",
"EvolutionCandidate",
@@ -72,7 +66,6 @@ __all__ = [
"RolePermission",
"Run",
"Skill",
"TimeAnniversary",
"User",
"UserRole",
"WorldEntity",
+2 -4
View File
@@ -14,13 +14,11 @@ class Run(Base):
__tablename__ = "runs"
id: Mapped[str] = mapped_column(String(80), primary_key=True, default=new_run_id)
# rewrite/1.0:照心试演(kind=rehearsal)无正式人物,character_id 允许为空
# rewrite/1.0:照心试演(kind=rehearsal)无正式人物,character_id 允许为空;
# 旧 chat_sessions 外键随线性会话模型退役移除
character_id: Mapped[str | None] = mapped_column(
String(80), ForeignKey("characters.id"), index=True, nullable=True
)
session_id: Mapped[str | None] = mapped_column(
String(80), ForeignKey("chat_sessions.id"), index=True
)
status: Mapped[str] = mapped_column(String(40), nullable=False, default="queued", index=True)
# run.kind:foreground / background_task / awakening / rehearsal(契约词典 §8.2)
kind: Mapped[str] = mapped_column(
+1 -12
View File
@@ -14,17 +14,14 @@ async def list_runs(
session: AsyncSession,
*,
character_id: str | None = None,
session_id: str | None = None,
status: str | None = None,
page: int = 1,
page_size: int = 20,
) -> tuple[list[Run], int]:
"""按创建时间倒序分页返回 Run;可选按人物/会话/状态过滤,返回 (条目, 总数)。"""
"""按创建时间倒序分页返回 Run;可选按人物/状态过滤,返回 (条目, 总数)。"""
conds = []
if character_id is not None:
conds.append(Run.character_id == character_id)
if session_id is not None:
conds.append(Run.session_id == session_id)
if status is not None:
conds.append(Run.status == status)
count_stmt = select(func.count()).select_from(Run)
@@ -127,11 +124,3 @@ async def summarize_by_trace(session: AsyncSession):
return [(row[0], int(row[1]), row[2]) for row in result.all()]
async def list_session_ids_by_runs(session: AsyncSession, run_ids: list[str]) -> list[str]:
"""按 run_id 集合查询关联的 chat session_id 去重列表(链路追踪:run → 对话)。"""
if not run_ids:
return []
result = await session.execute(
select(Run.session_id).where(Run.id.in_(run_ids), Run.session_id.is_not(None))
)
return sorted({row[0] for row in result.all() if row[0]})
-1
View File
@@ -69,7 +69,6 @@ class RunResponse(BaseModel):
character_id: str | None = Field(
default=None, description="所属人物 ID;照心试演(kind=rehearsal)无正式人物。"
)
session_id: str | None = Field(default=None, description="关联会话 ID。")
status: str = Field(
description="运行状态:queued / running / completed / failed / cancelled / interrupted。"
)
+2 -13
View File
@@ -262,14 +262,11 @@ async def get_trace(trace_id: str) -> TraceDetail:
*[audit.character_id for audit in audits if audit.character_id],
}
)
session_ids = (
sorted(await runtime_repo.list_session_ids_by_runs(session, run_ids)) if run_ids else []
)
return TraceDetail(
trace_id=trace_id,
character_ids=character_ids,
run_ids=run_ids,
session_ids=session_ids,
session_ids=[],
items=items,
)
@@ -342,15 +339,7 @@ async def list_traces(*, limit: int = 50) -> TraceListResponse:
).all():
_entry(tid)["character_ids"].add(cid)
# 对话归属(events.run_id → runs.session_id)
for tid, sid in (
await session.execute(
select(Event.trace_id, Run.session_id)
.join(Run, Event.run_id == Run.id)
.where(Event.trace_id.is_not(None), Run.session_id.is_not(None))
)
).all():
_entry(tid)["session_ids"].add(sid)
# 对话归属随线性会话模型退役;镜树归属随 M2c 司命台重设计接入(tree_id 聚合)
total = len(merged)
ordered = sorted(merged.values(), key=lambda e: e["last_seen"], reverse=True)
+5 -41
View File
@@ -10,7 +10,6 @@ from app.core.time import utc_now
from app.db.session import AsyncSessionLocal
from app.models.character import Character
from app.models.personality import PersonalityVersion
from app.models.time_anniversary import TimeAnniversary
from app.modules.character import (
MODEL_POLICY_CREATE_DEFAULTS,
SYSTEM_TEMPLATE_OWNER_SCOPE,
@@ -19,7 +18,6 @@ from app.modules.character import (
)
from app.repositories import characters as character_repo
from app.repositories import personality as personality_repo
from app.repositories import time_anniversaries as anniversary_repo
from app.repositories.json_utils import dumps, loads_dict
from app.schemas.character import (
ActivateCharacterResponse,
@@ -232,7 +230,7 @@ async def create_character_from_template(
session: AsyncSession, template: Character, owner_scope: str
) -> Character:
# 模板的 birthday 仅在兜底(空/缺失)时不继承;显式设置的有效生日原样保留,
# 由 _ensure_default_birthday 写入子角色的 profile 与 time_anniversaries 表。
# 由 _ensure_default_birthday 写入子角色的 profile。
profile = loads_dict(template.profile_json)
birthday = profile.get("birthday")
if not (isinstance(birthday, str) and birthday.strip()):
@@ -388,51 +386,17 @@ async def _audit_character(session: AsyncSession, character: Character, action:
async def _ensure_default_birthday(session: AsyncSession, character: Character) -> None:
"""birthday 缺失时用 created_at 兜底:写 profile.birthday 并建一条生日纪念日。
"""birthday 缺失时用 created_at 兜底:仅写 profile.birthday。
- 仅在 profile.birthday 为空/缺失时触发默认兜底;用户或模板已设则不覆盖。
- 无论 birthday 是否已设,只要角色尚无 birthday 纪念日,就建一条。
- profile.birthday 与 TimeAnniversary.date 取 birthday 值(显式)或 created_at 的 ISO 日期。
- 须在 character flush 后调用(依赖 id 与 created_at);同一事务内随外层 commit 落库。
纪念日表(time_anniversaries)随旧时间模型退役删除;辰正(M8 重实现)落地后
生日提醒改由时态感知按 profile.birthday 计算,不再预写行。
"""
profile = loads_dict(character.profile_json)
birthday = profile.get("birthday")
if isinstance(birthday, str) and birthday.strip():
# birthday 已设但可能无对应纪念日(如从模板继承),仅 ISO 格式生日补建
existing = await anniversary_repo.list_anniversaries(session, character.id)
if not any(a.kind == "birthday" for a in existing):
try:
datetime.strptime(birthday, "%Y-%m-%d")
except (ValueError, TypeError):
pass
else:
anniversary = TimeAnniversary(
character_id=character.id,
name=f"{character.display_name or character.name}·生辰",
calendar="solar",
date=birthday,
recurs_yearly=True,
kind="birthday",
subject_type="character_self",
source="auto",
)
await anniversary_repo.create_anniversary(session, anniversary)
return
birthday_iso = character.created_at.strftime("%Y-%m-%d")
profile["birthday"] = birthday_iso
profile["birthday"] = character.created_at.strftime("%Y-%m-%d")
character.profile_json = dumps(profile)
anniversary = TimeAnniversary(
character_id=character.id,
name=f"{character.display_name or character.name}·生辰",
calendar="solar",
date=birthday_iso,
recurs_yearly=True,
kind="birthday",
subject_type="character_self",
source="auto",
)
await anniversary_repo.create_anniversary(session, anniversary)
async def create_character(
session: AsyncSession,
+1 -5
View File
@@ -28,14 +28,12 @@ async def create_run(
session: AsyncSession,
*,
character_id: str,
session_id: str | None,
model_hint: str,
goal: str = "回复用户消息",
origin: str = "chat",
) -> Run:
run = Run(
character_id=character_id,
session_id=session_id,
status="running",
goal=goal,
origin=origin,
@@ -175,17 +173,15 @@ async def get_run_response(run_id: str) -> RunResponse:
async def list_runs_response(
*,
character_id: str | None = None,
session_id: str | None = None,
status: str | None = None,
page: int = 1,
page_size: int = 20,
) -> Page[RunResponse]:
"""Run 列表(按创建时间倒序);可选按人物/会话/状态过滤。归档页运行时间河数据源。"""
"""Run 列表(按创建时间倒序);可选按人物/状态过滤。归档页运行时间河数据源。"""
async with AsyncSessionLocal() as session:
items, total = await runtime_repo.list_runs(
session,
character_id=character_id,
session_id=session_id,
status=status,
page=page,
page_size=page_size,
-39
View File
@@ -6,7 +6,6 @@ from typing import Any
from app.core.time import to_iso, utc_now
from app.models.candidates import MemoryCandidate, ProfileCandidate
from app.models.character import Character
from app.models.chat import ChatMessage, ChatSession
from app.models.governance import (
AgentRosterEntry,
GenerationPreferences,
@@ -33,7 +32,6 @@ from app.schemas.admin import (
)
from app.schemas.candidates import MemoryCandidateResponse, ProfileCandidateResponse
from app.schemas.character import CharacterResponse, CharacterSummaryResponse
from app.schemas.chat import ChatMessageResponse, ChatSessionResponse
from app.schemas.imports import ImportItemResponse, ImportJobResponse
from app.schemas.memory import MemoryResponse
from app.schemas.personality import PersonalityProposalResponse, PersonalityResponse
@@ -125,42 +123,6 @@ def proposal_to_response(proposal: PersonalityProposal) -> PersonalityProposalRe
)
def chat_session_to_response(
chat_session: ChatSession,
*,
message_count: int = 0,
last_message_preview: str | None = None,
) -> ChatSessionResponse:
return ChatSessionResponse(
session_id=chat_session.id,
character_id=chat_session.character_id,
title=chat_session.title,
status=chat_session.status,
created_at=to_iso(chat_session.created_at) or "",
updated_at=to_iso(chat_session.updated_at) or "",
message_count=message_count,
last_message_preview=last_message_preview,
)
def chat_message_to_response(message: ChatMessage) -> ChatMessageResponse:
return ChatMessageResponse(
message_id=message.id,
session_id=message.session_id,
character_id=message.character_id,
role=message.role,
content=message.content,
modality=message.modality,
run_id=message.run_id,
metadata=loads_dict(message.metadata_json),
sensitivity=message.sensitivity,
versions=loads_list(message.versions_json),
active_version_idx=message.active_version_idx,
created_at=to_iso(message.created_at) or "",
deleted_at=to_iso(message.deleted_at),
)
def memory_to_response(memory: Memory) -> MemoryResponse:
return MemoryResponse(
id=memory.id,
@@ -185,7 +147,6 @@ def run_to_response(run: Run) -> RunResponse:
return RunResponse(
id=run.id,
character_id=run.character_id,
session_id=run.session_id,
status=run.status,
kind=run.kind,
goal=run.goal,