初始化提交
This commit is contained in:
commit
887c1f449e
963 files changed
+166869
No files matched your search
@@ -0,0 +1 @@
|
||||
"""数据访问层。"""
|
||||
@@ -0,0 +1,60 @@
|
||||
"""审计事件数据访问。"""
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.safety import AuditEvent
|
||||
|
||||
|
||||
async def add_audit_event(session: AsyncSession, event: AuditEvent) -> AuditEvent:
|
||||
session.add(event)
|
||||
await session.flush()
|
||||
return event
|
||||
|
||||
|
||||
async def list_audit_events(
|
||||
session: AsyncSession,
|
||||
*,
|
||||
pivot: str | None = None,
|
||||
character_id: str | None = None,
|
||||
page: int = 1,
|
||||
page_size: int = 50,
|
||||
) -> tuple[list[AuditEvent], int]:
|
||||
"""起居注时间河:按 created_at 倒序,可选按枢/人物过滤,返回 (条目, 总数)。"""
|
||||
conditions = []
|
||||
if pivot:
|
||||
conditions.append(AuditEvent.pivot == pivot)
|
||||
if character_id:
|
||||
conditions.append(AuditEvent.character_id == character_id)
|
||||
total_result = await session.execute(
|
||||
select(func.count()).select_from(AuditEvent).where(*conditions)
|
||||
)
|
||||
total = int(total_result.scalar_one())
|
||||
result = await session.execute(
|
||||
select(AuditEvent)
|
||||
.where(*conditions)
|
||||
.order_by(AuditEvent.created_at.desc())
|
||||
.offset((page - 1) * page_size)
|
||||
.limit(page_size)
|
||||
)
|
||||
return list(result.scalars().all()), total
|
||||
|
||||
|
||||
async def list_audit_events_by_trace(session: AsyncSession, trace_id: str) -> list[AuditEvent]:
|
||||
"""链路追踪:按 trace_id 查询审计事件,按 created_at 升序(与运行事件归并排序)。"""
|
||||
result = await session.execute(
|
||||
select(AuditEvent)
|
||||
.where(AuditEvent.trace_id == trace_id)
|
||||
.order_by(AuditEvent.created_at.asc())
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def summarize_by_trace(session: AsyncSession):
|
||||
"""GROUP BY trace_id:(trace_id, 计数, 最近时间),仅 trace_id 非空。供链路追踪列表聚合。"""
|
||||
result = await session.execute(
|
||||
select(AuditEvent.trace_id, func.count(), func.max(AuditEvent.created_at))
|
||||
.where(AuditEvent.trace_id.is_not(None))
|
||||
.group_by(AuditEvent.trace_id)
|
||||
)
|
||||
return [(row[0], int(row[1]), row[2]) for row in result.all()]
|
||||
@@ -0,0 +1,50 @@
|
||||
"""大周天运行记录数据访问。"""
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.big_cycle import BigCycle
|
||||
|
||||
|
||||
async def add_cycle(session: AsyncSession, cycle: BigCycle) -> BigCycle:
|
||||
session.add(cycle)
|
||||
await session.flush()
|
||||
return cycle
|
||||
|
||||
|
||||
async def get_cycle(session: AsyncSession, cycle_id: str) -> BigCycle | None:
|
||||
return await session.get(BigCycle, cycle_id)
|
||||
|
||||
|
||||
async def get_last_completed(session: AsyncSession, character_id: str) -> BigCycle | None:
|
||||
result = await session.execute(
|
||||
select(BigCycle)
|
||||
.where(
|
||||
BigCycle.character_id == character_id,
|
||||
BigCycle.completed_at.is_not(None),
|
||||
)
|
||||
.order_by(BigCycle.completed_at.desc())
|
||||
.limit(1)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def list_cycles(
|
||||
session: AsyncSession,
|
||||
character_id: str,
|
||||
*,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
) -> tuple[list[BigCycle], int]:
|
||||
conditions = [BigCycle.character_id == character_id]
|
||||
total = (
|
||||
await session.execute(select(func.count()).select_from(BigCycle).where(*conditions))
|
||||
).scalar_one()
|
||||
items_result = await session.execute(
|
||||
select(BigCycle)
|
||||
.where(*conditions)
|
||||
.order_by(BigCycle.created_at.desc())
|
||||
.offset(max(page - 1, 0) * page_size)
|
||||
.limit(page_size)
|
||||
)
|
||||
return list(items_result.scalars().all()), int(total or 0)
|
||||
@@ -0,0 +1,70 @@
|
||||
"""候选数据访问(api-spec §8;档案 / 记忆候选)。"""
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.candidates import MemoryCandidate, ProfileCandidate
|
||||
|
||||
|
||||
async def add_profile_candidates(session: AsyncSession, candidates: list[ProfileCandidate]) -> None:
|
||||
session.add_all(candidates)
|
||||
await session.flush()
|
||||
|
||||
|
||||
async def add_memory_candidates(session: AsyncSession, candidates: list[MemoryCandidate]) -> None:
|
||||
session.add_all(candidates)
|
||||
await session.flush()
|
||||
|
||||
|
||||
async def list_profile_candidates(
|
||||
session: AsyncSession,
|
||||
character_id: str,
|
||||
*,
|
||||
import_job_id: str | None = None,
|
||||
status: str | None = None,
|
||||
) -> list[ProfileCandidate]:
|
||||
conditions = [ProfileCandidate.character_id == character_id]
|
||||
if import_job_id is not None:
|
||||
conditions.append(ProfileCandidate.import_job_id == import_job_id)
|
||||
if status is not None:
|
||||
conditions.append(ProfileCandidate.status == status)
|
||||
result = await session.execute(
|
||||
select(ProfileCandidate).where(*conditions).order_by(ProfileCandidate.created_at.desc())
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def list_memory_candidates(
|
||||
session: AsyncSession,
|
||||
character_id: str,
|
||||
*,
|
||||
import_job_id: str | None = None,
|
||||
cycle_id: str | None = None,
|
||||
status: str | None = None,
|
||||
source: str | None = None,
|
||||
) -> list[MemoryCandidate]:
|
||||
conditions = [MemoryCandidate.character_id == character_id]
|
||||
if import_job_id is not None:
|
||||
conditions.append(MemoryCandidate.import_job_id == import_job_id)
|
||||
if cycle_id is not None:
|
||||
conditions.append(MemoryCandidate.cycle_id == cycle_id)
|
||||
if status is not None:
|
||||
conditions.append(MemoryCandidate.status == status)
|
||||
if source == "cycle":
|
||||
conditions.append(MemoryCandidate.import_job_id.is_(None))
|
||||
elif source == "import":
|
||||
conditions.append(MemoryCandidate.import_job_id.is_not(None))
|
||||
result = await session.execute(
|
||||
select(MemoryCandidate).where(*conditions).order_by(MemoryCandidate.created_at.desc())
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def get_profile_candidate(
|
||||
session: AsyncSession, candidate_id: str
|
||||
) -> ProfileCandidate | None:
|
||||
return await session.get(ProfileCandidate, candidate_id)
|
||||
|
||||
|
||||
async def get_memory_candidate(session: AsyncSession, candidate_id: str) -> MemoryCandidate | None:
|
||||
return await session.get(MemoryCandidate, candidate_id)
|
||||
@@ -0,0 +1,129 @@
|
||||
"""人物数据访问。"""
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.character import Character
|
||||
from app.models.memory import Memory
|
||||
|
||||
|
||||
async def get_active_character(session: AsyncSession, owner_scope: str) -> Character | None:
|
||||
result = await session.execute(
|
||||
select(Character)
|
||||
.where(
|
||||
Character.owner_scope == owner_scope,
|
||||
Character.status.in_(["active", "initialized"]),
|
||||
Character.is_system_template.is_(False),
|
||||
)
|
||||
.order_by(Character.created_at.asc())
|
||||
.limit(1)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def get_character(session: AsyncSession, character_id: str) -> Character | None:
|
||||
return await session.get(Character, character_id)
|
||||
|
||||
|
||||
async def get_system_template_by_key(session: AsyncSession, template_key: str) -> Character | None:
|
||||
result = await session.execute(
|
||||
select(Character)
|
||||
.where(
|
||||
Character.is_system_template.is_(True),
|
||||
Character.template_key == template_key,
|
||||
)
|
||||
.order_by(Character.created_at.asc())
|
||||
.limit(1)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def get_character_by_source_template(
|
||||
session: AsyncSession, source_template_id: str, owner_scope: str
|
||||
) -> Character | None:
|
||||
"""按来源模板 ID 查找已存在的用户人物,用于启动时跳过重复创建。"""
|
||||
result = await session.execute(
|
||||
select(Character)
|
||||
.where(
|
||||
Character.source_template_id == source_template_id,
|
||||
Character.owner_scope == owner_scope,
|
||||
Character.is_system_template.is_(False),
|
||||
)
|
||||
.order_by(Character.created_at.asc())
|
||||
.limit(1)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def add_character(session: AsyncSession, character: Character) -> Character:
|
||||
session.add(character)
|
||||
await session.flush()
|
||||
return character
|
||||
|
||||
|
||||
async def list_characters(
|
||||
session: AsyncSession,
|
||||
owner_scope: str,
|
||||
*,
|
||||
status: str | None = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
) -> tuple[list[Character], int]:
|
||||
"""列出某归属范围下的非模板人物,按创建时间倒序分页。"""
|
||||
conditions = [
|
||||
Character.owner_scope == owner_scope,
|
||||
Character.is_system_template.is_(False),
|
||||
]
|
||||
if status is not None:
|
||||
conditions.append(Character.status == status)
|
||||
|
||||
total = (
|
||||
await session.execute(select(func.count()).select_from(Character).where(*conditions))
|
||||
).scalar_one()
|
||||
|
||||
items_result = await session.execute(
|
||||
select(Character)
|
||||
.where(*conditions)
|
||||
.order_by(Character.created_at.desc())
|
||||
.offset(max(page - 1, 0) * page_size)
|
||||
.limit(page_size)
|
||||
)
|
||||
items = list(items_result.scalars().all())
|
||||
return items, int(total or 0)
|
||||
|
||||
|
||||
async def count_memories(session: AsyncSession, character_id: str) -> int:
|
||||
"""统计人物关联的未删除记忆数量(列表项 memory_count)。"""
|
||||
total = (
|
||||
await session.execute(
|
||||
select(func.count())
|
||||
.select_from(Memory)
|
||||
.where(Memory.character_id == character_id, Memory.deleted_at.is_(None))
|
||||
)
|
||||
).scalar_one()
|
||||
return int(total or 0)
|
||||
|
||||
|
||||
async def list_active_in_scope(
|
||||
session: AsyncSession, owner_scope: str, *, except_id: str
|
||||
) -> list[Character]:
|
||||
"""列出同归属范围下处于激活/已初始化态的其他人物(A1:激活互斥用)。"""
|
||||
result = await session.execute(
|
||||
select(Character).where(
|
||||
Character.owner_scope == owner_scope,
|
||||
Character.is_system_template.is_(False),
|
||||
Character.status.in_(["active", "initialized"]),
|
||||
Character.id != except_id,
|
||||
)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def list_system_templates(session: AsyncSession) -> list[Character]:
|
||||
"""列出所有系统模板(is_system_template=True),按 template_key 排序。"""
|
||||
result = await session.execute(
|
||||
select(Character)
|
||||
.where(Character.is_system_template.is_(True))
|
||||
.order_by(Character.template_key.asc())
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
@@ -0,0 +1,200 @@
|
||||
"""聊天数据访问。"""
|
||||
|
||||
from sqlalchemy import func, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.errors import AppError
|
||||
from app.models.chat import ChatMessage, ChatSession
|
||||
|
||||
|
||||
async def get_session(
|
||||
session: AsyncSession, session_id: str, *, owner_scope: str | None = None
|
||||
) -> ChatSession | None:
|
||||
stmt = select(ChatSession).where(ChatSession.id == session_id)
|
||||
if owner_scope is not None:
|
||||
stmt = stmt.where(ChatSession.owner_scope == owner_scope)
|
||||
result = await session.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def list_recent_messages(
|
||||
session: AsyncSession, session_id: str, *, limit: int = 12
|
||||
) -> list[ChatMessage]:
|
||||
result = await session.execute(
|
||||
select(ChatMessage)
|
||||
.where(ChatMessage.session_id == session_id, ChatMessage.deleted_at.is_(None))
|
||||
.order_by(ChatMessage.created_at.desc())
|
||||
.limit(limit)
|
||||
)
|
||||
return list(reversed(result.scalars().all()))
|
||||
|
||||
|
||||
async def list_sessions(
|
||||
session: AsyncSession,
|
||||
character_id: str,
|
||||
*,
|
||||
status: str | None = None,
|
||||
owner_scope: str | None = None,
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
) -> list[ChatSession]:
|
||||
conditions = [ChatSession.character_id == character_id, ChatSession.status != "deleted"]
|
||||
if status is not None:
|
||||
conditions.append(ChatSession.status == status)
|
||||
if owner_scope is not None:
|
||||
conditions.append(ChatSession.owner_scope == owner_scope)
|
||||
result = await session.execute(
|
||||
select(ChatSession)
|
||||
.where(*conditions)
|
||||
.order_by(ChatSession.updated_at.desc())
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def count_sessions(
|
||||
session: AsyncSession,
|
||||
character_id: str,
|
||||
*,
|
||||
status: str | None = None,
|
||||
owner_scope: str | None = None,
|
||||
) -> int:
|
||||
conditions = [ChatSession.character_id == character_id, ChatSession.status != "deleted"]
|
||||
if status is not None:
|
||||
conditions.append(ChatSession.status == status)
|
||||
if owner_scope is not None:
|
||||
conditions.append(ChatSession.owner_scope == owner_scope)
|
||||
result = await session.execute(select(func.count()).select_from(ChatSession).where(*conditions))
|
||||
return result.scalar_one()
|
||||
|
||||
|
||||
async def list_messages(
|
||||
session: AsyncSession, session_id: str, *, limit: int = 100, offset: int = 0
|
||||
) -> list[ChatMessage]:
|
||||
result = await session.execute(
|
||||
select(ChatMessage)
|
||||
.where(ChatMessage.session_id == session_id, ChatMessage.deleted_at.is_(None))
|
||||
.order_by(ChatMessage.created_at.asc())
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def count_messages(session: AsyncSession, session_id: str) -> int:
|
||||
result = await session.execute(
|
||||
select(func.count())
|
||||
.select_from(ChatMessage)
|
||||
.where(
|
||||
ChatMessage.session_id == session_id,
|
||||
ChatMessage.deleted_at.is_(None),
|
||||
)
|
||||
)
|
||||
return result.scalar_one()
|
||||
|
||||
|
||||
async def count_messages_by_sessions(
|
||||
session: AsyncSession, session_ids: list[str]
|
||||
) -> dict[str, int]:
|
||||
"""批量统计多个会话的未删除消息数(GROUP BY 聚合,避免列表查询 N+1)。"""
|
||||
if not session_ids:
|
||||
return {}
|
||||
result = await session.execute(
|
||||
select(ChatMessage.session_id, func.count())
|
||||
.where(
|
||||
ChatMessage.session_id.in_(session_ids),
|
||||
ChatMessage.deleted_at.is_(None),
|
||||
)
|
||||
.group_by(ChatMessage.session_id)
|
||||
)
|
||||
return {str(sid): int(cnt) for sid, cnt in result.all()}
|
||||
|
||||
|
||||
async def last_message_previews_by_sessions(
|
||||
session: AsyncSession, session_ids: list[str]
|
||||
) -> dict[str, str]:
|
||||
"""批量取每个会话最后一条未删除消息的内容(截断由调用方处理)。
|
||||
|
||||
用 row_number() 窗口函数取每个 session_id 的最新一条,兼容 SQLite(3.25+)
|
||||
与 PostgreSQL,避免逐会话查询(N+1)。无消息的会话不在结果中。
|
||||
"""
|
||||
if not session_ids:
|
||||
return {}
|
||||
latest = (
|
||||
select(
|
||||
ChatMessage.session_id,
|
||||
ChatMessage.content,
|
||||
func.row_number()
|
||||
.over(
|
||||
partition_by=ChatMessage.session_id,
|
||||
order_by=ChatMessage.created_at.desc(),
|
||||
)
|
||||
.label("rn"),
|
||||
).where(
|
||||
ChatMessage.session_id.in_(session_ids),
|
||||
ChatMessage.deleted_at.is_(None),
|
||||
)
|
||||
).subquery()
|
||||
result = await session.execute(
|
||||
select(latest.c.session_id, latest.c.content).where(latest.c.rn == 1)
|
||||
)
|
||||
return {str(sid): (content or "") for sid, content in result.all()}
|
||||
|
||||
|
||||
async def count_messages_by_role(session: AsyncSession, session_id: str, *, role: str) -> int:
|
||||
"""统计会话内指定角色的未删除消息数(用于判断首轮对话)。"""
|
||||
result = await session.execute(
|
||||
select(func.count())
|
||||
.select_from(ChatMessage)
|
||||
.where(
|
||||
ChatMessage.session_id == session_id,
|
||||
ChatMessage.role == role,
|
||||
ChatMessage.deleted_at.is_(None),
|
||||
)
|
||||
)
|
||||
return result.scalar_one()
|
||||
|
||||
|
||||
async def update_session_title(session: AsyncSession, session_id: str, title: str) -> None:
|
||||
"""更新会话标题;仅修改托管对象,提交时机由调用方控制。"""
|
||||
chat_session = await session.get(ChatSession, session_id)
|
||||
if chat_session is not None:
|
||||
chat_session.title = title
|
||||
|
||||
|
||||
async def soft_delete_session_messages(session: AsyncSession, session_id: str) -> None:
|
||||
"""软删除会话内全部未删除消息;提交时机由调用方控制。"""
|
||||
from app.core.time import utc_now
|
||||
|
||||
await session.execute(
|
||||
update(ChatMessage)
|
||||
.where(
|
||||
ChatMessage.session_id == session_id,
|
||||
ChatMessage.deleted_at.is_(None),
|
||||
)
|
||||
.values(deleted_at=utc_now())
|
||||
)
|
||||
|
||||
|
||||
async def get_message(session: AsyncSession, message_id: str) -> ChatMessage | None:
|
||||
"""按 ID 获取单条消息。"""
|
||||
return await session.get(ChatMessage, message_id)
|
||||
|
||||
|
||||
async def set_message_active_version(
|
||||
session: AsyncSession,
|
||||
message_id: str,
|
||||
version_idx: int,
|
||||
versions_json: str,
|
||||
) -> None:
|
||||
"""更新消息的活跃版本索引和 versions_json。"""
|
||||
from sqlalchemy import update
|
||||
|
||||
result = await session.execute(
|
||||
update(ChatMessage)
|
||||
.where(ChatMessage.id == message_id)
|
||||
.values(active_version_idx=version_idx, versions_json=versions_json)
|
||||
)
|
||||
if result.rowcount == 0:
|
||||
raise AppError("not_found", "消息不存在。")
|
||||
@@ -0,0 +1,97 @@
|
||||
"""内驱力数据访问(好奇问题 + 探索性次生目标)。"""
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.drive import CuriosityQuestion, ExploratorySubgoal
|
||||
|
||||
|
||||
async def add_curiosities(session: AsyncSession, curiosities: list[CuriosityQuestion]) -> None:
|
||||
session.add_all(curiosities)
|
||||
await session.flush()
|
||||
|
||||
|
||||
async def get_curiosity(session: AsyncSession, curiosity_id: str) -> CuriosityQuestion | None:
|
||||
return await session.get(CuriosityQuestion, curiosity_id)
|
||||
|
||||
|
||||
async def list_curiosities(
|
||||
session: AsyncSession,
|
||||
character_id: str,
|
||||
*,
|
||||
source: str | None = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
) -> tuple[list[CuriosityQuestion], int]:
|
||||
conditions = [CuriosityQuestion.character_id == character_id]
|
||||
if source is not None:
|
||||
conditions.append(CuriosityQuestion.source == source)
|
||||
total = (
|
||||
await session.execute(
|
||||
select(func.count()).select_from(CuriosityQuestion).where(*conditions)
|
||||
)
|
||||
).scalar_one()
|
||||
items = await session.execute(
|
||||
select(CuriosityQuestion)
|
||||
.where(*conditions)
|
||||
.order_by(CuriosityQuestion.created_at.desc())
|
||||
.offset(max(page - 1, 0) * page_size)
|
||||
.limit(page_size)
|
||||
)
|
||||
return list(items.scalars().all()), int(total or 0)
|
||||
|
||||
|
||||
async def add_subgoals(session: AsyncSession, subgoals: list[ExploratorySubgoal]) -> None:
|
||||
session.add_all(subgoals)
|
||||
await session.flush()
|
||||
|
||||
|
||||
async def get_subgoal(session: AsyncSession, subgoal_id: str) -> ExploratorySubgoal | None:
|
||||
return await session.get(ExploratorySubgoal, subgoal_id)
|
||||
|
||||
|
||||
async def list_subgoals(
|
||||
session: AsyncSession,
|
||||
character_id: str,
|
||||
*,
|
||||
status: str | None = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
) -> tuple[list[ExploratorySubgoal], int]:
|
||||
conditions = [ExploratorySubgoal.character_id == character_id]
|
||||
if status is not None:
|
||||
conditions.append(ExploratorySubgoal.status == status)
|
||||
total = (
|
||||
await session.execute(
|
||||
select(func.count()).select_from(ExploratorySubgoal).where(*conditions)
|
||||
)
|
||||
).scalar_one()
|
||||
items = await session.execute(
|
||||
select(ExploratorySubgoal)
|
||||
.where(*conditions)
|
||||
.order_by(ExploratorySubgoal.created_at.desc())
|
||||
.offset(max(page - 1, 0) * page_size)
|
||||
.limit(page_size)
|
||||
)
|
||||
return list(items.scalars().all()), int(total or 0)
|
||||
|
||||
|
||||
async def list_approved_subgoals(
|
||||
session: AsyncSession,
|
||||
character_id: str,
|
||||
limit: int = 5,
|
||||
) -> list[ExploratorySubgoal]:
|
||||
"""列出已批准(approved)的次生目标,按 created_at 升序排列。
|
||||
|
||||
供大周天自主探索执行阶段拾取。
|
||||
"""
|
||||
result = await session.execute(
|
||||
select(ExploratorySubgoal)
|
||||
.where(
|
||||
ExploratorySubgoal.character_id == character_id,
|
||||
ExploratorySubgoal.status == "approved",
|
||||
)
|
||||
.order_by(ExploratorySubgoal.created_at.asc())
|
||||
.limit(limit)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
@@ -0,0 +1,44 @@
|
||||
"""演化候选数据访问。"""
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.evolution import EvolutionCandidate
|
||||
|
||||
|
||||
async def add_candidates(session: AsyncSession, candidates: list[EvolutionCandidate]) -> None:
|
||||
session.add_all(candidates)
|
||||
await session.flush()
|
||||
|
||||
|
||||
async def get_candidate(session: AsyncSession, candidate_id: str) -> EvolutionCandidate | None:
|
||||
return await session.get(EvolutionCandidate, candidate_id)
|
||||
|
||||
|
||||
async def list_candidates(
|
||||
session: AsyncSession,
|
||||
character_id: str,
|
||||
*,
|
||||
status: str | None = None,
|
||||
candidate_type: str | None = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
) -> tuple[list[EvolutionCandidate], int]:
|
||||
conditions = [EvolutionCandidate.character_id == character_id]
|
||||
if status is not None:
|
||||
conditions.append(EvolutionCandidate.status == status)
|
||||
if candidate_type is not None:
|
||||
conditions.append(EvolutionCandidate.type == candidate_type)
|
||||
total = (
|
||||
await session.execute(
|
||||
select(func.count()).select_from(EvolutionCandidate).where(*conditions)
|
||||
)
|
||||
).scalar_one()
|
||||
items_result = await session.execute(
|
||||
select(EvolutionCandidate)
|
||||
.where(*conditions)
|
||||
.order_by(EvolutionCandidate.created_at.desc())
|
||||
.offset(max(page - 1, 0) * page_size)
|
||||
.limit(page_size)
|
||||
)
|
||||
return list(items_result.scalars().all()), int(total or 0)
|
||||
@@ -0,0 +1,180 @@
|
||||
"""司命台治理对象数据访问(api-spec §19.4)。
|
||||
|
||||
目标 / 感知通道 / 世界实体 / 计划 / 天工阁角色的 CRUD 数据访问;
|
||||
均为本地单用户全局对象,不挂 character_id。
|
||||
"""
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.time import utc_now
|
||||
from app.models.governance import (
|
||||
AgentRosterEntry,
|
||||
Goal,
|
||||
PerceptionChannel,
|
||||
Plan,
|
||||
ToolPolicy,
|
||||
WorldEntity,
|
||||
)
|
||||
|
||||
# —— 太一枢 · 目标 ——
|
||||
|
||||
|
||||
async def list_goals(session: AsyncSession) -> list[Goal]:
|
||||
result = await session.execute(
|
||||
select(Goal).order_by(Goal.priority.desc(), Goal.updated_at.desc())
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def get_goal(session: AsyncSession, goal_id: str) -> Goal | None:
|
||||
return await session.get(Goal, goal_id)
|
||||
|
||||
|
||||
async def add_goal(session: AsyncSession, goal: Goal) -> Goal:
|
||||
session.add(goal)
|
||||
await session.flush()
|
||||
return goal
|
||||
|
||||
|
||||
async def list_goals_by_character(
|
||||
session: AsyncSession,
|
||||
character_id: str,
|
||||
status_filter: str | None = None,
|
||||
) -> list[Goal]:
|
||||
"""按 character_id 获取目标列表;可选的 status 过滤。"""
|
||||
stmt = select(Goal).where(Goal.character_id == character_id)
|
||||
if status_filter:
|
||||
stmt = stmt.where(Goal.status == status_filter)
|
||||
stmt = stmt.order_by(Goal.priority.desc(), Goal.updated_at.desc())
|
||||
result = await session.execute(stmt)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def update_goal_progress(
|
||||
session: AsyncSession,
|
||||
goal_id: str,
|
||||
progress: float,
|
||||
) -> Goal | None:
|
||||
"""更新目标的 progress 与 updated_at 字段。"""
|
||||
goal = await session.get(Goal, goal_id)
|
||||
if goal is None:
|
||||
return None
|
||||
goal.progress = progress
|
||||
goal.updated_at = utc_now()
|
||||
await session.flush()
|
||||
return goal
|
||||
|
||||
|
||||
async def list_active_session_goal(session: AsyncSession, character_id: str) -> Goal | None:
|
||||
"""查找指定人物活跃的 session 级目标(各人物至多一个活跃 session 目标)。"""
|
||||
result = await session.execute(
|
||||
select(Goal)
|
||||
.where(
|
||||
Goal.character_id == character_id,
|
||||
Goal.level == "session",
|
||||
Goal.status == "active",
|
||||
)
|
||||
.limit(1)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
# —— 烛照枢 · 感知通道 ——
|
||||
|
||||
|
||||
async def list_perception_channels(session: AsyncSession) -> list[PerceptionChannel]:
|
||||
result = await session.execute(select(PerceptionChannel).order_by(PerceptionChannel.channel))
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def get_perception_channel(session: AsyncSession, channel: str) -> PerceptionChannel | None:
|
||||
return await session.get(PerceptionChannel, channel)
|
||||
|
||||
|
||||
async def add_perception_channel(
|
||||
session: AsyncSession, channel: PerceptionChannel
|
||||
) -> PerceptionChannel:
|
||||
session.add(channel)
|
||||
await session.flush()
|
||||
return channel
|
||||
|
||||
|
||||
# —— 河图枢 · 世界实体 ——
|
||||
|
||||
|
||||
async def list_world_entities(session: AsyncSession) -> list[WorldEntity]:
|
||||
result = await session.execute(select(WorldEntity).order_by(WorldEntity.updated_at.desc()))
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def get_world_entity(session: AsyncSession, entity_id: str) -> WorldEntity | None:
|
||||
return await session.get(WorldEntity, entity_id)
|
||||
|
||||
|
||||
async def add_world_entity(session: AsyncSession, entity: WorldEntity) -> WorldEntity:
|
||||
session.add(entity)
|
||||
await session.flush()
|
||||
return entity
|
||||
|
||||
|
||||
# —— 洛书枢 · 计划 ——
|
||||
|
||||
|
||||
async def list_plans(session: AsyncSession) -> list[Plan]:
|
||||
result = await session.execute(select(Plan).order_by(Plan.updated_at.desc()))
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def get_plan(session: AsyncSession, plan_id: str) -> Plan | None:
|
||||
return await session.get(Plan, plan_id)
|
||||
|
||||
|
||||
async def add_plan(session: AsyncSession, plan: Plan) -> Plan:
|
||||
session.add(plan)
|
||||
await session.flush()
|
||||
return plan
|
||||
|
||||
|
||||
# —— 天工阁枢 · 角色在岗 ——
|
||||
|
||||
|
||||
async def list_agent_roster(session: AsyncSession) -> list[AgentRosterEntry]:
|
||||
result = await session.execute(select(AgentRosterEntry).order_by(AgentRosterEntry.role))
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def get_agent(session: AsyncSession, role: str) -> AgentRosterEntry | None:
|
||||
return await session.get(AgentRosterEntry, role)
|
||||
|
||||
|
||||
async def add_agent(session: AsyncSession, agent: AgentRosterEntry) -> AgentRosterEntry:
|
||||
session.add(agent)
|
||||
await session.flush()
|
||||
return agent
|
||||
|
||||
|
||||
# —— 百工枢 · 工具策略(§15/§16 共享)——
|
||||
|
||||
|
||||
async def list_tool_policies(session: AsyncSession) -> list[ToolPolicy]:
|
||||
result = await session.execute(select(ToolPolicy).order_by(ToolPolicy.name))
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def get_tool_policy(session: AsyncSession, name: str) -> ToolPolicy | None:
|
||||
return await session.get(ToolPolicy, name)
|
||||
|
||||
|
||||
async def add_tool_policy(session: AsyncSession, policy: ToolPolicy) -> ToolPolicy:
|
||||
session.add(policy)
|
||||
await session.flush()
|
||||
return policy
|
||||
|
||||
|
||||
# —— 计数(供九枢健康度聚合)——
|
||||
|
||||
|
||||
async def count(session: AsyncSession, model: type) -> int:
|
||||
result = await session.execute(select(func.count()).select_from(model))
|
||||
return int(result.scalar_one())
|
||||
@@ -0,0 +1,33 @@
|
||||
"""幂等键数据访问。"""
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.idempotency import IdempotencyKey
|
||||
|
||||
|
||||
async def get_by_scope_key(session: AsyncSession, *, scope: str, key: str) -> IdempotencyKey | None:
|
||||
result = await session.execute(
|
||||
select(IdempotencyKey).where(IdempotencyKey.scope == scope, IdempotencyKey.key == key)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def list_by_trace(session: AsyncSession, trace_id: str) -> list[IdempotencyKey]:
|
||||
"""链路追踪:按 trace_id 查询幂等键(请求级),按 created_at 升序。"""
|
||||
result = await session.execute(
|
||||
select(IdempotencyKey)
|
||||
.where(IdempotencyKey.trace_id == trace_id)
|
||||
.order_by(IdempotencyKey.created_at.asc())
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def summarize_by_trace(session: AsyncSession):
|
||||
"""GROUP BY trace_id:(trace_id, 计数, 最近时间),仅 trace_id 非空。供链路追踪列表聚合。"""
|
||||
result = await session.execute(
|
||||
select(IdempotencyKey.trace_id, func.count(), func.max(IdempotencyKey.created_at))
|
||||
.where(IdempotencyKey.trace_id.is_not(None))
|
||||
.group_by(IdempotencyKey.trace_id)
|
||||
)
|
||||
return [(row[0], int(row[1]), row[2]) for row in result.all()]
|
||||
@@ -0,0 +1,65 @@
|
||||
"""资料导入数据访问(api-spec §8)。"""
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.imports import CharacterImportJob, ImportItem
|
||||
|
||||
|
||||
async def get_job(session: AsyncSession, job_id: str) -> CharacterImportJob | None:
|
||||
return await session.get(CharacterImportJob, job_id)
|
||||
|
||||
|
||||
async def list_jobs(
|
||||
session: AsyncSession,
|
||||
character_id: str,
|
||||
*,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
) -> tuple[list[CharacterImportJob], int]:
|
||||
conditions = [CharacterImportJob.character_id == character_id]
|
||||
total = (
|
||||
await session.execute(
|
||||
select(func.count()).select_from(CharacterImportJob).where(*conditions)
|
||||
)
|
||||
).scalar_one()
|
||||
items_result = await session.execute(
|
||||
select(CharacterImportJob)
|
||||
.where(*conditions)
|
||||
.order_by(CharacterImportJob.created_at.desc())
|
||||
.offset(max(page - 1, 0) * page_size)
|
||||
.limit(page_size)
|
||||
)
|
||||
return list(items_result.scalars().all()), int(total or 0)
|
||||
|
||||
|
||||
async def add_job(session: AsyncSession, job: CharacterImportJob) -> CharacterImportJob:
|
||||
session.add(job)
|
||||
await session.flush()
|
||||
return job
|
||||
|
||||
|
||||
async def list_items(
|
||||
session: AsyncSession,
|
||||
job_id: str,
|
||||
*,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
) -> tuple[list[ImportItem], int]:
|
||||
conditions = [ImportItem.import_job_id == job_id]
|
||||
total = (
|
||||
await session.execute(select(func.count()).select_from(ImportItem).where(*conditions))
|
||||
).scalar_one()
|
||||
items_result = await session.execute(
|
||||
select(ImportItem)
|
||||
.where(*conditions)
|
||||
.order_by(ImportItem.sequence.asc())
|
||||
.offset(max(page - 1, 0) * page_size)
|
||||
.limit(page_size)
|
||||
)
|
||||
return list(items_result.scalars().all()), int(total or 0)
|
||||
|
||||
|
||||
async def add_items(session: AsyncSession, items: list[ImportItem]) -> None:
|
||||
session.add_all(items)
|
||||
await session.flush()
|
||||
@@ -0,0 +1,22 @@
|
||||
"""JSON 文本字段转换工具。"""
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
|
||||
def dumps(value: Any) -> str:
|
||||
return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
|
||||
|
||||
|
||||
def loads_dict(value: str | None) -> dict[str, Any]:
|
||||
if not value:
|
||||
return {}
|
||||
data = json.loads(value)
|
||||
return data if isinstance(data, dict) else {}
|
||||
|
||||
|
||||
def loads_list(value: str | None) -> list[str]:
|
||||
if not value:
|
||||
return []
|
||||
data = json.loads(value)
|
||||
return data if isinstance(data, list) else []
|
||||
@@ -0,0 +1,81 @@
|
||||
"""世谱数据访问。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlalchemy import or_, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.lorebook import Lorebook, LorebookEntry
|
||||
|
||||
|
||||
async def list_active_books(
|
||||
session: AsyncSession,
|
||||
character_id: str,
|
||||
owner_scope: str,
|
||||
) -> list[Lorebook]:
|
||||
"""查角色的 active 世谱:character 作用域(按人物隔离)+ global 作用域(跨人物共享)。"""
|
||||
result = await session.execute(
|
||||
select(Lorebook).where(
|
||||
Lorebook.status == "active",
|
||||
Lorebook.owner_scope == owner_scope,
|
||||
or_(Lorebook.character_id == character_id, Lorebook.scope == "global"),
|
||||
)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def list_entries_for_books(
|
||||
session: AsyncSession,
|
||||
lorebook_ids: list[str],
|
||||
) -> list[LorebookEntry]:
|
||||
"""批量查多本书的启用条目,按 insertion_order 排序。"""
|
||||
if not lorebook_ids:
|
||||
return []
|
||||
result = await session.execute(
|
||||
select(LorebookEntry)
|
||||
.where(
|
||||
LorebookEntry.lorebook_id.in_(lorebook_ids),
|
||||
LorebookEntry.enabled.is_(True),
|
||||
)
|
||||
.order_by(LorebookEntry.insertion_order)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def get_book(session: AsyncSession, lorebook_id: str) -> Lorebook | None:
|
||||
return await session.get(Lorebook, lorebook_id)
|
||||
|
||||
|
||||
async def list_books(
|
||||
session: AsyncSession,
|
||||
character_id: str | None,
|
||||
owner_scope: str,
|
||||
*,
|
||||
scope: str | None = None,
|
||||
) -> list[Lorebook]:
|
||||
"""列出 active 世谱(archived 不返回)。"""
|
||||
conditions = [
|
||||
Lorebook.status == "active",
|
||||
Lorebook.owner_scope == owner_scope,
|
||||
]
|
||||
if character_id is not None:
|
||||
conditions.append(or_(Lorebook.character_id == character_id, Lorebook.scope == "global"))
|
||||
if scope is not None:
|
||||
conditions.append(Lorebook.scope == scope)
|
||||
result = await session.execute(
|
||||
select(Lorebook).where(*conditions).order_by(Lorebook.created_at.desc())
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def list_book_entries(session: AsyncSession, lorebook_id: str) -> list[LorebookEntry]:
|
||||
result = await session.execute(
|
||||
select(LorebookEntry)
|
||||
.where(LorebookEntry.lorebook_id == lorebook_id)
|
||||
.order_by(LorebookEntry.insertion_order)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def get_entry(session: AsyncSession, entry_id: str) -> LorebookEntry | None:
|
||||
return await session.get(LorebookEntry, entry_id)
|
||||
@@ -0,0 +1,32 @@
|
||||
"""MCP server 数据访问。"""
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.mcp_servers import McpServer
|
||||
|
||||
|
||||
async def add_server(session: AsyncSession, server: McpServer) -> None:
|
||||
session.add(server)
|
||||
await session.flush()
|
||||
|
||||
|
||||
async def get_server(session: AsyncSession, server_id: str) -> McpServer | None:
|
||||
return await session.get(McpServer, server_id)
|
||||
|
||||
|
||||
async def get_server_by_name(session: AsyncSession, name: str) -> McpServer | None:
|
||||
result = await session.execute(select(McpServer).where(McpServer.name == name))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def list_servers(session: AsyncSession, *, status: str | None = None) -> list[McpServer]:
|
||||
conditions = []
|
||||
if status is not None:
|
||||
conditions.append(McpServer.status == status)
|
||||
stmt = select(McpServer)
|
||||
if conditions:
|
||||
stmt = stmt.where(*conditions)
|
||||
stmt = stmt.order_by(McpServer.created_at.desc())
|
||||
result = await session.execute(stmt)
|
||||
return list(result.scalars().all())
|
||||
@@ -0,0 +1,73 @@
|
||||
"""记忆数据访问。"""
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.memory import Memory
|
||||
|
||||
|
||||
async def get_memory(session: AsyncSession, memory_id: str) -> Memory | None:
|
||||
return await session.get(Memory, memory_id)
|
||||
|
||||
|
||||
async def list_memories(
|
||||
session: AsyncSession,
|
||||
character_id: str,
|
||||
*,
|
||||
memory_type: str | None = None,
|
||||
source: str | None = None,
|
||||
page: int,
|
||||
page_size: int,
|
||||
active_only: bool = True,
|
||||
) -> tuple[list[Memory], int]:
|
||||
conditions = [Memory.character_id == character_id]
|
||||
if active_only:
|
||||
conditions.append(Memory.status == "active")
|
||||
conditions.append(Memory.deleted_at.is_(None))
|
||||
if memory_type is not None:
|
||||
conditions.append(Memory.type == memory_type)
|
||||
if source is not None:
|
||||
conditions.append(Memory.source == source)
|
||||
total_result = await session.execute(
|
||||
select(func.count()).select_from(Memory).where(*conditions)
|
||||
)
|
||||
total = int(total_result.scalar_one())
|
||||
result = await session.execute(
|
||||
select(Memory)
|
||||
.where(*conditions)
|
||||
.order_by(Memory.importance.desc(), Memory.updated_at.desc())
|
||||
.offset((page - 1) * page_size)
|
||||
.limit(page_size)
|
||||
)
|
||||
return list(result.scalars().all()), total
|
||||
|
||||
|
||||
async def list_injectable_memories(
|
||||
session: AsyncSession, character_id: str, *, limit: int
|
||||
) -> list[Memory]:
|
||||
result = await session.execute(
|
||||
select(Memory)
|
||||
.where(
|
||||
Memory.character_id == character_id,
|
||||
Memory.status == "active",
|
||||
Memory.deleted_at.is_(None),
|
||||
)
|
||||
.order_by(Memory.importance.desc(), Memory.updated_at.desc())
|
||||
.limit(limit)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def get_memories_by_ids(
|
||||
session: AsyncSession, memory_ids: list[str]
|
||||
) -> list[Memory]:
|
||||
"""按 ID 列表批量获取记忆(按重要性降序排序,而非 ID 传入顺序)。"""
|
||||
result = await session.execute(
|
||||
select(Memory).where(
|
||||
Memory.id.in_(memory_ids),
|
||||
Memory.status == "active",
|
||||
Memory.deleted_at.is_(None),
|
||||
)
|
||||
.order_by(Memory.importance.desc(), Memory.updated_at.desc())
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
@@ -0,0 +1,47 @@
|
||||
"""模型网关 endpoint 覆盖数据访问(api-spec §4.4)。
|
||||
|
||||
按 (logical_model_name, endpoint_name) 唯一;upsert 手动 find+update,兼容 SQLite。
|
||||
字段 None 表示不覆盖该字段(回退 yaml/env)。
|
||||
"""
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.governance import ModelEndpointOverride
|
||||
|
||||
|
||||
async def list_overrides(session: AsyncSession) -> list[ModelEndpointOverride]:
|
||||
result = await session.execute(select(ModelEndpointOverride))
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def get_override(
|
||||
session: AsyncSession,
|
||||
logical_model_name: str,
|
||||
endpoint_name: str,
|
||||
) -> ModelEndpointOverride | None:
|
||||
result = await session.execute(
|
||||
select(ModelEndpointOverride).where(
|
||||
ModelEndpointOverride.logical_model_name == logical_model_name,
|
||||
ModelEndpointOverride.endpoint_name == endpoint_name,
|
||||
)
|
||||
)
|
||||
return result.scalars().first()
|
||||
|
||||
|
||||
async def upsert_override(
|
||||
session: AsyncSession,
|
||||
override: ModelEndpointOverride,
|
||||
) -> ModelEndpointOverride:
|
||||
"""存在则更新非 None 字段,否则新建。返回最终实体(未 commit)。"""
|
||||
existing = await get_override(session, override.logical_model_name, override.endpoint_name)
|
||||
if existing is not None:
|
||||
existing.base_url = override.base_url
|
||||
existing.api_key = override.api_key
|
||||
existing.status = override.status
|
||||
existing.weight = override.weight
|
||||
existing.priority = override.priority
|
||||
return existing
|
||||
session.add(override)
|
||||
await session.flush()
|
||||
return override
|
||||
@@ -0,0 +1,84 @@
|
||||
"""人格数据访问。"""
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.personality import PersonalityProposal, PersonalityVersion
|
||||
|
||||
|
||||
async def get_personality(session: AsyncSession, personality_id: str) -> PersonalityVersion | None:
|
||||
return await session.get(PersonalityVersion, personality_id)
|
||||
|
||||
|
||||
async def get_latest_personality(
|
||||
session: AsyncSession, character_id: str
|
||||
) -> PersonalityVersion | None:
|
||||
result = await session.execute(
|
||||
select(PersonalityVersion)
|
||||
.where(PersonalityVersion.character_id == character_id)
|
||||
.order_by(PersonalityVersion.version.desc())
|
||||
.limit(1)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def list_personality_versions(
|
||||
session: AsyncSession,
|
||||
character_id: str,
|
||||
*,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
) -> tuple[list[PersonalityVersion], int]:
|
||||
"""按版本号倒序分页返回某人物的人格版本历史,返回 (条目, 总数)。"""
|
||||
cond = PersonalityVersion.character_id == character_id
|
||||
total = int(
|
||||
(
|
||||
await session.execute(select(func.count()).select_from(PersonalityVersion).where(cond))
|
||||
).scalar_one()
|
||||
)
|
||||
result = await session.execute(
|
||||
select(PersonalityVersion)
|
||||
.where(cond)
|
||||
.order_by(PersonalityVersion.version.desc())
|
||||
.offset((page - 1) * page_size)
|
||||
.limit(page_size)
|
||||
)
|
||||
return list(result.scalars().all()), total
|
||||
|
||||
|
||||
async def add_proposal(session: AsyncSession, proposal: PersonalityProposal) -> PersonalityProposal:
|
||||
session.add(proposal)
|
||||
await session.flush()
|
||||
return proposal
|
||||
|
||||
|
||||
async def get_proposal(session: AsyncSession, proposal_id: str) -> PersonalityProposal | None:
|
||||
return await session.get(PersonalityProposal, proposal_id)
|
||||
|
||||
|
||||
async def list_proposals(
|
||||
session: AsyncSession,
|
||||
*,
|
||||
character_id: str | None = None,
|
||||
status: str | None = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
) -> tuple[list[PersonalityProposal], int]:
|
||||
"""按创建时间倒序分页返回人格修改建议;可选按人物/状态过滤,返回 (条目, 总数)。"""
|
||||
conds = []
|
||||
if character_id is not None:
|
||||
conds.append(PersonalityProposal.character_id == character_id)
|
||||
if status is not None:
|
||||
conds.append(PersonalityProposal.status == status)
|
||||
count_stmt = select(func.count()).select_from(PersonalityProposal)
|
||||
list_stmt = select(PersonalityProposal)
|
||||
if conds:
|
||||
count_stmt = count_stmt.where(*conds)
|
||||
list_stmt = list_stmt.where(*conds)
|
||||
total = int((await session.execute(count_stmt)).scalar_one())
|
||||
result = await session.execute(
|
||||
list_stmt.order_by(PersonalityProposal.created_at.desc())
|
||||
.offset((page - 1) * page_size)
|
||||
.limit(page_size)
|
||||
)
|
||||
return list(result.scalars().all()), total
|
||||
@@ -0,0 +1,31 @@
|
||||
"""灵核情绪状态数据访问(设计 §11.4)。"""
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.personality_emotion import CharacterEmotionState
|
||||
|
||||
|
||||
async def get_state(session: AsyncSession, character_id: str) -> CharacterEmotionState | None:
|
||||
"""读取某角色当前情绪状态(单行 per character)。"""
|
||||
return await session.get(CharacterEmotionState, character_id)
|
||||
|
||||
|
||||
async def upsert_state(
|
||||
session: AsyncSession, state: CharacterEmotionState
|
||||
) -> CharacterEmotionState:
|
||||
"""写入情绪状态(存在则更新字段,不存在则插入)。
|
||||
|
||||
跨方言(SQLite + Postgres)ORM 层 upsert,不依赖 ON CONFLICT。
|
||||
``updated_at`` 由 SQLAlchemy ``onupdate`` 自动刷新。
|
||||
"""
|
||||
existing = await session.get(CharacterEmotionState, state.character_id)
|
||||
if existing is not None:
|
||||
existing.mood = state.mood
|
||||
existing.intensity = state.intensity
|
||||
existing.baseline_mood = state.baseline_mood
|
||||
existing.baseline_intensity = state.baseline_intensity
|
||||
existing.last_trigger_event_id = state.last_trigger_event_id
|
||||
return existing
|
||||
session.add(state)
|
||||
await session.flush()
|
||||
return state
|
||||
@@ -0,0 +1,24 @@
|
||||
"""生成偏好数据访问(模型网关 §4.3)。
|
||||
|
||||
本地单用户单行,按 owner_scope 固定;无则由 service 层返回默认值。
|
||||
"""
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.governance import GenerationPreferences
|
||||
|
||||
|
||||
async def get_preferences(session: AsyncSession, owner_scope: str) -> GenerationPreferences | None:
|
||||
result = await session.execute(
|
||||
select(GenerationPreferences).where(GenerationPreferences.owner_scope == owner_scope)
|
||||
)
|
||||
return result.scalars().first()
|
||||
|
||||
|
||||
async def add_preferences(
|
||||
session: AsyncSession, prefs: GenerationPreferences
|
||||
) -> GenerationPreferences:
|
||||
session.add(prefs)
|
||||
await session.flush()
|
||||
return prefs
|
||||
@@ -0,0 +1,137 @@
|
||||
"""运行时数据访问。"""
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.runtime import Event, Run
|
||||
|
||||
|
||||
async def get_run(session: AsyncSession, run_id: str) -> Run | None:
|
||||
return await session.get(Run, run_id)
|
||||
|
||||
|
||||
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;可选按人物/会话/状态过滤,返回 (条目, 总数)。"""
|
||||
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)
|
||||
list_stmt = select(Run)
|
||||
if conds:
|
||||
count_stmt = count_stmt.where(*conds)
|
||||
list_stmt = list_stmt.where(*conds)
|
||||
total = int((await session.execute(count_stmt)).scalar_one())
|
||||
result = await session.execute(
|
||||
list_stmt.order_by(Run.created_at.desc()).offset((page - 1) * page_size).limit(page_size)
|
||||
)
|
||||
return list(result.scalars().all()), total
|
||||
|
||||
|
||||
async def count_events(session: AsyncSession, run_id: str) -> int:
|
||||
result = await session.execute(
|
||||
select(func.count()).select_from(Event).where(Event.run_id == run_id)
|
||||
)
|
||||
return int(result.scalar_one())
|
||||
|
||||
|
||||
async def next_event_sequence(session: AsyncSession, run_id: str) -> int:
|
||||
result = await session.execute(select(func.max(Event.sequence)).where(Event.run_id == run_id))
|
||||
current = result.scalar_one_or_none()
|
||||
return int(current or 0) + 1
|
||||
|
||||
|
||||
async def get_event(session: AsyncSession, event_id: str) -> Event | None:
|
||||
return await session.get(Event, event_id)
|
||||
|
||||
|
||||
async def has_active_runs(
|
||||
session: AsyncSession,
|
||||
character_id: str,
|
||||
) -> bool:
|
||||
"""检查指定角色是否有活跃的进行中 run(大周天任务中断防护用)。"""
|
||||
result = await session.execute(
|
||||
select(func.count()).select_from(Run).where(
|
||||
Run.character_id == character_id,
|
||||
Run.status == "running",
|
||||
)
|
||||
)
|
||||
return int(result.scalar_one()) > 0
|
||||
|
||||
|
||||
async def list_events_by_run(
|
||||
session: AsyncSession,
|
||||
run_id: str,
|
||||
*,
|
||||
visibility: str | None = None,
|
||||
limit: int = 200,
|
||||
offset: int = 0,
|
||||
) -> tuple[list[Event], int]:
|
||||
"""按 run_id 查询事件,按 sequence 升序;可选 visibility 过滤。返回 (条目, 总数)。"""
|
||||
conditions = [Event.run_id == run_id]
|
||||
if visibility is not None:
|
||||
conditions.append(Event.visibility == visibility)
|
||||
total = int(
|
||||
(
|
||||
await session.execute(select(func.count()).select_from(Event).where(*conditions))
|
||||
).scalar_one()
|
||||
)
|
||||
result = await session.execute(
|
||||
select(Event).where(*conditions).order_by(Event.sequence.asc()).offset(offset).limit(limit)
|
||||
)
|
||||
return list(result.scalars().all()), total
|
||||
|
||||
|
||||
async def list_public_events_after(
|
||||
session: AsyncSession, run_id: str, *, after_sequence: int = 0, limit: int = 100
|
||||
) -> list[Event]:
|
||||
result = await session.execute(
|
||||
select(Event)
|
||||
.where(
|
||||
Event.run_id == run_id,
|
||||
Event.visibility == "public",
|
||||
Event.sequence > after_sequence,
|
||||
)
|
||||
.order_by(Event.sequence.asc())
|
||||
.limit(limit)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def list_events_by_trace(session: AsyncSession, trace_id: str) -> list[Event]:
|
||||
"""链路追踪:按 trace_id 查询运行事件,按 created_at 升序(与审计/幂等归并排序)。"""
|
||||
result = await session.execute(
|
||||
select(Event).where(Event.trace_id == trace_id).order_by(Event.created_at.asc())
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def summarize_by_trace(session: AsyncSession):
|
||||
"""GROUP BY trace_id:(trace_id, 计数, 最近时间),仅 trace_id 非空。供链路追踪列表聚合。"""
|
||||
result = await session.execute(
|
||||
select(Event.trace_id, func.count(), func.max(Event.created_at))
|
||||
.where(Event.trace_id.is_not(None))
|
||||
.group_by(Event.trace_id)
|
||||
)
|
||||
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]})
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Skill 数据访问。"""
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.skills import Skill
|
||||
|
||||
|
||||
async def add_skill(session: AsyncSession, skill: Skill) -> None:
|
||||
session.add(skill)
|
||||
await session.flush()
|
||||
|
||||
|
||||
async def get_skill(session: AsyncSession, skill_id: str) -> Skill | None:
|
||||
return await session.get(Skill, skill_id)
|
||||
|
||||
|
||||
async def get_skill_by_name(session: AsyncSession, name: str) -> Skill | None:
|
||||
result = await session.execute(select(Skill).where(Skill.name == name))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def list_skills(session: AsyncSession, *, status: str | None = None) -> list[Skill]:
|
||||
conditions = []
|
||||
if status is not None:
|
||||
conditions.append(Skill.status == status)
|
||||
stmt = select(Skill)
|
||||
if conditions:
|
||||
stmt = stmt.where(*conditions)
|
||||
stmt = stmt.order_by(Skill.created_at.desc())
|
||||
result = await session.execute(stmt)
|
||||
return list(result.scalars().all())
|
||||
@@ -0,0 +1,49 @@
|
||||
"""时态纪念日数据访问(辰正 §5.3)。"""
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.time_anniversary import TimeAnniversary
|
||||
|
||||
|
||||
async def create_anniversary(
|
||||
session: AsyncSession, anniversary: TimeAnniversary
|
||||
) -> TimeAnniversary:
|
||||
session.add(anniversary)
|
||||
await session.flush()
|
||||
await session.refresh(anniversary)
|
||||
return anniversary
|
||||
|
||||
|
||||
async def get_anniversary(session: AsyncSession, anniversary_id: str) -> TimeAnniversary | None:
|
||||
return await session.get(TimeAnniversary, anniversary_id)
|
||||
|
||||
|
||||
async def list_anniversaries(session: AsyncSession, character_id: str) -> list[TimeAnniversary]:
|
||||
result = await session.execute(
|
||||
select(TimeAnniversary)
|
||||
.where(TimeAnniversary.character_id == character_id)
|
||||
.order_by(TimeAnniversary.date.asc())
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def update_anniversary(
|
||||
session: AsyncSession, anniversary_id: str, **fields: object
|
||||
) -> TimeAnniversary | None:
|
||||
anniversary = await session.get(TimeAnniversary, anniversary_id)
|
||||
if anniversary is None:
|
||||
return None
|
||||
for key, value in fields.items():
|
||||
setattr(anniversary, key, value)
|
||||
await session.flush()
|
||||
return anniversary
|
||||
|
||||
|
||||
async def delete_anniversary(session: AsyncSession, anniversary_id: str) -> bool:
|
||||
anniversary = await session.get(TimeAnniversary, anniversary_id)
|
||||
if anniversary is None:
|
||||
return False
|
||||
await session.delete(anniversary)
|
||||
await session.flush()
|
||||
return True
|
||||
@@ -0,0 +1,104 @@
|
||||
"""世界模型运行时数据访问(api-spec §12,按人物隔离)。"""
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.world import CharacterWorldEntity, WorldRelation
|
||||
|
||||
|
||||
async def list_entities(
|
||||
session: AsyncSession,
|
||||
character_id: str,
|
||||
*,
|
||||
entity_type: str | None = None,
|
||||
status: str | None = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
) -> tuple[list[CharacterWorldEntity], int]:
|
||||
conditions = [CharacterWorldEntity.character_id == character_id]
|
||||
if entity_type is not None:
|
||||
conditions.append(CharacterWorldEntity.type == entity_type)
|
||||
if status is not None:
|
||||
conditions.append(CharacterWorldEntity.status == status)
|
||||
total = (
|
||||
await session.execute(
|
||||
select(func.count()).select_from(CharacterWorldEntity).where(*conditions)
|
||||
)
|
||||
).scalar_one()
|
||||
items_result = await session.execute(
|
||||
select(CharacterWorldEntity)
|
||||
.where(*conditions)
|
||||
.order_by(CharacterWorldEntity.updated_at.desc())
|
||||
.offset(max(page - 1, 0) * page_size)
|
||||
.limit(page_size)
|
||||
)
|
||||
return list(items_result.scalars().all()), int(total or 0)
|
||||
|
||||
|
||||
async def get_entity(session: AsyncSession, entity_id: str) -> CharacterWorldEntity | None:
|
||||
return await session.get(CharacterWorldEntity, entity_id)
|
||||
|
||||
|
||||
async def add_entity(session: AsyncSession, entity: CharacterWorldEntity) -> CharacterWorldEntity:
|
||||
session.add(entity)
|
||||
await session.flush()
|
||||
return entity
|
||||
|
||||
|
||||
async def list_relations(
|
||||
session: AsyncSession,
|
||||
character_id: str,
|
||||
*,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
) -> tuple[list[WorldRelation], int]:
|
||||
conditions = [WorldRelation.character_id == character_id]
|
||||
total = (
|
||||
await session.execute(select(func.count()).select_from(WorldRelation).where(*conditions))
|
||||
).scalar_one()
|
||||
items_result = await session.execute(
|
||||
select(WorldRelation)
|
||||
.where(*conditions)
|
||||
.order_by(WorldRelation.updated_at.desc())
|
||||
.offset(max(page - 1, 0) * page_size)
|
||||
.limit(page_size)
|
||||
)
|
||||
return list(items_result.scalars().all()), int(total or 0)
|
||||
|
||||
|
||||
async def get_relation(session: AsyncSession, relation_id: str) -> WorldRelation | None:
|
||||
return await session.get(WorldRelation, relation_id)
|
||||
|
||||
|
||||
async def add_relation(session: AsyncSession, relation: WorldRelation) -> WorldRelation:
|
||||
session.add(relation)
|
||||
await session.flush()
|
||||
return relation
|
||||
|
||||
|
||||
async def delete_relation(session: AsyncSession, relation_id: str) -> None:
|
||||
"""删除世界关系。"""
|
||||
relation = await session.get(WorldRelation, relation_id)
|
||||
if relation:
|
||||
await session.delete(relation)
|
||||
await session.flush()
|
||||
|
||||
|
||||
async def list_all_entities(session: AsyncSession, character_id: str) -> list[CharacterWorldEntity]:
|
||||
"""获取人物的所有世界实体(无分页)。"""
|
||||
result = await session.execute(
|
||||
select(CharacterWorldEntity)
|
||||
.where(CharacterWorldEntity.character_id == character_id)
|
||||
.order_by(CharacterWorldEntity.updated_at.desc())
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def list_all_relations(session: AsyncSession, character_id: str) -> list[WorldRelation]:
|
||||
"""获取人物的所有关系(无分页)。"""
|
||||
result = await session.execute(
|
||||
select(WorldRelation)
|
||||
.where(WorldRelation.character_id == character_id)
|
||||
.order_by(WorldRelation.updated_at.desc())
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
Reference in new issue
Block a user