feat: 实现起卦、设置与积分系统

This commit is contained in:
qzl
2026-04-03 16:56:47 +08:00
parent 31594558eb
commit f245eec5f6
170 changed files with 20728 additions and 328 deletions
+13 -3
View File
@@ -1,11 +1,21 @@
from __future__ import annotations
from models.llm import Llm
from models.llm_factory import LlmFactory
from models.system_agents import SystemAgents
from .agent_chat_message import AgentChatMessage
from .agent_chat_session import AgentChatSession
from .llm import Llm
from .llm_factory import LlmFactory
from .points_ledger import PointsLedger
from .profile import Profile
from .system_agents import SystemAgents
from .user_points import UserPoints
__all__ = [
"AgentChatMessage",
"AgentChatSession",
"Llm",
"LlmFactory",
"PointsLedger",
"Profile",
"SystemAgents",
"UserPoints",
]
+65
View File
@@ -0,0 +1,65 @@
from __future__ import annotations
from decimal import Decimal
import uuid
from sqlalchemy import (
BigInteger,
JSON,
Enum as SqlEnum,
ForeignKey,
Integer,
Numeric,
String,
Text,
UniqueConstraint,
)
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import Mapped, mapped_column
from core.db.base import Base, SoftDeleteMixin, TimestampMixin
from schemas.enums import AgentChatMessageRole
__all__ = ["AgentChatMessage", "AgentChatMessageRole"]
class AgentChatMessage(TimestampMixin, SoftDeleteMixin, Base):
__tablename__: str = "messages"
__table_args__: tuple[UniqueConstraint] = (
UniqueConstraint("session_id", "seq", name="uq_messages_session_seq"),
)
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
session_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("sessions.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
seq: Mapped[int] = mapped_column(Integer, nullable=False)
role: Mapped[AgentChatMessageRole] = mapped_column(
SqlEnum(
AgentChatMessageRole,
name="agent_chat_message_role",
native_enum=False,
values_callable=lambda enum_cls: [item.value for item in enum_cls],
),
nullable=False,
)
content: Mapped[str] = mapped_column(Text, nullable=False)
model_code: Mapped[str | None] = mapped_column(String(50), nullable=True)
tool_name: Mapped[str | None] = mapped_column(String(100), nullable=True)
input_tokens: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
output_tokens: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
cost: Mapped[Decimal] = mapped_column(Numeric(12, 6), nullable=False, default=0)
latency_ms: Mapped[int | None] = mapped_column(Integer, nullable=True)
visibility_mask: Mapped[int] = mapped_column(
BigInteger,
nullable=False,
default=0,
)
metadata_json: Mapped[dict[str, object] | None] = mapped_column(
"metadata", JSON().with_variant(JSONB, "postgresql"), nullable=True
)
+75
View File
@@ -0,0 +1,75 @@
from __future__ import annotations
from datetime import datetime
from decimal import Decimal
import uuid
from sqlalchemy import (
DateTime,
JSON,
Enum as SqlEnum,
Integer,
Numeric,
String,
func,
text,
)
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import Mapped, mapped_column
from core.db.base import Base, SoftDeleteMixin, TimestampMixin
from schemas.enums import AgentChatSessionStatus, SessionType
__all__ = ["AgentChatSession", "AgentChatSessionStatus", "SessionType"]
class AgentChatSession(TimestampMixin, SoftDeleteMixin, Base):
__tablename__: str = "sessions"
__table_args__ = {"extend_existing": True}
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
user_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
nullable=False,
index=True,
)
session_type: Mapped[SessionType] = mapped_column(
String(20),
nullable=False,
default=SessionType.CHAT,
)
job_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True),
nullable=True,
)
title: Mapped[str | None] = mapped_column(String(255), nullable=True)
status: Mapped[AgentChatSessionStatus] = mapped_column(
SqlEnum(
AgentChatSessionStatus,
name="agent_chat_session_status",
native_enum=False,
values_callable=lambda enum_cls: [item.value for item in enum_cls],
),
nullable=False,
default=AgentChatSessionStatus.PENDING,
)
last_activity_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
nullable=False,
)
message_count: Mapped[int] = mapped_column(
Integer, nullable=False, server_default=text("0")
)
total_tokens: Mapped[int] = mapped_column(
Integer, nullable=False, server_default=text("0")
)
total_cost: Mapped[Decimal] = mapped_column(
Numeric(12, 6), nullable=False, server_default=text("0")
)
state_snapshot: Mapped[dict | None] = mapped_column(
JSON().with_variant(JSONB, "postgresql"),
nullable=True,
)
+113
View File
@@ -0,0 +1,113 @@
from __future__ import annotations
import uuid
from sqlalchemy import (
BigInteger,
CheckConstraint,
Index,
JSON,
SmallInteger,
String,
UniqueConstraint,
text,
)
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import Mapped, mapped_column
from core.db.base import Base, TimestampMixin
class PointsLedger(TimestampMixin, Base):
__tablename__ = "points_ledger"
__table_args__ = (
CheckConstraint("amount > 0", name="ck_points_ledger_amount_positive"),
CheckConstraint(
"direction in (1, -1)", name="ck_points_ledger_direction_valid"
),
CheckConstraint(
"balance_after >= 0", name="ck_points_ledger_balance_after_non_negative"
),
CheckConstraint(
"change_type in ('register', 'consume', 'grant', 'adjust')",
name="ck_points_ledger_change_type",
),
CheckConstraint(
"biz_type is null or biz_type = 'chat'",
name="ck_points_ledger_biz_type",
),
CheckConstraint(
"((change_type = 'register' and biz_type is null and biz_id is null) or "
"(change_type in ('consume', 'grant', 'adjust') and biz_type = 'chat' and biz_id is not null))",
name="ck_points_ledger_biz_binding",
),
CheckConstraint(
"((change_type in ('register', 'grant') and direction = 1) or "
"(change_type = 'consume' and direction = -1) or "
"(change_type = 'adjust' and direction in (1, -1)))",
name="ck_points_ledger_direction_by_change_type",
),
CheckConstraint(
"jsonb_typeof(metadata) = 'object'",
name="ck_points_ledger_metadata_object",
),
CheckConstraint(
"metadata->>'schema_version' = '1' and "
"metadata->>'operator_type' in ('user', 'system', 'admin') and "
"coalesce(metadata->>'run_id', '') <> '' and "
"(not (metadata ? 'ext') or jsonb_typeof(metadata->'ext') = 'object')",
name="ck_points_ledger_metadata_common",
),
CheckConstraint(
"(change_type <> 'register' or not (metadata ? 'charge'))",
name="ck_points_ledger_metadata_register_shape",
),
CheckConstraint(
"(change_type <> 'consume' or ("
"(metadata ? 'charge') and jsonb_typeof(metadata->'charge') = 'object' and "
"(metadata->'charge' ? 'message_id') and (metadata->'charge' ? 'message_seq') and "
"(metadata->'charge' ? 'model_code') and (metadata->'charge' ? 'input_tokens') and "
"(metadata->'charge' ? 'output_tokens') and (metadata->'charge' ? 'cost')))",
name="ck_points_ledger_metadata_consume_shape",
),
CheckConstraint(
"(change_type <> 'adjust' or ("
"(metadata ? 'ext') and (metadata->'ext' ? 'ticket_id') and "
"coalesce(metadata #>> '{ext,ticket_id}', '') <> ''))",
name="ck_points_ledger_metadata_adjust_shape",
),
UniqueConstraint("user_id", "event_id", name="uq_points_ledger_user_event"),
Index("ix_points_ledger_user_created_at", "user_id", text("created_at DESC")),
Index("ix_points_ledger_biz_type_biz_id", "biz_type", "biz_id"),
)
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
primary_key=True,
default=uuid.uuid4,
)
user_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
nullable=False,
)
direction: Mapped[int] = mapped_column(SmallInteger, nullable=False)
amount: Mapped[int] = mapped_column(BigInteger, nullable=False)
balance_after: Mapped[int] = mapped_column(BigInteger, nullable=False)
change_type: Mapped[str] = mapped_column(String(16), nullable=False)
biz_type: Mapped[str | None] = mapped_column(String(16), nullable=True)
biz_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True),
nullable=True,
)
event_id: Mapped[str] = mapped_column(String(64), nullable=False)
operator_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True),
nullable=True,
)
metadata_json: Mapped[dict[str, object]] = mapped_column(
"metadata",
JSON().with_variant(JSONB, "postgresql"),
nullable=False,
server_default=text("'{}'::jsonb"),
default=dict,
)
+35
View File
@@ -0,0 +1,35 @@
from __future__ import annotations
import uuid
from sqlalchemy import CheckConstraint, ForeignKey, Index, JSON, String, Text, text
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import Mapped, mapped_column
from core.db.base import Base, SoftDeleteMixin, TimestampMixin
class Profile(TimestampMixin, SoftDeleteMixin, Base):
__tablename__ = "profiles"
__table_args__ = (
CheckConstraint(
"char_length(username) >= 1", name="ck_profiles_username_non_empty"
),
Index("ix_profiles_username", "username"),
Index("ix_profiles_settings_gin", "settings", postgresql_using="gin"),
)
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("auth.users.id", ondelete="CASCADE"),
primary_key=True,
)
username: Mapped[str] = mapped_column(String(30), nullable=False)
avatar_url: Mapped[str | None] = mapped_column(Text, nullable=True)
bio: Mapped[str | None] = mapped_column(String(200), nullable=True)
settings: Mapped[dict[str, object]] = mapped_column(
JSON().with_variant(JSONB, "postgresql"),
nullable=False,
server_default=text("'{}'::jsonb"),
default=dict,
)
+58
View File
@@ -0,0 +1,58 @@
from __future__ import annotations
import uuid
from sqlalchemy import BigInteger, CheckConstraint, Integer, text
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column
from core.db.base import Base, TimestampMixin
class UserPoints(TimestampMixin, Base):
__tablename__ = "user_points"
__table_args__ = (
CheckConstraint("balance >= 0", name="ck_user_points_balance_non_negative"),
CheckConstraint(
"frozen_balance >= 0", name="ck_user_points_frozen_balance_non_negative"
),
CheckConstraint(
"lifetime_earned >= 0", name="ck_user_points_lifetime_earned_non_negative"
),
CheckConstraint(
"lifetime_spent >= 0", name="ck_user_points_lifetime_spent_non_negative"
),
CheckConstraint(
"frozen_balance <= balance", name="ck_user_points_frozen_le_balance"
),
)
user_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
primary_key=True,
)
balance: Mapped[int] = mapped_column(
BigInteger,
nullable=False,
server_default=text("0"),
)
frozen_balance: Mapped[int] = mapped_column(
BigInteger,
nullable=False,
server_default=text("0"),
)
lifetime_earned: Mapped[int] = mapped_column(
BigInteger,
nullable=False,
server_default=text("0"),
)
lifetime_spent: Mapped[int] = mapped_column(
BigInteger,
nullable=False,
server_default=text("0"),
)
version: Mapped[int] = mapped_column(
Integer,
nullable=False,
server_default=text("0"),
)