CI / lint / typecheck / import-linter / tests / secrets (push) Canceled after 0s
- agent: ag_conversation/ag_message 表(迁移0007),上下文组装注入最近报告画像摘要 (report.profile.latest_profile_summary 经 apps 层闭包注入,同层不互调),历史带最近 AGENT_HISTORY_TURNS=10 轮;POST/GET /agent/conversations 与消息历史接口 - agent 修复:ChatRequest.conversation_id 改 str(雪花ID超2^53,JS Number丢精度); _build_context 挪进 reserve commit 后同一 try,异常走 refund 兜底 - report: rp_feedback(维度反馈 upsert,认同率/修正率 SQL 可算,T-P3-21 最小版—— L1 记忆未建,不回写结论)+ rp_reservation(预约留资幂等,T-P3-22,预约率口径 行数/full_report.card_view 去重曝光,门槛≥8%),迁移0008 - 测试:全量 606 passed;ruff/mypy/lint-imports 全绿;E2E 真实 deepseek-v4-flash 验证 画像注入与历史携带
647 lines
30 KiB
Python
647 lines
30 KiB
Python
"""SQLAlchemy 2.0 ORM models.
|
||
|
||
Scope of this scaffold: only the tables needed to prove the vertical slice
|
||
(架构文档 §6.2 identity / billing / task / 观测与成本) plus the Sprint batches
|
||
that have landed since (kb_ / rp_ / ag_). Remaining domains (pf_ / tr_ / cp_)
|
||
are intentionally absent — see packages/*/README.md for what still has to be built.
|
||
|
||
Conventions enforced here (架构文档 §6.1):
|
||
1. 表名带域前缀 (`id_` / `bl_` / `tk_` / `ob_`),为将来拆库预留边界。
|
||
2. 主键统一雪花 ID (bigint),不用自增、不用 UUID。
|
||
3. 涉及配额的表必须有幂等键与流水。
|
||
4. JSONB 只用于「结构会演进且不需要索引全部字段」的场景。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from datetime import datetime
|
||
from decimal import Decimal
|
||
from typing import Any
|
||
|
||
from pgvector.sqlalchemy import Vector
|
||
from sqlalchemy import (
|
||
BigInteger,
|
||
Boolean,
|
||
Computed,
|
||
DateTime,
|
||
Index,
|
||
Integer,
|
||
Numeric,
|
||
PrimaryKeyConstraint,
|
||
SmallInteger,
|
||
String,
|
||
Text,
|
||
UniqueConstraint,
|
||
func,
|
||
text,
|
||
)
|
||
from sqlalchemy.dialects.postgresql import JSONB, TSVECTOR
|
||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||
|
||
|
||
class Base(DeclarativeBase):
|
||
"""Declarative base; ``Base.metadata`` is Alembic's ``target_metadata``."""
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# identity 域
|
||
# ---------------------------------------------------------------------------
|
||
class User(Base):
|
||
__tablename__ = "id_user"
|
||
|
||
user_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
|
||
status: Mapped[str] = mapped_column(String(16), nullable=False, default="active")
|
||
nickname: Mapped[str | None] = mapped_column(String(64))
|
||
avatar_key: Mapped[str | None] = mapped_column(String(255))
|
||
register_source: Mapped[str] = mapped_column(String(32), nullable=False, default="unknown")
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
last_active_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||
deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||
|
||
__table_args__ = (Index("ix_id_user_status", "status"),)
|
||
|
||
|
||
class UserIdentity(Base):
|
||
"""External identity mapping. UNIQUE(provider, union_id) per 架构文档 §6.2."""
|
||
|
||
__tablename__ = "id_user_identity"
|
||
|
||
identity_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
|
||
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||
provider: Mapped[str] = mapped_column(String(32), nullable=False)
|
||
union_id: Mapped[str] = mapped_column(String(128), nullable=False)
|
||
open_id: Mapped[str | None] = mapped_column(String(128))
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
|
||
__table_args__ = (
|
||
UniqueConstraint("provider", "union_id", name="uq_id_user_identity_provider_union"),
|
||
Index("ix_id_user_identity_user", "user_id"),
|
||
)
|
||
|
||
|
||
class Consent(Base):
|
||
"""CR-01 留证:人脸的单独同意必须独立成行,不得混入总协议。"""
|
||
|
||
__tablename__ = "id_consent"
|
||
|
||
consent_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
|
||
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||
consent_type: Mapped[str] = mapped_column(String(32), nullable=False)
|
||
doc_version: Mapped[str] = mapped_column(String(32), nullable=False)
|
||
granted_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||
client_ip: Mapped[str | None] = mapped_column(String(64))
|
||
ua: Mapped[str | None] = mapped_column(String(512))
|
||
|
||
__table_args__ = (Index("ix_id_consent_user_type", "user_id", "consent_type"),)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# billing 域(V1:配额与计量,无支付)
|
||
# ---------------------------------------------------------------------------
|
||
class QuotaRule(Base):
|
||
"""FR-BILL-04 配额规则配置化:改配置不发版。"""
|
||
|
||
__tablename__ = "bl_quota_rule"
|
||
|
||
quota_key: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||
period_type: Mapped[str] = mapped_column(String(16), nullable=False) # day/month/lifetime
|
||
base_limit: Mapped[int] = mapped_column(Integer, nullable=False)
|
||
task_bonus_max: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||
hard_cap: Mapped[int] = mapped_column(Integer, nullable=False)
|
||
description: Mapped[str | None] = mapped_column(String(255))
|
||
updated_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
|
||
|
||
class Quota(Base):
|
||
"""周期配额计量:按 ``period_key`` 自然重置,无需定时任务清零。"""
|
||
|
||
__tablename__ = "bl_quota"
|
||
|
||
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||
quota_key: Mapped[str] = mapped_column(String(64), nullable=False)
|
||
period_key: Mapped[str] = mapped_column(String(32), nullable=False)
|
||
used: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||
# 本周期创建时从规则拷下的 base_limit。规则中途被改,本周期仍按快照判定。
|
||
limit_value: Mapped[int] = mapped_column(Integer, nullable=False)
|
||
bonus_granted: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||
updated_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
|
||
__table_args__ = (
|
||
PrimaryKeyConstraint("user_id", "quota_key", "period_key", name="pk_bl_quota"),
|
||
)
|
||
|
||
|
||
class Ledger(Base):
|
||
"""流水是真相,余额是缓存 (架构文档 §6.1 原则 4 / §6.2 bl_ledger).
|
||
|
||
``tx_type`` 在 V1 只用 system_grant / consume / refund;V2 增加
|
||
purchase / gift —— 这是 V1→V2 免数据迁移的关键,不要拆表。
|
||
``status`` 承载 §7.4 的预扣三段式:reserved → committed / refunded。
|
||
"""
|
||
|
||
__tablename__ = "bl_ledger"
|
||
|
||
tx_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
|
||
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||
delta: Mapped[int] = mapped_column(Integer, nullable=False)
|
||
tx_type: Mapped[str] = mapped_column(String(24), nullable=False)
|
||
status: Mapped[str] = mapped_column(String(16), nullable=False, default="committed")
|
||
reason: Mapped[str | None] = mapped_column(String(128))
|
||
ref_type: Mapped[str | None] = mapped_column(String(32))
|
||
ref_id: Mapped[str | None] = mapped_column(String(64))
|
||
quota_key: Mapped[str | None] = mapped_column(String(64))
|
||
period_key: Mapped[str | None] = mapped_column(String(32))
|
||
balance_after: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||
idempotency_key: Mapped[str] = mapped_column(String(128), nullable=False)
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
|
||
__table_args__ = (
|
||
UniqueConstraint("idempotency_key", name="uq_bl_ledger_idempotency_key"),
|
||
Index("ix_bl_ledger_user_created", "user_id", "created_at"),
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# task 域(统一任务中心)
|
||
# ---------------------------------------------------------------------------
|
||
class Task(Base):
|
||
"""全系统唯一的长耗时任务视图,前端任务中心直接读它 (架构文档 §6.2)。"""
|
||
|
||
__tablename__ = "tk_task"
|
||
|
||
task_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
|
||
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||
task_type: Mapped[str] = mapped_column(String(32), nullable=False)
|
||
status: Mapped[str] = mapped_column(String(16), nullable=False)
|
||
progress: Mapped[int] = mapped_column(SmallInteger, nullable=False, default=0)
|
||
stage_text: Mapped[str | None] = mapped_column(String(128))
|
||
idempotency_key: Mapped[str] = mapped_column(String(128), nullable=False)
|
||
payload: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
|
||
result: Mapped[dict[str, Any] | None] = mapped_column(JSONB)
|
||
error_code: Mapped[int | None] = mapped_column(Integer)
|
||
error_message: Mapped[str | None] = mapped_column(String(512))
|
||
retry_count: Mapped[int] = mapped_column(SmallInteger, nullable=False, default=0)
|
||
quota_reserved: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||
reserve_tx_id: Mapped[int | None] = mapped_column(BigInteger)
|
||
submitted_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||
finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||
eta_seconds: Mapped[int | None] = mapped_column(Integer)
|
||
|
||
__table_args__ = (
|
||
UniqueConstraint("idempotency_key", name="uq_tk_task_idempotency_key"),
|
||
Index("ix_tk_task_user_status", "user_id", "status"),
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 观测与成本
|
||
# ---------------------------------------------------------------------------
|
||
class AiCall(Base):
|
||
"""成本归因的唯一数据源 (架构文档 §13, FR-BILL-08).
|
||
|
||
**不允许「先上线后补埋点」** —— V1 不收费,成本是唯一财务变量,且这张表
|
||
是 V2 定价的唯一数据来源。每一次 provider 调用(含失败与结构化重试)都写
|
||
一行。生产环境按月分区。
|
||
"""
|
||
|
||
__tablename__ = "ob_ai_call"
|
||
|
||
call_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
|
||
user_id: Mapped[int | None] = mapped_column(BigInteger)
|
||
scene: Mapped[str] = mapped_column(String(48), nullable=False)
|
||
provider: Mapped[str] = mapped_column(String(32), nullable=False)
|
||
model: Mapped[str] = mapped_column(String(64), nullable=False)
|
||
# Prompt 版本:架构文档 §8.1「可复现」要求记录,§6.2 的列清单漏了,这里补上。
|
||
prompt_id: Mapped[str | None] = mapped_column(String(64))
|
||
prompt_version: Mapped[str | None] = mapped_column(String(16))
|
||
tokens_in: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||
tokens_out: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||
tokens_cached: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||
# 思考模式 token 单独记,不并入 tokens_out:它按输出价计费,混在一起就看不出
|
||
# 开关思考模式的成本差异,而这正是 T-P1-24a 要长期盯的数。
|
||
tokens_reasoning: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||
latency_ms: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||
cost_cents: Mapped[Decimal] = mapped_column(
|
||
Numeric(14, 6), nullable=False, default=Decimal("0")
|
||
)
|
||
# 该次调用适用的峰谷档位。没有它就无法事后复核 cost_cents ——
|
||
# 单价随时刻变化,事后按「当前价」回算会得出不同结果 (T-P1-25c)。
|
||
rate_tier: Mapped[str | None] = mapped_column(String(16))
|
||
success: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||
error_code: Mapped[str | None] = mapped_column(String(64))
|
||
error_message: Mapped[str | None] = mapped_column(Text)
|
||
ref_type: Mapped[str | None] = mapped_column(String(32))
|
||
ref_id: Mapped[str | None] = mapped_column(String(64))
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
|
||
__table_args__ = (
|
||
Index("ix_ob_ai_call_created", "created_at"),
|
||
Index("ix_ob_ai_call_scene_created", "scene", "created_at"),
|
||
Index("ix_ob_ai_call_user_created", "user_id", "created_at"),
|
||
)
|
||
|
||
|
||
class UserSession(Base):
|
||
"""refresh token 的服务端会话(架构文档 §6.2 id_session,FR-ACC-05).
|
||
|
||
刻意叫 UserSession 而不是 Session:避免与 SQLAlchemy 的 AsyncSession 混淆。
|
||
refresh token 本体是 opaque 随机串(不出现在任何存储里),库里只存 sha256
|
||
哈希 —— 泄漏数据库不等于泄漏令牌。轮换 = 旧行 revoked + 新行,同一事务。
|
||
"""
|
||
|
||
__tablename__ = "id_session"
|
||
|
||
session_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
|
||
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||
refresh_token_hash: Mapped[str] = mapped_column(String(64), nullable=False)
|
||
expire_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||
revoked: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
|
||
__table_args__ = (
|
||
UniqueConstraint("refresh_token_hash", name="uq_id_session_token_hash"),
|
||
Index("ix_id_session_user", "user_id"),
|
||
)
|
||
|
||
|
||
class ObEvent(Base):
|
||
"""客户端埋点事件(架构文档 §6.2 ob_event,FR-SYS-06).
|
||
|
||
架构原文「按月分区」—— V1 先普通表(与 ob_ai_call 同样处理),
|
||
数据量上来后再分区,先不预支复杂度。
|
||
"""
|
||
|
||
__tablename__ = "ob_event"
|
||
|
||
event_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
|
||
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||
event_name: Mapped[str] = mapped_column(String(64), nullable=False)
|
||
props: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
|
||
client_ts: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||
server_ts: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
|
||
__table_args__ = (
|
||
Index("ix_ob_event_name_ts", "event_name", "server_ts"),
|
||
Index("ix_ob_event_user_ts", "user_id", "server_ts"),
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# kb 域(知识库,架构文档 §6.2)
|
||
# ---------------------------------------------------------------------------
|
||
class KbEntity(Base):
|
||
"""12 类实体统一表:attrs JSONB 承载类目差异化字段(架构 §6.2).
|
||
|
||
状态机 draft→reviewing→published→archived 由 service 层校验;
|
||
published 实体不可直接改(version 递增需回 draft 重走评审)。
|
||
"""
|
||
|
||
__tablename__ = "kb_entity"
|
||
|
||
entity_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
|
||
entity_type: Mapped[str] = mapped_column(String(32), nullable=False)
|
||
name: Mapped[str] = mapped_column(String(128), nullable=False)
|
||
attrs: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
|
||
status: Mapped[str] = mapped_column(String(16), nullable=False, default="draft")
|
||
version: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
||
source: Mapped[str | None] = mapped_column(String(255))
|
||
license: Mapped[str | None] = mapped_column(String(64))
|
||
updated_by: Mapped[str | None] = mapped_column(String(64))
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
updated_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
|
||
__table_args__ = (
|
||
UniqueConstraint("entity_type", "name", name="uq_kb_entity_type_name"),
|
||
Index("ix_kb_entity_type_status", "entity_type", "status"),
|
||
)
|
||
|
||
|
||
class KbRelation(Base):
|
||
"""邻接表 + 递归 CTE 做 1~3 跳(TD-09:不上 Neo4j)."""
|
||
|
||
__tablename__ = "kb_relation"
|
||
|
||
relation_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
|
||
from_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||
rel_type: Mapped[str] = mapped_column(String(64), nullable=False)
|
||
to_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||
weight: Mapped[Decimal] = mapped_column(Numeric(4, 3), nullable=False, default=Decimal("1"))
|
||
confidence: Mapped[Decimal] = mapped_column(Numeric(4, 3), nullable=False, default=Decimal("1"))
|
||
evidence: Mapped[str | None] = mapped_column(String(255))
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
|
||
__table_args__ = (
|
||
UniqueConstraint("from_id", "rel_type", "to_id", name="uq_kb_relation_triple"),
|
||
Index("ix_kb_relation_to", "to_id"),
|
||
)
|
||
|
||
|
||
class KbRule(Base):
|
||
"""决策表(TD-10 自研规则引擎)。conditions/effects 为 JSONB DSL."""
|
||
|
||
__tablename__ = "kb_rule"
|
||
|
||
rule_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
|
||
rule_key: Mapped[str] = mapped_column(String(64), nullable=False)
|
||
category: Mapped[str] = mapped_column(String(32), nullable=False)
|
||
version: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
||
status: Mapped[str] = mapped_column(String(16), nullable=False, default="draft")
|
||
priority: Mapped[int] = mapped_column(Integer, nullable=False, default=60)
|
||
conditions: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False)
|
||
effects: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False)
|
||
is_hard: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||
confidence: Mapped[Decimal] = mapped_column(
|
||
Numeric(4, 3), nullable=False, default=Decimal("0.9")
|
||
)
|
||
evidence: Mapped[str | None] = mapped_column(String(255))
|
||
author: Mapped[str | None] = mapped_column(String(64))
|
||
reviewer: Mapped[str | None] = mapped_column(String(64))
|
||
published_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
|
||
__table_args__ = (
|
||
UniqueConstraint("rule_key", name="uq_kb_rule_key"),
|
||
Index("ix_kb_rule_status_priority", "status", "priority"),
|
||
)
|
||
|
||
|
||
class KbChunk(Base):
|
||
"""向量 + 全文双索引,支撑混合检索(架构 §8.3).
|
||
|
||
embedding_model/version 必填:没有版本号的向量不可重跑(T-P2-03)。
|
||
tsv 是生成列——写入时不要手工填。
|
||
"""
|
||
|
||
__tablename__ = "kb_chunk"
|
||
|
||
chunk_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
|
||
entity_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||
chunk_type: Mapped[str] = mapped_column(String(32), nullable=False, default="text")
|
||
text: Mapped[str] = mapped_column(Text, nullable=False)
|
||
embedding = mapped_column(Vector(1024), nullable=False)
|
||
embedding_model: Mapped[str] = mapped_column(String(64), nullable=False)
|
||
embedding_version: Mapped[str] = mapped_column(String(16), nullable=False)
|
||
tsv = mapped_column(
|
||
TSVECTOR, Computed("to_tsvector('simple', text)", persisted=True), nullable=False
|
||
)
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
|
||
__table_args__ = (Index("ix_kb_chunk_entity", "entity_id"),)
|
||
|
||
|
||
class KbAsset(Base):
|
||
"""图像资产。tryon_ready 是换装闭环的关键标志位(架构 §6.2)."""
|
||
|
||
__tablename__ = "kb_asset"
|
||
|
||
asset_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
|
||
entity_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||
asset_type: Mapped[str] = mapped_column(String(32), nullable=False)
|
||
cos_key: Mapped[str] = mapped_column(String(255), nullable=False)
|
||
spec_check: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
|
||
tryon_ready: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||
review_status: Mapped[str] = mapped_column(String(16), nullable=False, default="pending")
|
||
license: Mapped[str | None] = mapped_column(String(64))
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
|
||
__table_args__ = (
|
||
Index("ix_kb_asset_entity", "entity_id"),
|
||
Index("ix_kb_asset_tryon_ready", "tryon_ready", postgresql_where=text("tryon_ready")),
|
||
)
|
||
|
||
|
||
class KbColor(Base):
|
||
"""自建色板(TD-18:规避「四季色彩/CMB」商标)。Lab 用于 ΔE 数值匹配."""
|
||
|
||
__tablename__ = "kb_color"
|
||
|
||
color_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
|
||
season_code: Mapped[str] = mapped_column(String(16), nullable=False)
|
||
role: Mapped[str] = mapped_column(String(8), nullable=False)
|
||
lab_l: Mapped[Decimal] = mapped_column(Numeric(6, 2), nullable=False)
|
||
lab_a: Mapped[Decimal] = mapped_column(Numeric(6, 2), nullable=False)
|
||
lab_b: Mapped[Decimal] = mapped_column(Numeric(6, 2), nullable=False)
|
||
hex: Mapped[str] = mapped_column(String(7), nullable=False)
|
||
name_cn: Mapped[str] = mapped_column(String(64), nullable=False)
|
||
|
||
__table_args__ = (Index("ix_kb_color_season", "season_code"),)
|
||
|
||
|
||
class KbGap(Base):
|
||
"""知识缺口埋点:无检索结果的查询,驱动内容生产(T-P2-06)."""
|
||
|
||
__tablename__ = "kb_gap"
|
||
|
||
gap_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
|
||
query_text: Mapped[str] = mapped_column(Text, nullable=False)
|
||
filter_ctx: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
|
||
hit_count: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
||
first_seen_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
last_seen_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
|
||
__table_args__ = (Index("ix_kb_gap_query", "query_text"),)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# report 域(美学报告,架构文档 §6.2 / §7.1)
|
||
# ---------------------------------------------------------------------------
|
||
class RpReport(Base):
|
||
"""报告主表。result JSONB 同时承载流水线中间态与最终产物(按 stage 键)——
|
||
阶段逐段落库是 T-P3-09「失败不需重新上传」的结构基础。"""
|
||
|
||
__tablename__ = "rp_report"
|
||
|
||
report_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
|
||
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||
tier: Mapped[str] = mapped_column(String(8), nullable=False, default="basic")
|
||
status: Mapped[str] = mapped_column(String(16), nullable=False, default="pending")
|
||
version: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
||
result: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
|
||
model_info: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
|
||
content_code: Mapped[str | None] = mapped_column(String(32))
|
||
rendered_asset_key: Mapped[str | None] = mapped_column(String(255))
|
||
cost_cents: Mapped[Decimal] = mapped_column(
|
||
Numeric(14, 6), nullable=False, default=Decimal("0")
|
||
)
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||
|
||
__table_args__ = (Index("ix_rp_report_user_created", "user_id", "created_at"),)
|
||
|
||
|
||
class RpInput(Base):
|
||
"""报告输入。derived_features 是视觉分析的中间态:重试/重新生成时
|
||
已存在则跳过视觉阶段(T-P3-09)。只存派生特征与元信息,不存原图(CR-02)。"""
|
||
|
||
__tablename__ = "rp_input"
|
||
|
||
input_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
|
||
report_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||
questionnaire: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
|
||
photo_meta: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
|
||
derived_features: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
|
||
|
||
__table_args__ = (UniqueConstraint("report_id", name="uq_rp_input_report"),)
|
||
|
||
|
||
class RpShare(Base):
|
||
"""分享链接:可失效、可撤回(V1 只建结构,路由后续任务)。"""
|
||
|
||
__tablename__ = "rp_share"
|
||
|
||
share_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
|
||
report_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||
share_token: Mapped[str] = mapped_column(String(64), nullable=False)
|
||
expire_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||
view_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||
revoked: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
|
||
__table_args__ = (UniqueConstraint("share_token", name="uq_rp_share_token"),)
|
||
|
||
|
||
class RpFeedback(Base):
|
||
"""维度级结论反馈(T-P3-21 最小可用版):准/不准 + 可选修正文本。
|
||
|
||
同维度重复反馈是覆盖更新(upsert),由 uq_rp_feedback_report_dimension
|
||
兜底;认同率/修正率两项指标从本表 SQL 直接算出。
|
||
有意裁剪:不回写报告结论、不写 L1 记忆(记忆系统未建,见迁移 0008)。"""
|
||
|
||
__tablename__ = "rp_feedback"
|
||
|
||
feedback_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
|
||
report_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||
dimension_key: Mapped[str] = mapped_column(String(64), nullable=False)
|
||
verdict: Mapped[str] = mapped_column(String(16), nullable=False) # accurate | inaccurate
|
||
correction: Mapped[str | None] = mapped_column(Text)
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
updated_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
|
||
)
|
||
|
||
__table_args__ = (
|
||
UniqueConstraint("report_id", "dimension_key", name="uq_rp_feedback_report_dimension"),
|
||
Index("ix_rp_feedback_user_created", "user_id", "created_at"),
|
||
)
|
||
|
||
|
||
class RpReservation(Base):
|
||
"""预约留资(T-P3-22):topic 预留通用性,首期只有 full_report。
|
||
|
||
uq_rp_reservation_user_topic 承载幂等:重复预约返回已有记录(created=false)。
|
||
指标口径:预约率 = topic='full_report' 行数 ÷ 埋点 full_report.card_view
|
||
去重曝光数;决策门槛 ≥8%(产品方案 §2.2.3)。"""
|
||
|
||
__tablename__ = "rp_reservation"
|
||
|
||
reservation_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
|
||
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||
topic: Mapped[str] = mapped_column(String(32), nullable=False)
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
|
||
__table_args__ = (UniqueConstraint("user_id", "topic", name="uq_rp_reservation_user_topic"),)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# agent 域(FR-AGT-09 / T-P4-01:会话与消息,含检索轨迹与规则命中)
|
||
# ---------------------------------------------------------------------------
|
||
class AgConversation(Base):
|
||
"""对话会话。title 取首条用户消息的截断,列表页展示用。
|
||
|
||
保留策略(合规方案 §6:对话原文 90 天)由后续清理任务按
|
||
``last_message_at`` 执行,表结构先把时间锚点留好。"""
|
||
|
||
__tablename__ = "ag_conversation"
|
||
|
||
conversation_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
|
||
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||
title: Mapped[str] = mapped_column(String(64), nullable=False, default="")
|
||
message_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||
last_message_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
|
||
__table_args__ = (Index("ix_ag_conversation_user_active", "user_id", "last_message_at"),)
|
||
|
||
|
||
class AgMessage(Base):
|
||
"""单条对话消息。``retrieval_trace`` / ``rule_hits`` 是 FR-AGT-09「每条回答
|
||
可复盘」的复盘锚点:检索与规则命中在对应步骤(架构 §7.2 ④)落地前留空。
|
||
|
||
``call_id`` 关联 ``ob_ai_call``,一条 assistant 消息的模型/token/成本可
|
||
一路追到账本;user 消息没有 LLM 调用,这些字段恒 NULL。"""
|
||
|
||
__tablename__ = "ag_message"
|
||
|
||
message_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
|
||
conversation_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||
role: Mapped[str] = mapped_column(String(16), nullable=False) # user / assistant
|
||
content: Mapped[str] = mapped_column(Text, nullable=False)
|
||
call_id: Mapped[int | None] = mapped_column(BigInteger)
|
||
model: Mapped[str | None] = mapped_column(String(64))
|
||
tokens_in: Mapped[int | None] = mapped_column(Integer)
|
||
tokens_out: Mapped[int | None] = mapped_column(Integer)
|
||
tokens_cached: Mapped[int | None] = mapped_column(Integer)
|
||
latency_ms: Mapped[int | None] = mapped_column(Integer)
|
||
cost_cents: Mapped[Decimal | None] = mapped_column(Numeric(14, 6))
|
||
retrieval_trace: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
|
||
rule_hits: Mapped[list[Any]] = mapped_column(JSONB, nullable=False, default=list)
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||
)
|
||
|
||
__table_args__ = (
|
||
Index("ix_ag_message_conversation_created", "conversation_id", "created_at"),
|
||
Index("ix_ag_message_user_created", "user_id", "created_at"),
|
||
)
|