知识以前只能"填个标题就完成",正文、标签、归类都要点进去再补。现在新建 就是一张完整的备忘录:标题、类型、分类、标签、正文一次写完,存完直接打开 详情继续编辑;知识页左侧多了工作区三级分类树,可按分类子树 / 未分类 / 标签 筛选。凡是填信息的抽屉都加宽了,还能一键展开整屏。 后端 - 新增 `knowledge_category`(迁移 0007):`workspace_id NOT NULL` + 自引用 `parent_id`、`depth` 1..3 检查约束、同层重名唯一索引(lower(name))、 RLS 策略与 `dadao_app` 授权;`knowledge_doc.category_id` 外键 ON DELETE SET NULL。 - 新增 `/knowledge/categories` 增删改查:超三级 `7007`、同层重名 `7008`、 跨租户或不存在 `7006`;`move=true` 才改父级,成环与移动后超过三级的子树 一律拒绝,删除有知识的分类要 `force=true`(知识变未分类)。 - 知识列表支持 `category_id`(含子树)、`uncategorized`、`tag`;输出带 `category`(含 `path` / `full_path`)。 - PATCH 只改分类或标签也会推进 `version`,避免"只动标签"之后下次保存撞 乐观锁 `4009`。 Web - `Sheet` 增 `size="md/lg/xl"` 与展开整屏按钮;任务、工单、知识抽屉统一 加宽(`TaskDetail` / `Tickets` / `Knowledge`)。 - 知识页:左侧三级分类树(新建、按子树筛选、未分类)、`KnowledgeComposer` 一次写全新的备忘录、列表卡片露出分类路径与标签、详情抽屉里可直接改分类 与标签。 - 抽屉详情只拉一次:重复的 effect 或迟到的响应不再覆盖用户正在敲的内容。 安卓 - `Api.kt`:分类模型与接口、新建/更新带 `tag_names`/`category_id`、 列表带分类与标签筛选。 - 知识页重做:新建改为近全屏大对话框(标题/类型/分类/标签/正文),列表 顶部按分类筛选,详情里能改分类与标签后单独保存。 文档与测试 - `2.Design/API.md`:5.1/5.3/5.4 补分类与筛选参数,新增 5.5 分类接口章节, 错误码补 `7006`/`7007`/`7008`。 - `2.Design/ROADMAP-M0-M1.md`:新增 3.11(三级分类)、3.12(备忘录形态)。 - 新增 `backend/tests/test_knowledge_categories.py`(8 例):备忘形态落库、 三级到底、移动越界与成环、重名、按子树/未分类/标签/关键词筛选、删分类需 `force`、跨租户隔离。 - 新增 `web/e2e/knowledge-notebook.mjs`(10 项,全绿):建两级分类 → 一次 写全 → 卡片与筛选 → 抽屉改标签/分类 → 正文落库 → 重名 7008。 验证:`pytest` 93 passed、`ruff check` / `ruff format --check` 全过、 `tsc --noEmit` 通过、`vite build` 通过、`knowledge-notebook.mjs` 10 项全绿。 安卓端本机只有 JDK 25,Gradle 8.7 / AGP 8.7.3 不支持,本次未做编译验证。
This commit is contained in:
@@ -39,6 +39,36 @@ DOC_TYPES: tuple[str, ...] = ("note", "howto", "faq", "postmortem", "decision")
|
||||
SOURCE_KINDS: tuple[str, ...] = ("manual", "task", "ticket")
|
||||
GENERATED_BY: tuple[str, ...] = ("human", "rule")
|
||||
|
||||
# 分类树最多三级。再深就不是"归档"而是"迷宫"了:找东西的人记不住第四层叫什么。
|
||||
MAX_CATEGORY_DEPTH = 3
|
||||
|
||||
|
||||
class KnowledgeCategory(Base, Timestamps):
|
||||
"""知识分类:一级大类 → 二级 → 三级,深不过三。
|
||||
|
||||
`depth` 是冗余列,但值得:每次筛选、移动子树都要判断层级,存下来就不用
|
||||
递归数祖先。写入时由 API 计算,读到不一致的行属于数据事故。
|
||||
"""
|
||||
|
||||
__tablename__ = "knowledge_category"
|
||||
__table_args__ = (
|
||||
Index("ix_kcat_ws_parent", "workspace_id", "parent_id"),
|
||||
CheckConstraint("depth BETWEEN 1 AND 3", name="ck_kcat_depth"),
|
||||
CheckConstraint("length(btrim(name)) > 0", name="ck_kcat_name"),
|
||||
{"comment": "知识分类树,深不过三"},
|
||||
)
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(PgUUID(as_uuid=True), primary_key=True, default=new_uuid)
|
||||
workspace_id: Mapped[uuid.UUID] = mapped_column(
|
||||
PgUUID(as_uuid=True), ForeignKey("workspace.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
parent_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||
PgUUID(as_uuid=True), ForeignKey("knowledge_category.id", ondelete="CASCADE")
|
||||
)
|
||||
name: Mapped[str] = mapped_column(String(40), nullable=False)
|
||||
depth: Mapped[int] = mapped_column(nullable=False, default=1)
|
||||
sort_order: Mapped[int] = mapped_column(nullable=False, default=0)
|
||||
|
||||
|
||||
class KnowledgeDoc(Base, Timestamps):
|
||||
"""知识的附加信息,与 item 一比一。"""
|
||||
@@ -47,6 +77,7 @@ class KnowledgeDoc(Base, Timestamps):
|
||||
__table_args__ = (
|
||||
Index("ix_knowledge_ws_status", "workspace_id", "status_hint"),
|
||||
Index("ix_knowledge_ws_source", "workspace_id", "source_item_id"),
|
||||
Index("ix_knowledge_ws_category", "workspace_id", "category_id"),
|
||||
CheckConstraint(
|
||||
"source_kind = 'manual' OR source_item_id IS NOT NULL",
|
||||
name="ck_knowledge_source_required",
|
||||
@@ -72,6 +103,10 @@ class KnowledgeDoc(Base, Timestamps):
|
||||
doc_type: Mapped[str] = mapped_column(String(16), nullable=False, default="note")
|
||||
# 冗余一份状态:列表页要按知识自己的口径筛,不想每次都回 item 表
|
||||
status_hint: Mapped[str] = mapped_column(String(16), nullable=False, default="draft")
|
||||
# 分类可以为空("未分类");分类被删时知识不跟着消失,只是回到未分类
|
||||
category_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||
PgUUID(as_uuid=True), ForeignKey("knowledge_category.id", ondelete="SET NULL")
|
||||
)
|
||||
summary: Mapped[str] = mapped_column(Text, nullable=False, default="")
|
||||
generated_by: Mapped[str] = mapped_column(String(16), nullable=False, default="human")
|
||||
published_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
@@ -109,4 +144,4 @@ class Chunk(Base):
|
||||
embed_model: Mapped[str | None] = mapped_column(String(64))
|
||||
|
||||
|
||||
KNOWLEDGE_TENANT_TABLES: tuple[str, ...] = ("knowledge_doc", "chunk")
|
||||
KNOWLEDGE_TENANT_TABLES: tuple[str, ...] = ("knowledge_category", "knowledge_doc", "chunk")
|
||||
|
||||
Reference in New Issue
Block a user