chore(prep): shared groundwork for the Sprint-2 parallel batch
第二批(T6~T11)协调提交: - 迁移 0005:kb 七表(entity/relation/rule/chunk vector1024+tsv 生成列/ asset/color/gap),HNSW+GIN 索引,FK 级联 - ORM:七个 Kb* 模型(pgvector Vector/TSVECTOR/Computed) - 配置:检索与 embedding、华为 JWKS、短信全参数组 - 错误码:40110/40111/42910/42911/61000 - 骨架路由:kb.py + auth_sms.py(已注册);依赖:tencentcloud-sdk-python-sms + spike extra(onnxruntime/opencv);gitignore:models/ 与 spike 输出
This commit is contained in:
@@ -168,6 +168,41 @@ class Settings(BaseSettings):
|
||||
alias="UPLOAD_SCENES_JSON",
|
||||
)
|
||||
|
||||
# --- 知识库与检索(架构 §8.3,TD-08)--------------------------------------
|
||||
kb_vector_dimensions: int = 1024
|
||||
# 当前默认 = 确定性假实现;真实 BGE-M3 ONNX 是后续任务,届时只换这里。
|
||||
kb_embedding_model: str = "deterministic-v1"
|
||||
kb_embedding_device: str = "cpu"
|
||||
retrieval_top_k: int = 50
|
||||
# 重排权重(架构 §8.3 唯一口径):0.45 向量 + 0.2 关键词 + 0.25 规则 + 0.1 新鲜度。
|
||||
retrieval_rerank_weights: dict[str, float] = Field(
|
||||
default_factory=lambda: {"vector": 0.45, "keyword": 0.2, "rule": 0.25, "freshness": 0.1},
|
||||
alias="RETRIEVAL_RERANK_WEIGHTS_JSON",
|
||||
)
|
||||
|
||||
# --- 华为账号登录(FR-ACC-01;联调待 AGC 配置,见《前置开通清单》§3)------
|
||||
huawei_jwks_url: str = "https://oauth-login.cloud.huawei.com/oauth2/v3/certs"
|
||||
huawei_audience: str = "" # = AGC App ID;空配置时验签必须拒绝而不是跳过
|
||||
huawei_issuer: str = "https://accounts.huawei.com"
|
||||
huawei_jwks_cache_seconds: int = 86400
|
||||
|
||||
# --- 手机号验证码登录(FR-ACC-04/14,TD-32)--------------------------------
|
||||
# fake = 验证码打日志(dev);tencent = 真实发送(待签名审核通过后切换)
|
||||
sms_provider: str = "fake"
|
||||
tencent_sms_secret_id: str = ""
|
||||
tencent_sms_secret_key: str = ""
|
||||
tencent_sms_sdk_app_id: str = ""
|
||||
tencent_sms_sign_name: str = ""
|
||||
tencent_sms_template_id: str = ""
|
||||
sms_code_ttl_seconds: int = 300
|
||||
sms_code_length: int = 6
|
||||
sms_verify_max_attempts: int = 5
|
||||
sms_resend_cooldown_seconds: int = 60
|
||||
sms_hourly_limit: int = 5
|
||||
sms_daily_limit: int = 10
|
||||
sms_ip_daily_limit: int = 20
|
||||
sms_device_daily_limit: int = 10
|
||||
|
||||
# --- Observability -----------------------------------------------------
|
||||
otel_enabled: bool = False
|
||||
otel_service_name: str = "meida-api"
|
||||
@@ -182,6 +217,7 @@ class Settings(BaseSettings):
|
||||
"llm_budget_tiers",
|
||||
"task_bonus_rules",
|
||||
"upload_scenes",
|
||||
"retrieval_rerank_weights",
|
||||
mode="before",
|
||||
)
|
||||
@classmethod
|
||||
|
||||
@@ -32,6 +32,8 @@ class ErrorCode(IntEnum):
|
||||
REFRESH_TOKEN_INVALID = 40103
|
||||
REFRESH_TOKEN_EXPIRED = 40104
|
||||
SESSION_REVOKED = 40105
|
||||
SMS_CODE_INVALID = 40110
|
||||
SMS_CODE_EXPIRED = 40111
|
||||
FORBIDDEN = 40300
|
||||
QUOTA_EXHAUSTED = 40301
|
||||
CONSENT_REQUIRED = 40302
|
||||
@@ -42,6 +44,8 @@ class ErrorCode(IntEnum):
|
||||
STATE_CONFLICT = 40900
|
||||
TASK_BONUS_ALREADY_CLAIMED = 40910
|
||||
RATE_LIMITED = 42900
|
||||
SMS_SEND_TOO_FREQUENT = 42910
|
||||
SMS_DAILY_LIMIT = 42911
|
||||
|
||||
# --- 5xxxx server -----------------------------------------------------
|
||||
INTERNAL_ERROR = 50000
|
||||
@@ -58,6 +62,7 @@ class ErrorCode(IntEnum):
|
||||
LLM_PROMPT_CACHE_VIOLATION = 60004
|
||||
MODERATION_UNAVAILABLE = 60005
|
||||
LLM_SCENE_DEGRADED = 60006
|
||||
SMS_PROVIDER_FAILED = 61000
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
@@ -84,6 +89,8 @@ ERROR_META: dict[ErrorCode, ErrorMeta] = {
|
||||
ErrorCode.REFRESH_TOKEN_INVALID: ErrorMeta(401, "登录状态无效", "请重新登录"),
|
||||
ErrorCode.REFRESH_TOKEN_EXPIRED: ErrorMeta(401, "登录状态已过期", "请重新登录"),
|
||||
ErrorCode.SESSION_REVOKED: ErrorMeta(401, "该会话已登出", "请重新登录"),
|
||||
ErrorCode.SMS_CODE_INVALID: ErrorMeta(401, "验证码错误", "请检查后重试"),
|
||||
ErrorCode.SMS_CODE_EXPIRED: ErrorMeta(401, "验证码已过期", "请重新获取"),
|
||||
ErrorCode.FORBIDDEN: ErrorMeta(403, "没有访问权限"),
|
||||
ErrorCode.QUOTA_EXHAUSTED: ErrorMeta(403, "今日额度已用完", "明天恢复,或完成任务多换几次"),
|
||||
ErrorCode.CONSENT_REQUIRED: ErrorMeta(403, "需要先完成授权", "请阅读并同意后继续"),
|
||||
@@ -98,6 +105,8 @@ ERROR_META: dict[ErrorCode, ErrorMeta] = {
|
||||
409, "这个周期的任务奖励已领取", "下个周期再来"
|
||||
),
|
||||
ErrorCode.RATE_LIMITED: ErrorMeta(429, "操作太频繁了", "请稍后再试"),
|
||||
ErrorCode.SMS_SEND_TOO_FREQUENT: ErrorMeta(429, "发送太频繁了", "请稍后再试"),
|
||||
ErrorCode.SMS_DAILY_LIMIT: ErrorMeta(429, "今日发送次数已达上限", "明天再试"),
|
||||
ErrorCode.INTERNAL_ERROR: ErrorMeta(500, "服务开小差了", "请稍后重试"),
|
||||
ErrorCode.DB_ERROR: ErrorMeta(500, "服务开小差了", "请稍后重试"),
|
||||
ErrorCode.TASK_STATE_INVALID: ErrorMeta(500, "任务状态异常", "请到任务中心重试"),
|
||||
@@ -120,6 +129,7 @@ ERROR_META: dict[ErrorCode, ErrorMeta] = {
|
||||
ErrorCode.LLM_SCENE_DEGRADED: ErrorMeta(
|
||||
503, "该功能今日暂时关闭", "AI 服务今日已达服务上限,明天恢复"
|
||||
),
|
||||
ErrorCode.SMS_PROVIDER_FAILED: ErrorMeta(502, "短信服务暂时不可用", "请稍后重试"),
|
||||
}
|
||||
|
||||
|
||||
|
||||
+176
-1
@@ -18,9 +18,11 @@ 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,
|
||||
@@ -31,8 +33,9 @@ from sqlalchemy import (
|
||||
Text,
|
||||
UniqueConstraint,
|
||||
func,
|
||||
text,
|
||||
)
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.dialects.postgresql import JSONB, TSVECTOR
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||
|
||||
|
||||
@@ -304,3 +307,175 @@ class ObEvent(Base):
|
||||
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"),)
|
||||
|
||||
Reference in New Issue
Block a user