refactor(schema): 重构数据库模型和 schema,清理废弃表

This commit is contained in:
qzl
2026-04-07 18:43:34 +08:00
parent a65d041436
commit b18a205bf3
19 changed files with 101 additions and 426 deletions
+2
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
from .agent_chat_message import AgentChatMessage
from .agent_chat_session import AgentChatSession
from .auth_user import AuthUser
from .invite_code import InviteCode
from .llm import Llm
from .llm_factory import LlmFactory
from .points_ledger import PointsLedger
@@ -14,6 +15,7 @@ __all__ = [
"AgentChatMessage",
"AgentChatSession",
"AuthUser",
"InviteCode",
"Llm",
"LlmFactory",
"PointsLedger",
+1 -3
View File
@@ -21,6 +21,4 @@ class Llm(TimestampMixin, SoftDeleteMixin, Base):
nullable=False,
index=True,
)
model_code: Mapped[str] = mapped_column(
String(50), nullable=False, unique=True, index=True
)
model_code: Mapped[str] = mapped_column(String(50), nullable=False, unique=True)
+1 -3
View File
@@ -15,8 +15,6 @@ class LlmFactory(TimestampMixin, SoftDeleteMixin, Base):
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
name: Mapped[str] = mapped_column(
String(50), nullable=False, unique=True, index=True
)
name: Mapped[str] = mapped_column(String(50), nullable=False, unique=True)
request_url: Mapped[str] = mapped_column(String(255), nullable=False)
avatar: Mapped[str | None] = mapped_column(Text, nullable=True)
+5
View File
@@ -34,3 +34,8 @@ class Profile(TimestampMixin, SoftDeleteMixin, Base):
server_default=text("'{}'::jsonb"),
default=dict,
)
referred_by: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True),
ForeignKey("profiles.id", ondelete="SET NULL"),
nullable=True,
)
+3 -16
View File
@@ -3,7 +3,7 @@ from __future__ import annotations
from enum import Enum
from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field, model_validator
from pydantic import BaseModel, ConfigDict, Field
from schemas.agent.ui_hints import UiHintsPayload
from schemas.domain.divination import DerivedDivinationData
@@ -45,7 +45,6 @@ class WorkerAgentOutputLite(BaseModel):
status: RunStatus = RunStatus.SUCCESS
sign_level: Literal["上上签", "中上签", "中下签", "下下签"]
summary: str = Field(min_length=1, max_length=300)
conclusion: list[str] = Field(min_length=1, max_length=6)
focus_points: list[str] = Field(default_factory=list, max_length=6)
advice: list[str] = Field(min_length=1, max_length=6)
@@ -53,20 +52,6 @@ class WorkerAgentOutputLite(BaseModel):
answer: str = Field(min_length=1, max_length=4000)
error: ErrorInfo | None = None
# Backward-compatible shadow fields for legacy consumers.
key_points: list[str] = Field(default_factory=list, max_length=6)
result_type: str = Field(default="structured_payload")
suggested_actions: list[str] = Field(default_factory=list, max_length=6)
divination_derived: DerivedDivinationData | None = None
@model_validator(mode="after")
def sync_compatibility_fields(self) -> WorkerAgentOutputLite:
if not self.key_points and self.focus_points:
self.key_points = list(self.focus_points)
if not self.suggested_actions and self.advice:
self.suggested_actions = list(self.advice)
return self
class WorkerAgentOutputRich(WorkerAgentOutputLite):
ui_hints: UiHintsPayload | None = None
@@ -75,6 +60,8 @@ class WorkerAgentOutputRich(WorkerAgentOutputLite):
class AgentOutput(WorkerAgentOutputRich):
model_config = ConfigDict(extra="forbid")
divination_derived: DerivedDivinationData | None = None
WorkerAgentOutput = WorkerAgentOutputLite | WorkerAgentOutputRich
-124
View File
@@ -1,124 +0,0 @@
from __future__ import annotations
from datetime import datetime
from enum import Enum
from typing import Protocol
from uuid import UUID
from core.agentscope.tools.tool_config import AgentTool
from pydantic import BaseModel, ConfigDict, Field, model_validator
from schemas.enums import AutomationJobStatus, ScheduleType
class AutomationJobLike(Protocol):
id: UUID
owner_id: UUID
bootstrap_key: str | None
title: str
config: dict[str, object]
next_run_at: datetime
timezone: str
last_run_at: datetime | None
status: AutomationJobStatus
created_by: UUID | None
created_at: datetime
updated_at: datetime
class ContextSource(str, Enum):
LATEST_CHAT = "latest_chat"
class ContextWindowMode(str, Enum):
DAY = "day"
NUMBER = "number"
class MessageContextConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
source: ContextSource = ContextSource.LATEST_CHAT
window_mode: ContextWindowMode = ContextWindowMode.DAY
window_count: int = Field(default=2, ge=1, le=200)
class ScheduleRunAt(BaseModel):
model_config = ConfigDict(extra="forbid")
hour: int = Field(default=8, ge=0, le=23)
minute: int = Field(default=0, ge=0, le=59)
class ScheduleConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
type: ScheduleType
run_at: ScheduleRunAt
weekdays: list[int] | None = None
@model_validator(mode="after")
def validate_weekdays(self) -> "ScheduleConfig":
if self.type == ScheduleType.WEEKLY:
if not self.weekdays:
raise ValueError("weekdays is required when schedule type is weekly")
invalid = [day for day in self.weekdays if day < 1 or day > 7]
if invalid:
raise ValueError("weekdays must be within 1-7")
self.weekdays = sorted(set(self.weekdays))
else:
self.weekdays = None
return self
class RuntimeConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
enabled_tools: list[AgentTool] = Field(default_factory=list, max_length=32)
context: MessageContextConfig = Field(default_factory=MessageContextConfig)
class AutomationJobConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
enabled_tools: list[AgentTool] | None = Field(default=None, max_length=32)
context: MessageContextConfig | None = None
input_template: str | None = Field(default=None, min_length=1, max_length=4000)
schedule: ScheduleConfig | None = None
class AutomationJob(BaseModel):
model_config = ConfigDict(extra="forbid")
id: UUID
owner_id: UUID
bootstrap_key: str | None = Field(default=None, min_length=1, max_length=64)
title: str = Field(..., min_length=1, max_length=255)
config: AutomationJobConfig
next_run_at: datetime
timezone: str = Field(default="UTC", min_length=1, max_length=50)
last_run_at: datetime | None = None
status: AutomationJobStatus
created_by: UUID | None = None
created_at: datetime
updated_at: datetime
@classmethod
def from_orm(cls, obj: object) -> "AutomationJob":
return cls(
id=getattr(obj, "id"),
owner_id=getattr(obj, "owner_id"),
bootstrap_key=getattr(obj, "bootstrap_key"),
title=getattr(obj, "title"),
config=AutomationJobConfig.model_validate(getattr(obj, "config", {}) or {}),
next_run_at=getattr(obj, "next_run_at"),
timezone=getattr(obj, "timezone"),
last_run_at=getattr(obj, "last_run_at"),
status=getattr(obj, "status"),
created_by=getattr(obj, "created_by"),
created_at=getattr(obj, "created_at"),
updated_at=getattr(obj, "updated_at"),
)
@property
def is_system(self) -> bool:
return self.bootstrap_key is not None
@@ -1,11 +0,0 @@
from __future__ import annotations
from typing import ClassVar
from pydantic import BaseModel, ConfigDict
class SessionStateSnapshot(BaseModel):
model_config: ClassVar[ConfigDict] = ConfigDict(extra="allow")
pass
-81
View File
@@ -1,81 +0,0 @@
from __future__ import annotations
import json
from typing import ClassVar, Literal, Union
from pydantic import BaseModel, ConfigDict, Field
from schemas.enums import InboxMessageStatus, InboxMessageType
__all__ = [
"InboxMessageType",
"InboxMessageStatus",
"CalendarInviteContent",
"CalendarUpdateContent",
"CalendarDeleteContent",
"FriendshipContent",
"CalendarContent",
"InboxMessageContent",
"parse_calendar_content",
]
class CalendarInviteContent(BaseModel):
model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid")
type: Literal["invite"]
permission: int = Field(..., description="权限: 1=view, 4=edit, 8=invite")
action: Literal["pending"] = "pending"
class CalendarUpdateContent(BaseModel):
model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid")
type: Literal["update"]
title: str = Field(..., description="事件标题")
action: Literal["updated"] = "updated"
class CalendarDeleteContent(BaseModel):
model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid")
type: Literal["delete"]
title: str = Field(..., description="事件标题")
action: Literal["deleted"] = "deleted"
class FriendshipContent(BaseModel):
model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid")
type: Literal["request"]
message: str | None = Field(None, description="好友申请消息")
CalendarContent = Union[
CalendarInviteContent,
CalendarUpdateContent,
CalendarDeleteContent,
]
InboxMessageContent = Union[
CalendarInviteContent,
CalendarUpdateContent,
CalendarDeleteContent,
FriendshipContent,
]
def parse_calendar_content(content: str | None) -> CalendarContent | None:
if not content:
return None
try:
data = json.loads(content)
content_type = data.get("type")
if content_type == "invite":
return CalendarInviteContent(**data)
if content_type == "update":
return CalendarUpdateContent(**data)
if content_type == "delete":
return CalendarDeleteContent(**data)
raise ValueError(f"Unknown calendar content type: {content_type}")
except Exception:
return None
+50
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
from datetime import datetime
from decimal import Decimal
from typing import Literal
from uuid import UUID
@@ -7,6 +8,7 @@ from uuid import UUID
from pydantic import BaseModel, ConfigDict, Field, model_validator
from ..enums import (
PointsBizType,
PointsChangeType,
PointsOperatorType,
)
@@ -76,3 +78,51 @@ def parse_points_ledger_metadata(
if change_type == PointsChangeType.GRANT:
return GrantLedgerMetadata.model_validate(payload)
return AdjustLedgerMetadata.model_validate(payload)
class ApplyPointsChangeCommand(BaseModel):
model_config = ConfigDict(extra="forbid")
user_id: UUID
change_type: PointsChangeType
biz_type: PointsBizType | None = None
biz_id: UUID | None = None
event_id: str = Field(min_length=1, max_length=64)
amount: int = Field(gt=0)
direction: Literal[1, -1]
operator_id: UUID | None = None
metadata: PointsLedgerMetadata
occurred_at: datetime | None = None
@model_validator(mode="after")
def validate_change_type_contract(self) -> "ApplyPointsChangeCommand":
if self.change_type == PointsChangeType.REGISTER:
if (
self.direction != 1
or self.biz_type is not None
or self.biz_id is not None
):
raise ValueError("register must use direction=1 and no biz binding")
return self
if self.change_type == PointsChangeType.CONSUME:
if (
self.direction != -1
or self.biz_type != PointsBizType.CHAT
or self.biz_id is None
):
raise ValueError("consume must use direction=-1 and chat binding")
return self
if self.change_type == PointsChangeType.GRANT:
if (
self.direction != 1
or self.biz_type != PointsBizType.CHAT
or self.biz_id is None
):
raise ValueError("grant must use direction=1 and chat binding")
return self
if self.biz_type != PointsBizType.CHAT or self.biz_id is None:
raise ValueError("adjust must use chat binding")
return self
-43
View File
@@ -1,43 +0,0 @@
from __future__ import annotations
from enum import Enum
from typing import ClassVar, Literal
from uuid import UUID
from pydantic import BaseModel, ConfigDict, Field
from schemas.enums import ScheduleItemSourceType, ScheduleItemStatus
__all__ = [
"AttachmentType",
"ScheduleItemMetadataAttachment",
"ScheduleItemMetadata",
"ScheduleItemSourceType",
"ScheduleItemStatus",
]
class AttachmentType(str, Enum):
DOCUMENT = "document"
REMINDER = "reminder"
class ScheduleItemMetadataAttachment(BaseModel):
model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid")
name: str
type: AttachmentType
visible_to: list[UUID] = Field(default_factory=list)
url: str | None = None
note: str | None = None
content: str | None = None
class ScheduleItemMetadata(BaseModel):
model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid")
color: str | None = Field(default=None, pattern=r"^#[0-9A-Fa-f]{6}$")
location: str | None = None
notes: str | None = None
attachments: list[ScheduleItemMetadataAttachment] = Field(default_factory=list)
reminder_minutes: int | None = Field(default=None, ge=0, le=10080)
version: Literal[1] = 1
-7
View File
@@ -1,7 +0,0 @@
from __future__ import annotations
from typing import Annotated
from pydantic import Field
TodoOrder = Annotated[int, Field(ge=0)]
-88
View File
@@ -1,88 +0,0 @@
from __future__ import annotations
from datetime import datetime
from typing import Literal
from uuid import UUID
from pydantic import BaseModel, ConfigDict, Field, model_validator
from ..domain.points import PointsLedgerMetadata
from ..enums import PointsBizType, PointsChangeType
class UserPointsSnapshot(BaseModel):
model_config = ConfigDict(from_attributes=True)
user_id: UUID
balance: int = Field(ge=0)
frozen_balance: int = Field(ge=0)
lifetime_earned: int = Field(ge=0)
lifetime_spent: int = Field(ge=0)
version: int = Field(ge=0)
created_at: datetime
updated_at: datetime
class PointsLedgerEntry(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: UUID
user_id: UUID
direction: Literal[1, -1]
amount: int = Field(gt=0)
balance_after: int = Field(ge=0)
change_type: PointsChangeType
biz_type: PointsBizType | None = None
biz_id: UUID | None = None
event_id: str = Field(min_length=1, max_length=64)
operator_id: UUID | None = None
metadata: PointsLedgerMetadata = Field(validation_alias="metadata_json")
created_at: datetime
class ApplyPointsChangeCommand(BaseModel):
model_config = ConfigDict(extra="forbid")
user_id: UUID
change_type: PointsChangeType
biz_type: PointsBizType | None = None
biz_id: UUID | None = None
event_id: str = Field(min_length=1, max_length=64)
amount: int = Field(gt=0)
direction: Literal[1, -1]
operator_id: UUID | None = None
metadata: PointsLedgerMetadata
occurred_at: datetime | None = None
@model_validator(mode="after")
def validate_change_type_contract(self) -> "ApplyPointsChangeCommand":
if self.change_type == PointsChangeType.REGISTER:
if (
self.direction != 1
or self.biz_type is not None
or self.biz_id is not None
):
raise ValueError("register must use direction=1 and no biz binding")
return self
if self.change_type == PointsChangeType.CONSUME:
if (
self.direction != -1
or self.biz_type != PointsBizType.CHAT
or self.biz_id is None
):
raise ValueError("consume must use direction=-1 and chat binding")
return self
if self.change_type == PointsChangeType.GRANT:
if (
self.direction != 1
or self.biz_type != PointsBizType.CHAT
or self.biz_id is None
):
raise ValueError("grant must use direction=1 and chat binding")
return self
if self.biz_type != PointsBizType.CHAT or self.biz_id is None:
raise ValueError("adjust must use chat binding")
return self
+8 -3
View File
@@ -41,17 +41,22 @@ class PreferenceSettings(BaseModel):
return normalized
class NotificationSettings(BaseModel):
allow_notifications: bool = True
allow_vibration: bool = True
class ProfileSettingsV1(BaseModel):
version: Literal[1] = 1
preferences: PreferenceSettings = Field(default_factory=PreferenceSettings)
privacy: dict = Field(default_factory=dict)
notification: dict = Field(default_factory=dict)
privacy: dict[str, object] = Field(default_factory=dict)
notification: NotificationSettings = Field(default_factory=NotificationSettings)
ProfileSettingsUnion = ProfileSettingsV1
def parse_profile_settings(raw: dict | None) -> ProfileSettingsUnion:
def parse_profile_settings(raw: dict[str, object] | None) -> ProfileSettingsUnion:
payload = dict(raw or {})
payload.setdefault("version", 1)
return ProfileSettingsV1.model_validate(payload)
-4
View File
@@ -208,15 +208,11 @@ class HistoryAgentOutput(BaseModel):
status: Literal["success", "failed"] | None = None
sign_level: Literal["上上签", "中上签", "中下签", "下下签"] | None = None
summary: str | None = None
conclusion: list[str] = Field(default_factory=list)
focus_points: list[str] = Field(default_factory=list)
advice: list[str] = Field(default_factory=list)
keywords: list[str] = Field(default_factory=list)
answer: str | None = None
key_points: list[str] = Field(default_factory=list)
result_type: str | None = None
suggested_actions: list[str] = Field(default_factory=list)
divination_derived: DerivedDivinationData | None = None
+1 -36
View File
@@ -109,11 +109,7 @@ def _extract_worker_agent_output(
try:
agent_output = AgentOutput.model_validate(agent_output_data)
except Exception:
normalized_payload = _normalize_agent_output_payload(agent_output_data)
try:
agent_output = AgentOutput.model_validate(normalized_payload)
except Exception:
return None
return None
if not agent_output:
return None
@@ -123,37 +119,6 @@ def _extract_worker_agent_output(
return payload or None
def _normalize_agent_output_payload(agent_output_data: Any) -> dict[str, Any] | None:
if not isinstance(agent_output_data, dict):
return None
normalized = dict(agent_output_data)
derived = normalized.get("divination_derived")
if isinstance(derived, dict):
normalized["divination_derived"] = _normalize_divination_derived(derived)
return normalized
def _normalize_divination_derived(value: Any) -> Any:
if isinstance(value, dict):
result: dict[str, Any] = {}
for key, item in value.items():
normalized_key = _snake_to_camel(key)
result[normalized_key] = _normalize_divination_derived(item)
return result
if isinstance(value, list):
return [_normalize_divination_derived(item) for item in value]
return value
def _snake_to_camel(value: str) -> str:
if "_" not in value:
return value
parts = value.split("_")
if not parts:
return value
return parts[0] + "".join(part[:1].upper() + part[1:] for part in parts[1:])
def mime_to_suffix(mime_type: str) -> str:
mapping = {
"image/png": "png",
+1 -1
View File
@@ -8,7 +8,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from models.points_ledger import PointsLedger
from models.user_points import UserPoints
from schemas.shared.points import ApplyPointsChangeCommand
from schemas.domain.points import ApplyPointsChangeCommand
class PointsRepository:
+1 -1
View File
@@ -8,7 +8,7 @@ from uuid import UUID, uuid4
from core.http.errors import ApiProblemError, problem_payload
from schemas.domain.points import ConsumeLedgerMetadata, PointsChargeSnapshot
from schemas.enums import PointsBizType, PointsChangeType, PointsOperatorType
from schemas.shared.points import ApplyPointsChangeCommand
from schemas.domain.points import ApplyPointsChangeCommand
from v1.points.repository import PointsRepository
RUN_POINTS_COST = 20