Files
awoooi/apps/api/src/db/awooop_models.py
Your Name 795085170a
All checks were successful
Code Review / ai-code-review (push) Successful in 10s
CD Pipeline / tests (push) Successful in 1m23s
CD Pipeline / build-and-deploy (push) Successful in 3m37s
CD Pipeline / post-deploy-checks (push) Successful in 1m34s
feat(awooop): persist inbound source envelopes
2026-05-13 21:29:04 +08:00

706 lines
30 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
AwoooP Control Plane Models
============================
Phase 1 新表:六合約 control plane、tenant 隔離、principal mapping。
ADR-111~1182026-05-04 ogt + Claude Sonnet 4.6
"""
from __future__ import annotations
from datetime import datetime
from decimal import Decimal
from typing import Any
from uuid import UUID
from sqlalchemy import (
Boolean,
CheckConstraint,
ForeignKey,
Index,
Integer,
Numeric,
SmallInteger,
String,
Text,
UniqueConstraint,
text,
)
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column
from src.db.base import Base
class AwoooPProject(Base):
"""租戶主表ADR-111 bootstrapADR-115 tenant onboarding"""
__tablename__ = "awooop_projects"
__table_args__ = (
CheckConstraint(
"migration_mode IN ('legacy_awoooi_default','shadow','canary','active')",
name="chk_migration_mode",
),
CheckConstraint(
"budget_limit_usd IS NULL OR budget_limit_usd >= 0",
name="chk_budget_non_negative",
),
CheckConstraint(
"jsonb_typeof(allowed_channels) = 'array'",
name="chk_allowed_channels_array",
),
)
project_id: Mapped[str] = mapped_column(String(64), primary_key=True)
display_name: Mapped[str] = mapped_column(String(256), nullable=False)
migration_mode: Mapped[str] = mapped_column(
String(32), nullable=False, default="legacy_awoooi_default"
)
budget_limit_usd: Mapped[Decimal | None] = mapped_column(
Numeric(14, 4), nullable=True
)
allowed_channels: Mapped[list[Any]] = mapped_column(
JSONB, nullable=False, server_default=text("'[]'::jsonb")
)
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
created_at: Mapped[datetime] = mapped_column(
nullable=False, server_default=text("NOW()")
)
updated_at: Mapped[datetime] = mapped_column(
nullable=False, server_default=text("NOW()")
)
class AwoooPContractRevision(Base):
"""六合約共用 revision 表append-onlyADR-107/ADR-112"""
__tablename__ = "awooop_contract_revisions"
__table_args__ = (
UniqueConstraint(
"project_id", "contract_family", "contract_id",
"version_major", "version_minor",
name="uq_revision_version",
),
CheckConstraint(
"contract_family IN ("
"'project_tenant','agent','mcp_gateway','policy_routing',"
"'runtime_run_state','channel_event','platform_resource')",
name="chk_contract_family",
),
CheckConstraint(
"lifecycle_status IN ('draft','published','active','revoked')",
name="chk_lifecycle",
),
CheckConstraint("version_major >= 0", name="chk_version_major_non_neg"),
CheckConstraint("version_minor >= 0", name="chk_version_minor_non_neg"),
CheckConstraint(
r"body_hash ~ '^[0-9a-f]{64}$'", name="chk_body_hash_format"
),
Index(
"idx_revisions_lookup",
"project_id", "contract_family", "contract_id",
"lifecycle_status", "version_major", "version_minor",
),
Index("idx_revisions_hash", "body_hash"),
)
revision_id: Mapped[UUID] = mapped_column(
primary_key=True, server_default=text("gen_random_uuid()")
)
project_id: Mapped[str] = mapped_column(
String(64), ForeignKey("awooop_projects.project_id"), nullable=False
)
contract_family: Mapped[str] = mapped_column(String(32), nullable=False)
contract_id: Mapped[str] = mapped_column(String(128), nullable=False)
version_major: Mapped[int] = mapped_column(SmallInteger, nullable=False, default=1)
version_minor: Mapped[int] = mapped_column(SmallInteger, nullable=False, default=0)
lifecycle_status: Mapped[str] = mapped_column(
String(16), nullable=False, default="draft"
)
body_json: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False)
body_hash: Mapped[str] = mapped_column(String(64), nullable=False)
body_schema_version: Mapped[str] = mapped_column(
String(16), nullable=False, default="v1.0"
)
publish_signature: Mapped[str | None] = mapped_column(String(128), nullable=True)
publisher_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
published_at: Mapped[datetime | None] = mapped_column(nullable=True)
created_at: Mapped[datetime] = mapped_column(
nullable=False, server_default=text("NOW()")
)
class AwoooPActiveRevision(Base):
"""Active revision pointerADR-107/ADR-113"""
__tablename__ = "awooop_active_revisions"
__table_args__ = (
UniqueConstraint(
"project_id", "contract_family", "contract_id",
name="uq_active_pointer",
),
)
pointer_id: Mapped[UUID] = mapped_column(
primary_key=True, server_default=text("gen_random_uuid()")
)
project_id: Mapped[str] = mapped_column(
String(64), ForeignKey("awooop_projects.project_id"), nullable=False
)
contract_family: Mapped[str] = mapped_column(String(32), nullable=False)
contract_id: Mapped[str] = mapped_column(String(128), nullable=False)
active_revision_id: Mapped[UUID] = mapped_column(
ForeignKey("awooop_contract_revisions.revision_id", ondelete="RESTRICT"),
nullable=False,
)
updated_at: Mapped[datetime] = mapped_column(
nullable=False, server_default=text("NOW()")
)
class AwoooPContractOutbox(Base):
"""Transactional outbox for contract revision invalidationADR-113"""
__tablename__ = "awooop_contract_outbox"
__table_args__ = (
UniqueConstraint("new_revision_id", "event_type", name="uq_outbox_event"),
Index(
"idx_outbox_pending",
"next_retry_at", "created_at",
postgresql_where=text("delivered_at IS NULL"),
),
Index(
"idx_outbox_backlog_per_project",
"project_id", "created_at",
postgresql_where=text("delivered_at IS NULL"),
),
)
event_id: Mapped[UUID] = mapped_column(
primary_key=True, server_default=text("gen_random_uuid()")
)
event_type: Mapped[str] = mapped_column(String(64), nullable=False)
project_id: Mapped[str] = mapped_column(
String(64), ForeignKey("awooop_projects.project_id"), nullable=False
)
contract_family: Mapped[str] = mapped_column(String(32), nullable=False)
contract_id: Mapped[str] = mapped_column(String(128), nullable=False)
old_revision_id: Mapped[UUID | None] = mapped_column(
ForeignKey("awooop_contract_revisions.revision_id"), nullable=True
)
new_revision_id: Mapped[UUID] = mapped_column(
ForeignKey("awooop_contract_revisions.revision_id"), nullable=False
)
created_at: Mapped[datetime] = mapped_column(
nullable=False, server_default=text("NOW()")
)
delivered_at: Mapped[datetime | None] = mapped_column(nullable=True)
relay_attempts: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
next_retry_at: Mapped[datetime | None] = mapped_column(nullable=True)
last_error: Mapped[str | None] = mapped_column(Text, nullable=True)
class AwoooPChannelEventDedupe(Base):
"""Channel event idempotency keyADR-114partitioned by created_at"""
__tablename__ = "awooop_channel_event_dedupe"
__table_args__ = (
UniqueConstraint(
"project_id", "channel_type", "provider_event_id", "created_at",
name="uq_channel_event_dedupe",
),
Index("idx_dedupe_run", "run_id"),
)
# Composite PKpartition key 必須是 PK 一部分)
# SQLAlchemy 2.x 要求 primary_key=True 標在 mapped_column不能用 __mapper_args__ 字串 list
dedupe_id: Mapped[UUID] = mapped_column(
primary_key=True, server_default=text("gen_random_uuid()")
)
project_id: Mapped[str] = mapped_column(String(64), nullable=False)
channel_type: Mapped[str] = mapped_column(String(32), nullable=False)
provider_event_id: Mapped[str] = mapped_column(String(256), nullable=False)
run_id: Mapped[UUID] = mapped_column(nullable=False)
created_at: Mapped[datetime] = mapped_column(
primary_key=True, server_default=text("NOW()")
)
class AwoooPPlatformSubject(Base):
"""Canonical principal mappingADR-115"""
__tablename__ = "awooop_platform_subjects"
__table_args__ = (
UniqueConstraint(
"project_id", "channel_type", "channel_user_id",
name="uq_platform_subject",
),
CheckConstraint(
"jsonb_typeof(roles) = 'array'", name="chk_roles_array"
),
Index(
"idx_platform_subjects_lookup",
"project_id", "channel_type", "channel_user_id",
),
Index(
"idx_platform_subjects_resolve",
"project_id", "platform_subject_id",
),
Index(
"idx_platform_subjects_last_seen",
"project_id", "last_seen_at",
),
)
subject_id: Mapped[UUID] = mapped_column(
primary_key=True, server_default=text("gen_random_uuid()")
)
project_id: Mapped[str] = mapped_column(
String(64), ForeignKey("awooop_projects.project_id"), nullable=False
)
channel_type: Mapped[str] = mapped_column(String(32), nullable=False)
channel_user_id: Mapped[str] = mapped_column(String(256), nullable=False)
channel_chat_id: Mapped[str | None] = mapped_column(String(256), nullable=True)
platform_subject_id: Mapped[str] = mapped_column(String(128), nullable=False)
display_name: Mapped[str | None] = mapped_column(String(256), nullable=True)
roles: Mapped[list[str]] = mapped_column(
JSONB, nullable=False, server_default=text("'[]'::jsonb")
)
first_seen_at: Mapped[datetime] = mapped_column(
nullable=False, server_default=text("NOW()")
)
last_seen_at: Mapped[datetime] = mapped_column(
nullable=False, server_default=text("NOW()")
)
class AwoooPProjectMigrationState(Base):
"""Strangler Fig migration state per project × capabilityADR-106 遷移追蹤)"""
__tablename__ = "awooop_project_migration_state"
__table_args__ = (
UniqueConstraint("project_id", "capability", name="uq_project_capability"),
CheckConstraint(
"capability IN ("
"'run_execution','contract_governance',"
"'budget_tracking','principal_mapping')",
name="chk_capability",
),
CheckConstraint(
"current_phase IN ("
"'legacy_awoooi_default','shadow','canary',"
"'read_only','suggest','auto_remediate')",
name="chk_phase",
),
)
state_id: Mapped[UUID] = mapped_column(
primary_key=True, server_default=text("gen_random_uuid()")
)
project_id: Mapped[str] = mapped_column(
String(64), ForeignKey("awooop_projects.project_id"), nullable=False
)
capability: Mapped[str] = mapped_column(String(64), nullable=False)
current_phase: Mapped[str] = mapped_column(
String(32), nullable=False, default="legacy_awoooi_default"
)
phase_entered_at: Mapped[datetime] = mapped_column(
nullable=False, server_default=text("NOW()")
)
updated_at: Mapped[datetime] = mapped_column(
nullable=False, server_default=text("NOW()")
)
# ─────────────────────────────────────────────────────────────────────────────
# Phase 4: Run State MachineADR-114/ADR-119
# 2026-05-04 ogt + Claude Sonnet 4.6
# ─────────────────────────────────────────────────────────────────────────────
class AwoooPRunState(Base):
"""Run FSM 主表SKIP LOCKED worker leaseADR-114"""
__tablename__ = "awooop_run_state"
__table_args__ = (
CheckConstraint(
"state IN ("
"'pending','running','waiting_tool',"
"'waiting_approval','completed','failed','cancelled','timeout')",
name="chk_run_state",
),
Index("idx_run_state_pending", "project_id", "created_at",
postgresql_where=text("state = 'pending' AND lease_until IS NULL")),
Index("idx_run_state_stale", "lease_until",
postgresql_where=text("state = 'running' AND lease_until IS NOT NULL")),
Index("idx_run_state_project_timeline", "project_id", "created_at"),
Index("idx_run_state_trace_id", "trace_id",
postgresql_where=text("trace_id IS NOT NULL")),
)
run_id: Mapped[UUID] = mapped_column(primary_key=True)
project_id: Mapped[str] = mapped_column(
String(64), ForeignKey("awooop_projects.project_id"), nullable=False
)
agent_id: Mapped[str] = mapped_column(String(128), nullable=False)
state: Mapped[str] = mapped_column(String(32), nullable=False, default="pending")
lease_until: Mapped[datetime | None] = mapped_column(nullable=True)
heartbeat_at: Mapped[datetime | None] = mapped_column(nullable=True)
worker_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
attempt_count: Mapped[int] = mapped_column(SmallInteger, nullable=False, default=0)
max_attempts: Mapped[int] = mapped_column(SmallInteger, nullable=False, default=3)
trace_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
trigger_type: Mapped[str | None] = mapped_column(String(32), nullable=True)
trigger_ref: Mapped[str | None] = mapped_column(String(256), nullable=True)
is_shadow: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
input_sha256: Mapped[str | None] = mapped_column(String(64), nullable=True)
output_sha256: Mapped[str | None] = mapped_column(String(64), nullable=True)
cost_usd: Mapped[Decimal] = mapped_column(
Numeric(10, 4), nullable=False, default=Decimal("0.0000")
)
step_count: Mapped[int] = mapped_column(SmallInteger, nullable=False, default=0)
error_code: Mapped[str | None] = mapped_column(String(64), nullable=True)
error_detail: Mapped[str | None] = mapped_column(Text, nullable=True)
created_at: Mapped[datetime] = mapped_column(
nullable=False, server_default=text("NOW()")
)
started_at: Mapped[datetime | None] = mapped_column(nullable=True)
completed_at: Mapped[datetime | None] = mapped_column(nullable=True)
timeout_at: Mapped[datetime | None] = mapped_column(nullable=True)
class AwoooPRunStepJournal(Base):
"""SAGA step journalADR-119— 每個 tool call 獨立記錄"""
__tablename__ = "awooop_run_step_journal"
__table_args__ = (
UniqueConstraint("run_id", "step_seq", name="uix_run_step_seq"),
CheckConstraint(
"result_status IN ('pending','success','failed','compensated')",
name="chk_step_result_status",
),
Index("idx_run_step_run_id", "run_id", "step_seq"),
)
step_id: Mapped[UUID] = mapped_column(
primary_key=True, server_default=text("gen_random_uuid()")
)
run_id: Mapped[UUID] = mapped_column(
ForeignKey("awooop_run_state.run_id", ondelete="CASCADE"), nullable=False
)
project_id: Mapped[str] = mapped_column(String(64), nullable=False)
step_seq: Mapped[int] = mapped_column(SmallInteger, nullable=False)
tool_name: Mapped[str] = mapped_column(String(128), nullable=False)
mcp_gateway_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
input_hash: Mapped[str | None] = mapped_column(String(64), nullable=True)
output_hash: Mapped[str | None] = mapped_column(String(64), nullable=True)
compensation_json: Mapped[dict[str, Any] | None] = mapped_column(JSONB, nullable=True)
result_status: Mapped[str] = mapped_column(String(16), nullable=False, default="pending")
error_code: Mapped[str | None] = mapped_column(String(64), nullable=True)
was_blocked: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
block_reason: Mapped[str | None] = mapped_column(String(128), nullable=True)
created_at: Mapped[datetime] = mapped_column(
nullable=False, server_default=text("NOW()")
)
completed_at: Mapped[datetime | None] = mapped_column(nullable=True)
latency_ms: Mapped[int | None] = mapped_column(Integer, nullable=True)
class AwoooPRunIdempotency(Base):
"""Run 去重冪等表ADR-114— (project_id, channel_type, provider_event_id) → run_id"""
__tablename__ = "awooop_run_idempotency"
__table_args__ = (
UniqueConstraint(
"project_id", "channel_type", "provider_event_id",
name="uix_run_idempotency_key",
),
Index("idx_run_idempotency_run_id", "run_id"),
)
idempotency_id: Mapped[UUID] = mapped_column(
primary_key=True, server_default=text("gen_random_uuid()")
)
project_id: Mapped[str] = mapped_column(String(64), nullable=False)
channel_type: Mapped[str] = mapped_column(String(32), nullable=False)
provider_event_id: Mapped[str] = mapped_column(String(256), nullable=False)
run_id: Mapped[UUID] = mapped_column(
ForeignKey("awooop_run_state.run_id"), nullable=False
)
created_at: Mapped[datetime] = mapped_column(
nullable=False, server_default=text("NOW()")
)
# =============================================================================
# Phase 5: MCP Gateway 四表ADR-116/ADR-1182026-05-04
# =============================================================================
class AwoooPMcpToolRegistry(Base):
"""MCP Tool 白名單Gate 3: Tool"""
__tablename__ = "awooop_mcp_tool_registry"
__table_args__ = (
CheckConstraint(
"tool_type IN ('builtin','mcp_server','custom')",
name="chk_tool_type",
),
CheckConstraint(
"jsonb_typeof(allowed_scopes) = 'array'",
name="chk_allowed_scopes_array",
),
UniqueConstraint("project_id", "tool_name", name="uix_tool_registry_project_name"),
Index("idx_mcp_tool_registry_project", "project_id", "is_active"),
)
tool_id: Mapped[UUID] = mapped_column(
primary_key=True, server_default=text("gen_random_uuid()")
)
project_id: Mapped[str] = mapped_column(
String(64), ForeignKey("awooop_projects.project_id", ondelete="CASCADE"), nullable=False
)
tool_name: Mapped[str] = mapped_column(String(128), nullable=False)
tool_type: Mapped[str] = mapped_column(String(32), nullable=False)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
allowed_scopes: Mapped[list[Any]] = mapped_column(JSONB, nullable=False, default=list)
environment_tags: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
created_at: Mapped[datetime] = mapped_column(
nullable=False, server_default=text("NOW()")
)
updated_at: Mapped[datetime] = mapped_column(
nullable=False, server_default=text("NOW()")
)
class AwoooPMcpGrant(Base):
"""Agent × Tool 授權記錄Gate 2 + Gate 3"""
__tablename__ = "awooop_mcp_grants"
__table_args__ = (
CheckConstraint(
"jsonb_typeof(granted_scopes) = 'array'",
name="chk_grant_scopes_array",
),
CheckConstraint(
"(is_revoked = FALSE AND revoked_at IS NULL AND revoked_by IS NULL)"
" OR (is_revoked = TRUE AND revoked_at IS NOT NULL)",
name="chk_revoke_consistency",
),
UniqueConstraint("project_id", "agent_id", "tool_id", name="uix_mcp_grant_agent_tool"),
Index(
"idx_mcp_grants_lookup", "project_id", "agent_id", "tool_id",
postgresql_where=text("is_revoked = FALSE"),
),
)
grant_id: Mapped[UUID] = mapped_column(
primary_key=True, server_default=text("gen_random_uuid()")
)
project_id: Mapped[str] = mapped_column(
String(64), ForeignKey("awooop_projects.project_id", ondelete="CASCADE"), nullable=False
)
agent_id: Mapped[str] = mapped_column(String(128), nullable=False)
tool_id: Mapped[UUID] = mapped_column(
ForeignKey("awooop_mcp_tool_registry.tool_id", ondelete="CASCADE"), nullable=False
)
granted_by: Mapped[str] = mapped_column(String(128), nullable=False)
granted_scopes: Mapped[list[Any]] = mapped_column(JSONB, nullable=False, default=list)
expires_at: Mapped[datetime | None] = mapped_column(nullable=True)
is_revoked: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
revoked_at: Mapped[datetime | None] = mapped_column(nullable=True)
revoked_by: Mapped[str | None] = mapped_column(String(128), nullable=True)
created_at: Mapped[datetime] = mapped_column(
nullable=False, server_default=text("NOW()")
)
class AwoooPMcpCredentialRef(Base):
"""k8s Secret 參照ADR-118 credential isolation— 只存路徑,不存明文"""
__tablename__ = "awooop_mcp_credential_refs"
__table_args__ = (
CheckConstraint(
r"k8s_secret_ref ~ '^[a-z0-9-]+/[a-z0-9-]+#[a-zA-Z0-9_-]+$'",
name="chk_k8s_ref_format",
),
CheckConstraint(
r"value_sha256 IS NULL OR value_sha256 ~ '^[0-9a-f]{64}$'",
name="chk_value_sha256_hex",
),
UniqueConstraint("tool_id", "k8s_secret_ref", name="uix_credential_ref_tool"),
Index("idx_mcp_cred_refs_tool", "tool_id", postgresql_where=text("is_active = TRUE")),
)
ref_id: Mapped[UUID] = mapped_column(
primary_key=True, server_default=text("gen_random_uuid()")
)
tool_id: Mapped[UUID] = mapped_column(
ForeignKey("awooop_mcp_tool_registry.tool_id", ondelete="CASCADE"), nullable=False
)
project_id: Mapped[str] = mapped_column(
String(64), ForeignKey("awooop_projects.project_id", ondelete="CASCADE"), nullable=False
)
k8s_secret_ref: Mapped[str] = mapped_column(String(256), nullable=False)
value_sha256: Mapped[str | None] = mapped_column(String(64), nullable=True)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
created_at: Mapped[datetime] = mapped_column(
nullable=False, server_default=text("NOW()")
)
rotated_at: Mapped[datetime | None] = mapped_column(nullable=True)
class AwoooPMcpGatewayAudit(Base):
"""MCP Gateway call 稽核日誌ADR-116 P1-09"""
__tablename__ = "awooop_mcp_gateway_audit"
__table_args__ = (
CheckConstraint(
"result_status IN ('success','blocked','failed','timeout')",
name="chk_gateway_result_status",
),
CheckConstraint(
"block_gate IS NULL OR (block_gate >= 1 AND block_gate <= 5)",
name="chk_block_gate_range",
),
Index("idx_mcp_audit_run", "project_id", "run_id", "created_at"),
Index(
"idx_mcp_audit_blocked", "project_id", "block_gate", "created_at",
postgresql_where=text("result_status = 'blocked'"),
),
)
call_id: Mapped[UUID] = mapped_column(
primary_key=True, server_default=text("gen_random_uuid()")
)
project_id: Mapped[str] = mapped_column(String(64), nullable=False)
run_id: Mapped[UUID | None] = mapped_column(nullable=True)
trace_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
agent_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
tool_id: Mapped[UUID | None] = mapped_column(
ForeignKey("awooop_mcp_tool_registry.tool_id"), nullable=True
)
tool_name: Mapped[str] = mapped_column(String(128), nullable=False)
credential_ref: Mapped[str | None] = mapped_column(String(256), nullable=True)
input_hash: Mapped[str | None] = mapped_column(String(64), nullable=True)
output_hash: Mapped[str | None] = mapped_column(String(64), nullable=True)
gate_result: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
result_status: Mapped[str] = mapped_column(String(16), nullable=False)
block_gate: Mapped[int | None] = mapped_column(SmallInteger, nullable=True)
block_reason: Mapped[str | None] = mapped_column(String(256), nullable=True)
latency_ms: Mapped[int | None] = mapped_column(Integer, nullable=True)
created_at: Mapped[datetime] = mapped_column(
nullable=False, server_default=text("NOW()")
)
# =============================================================================
# Phase 7: Channel Hub 雙表ADR-106 channel_event family2026-05-04
# =============================================================================
class AwoooPConversationEvent(Base):
"""入站 Channel Event 鏡像Telegram/LINE inbound不儲存明文"""
__tablename__ = "awooop_conversation_event"
__table_args__ = (
CheckConstraint(
"channel_type IN ('telegram','line','slack','api','internal')",
name="chk_conv_event_channel_type",
),
CheckConstraint(
"content_type IN ('text','photo','document','command','callback_query')",
name="chk_conv_event_content_type",
),
UniqueConstraint(
"project_id", "channel_type", "provider_event_id",
name="uix_conv_event_dedup",
),
Index("idx_conv_event_run", "project_id", "run_id", "received_at"),
Index("idx_conv_event_subject", "project_id", "platform_subject_id", "received_at"),
)
event_id: Mapped[UUID] = mapped_column(
primary_key=True, server_default=text("gen_random_uuid()")
)
project_id: Mapped[str] = mapped_column(
String(64), ForeignKey("awooop_projects.project_id", ondelete="CASCADE"), nullable=False
)
channel_type: Mapped[str] = mapped_column(String(32), nullable=False)
provider_event_id: Mapped[str] = mapped_column(String(256), nullable=False)
platform_subject_id: Mapped[str | None] = mapped_column(String(128), nullable=True)
channel_user_id: Mapped[str | None] = mapped_column(String(256), nullable=True)
channel_chat_id: Mapped[str | None] = mapped_column(String(256), nullable=True)
run_id: Mapped[UUID | None] = mapped_column(nullable=True)
content_type: Mapped[str] = mapped_column(String(32), nullable=False, default="text")
content_hash: Mapped[str | None] = mapped_column(String(64), nullable=True)
content_preview: Mapped[str | None] = mapped_column(String(256), nullable=True)
content_redacted: Mapped[str | None] = mapped_column(Text, nullable=True)
redaction_version: Mapped[str] = mapped_column(
String(32), nullable=False, server_default=text("'audit_sink_v1'")
)
source_envelope: Mapped[dict[str, Any]] = mapped_column(
JSONB, nullable=False, server_default=text("'{}'::jsonb")
)
attachment_sha256: Mapped[str | None] = mapped_column(String(64), nullable=True)
is_duplicate: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
provider_ts: Mapped[datetime | None] = mapped_column(nullable=True)
received_at: Mapped[datetime] = mapped_column(
nullable=False, server_default=text("NOW()")
)
class AwoooPOutboundMessage(Base):
"""出站訊息記錄interim/final/approval_request + shadow status"""
__tablename__ = "awooop_outbound_message"
__table_args__ = (
CheckConstraint(
"channel_type IN ('telegram','line','slack','api','internal')",
name="chk_outbound_channel_type",
),
CheckConstraint(
"message_type IN ('interim','final','error','approval_request')",
name="chk_outbound_message_type",
),
CheckConstraint(
"send_status IN ('pending','sent','failed','shadow')",
name="chk_outbound_send_status",
),
Index("idx_outbound_msg_run", "project_id", "run_id", "queued_at"),
Index(
"idx_outbound_msg_pending", "project_id", "channel_type", "queued_at",
postgresql_where=text("send_status = 'pending'"),
),
)
message_id: Mapped[UUID] = mapped_column(
primary_key=True, server_default=text("gen_random_uuid()")
)
project_id: Mapped[str] = mapped_column(
String(64), ForeignKey("awooop_projects.project_id", ondelete="CASCADE"), nullable=False
)
run_id: Mapped[UUID] = mapped_column(nullable=False)
conversation_event_id: Mapped[UUID | None] = mapped_column(nullable=True)
channel_type: Mapped[str] = mapped_column(String(32), nullable=False)
channel_chat_id: Mapped[str] = mapped_column(String(256), nullable=False)
message_type: Mapped[str] = mapped_column(String(32), nullable=False)
content_hash: Mapped[str | None] = mapped_column(String(64), nullable=True)
content_preview: Mapped[str | None] = mapped_column(String(256), nullable=True)
content_redacted: Mapped[str | None] = mapped_column(Text, nullable=True)
redaction_version: Mapped[str] = mapped_column(
String(32), nullable=False, server_default=text("'audit_sink_v1'")
)
source_envelope: Mapped[dict[str, Any]] = mapped_column(
JSONB, nullable=False, server_default=text("'{}'::jsonb")
)
provider_message_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
send_status: Mapped[str] = mapped_column(String(16), nullable=False, default="pending")
send_error: Mapped[str | None] = mapped_column(Text, nullable=True)
queued_at: Mapped[datetime] = mapped_column(
nullable=False, server_default=text("NOW()")
)
sent_at: Mapped[datetime | None] = mapped_column(nullable=True)
triggered_by_state: Mapped[str | None] = mapped_column(String(32), nullable=True)
waiting_since: Mapped[datetime | None] = mapped_column(nullable=True)