Files
DadaoTask/backend/alembic/versions/0004_knowledge.py

119 lines
5.1 KiB
Python
Raw Permalink 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.
"""M3 知识域:knowledge_doc + 扩宽 item.status(加知识四态)。
三个需要注意的点:
1. **item.status 的 CHECK 再扩一次**。0003 里是"任务 ∪ 工单",知识的
`draft/in_review/published/archived` 会被直接拦下。这里改为三者并集;
"哪种 kind 只能用哪组状态"由 `app/domain/*.py` 的状态机保证,不靠数据库
(CHECK 不能跨行判断 kind 与 status 的组合)。
2. **知识的来源写在数据库里**。`source_kind <> 'manual'` 时 `source_item_id` 必填。
这条 CHECK 是 D3 的物理落点:LLM 自动归档出来的知识若没有来源,用户无法核对,
等于往库里塞断言。
3. **downgrade 会删掉所有知识**(kind='knowledge' 的 item 及其扩展),原因与 0003 相同:
旧约束容纳不下知识状态,不回删就回不到 0003。
"""
from __future__ import annotations
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql
from app.roles import APP_ROLE
revision: str = "0004"
down_revision: str | None = "0003"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
TENANT_TABLES_0004: tuple[str, ...] = ("knowledge_doc",)
TASK_STATUSES = ("inbox", "todo", "doing", "done", "blocked", "canceled")
TICKET_STATUSES = ("open", "accepted", "in_progress", "pending_verify", "done", "canceled")
KNOWLEDGE_STATUSES = ("draft", "in_review", "published", "archived")
def upgrade() -> None:
# 1) item.status 放宽到 任务 ∪ 工单 ∪ 知识
op.execute("ALTER TABLE item DROP CONSTRAINT IF EXISTS ck_item_status")
statuses = ",".join(
f"'{s}'" for s in (*TASK_STATUSES, *TICKET_STATUSES, *KNOWLEDGE_STATUSES)
)
op.execute(f"ALTER TABLE item ADD CONSTRAINT ck_item_status CHECK (status IN ({statuses}))")
# 2) knowledge_doc
op.create_table(
"knowledge_doc",
sa.Column(
"item_id",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("item.id", ondelete="CASCADE"),
primary_key=True,
),
sa.Column(
"workspace_id",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("workspace.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column(
"author_id",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("app_user.id", ondelete="RESTRICT"),
nullable=False,
),
sa.Column(
"source_item_id",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("item.id", ondelete="SET NULL"),
nullable=True,
),
sa.Column("source_kind", sa.String(16), nullable=False, server_default="manual"),
sa.Column("doc_type", sa.String(16), nullable=False, server_default="note"),
sa.Column("status_hint", sa.String(16), nullable=False, server_default="draft"),
sa.Column("summary", sa.Text(), nullable=False, server_default=""),
sa.Column("generated_by", sa.String(16), nullable=False, server_default="human"),
sa.Column("published_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("archived_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("chunk_count", sa.Integer(), nullable=False, server_default="0"),
sa.Column(
"created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False
),
sa.Column(
"updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False
),
sa.Column("version", sa.Integer(), nullable=False, server_default="1"),
sa.CheckConstraint(
"source_kind = 'manual' OR source_item_id IS NOT NULL",
name="ck_knowledge_source_required",
),
sa.CheckConstraint("chunk_count >= 0", name="ck_knowledge_chunk_count"),
)
op.create_index("ix_knowledge_ws_status", "knowledge_doc", ["workspace_id", "status_hint"])
op.create_index("ix_knowledge_ws_source", "knowledge_doc", ["workspace_id", "source_item_id"])
# 3) RLS + 授权
op.execute("ALTER TABLE knowledge_doc ENABLE ROW LEVEL SECURITY")
op.execute(
"CREATE POLICY knowledge_doc_isolation ON knowledge_doc "
"USING (workspace_id = app_ws_id()) WITH CHECK (workspace_id = app_ws_id())"
)
op.execute(
f"GRANT SELECT, INSERT, UPDATE, DELETE ON {', '.join(TENANT_TABLES_0004)} TO {APP_ROLE}"
)
def downgrade() -> None:
# 旧约束容纳不下知识状态:先清掉知识 item,才能回到 0003 的约束
op.execute("DELETE FROM item WHERE kind = 'knowledge'")
op.execute("DROP POLICY IF EXISTS knowledge_doc_isolation ON knowledge_doc")
op.drop_index("ix_knowledge_ws_source", table_name="knowledge_doc")
op.drop_index("ix_knowledge_ws_status", table_name="knowledge_doc")
op.drop_table("knowledge_doc")
op.execute("ALTER TABLE item DROP CONSTRAINT IF EXISTS ck_item_status")
statuses = ",".join(f"'{s}'" for s in (*TASK_STATUSES, *TICKET_STATUSES))
op.execute(f"ALTER TABLE item ADD CONSTRAINT ck_item_status CHECK (status IN ({statuses}))")