From ed31af3f46227feff07166dfb7546ed46514be50 Mon Sep 17 00:00:00 2001 From: Steven Date: Fri, 30 Oct 2026 03:51:43 +0800 Subject: [PATCH] =?UTF-8?q?feat(knowledge):=20=E7=9F=A5=E8=AF=86=E6=94=B9?= =?UTF-8?q?=E6=88=90=E5=A4=87=E5=BF=98=E5=BD=95=E5=BD=A2=E6=80=81=EF=BC=8C?= =?UTF-8?q?=E4=B8=80=E6=AC=A1=E5=86=99=E5=85=A8=20+=20=E4=B8=89=E7=BA=A7?= =?UTF-8?q?=E5=88=86=E7=B1=BB=EF=BC=8C=E6=8A=BD=E5=B1=89=E5=8A=A0=E5=AE=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 知识以前只能"填个标题就完成",正文、标签、归类都要点进去再补。现在新建 就是一张完整的备忘录:标题、类型、分类、标签、正文一次写完,存完直接打开 详情继续编辑;知识页左侧多了工作区三级分类树,可按分类子树 / 未分类 / 标签 筛选。凡是填信息的抽屉都加宽了,还能一键展开整屏。 后端 - 新增 `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 不支持,本次未做编译验证。 --- 2.Design/API.md | 51 +- 2.Design/ROADMAP-M0-M1.md | 3 + 2.Design/screens/knowledge-categories.png | Bin 0 -> 49673 bytes 2.Design/screens/knowledge-composer.png | Bin 0 -> 77730 bytes .../src/main/java/com/dadao/task/data/Api.kt | 59 +- .../java/com/dadao/task/ui/KnowledgeScreen.kt | 412 ++++++++++--- .../versions/0007_knowledge_categories.py | 79 +++ backend/app/api/knowledge.py | 351 +++++++++++- backend/app/errors.py | 6 + backend/app/models_knowledge.py | 37 +- backend/tests/test_knowledge_categories.py | 280 +++++++++ web/e2e/knowledge-notebook.mjs | 201 +++++++ web/src/components/TaskDetail.tsx | 2 +- web/src/components/icons.tsx | 13 + web/src/components/ui.tsx | 45 +- web/src/lib/types.ts | 17 + web/src/pages/Knowledge.tsx | 539 +++++++++++++++--- web/src/pages/Tickets.tsx | 1 + 18 files changed, 1932 insertions(+), 164 deletions(-) create mode 100644 2.Design/screens/knowledge-categories.png create mode 100644 2.Design/screens/knowledge-composer.png create mode 100644 backend/alembic/versions/0007_knowledge_categories.py create mode 100644 backend/tests/test_knowledge_categories.py create mode 100644 web/e2e/knowledge-notebook.mjs diff --git a/2.Design/API.md b/2.Design/API.md index 50f20cb..64cfa02 100644 --- a/2.Design/API.md +++ b/2.Design/API.md @@ -342,22 +342,30 @@ Web 端未读数由 `AppShell` 每 30 秒轮询一次;在通知页标已读后 | `status` | 可重复;`draft`/`in_review`/`published`/`archived`,不传为全部 | | `doc_type` | `note`/`howto`/`faq`/`postmortem`/`decision` | | `source_item_id` | 按来源事项筛 | +| `category_id` | 按分类筛,**包含子分类** | +| `uncategorized` | `true` 只看未分类 | +| `tag` | 按标签名筛(不区分大小写) | | `q` | 标题 / 正文 / 摘要子串 | | `limit` | 1–100,默认 30 | | `cursor` | 上一页的 `next_cursor` | -返回 `{items, next_cursor, has_more}`。每条是 `item` 字段 + `knowledge` + `source`: +返回 `{items, next_cursor, has_more}`。每条是 `item` 字段 + `knowledge` + `category` + `source`: ```json { "id": "…", "kind": "knowledge", "title": "发票原件必须一起寄", - "body_md": "…", "status": "published", "tags": [], + "body_md": "…", "status": "published", "tags": [{"id": "…", "name": "财务"}], "knowledge": { "doc_type": "howto", "summary": "…", "status_hint": "published", "source_kind": "task", "source_item_id": "…", "generated_by": "human", "published_at": "2026-10-28T05:54:44Z", "archived_at": null, "chunk_count": 0, "author_id": "…" }, + "category": { + "id": "…", "parent_id": "…", "name": "回滚", "depth": 3, + "sort_order": 0, "version": 1, + "path": ["研发", "部署", "回滚"], "full_path": "研发 / 部署 / 回滚" + }, "source": {"id": "…", "kind": "task", "title": "发票原件必须一起寄", "status": "done"} } ``` @@ -374,19 +382,43 @@ Web 端未读数由 `AppShell` 每 30 秒轮询一次;在通知页标已读后 ```json {"title": "报销单要贴发票原件", "body_md": "…", "doc_type": "note", + "tag_names": ["财务", "报销"], "category_id": "可选的分类 id", "source_item_id": "可选的来源事项 id"} ``` - 不带 `source_item_id` → `source_kind=manual`(手记),允许没有来源。 - 带 → 来源必须是同工作区的 `task`/`ticket`,否则 `7005`。 +- `category_id` 可省(未分类);跨工作区的分类一律按不存在处理(`7006`)。 +- `tag_names` 按名字去重创建,空格/逗号分隔由前端拆好再传。 - 新知识一律 `draft`,不会直接对外可见。 ### 5.4 `PATCH /knowledge/{item_id}` — 修改内容 -必须带 `version`(乐观锁,冲突 `4009`)。可改 `title`/`body_md`/`summary`/`doc_type`。 +必须带 `version`(乐观锁,冲突 `4009`)。可改 `title`/`body_md`/`summary`/`doc_type`/`tag_names`/`category_id`; +传 `clear_category: true` 表示移到未分类。 **已发布的不许直接改**(`7003`,先撤回审议或归档),避免"发布后被悄悄改写"。 -### 5.5 `POST /knowledge/{item_id}/status` — 状态动作 +### 5.5 知识分类 `/knowledge/categories` + +分类是工作区级的三级小树,用来给知识安一个位置;它**不是权限边界**,只是归类。 + +| 方法 | 路径 | 说明 | +|---|---|---| +| GET | `/knowledge/categories` | 扁平列表(按 depth/sort_order 排好),每条带 `doc_count`、`path`、`full_path`;另有 `max_depth=3` | +| POST | `/knowledge/categories` | `{name, parent_id?, sort_order?}` | +| PATCH | `/knowledge/categories/{id}` | `{name?, parent_id?, move?, sort_order?, version}` | +| DELETE | `/knowledge/categories/{id}?force=false` | 有子分类不让删;有知识要 `force=true`,删后知识变未分类 | + +三条规则: + +1. **最多三级**。第四级不是"更细",是"找不到",创建/移动越界一律 `7007`。 +2. **同一层不能重名**(不区分大小写),否则 `7008`;不同层可以同名。 +3. **移动带着子树走**:把一棵三层树挪到第二层下面会让最深节点到第五层,这种移动直接拒绝(`7007`), + 不静默改层级。 + +`PATCH` 里 `move=true` 才处理 `parent_id`,避免"只改名字"被误当成移动。改名的 `version` 冲突是 `4009`。 + +### 5.6 `POST /knowledge/{item_id}/status` — 状态动作 | action | 允许的前置状态 | 结果 | 谁能做 | |---|---|---|---| @@ -401,7 +433,7 @@ Web 端未读数由 `AppShell` 每 30 秒轮询一次;在通知页标已读后 **发布要 owner/admin 的理由**:发布等于团队为这条结论背书,不能谁都能推。 -### 5.6 `POST /items/{item_id}/knowledge` — 把一件事沉淀成知识 +### 5.7 `POST /items/{item_id}/knowledge` — 把一件事沉淀成知识 Query:`doc_type`(默认 `postmortem`)。来源可以是任务或工单;工单会带出验收结论。 返回与 5.2 相同的知识对象;同一来源重复沉淀 → `7002`。 @@ -409,7 +441,7 @@ Query:`doc_type`(默认 `postmortem`)。来源可以是任务或工单; **工单验收通过时,后端会自动调用这条逻辑**(`generated_by=rule`,`doc_type=postmortem`, 正文开头固定一句"这份草稿由系统根据来源自动整理,请先核对再发布"),所以用户不需要记得点。 -### 5.7 检索 `GET /knowledge/search` +### 5.8 检索 `GET /knowledge/search` Query: | 参数 | 说明 | @@ -434,7 +466,7 @@ Query: 2. **结果必须带来源**,能点回原文。这是 D3(知识必须有来源)的延伸:不给"出处不存在"的答案。 3. **宁可返回空,也不凑数**。检索失败比误导便宜。 -发布(`5.5` 的 `publish`)时会自动重建索引,用户发布完立刻能搜到; +发布(`5.6` 的 `publish`)时会自动重建索引,用户发布完立刻能搜到; 但改完正文不会自动重建,需要调用下面这条。 ### `POST /knowledge/{item_id}/reindex` — 重建切片 @@ -447,7 +479,7 @@ Query: 将来换 pgvector 只改列类型与索引,接口不变。换 embedding 模型(`EMBEDDING_PROVIDER`)后 **必须整体重建**,否则新旧向量混在一张表里,分数没法比较。 -### 5.8 知识的错误码 +### 5.9 知识的错误码 | 码 | HTTP | 含义 | |---|---|---| @@ -456,6 +488,9 @@ Query: | 7003 | 409 | 当前状态不允许这个动作 | | 7004 | 403 | 权限不足(如非管理员发布) | | 7005 | 409 | 来源事项无效或缺失 | +| 7006 | 404 | 分类不存在,或不是本工作区的 | +| 7007 | 422 | 分类层级超过三级,或移动后会让子树超过三级 | +| 7008 | 409 | 分类重名 / 还有子分类 / 分类下还有知识(需 `force`) | --- diff --git a/2.Design/ROADMAP-M0-M1.md b/2.Design/ROADMAP-M0-M1.md index d9b2e13..3bf1f70 100644 --- a/2.Design/ROADMAP-M0-M1.md +++ b/2.Design/ROADMAP-M0-M1.md @@ -291,11 +291,14 @@ | 3.8.4 | 索引生命周期:发布自动建、改正文需 `reindex`、换模型整体重建 | 0.5 | [x] `POST /knowledge/{id}/reindex` | | 3.9 | 安卓知识列表 | 1.0 | [x] 只读列表 + 详情动作(编辑正文走安卓编辑器) | | 3.10 | 文档与 e2e | 1.0 | [x] `knowledge.mjs` 7 项 + `retrieval.mjs` 6 项 | +| 3.11 | 知识分类:工作区三级小树(`knowledge_category` + 迁移 0007 + RLS),知识可归一类,列表按分类子树 / 未分类 / 标签筛选 | 1.0 | [x] 增删改移 + 重名 `7008`、超三级 `7007`、删分类要 `force` | +| 3.12 | 备忘录形态:新建知识一次写全(标题/类型/分类/标签/正文),不再是"填个标题就完成";抽屉加宽 + 可整屏 | 1.0 | [x] Web `KnowledgeComposer` + 安卓大对话框;`Sheet` 增 `size="md/lg/xl"` 与展开按钮;截图 `2.Design/screens/knowledge-composer.png`、`knowledge-categories.png` | ### M3 完成定义(DOD) - [x] 一件事办完 → 自动出现一份草稿 → 核对 → 发布,全程不用手动复制粘贴 - [x] 每条知识都能追到来源(除手记),点开能看到来源事项 - [x] 非管理员发布被拒;跨租户看不到别人的知识 +- [x] 知识是备忘录:标题、正文、标签、三级分类、搜索;新建时就能把这些一次填完(Web 与安卓一致) - [x] 切片与语义检索跑通(`real[]` + SQL 余弦;pgvector 仍待独立实例) - [ ] 真实用两周:新同事靠知识库自己解决过一次问题(需要真实团队) diff --git a/2.Design/screens/knowledge-categories.png b/2.Design/screens/knowledge-categories.png new file mode 100644 index 0000000000000000000000000000000000000000..03e003e5e15212359c89495d4ead32c256f9db42 GIT binary patch literal 49673 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 TyGcDb19|^e`c1Kj*0=uy4Ij}B literal 0 HcmV?d00001 diff --git a/2.Design/screens/knowledge-composer.png b/2.Design/screens/knowledge-composer.png new file mode 100644 index 0000000000000000000000000000000000000000..5d41f43585db9ebf996f8564a1a967b49019ad4f GIT binary patch literal 77730 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 = emptyList(), + @SerialName("full_path") val fullPath: String = "", + @SerialName("doc_count") val docCount: Int = 0, +) + +@Serializable +data class KnowledgeCategoryPage( + val items: List = emptyList(), + @SerialName("max_depth") val maxDepth: Int = 3, +) + @Serializable data class KnowledgeDoc( val id: String, @@ -245,6 +265,7 @@ data class KnowledgeDoc( val version: Int = 1, val tags: List = emptyList(), val knowledge: KnowledgeInfo = KnowledgeInfo(), + val category: KnowledgeCategory? = null, val source: SourceRef? = null, val attachments: List = emptyList(), ) @@ -325,6 +346,8 @@ private data class KnowledgeCreateRequest( val title: String, @SerialName("body_md") val bodyMd: String, @SerialName("doc_type") val docType: String, + @SerialName("tag_names") val tagNames: List = emptyList(), + @SerialName("category_id") val categoryId: String? = null, ) @Serializable @@ -351,6 +374,10 @@ private data class KnowledgeUpdateRequest( val version: Int, val title: String? = null, @SerialName("body_md") val bodyMd: String? = null, + @SerialName("tag_names") val tagNames: List? = null, + @SerialName("category_id") val categoryId: String? = null, + // 想把分类清成"未分类"就传 clear_category=true:只传 null 会被服务端当成"不改" + @SerialName("clear_category") val clearCategory: Boolean = false, ) /** @@ -666,20 +693,39 @@ class Api(private val tokens: TokenStore) { workspaceId: String, statuses: List = listOf("draft", "in_review"), query: String? = null, + categoryId: String? = null, + uncategorized: Boolean = false, ): KnowledgePage { val params = buildString { append("?limit=30") statuses.forEach { append("&status=").append(it) } - if (!query.isNullOrBlank()) append("&q=").append(java.net.URLEncoder.encode(query, "UTF-8")) + if (!query.isNullOrBlank()) { + append("&q=").append(java.net.URLEncoder.encode(query, "UTF-8")) + } + if (categoryId != null) append("&category_id=").append(categoryId) + if (uncategorized) append("&uncategorized=true") } return call(path = "/workspaces/$workspaceId/knowledge$params", auth = true) } + /** 工作区三级分类小树(扁平返回,depth 表示层级)。 */ + suspend fun knowledgeCategories(workspaceId: String): KnowledgeCategoryPage = + call(path = "/workspaces/$workspaceId/knowledge/categories", auth = true) + /** 手记:没有来源的知识,得自己写清楚。 */ - suspend fun createKnowledge(workspaceId: String, title: String, body: String = ""): KnowledgeDoc = + suspend fun createKnowledge( + workspaceId: String, + title: String, + body: String = "", + docType: String = "note", + tags: List = emptyList(), + categoryId: String? = null, + ): KnowledgeDoc = call( path = "/workspaces/$workspaceId/knowledge", - body = json.encodeToString(KnowledgeCreateRequest(title, body, "note")), + body = json.encodeToString( + KnowledgeCreateRequest(title, body, docType, tags, categoryId), + ), auth = true, ) @@ -709,9 +755,14 @@ class Api(private val tokens: TokenStore) { version: Int, title: String? = null, body: String? = null, + tags: List? = null, + categoryId: String? = null, + clearCategory: Boolean = false, ): KnowledgeDoc = call( path = "/workspaces/$workspaceId/knowledge/$itemId", - body = json.encodeToString(KnowledgeUpdateRequest(version, title, body)), + body = json.encodeToString( + KnowledgeUpdateRequest(version, title, body, tags, categoryId, clearCategory), + ), auth = true, method = "PATCH", ) diff --git a/android/app/src/main/java/com/dadao/task/ui/KnowledgeScreen.kt b/android/app/src/main/java/com/dadao/task/ui/KnowledgeScreen.kt index 58de4c3..34c9855 100644 --- a/android/app/src/main/java/com/dadao/task/ui/KnowledgeScreen.kt +++ b/android/app/src/main/java/com/dadao/task/ui/KnowledgeScreen.kt @@ -1,9 +1,12 @@ package com.dadao.task.ui +import androidx.compose.foundation.background +import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -33,8 +36,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties import com.dadao.task.data.Api import com.dadao.task.data.ApiException +import com.dadao.task.data.KnowledgeCategory import com.dadao.task.data.KnowledgeDoc import com.dadao.task.data.Workspace import kotlinx.coroutines.launch @@ -46,13 +52,26 @@ private val knowledgeTabs = listOf( "all" to "全部", ) -private fun docTypeLabel(type: String): String = when (type) { - "howto" -> "怎么做" - "faq" -> "问答" - "postmortem" -> "复盘" - "decision" -> "决策" - else -> "笔记" -} +private val docTypes = listOf( + "note" to "笔记", + "howto" to "怎么做", + "faq" to "问答", + "postmortem" to "复盘", + "decision" to "决策", +) + +private fun docTypeLabel(type: String): String = + docTypes.firstOrNull { it.first == type }?.second ?: "笔记" + +private fun parseTags(raw: String): List = + raw.split(' ', '\n', ',', ',', '、') + .map { it.trim().removePrefix("#") } + .filter { it.isNotBlank() } + .distinct() + +/** 分类在筛选条里的显示名:用点前缀表达层级,手机上比缩进更稳。 */ +private fun catLabel(cat: KnowledgeCategory): String = + "·".repeat((cat.depth - 1).coerceAtLeast(0)) + cat.name @Composable fun KnowledgeScreen( @@ -66,12 +85,18 @@ fun KnowledgeScreen( val scope = rememberCoroutineScope() var tab by remember { mutableStateOf("draft") } var docs by remember { mutableStateOf>(emptyList()) } + var categories by remember { mutableStateOf>(emptyList()) } var loading by remember { mutableStateOf(true) } var message by remember { mutableStateOf(null) } var open by remember { mutableStateOf(null) } var adding by remember { mutableStateOf(false) } - var newTitle by remember { mutableStateOf("") } - var newBody by remember { mutableStateOf("") } + // null = 全部;"none" = 未分类;其它是分类 id(服务端含子分类) + var catFilter by remember { mutableStateOf(null) } + + suspend fun loadCategories() { + runCatching { api.knowledgeCategories(workspace.id) } + .onSuccess { categories = it.items } + } suspend fun reload() { loading = true @@ -81,7 +106,15 @@ fun KnowledgeScreen( "all" -> listOf("draft", "in_review", "published", "archived") else -> listOf("draft", "in_review") } - runCatching { api.knowledge(workspace.id, statuses) } + val filter = catFilter + runCatching { + api.knowledge( + workspace.id, + statuses, + categoryId = if (filter != null && filter != "none") filter else null, + uncategorized = filter == "none", + ) + } .onSuccess { docs = it.items message = null @@ -90,7 +123,8 @@ fun KnowledgeScreen( loading = false } - LaunchedEffect(tab) { reload() } + LaunchedEffect(tab, catFilter) { reload() } + LaunchedEffect(Unit) { loadCategories() } // 从编辑器保存回来:本页是新建出来的,刷完列表再把那条详情重新打开。 LaunchedEffect(reopenItem) { @@ -125,11 +159,30 @@ fun KnowledgeScreen( } } Spacer(Modifier.height(10.dp)) - GhostButton("记一条", onClick = { - adding = true - newTitle = "" - newBody = "" - }) + Row( + modifier = Modifier.horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + SurfaceChip( + text = "全部分类", + selected = catFilter == null, + onClick = { catFilter = null }, + ) + SurfaceChip( + text = "未分类", + selected = catFilter == "none", + onClick = { catFilter = "none" }, + ) + categories.forEach { cat -> + SurfaceChip( + text = catLabel(cat), + selected = catFilter == cat.id, + onClick = { catFilter = cat.id }, + ) + } + } + Spacer(Modifier.height(10.dp)) + GhostButton("记一条", onClick = { adding = true }) } } @@ -141,7 +194,7 @@ fun KnowledgeScreen( if (!loading && docs.isEmpty()) { item { EmptyState( - "还没有待核对的知识", + if (catFilter == null) "还没有待核对的知识" else "这个分类下还没有知识", "把工单验收通过,或在任务详情里点「沉淀成知识」", ) } @@ -165,6 +218,18 @@ fun KnowledgeScreen( fontWeight = FontWeight.Medium, ) Spacer(Modifier.height(6.dp)) + val meta = buildList { + doc.category?.let { add(it.fullPath.ifBlank { it.name }) } + doc.tags.forEach { add("#${it.name}") } + } + if (meta.isNotEmpty()) { + Text( + meta.joinToString(" "), + style = MaterialTheme.typography.labelSmall, + color = Primary, + ) + Spacer(Modifier.height(4.dp)) + } Text( sourceLine(doc), style = MaterialTheme.typography.labelSmall, @@ -182,68 +247,35 @@ fun KnowledgeScreen( api = api, workspace = workspace, doc = target, + categories = categories, onClose = { open = null }, onEdit = onEdit, onChanged = { - scope.launch { reload() } + scope.launch { + reload() + loadCategories() + } }, + onCategoriesChanged = { scope.launch { loadCategories() } }, onFail = { fail(it) }, ) } if (adding) { - AlertDialog( - onDismissRequest = { adding = false }, - title = { Text("记一条", style = MaterialTheme.typography.titleMedium) }, - text = { - Column { - Text( - "手记没有来源,想沉淀某件具体的事,去那条任务的详情里点「沉淀成知识」。", - style = MaterialTheme.typography.bodySmall, - color = InkMuted, - ) - Spacer(Modifier.height(10.dp)) - OutlinedTextField( - value = newTitle, - onValueChange = { newTitle = it }, - modifier = Modifier.fillMaxWidth(), - placeholder = { Text("一句话说清这条知识是什么", color = InkMuted) }, - shape = RoundedCornerShape(10.dp), - colors = TextFieldDefaults.colors( - focusedContainerColor = Color.White, - unfocusedContainerColor = Color.White, - focusedIndicatorColor = Primary, - unfocusedIndicatorColor = Line, - ), - ) - Spacer(Modifier.height(8.dp)) - OutlinedTextField( - value = newBody, - onValueChange = { newBody = it }, - modifier = Modifier.fillMaxWidth().heightIn(min = 90.dp), - placeholder = { Text("正文(可空,之后再补)", color = InkMuted) }, - shape = RoundedCornerShape(10.dp), - colors = TextFieldDefaults.colors( - focusedContainerColor = Color.White, - unfocusedContainerColor = Color.White, - focusedIndicatorColor = Primary, - unfocusedIndicatorColor = Line, - ), - ) + KnowledgeComposerDialog( + api = api, + workspace = workspace, + categories = categories, + onDismiss = { adding = false }, + onCreated = { created -> + adding = false + scope.launch { + loadCategories() + reload() + open = created } }, - confirmButton = { - TextButton(onClick = { - scope.launch { - adding = false - runCatching { api.createKnowledge(workspace.id, newTitle.trim(), newBody) } - .onSuccess { reload() } - .onFailure { fail(it) } - } - }) { Text("保存草稿") } - }, - dismissButton = { TextButton(onClick = { adding = false }) { Text("取消") } }, - containerColor = Paper, + onFail = { fail(it) }, ) } } @@ -257,18 +289,182 @@ private fun sourceLine(doc: KnowledgeDoc): String { } } +/** + * 新建知识:手机上也要能一次写全(标题/类型/分类/标签/正文)。 + * 用整屏 94% 宽的 Dialog 而不是 AlertDialog —— AlertDialog 的宽度上限装不下一篇备忘录。 + */ +@Composable +private fun KnowledgeComposerDialog( + api: Api, + workspace: Workspace, + categories: List, + onDismiss: () -> Unit, + onCreated: (KnowledgeDoc) -> Unit, + onFail: (Throwable) -> Unit, +) { + val scope = rememberCoroutineScope() + var title by remember { mutableStateOf("") } + var docType by remember { mutableStateOf("note") } + var categoryId by remember { mutableStateOf(null) } + var tags by remember { mutableStateOf("") } + var body by remember { mutableStateOf("") } + var saving by remember { mutableStateOf(false) } + + Dialog( + onDismissRequest = onDismiss, + properties = DialogProperties(usePlatformDefaultWidth = false), + ) { + Column( + Modifier + .fillMaxWidth(0.94f) + .fillMaxHeight(0.88f) + .background(Paper, RoundedCornerShape(16.dp)) + .padding(16.dp), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + "记一条知识", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.weight(1f), + ) + GhostButton("取消", onClick = onDismiss) + Spacer(Modifier.width(8.dp)) + PrimaryButton( + "保存", + enabled = !saving && title.isNotBlank(), + onClick = { + scope.launch { + saving = true + runCatching { + api.createKnowledge( + workspace.id, + title.trim(), + body, + docType, + parseTags(tags), + categoryId, + ) + } + .onSuccess { onCreated(it) } + .onFailure { onFail(it) } + saving = false + } + }, + ) + } + Spacer(Modifier.height(12.dp)) + Column( + Modifier.weight(1f).verticalScroll(rememberScrollState()), + ) { + OutlinedTextField( + value = title, + onValueChange = { title = it }, + modifier = Modifier.fillMaxWidth(), + placeholder = { Text("一句话说清这条知识是什么", color = InkMuted) }, + shape = RoundedCornerShape(10.dp), + colors = TextFieldDefaults.colors( + focusedContainerColor = Color.White, + unfocusedContainerColor = Color.White, + focusedIndicatorColor = Primary, + unfocusedIndicatorColor = Line, + ), + ) + Spacer(Modifier.height(10.dp)) + SectionLabel("类型") + Row( + modifier = Modifier.horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + docTypes.forEach { (key, label) -> + SurfaceChip( + text = label, + selected = docType == key, + onClick = { docType = key }, + ) + } + } + Spacer(Modifier.height(10.dp)) + SectionLabel("分类") + Row( + modifier = Modifier.horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + SurfaceChip( + text = "未分类", + selected = categoryId == null, + onClick = { categoryId = null }, + ) + categories.forEach { cat -> + SurfaceChip( + text = catLabel(cat), + selected = categoryId == cat.id, + onClick = { categoryId = cat.id }, + ) + } + } + if (categories.isEmpty()) { + Text( + "还没有分类,Web 端可以建三级分类", + style = MaterialTheme.typography.labelSmall, + color = InkMuted, + modifier = Modifier.padding(top = 4.dp), + ) + } + Spacer(Modifier.height(10.dp)) + OutlinedTextField( + value = tags, + onValueChange = { tags = it }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + placeholder = { Text("标签,空格或逗号分隔", color = InkMuted) }, + shape = RoundedCornerShape(10.dp), + colors = TextFieldDefaults.colors( + focusedContainerColor = Color.White, + unfocusedContainerColor = Color.White, + focusedIndicatorColor = Primary, + unfocusedIndicatorColor = Line, + ), + ) + Spacer(Modifier.height(10.dp)) + SectionLabel("正文 · Markdown") + OutlinedTextField( + value = body, + onValueChange = { body = it }, + modifier = Modifier.fillMaxWidth().heightIn(min = 240.dp), + placeholder = { Text("背景、做法、结论……写清楚以后自己还能看懂", color = InkMuted) }, + shape = RoundedCornerShape(10.dp), + colors = TextFieldDefaults.colors( + focusedContainerColor = Color.White, + unfocusedContainerColor = Color.White, + focusedIndicatorColor = Primary, + unfocusedIndicatorColor = Line, + ), + ) + Spacer(Modifier.height(12.dp)) + } + } + } +} + @Composable private fun KnowledgeDetailDialog( api: Api, workspace: Workspace, doc: KnowledgeDoc, + categories: List, onClose: () -> Unit, onChanged: () -> Unit, + onCategoriesChanged: () -> Unit, onFail: (Throwable) -> Unit, onEdit: (EditRequest) -> Unit, ) { val scope = rememberCoroutineScope() var action by remember { mutableStateOf(doc.status) } + var version by remember { mutableStateOf(doc.version) } + var categoryId by remember { mutableStateOf(doc.category?.id) } + var tags by remember { mutableStateOf(doc.tags.joinToString(" ") { it.name }) } + var savingMeta by remember { mutableStateOf(false) } + var metaNotice by remember { mutableStateOf(null) } val actions = when (action) { "draft" -> listOf("submit_review" to "提交审议", "publish" to "发布", "archive" to "归档") "in_review" -> listOf("withdraw" to "撤回审议", "publish" to "发布", "archive" to "归档") @@ -292,15 +488,61 @@ private fun KnowledgeDetailDialog( }, text = { Column( - Modifier.heightIn(max = 420.dp).verticalScroll(rememberScrollState()), + Modifier.heightIn(max = 460.dp).verticalScroll(rememberScrollState()), ) { Text(sourceLine(doc), style = MaterialTheme.typography.labelSmall, color = InkMuted) Spacer(Modifier.height(10.dp)) Text( - doc.bodyMd.ifBlank { "(还没有正文,点「补一句」在 Web 端补齐更快)" }, + doc.bodyMd.ifBlank { "(还没有正文,点「编辑正文」补齐)" }, style = MaterialTheme.typography.bodySmall, color = InkSoft, ) + + // 已发布的内容不能直接改,分类/标签也跟着锁住,避免"发布后又悄悄换归属" + if (action != "published") { + Spacer(Modifier.height(14.dp)) + SectionLabel("分类") + Row( + modifier = Modifier.horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + SurfaceChip( + text = "未分类", + selected = categoryId == null, + onClick = { categoryId = null }, + ) + categories.forEach { cat -> + SurfaceChip( + text = catLabel(cat), + selected = categoryId == cat.id, + onClick = { categoryId = cat.id }, + ) + } + } + Spacer(Modifier.height(10.dp)) + OutlinedTextField( + value = tags, + onValueChange = { tags = it }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + placeholder = { Text("标签,空格或逗号分隔", color = InkMuted) }, + shape = RoundedCornerShape(10.dp), + colors = TextFieldDefaults.colors( + focusedContainerColor = Color.White, + unfocusedContainerColor = Color.White, + focusedIndicatorColor = Primary, + unfocusedIndicatorColor = Line, + ), + ) + if (metaNotice != null) { + Text( + metaNotice.orEmpty(), + style = MaterialTheme.typography.labelSmall, + color = Primary, + modifier = Modifier.padding(top = 6.dp), + ) + } + } } }, confirmButton = { @@ -312,7 +554,7 @@ private fun KnowledgeDetailDialog( itemId = doc.id, title = doc.title, body = doc.bodyMd, - version = doc.version, + version = version, titleEnabled = action == "draft", knowledge = true, ), @@ -320,6 +562,34 @@ private fun KnowledgeDetailDialog( onClose() }, ) { Text("编辑正文") } + if (action != "published") { + TextButton( + enabled = !savingMeta, + onClick = { + scope.launch { + savingMeta = true + runCatching { + api.updateKnowledge( + workspace.id, + doc.id, + version, + tags = parseTags(tags), + categoryId = categoryId, + clearCategory = categoryId == null, + ) + } + .onSuccess { + version = it.version + metaNotice = "分类与标签已保存" + onChanged() + onCategoriesChanged() + } + .onFailure { onFail(it) } + savingMeta = false + } + }, + ) { Text("保存分类标签") } + } actions.forEach { (key, label) -> TextButton(onClick = { scope.launch { diff --git a/backend/alembic/versions/0007_knowledge_categories.py b/backend/alembic/versions/0007_knowledge_categories.py new file mode 100644 index 0000000..e79a476 --- /dev/null +++ b/backend/alembic/versions/0007_knowledge_categories.py @@ -0,0 +1,79 @@ +"""知识分类(M3 补充) + +Revision ID: 0007 +Revises: 0006 +Create Date: 2026-10-30 + +知识库的基本形态是"备忘录",不是"任务":一条知识要有标题、正文、标签, +还要能归类。这里补上分类树(一级大类 / 二级 / 三级,深不过三)。 + +两个取舍: + +1. **分类独立成表,不塞进 item**。分类是工作区级的字典,一条知识挂一个分类; + 塞进 item 会让"分类"变成每条知识自己的一份副本,改个名字要改几百行。 +2. **删分类不删知识**。外键是 `ON DELETE SET NULL`:知识退回"未分类", + 分类只是入口,不是知识的容器。真要连内容一起删,那是删知识,得单独确认。 +""" + +from __future__ import annotations + +from collections.abc import Sequence + +from alembic import op + +revision: str = "0007" +down_revision: str = "0006" +branch_labels: Sequence[str] | None = None +depends_on: Sequence[str] | None = None + +CREATE_SQL = """ +CREATE TABLE knowledge_category ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + workspace_id uuid NOT NULL REFERENCES workspace(id) ON DELETE CASCADE, + parent_id uuid REFERENCES knowledge_category(id) ON DELETE CASCADE, + name varchar(40) NOT NULL, + depth integer NOT NULL DEFAULT 1, + sort_order integer NOT NULL DEFAULT 0, + version integer NOT NULL DEFAULT 1, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + CONSTRAINT ck_kcat_depth CHECK (depth BETWEEN 1 AND 3), + CONSTRAINT ck_kcat_name CHECK (length(btrim(name)) > 0) +) +""" + +INDEX_STATEMENTS = ( + "CREATE INDEX ix_kcat_ws_parent ON knowledge_category (workspace_id, parent_id)", + # 同一父级下不许重名。根分类的 parent_id 是 NULL,而 PG 里 NULL 互不相等, + # 所以根要单独一条 `WHERE parent_id IS NULL` 的部分索引,否则可以建出两个同名大类。 + "CREATE UNIQUE INDEX uq_kcat_ws_parent_name ON knowledge_category " + "(workspace_id, parent_id, lower(name)) WHERE parent_id IS NOT NULL", + "CREATE UNIQUE INDEX uq_kcat_ws_root_name ON knowledge_category " + "(workspace_id, lower(name)) WHERE parent_id IS NULL", + "COMMENT ON TABLE knowledge_category IS '知识分类树,深不过三'", + "ALTER TABLE knowledge_doc ADD COLUMN category_id uuid " + "REFERENCES knowledge_category(id) ON DELETE SET NULL", + "CREATE INDEX ix_knowledge_ws_category ON knowledge_doc (workspace_id, category_id)", +) + +POLICY_SQL = ( + "ALTER TABLE knowledge_category ENABLE ROW LEVEL SECURITY", + "CREATE POLICY knowledge_category_isolation ON knowledge_category " + "USING (workspace_id = app_ws_id()) WITH CHECK (workspace_id = app_ws_id())", + "GRANT SELECT, INSERT, UPDATE, DELETE ON knowledge_category TO dadao_app", +) + + +def upgrade() -> None: + op.execute(CREATE_SQL) + for statement in INDEX_STATEMENTS: + op.execute(statement) + for statement in POLICY_SQL: + op.execute(statement) + + +def downgrade() -> None: + op.execute("DROP INDEX IF EXISTS ix_knowledge_ws_category") + op.execute("ALTER TABLE knowledge_doc DROP COLUMN IF EXISTS category_id") + op.execute("DROP POLICY IF EXISTS knowledge_category_isolation ON knowledge_category") + op.execute("DROP TABLE IF EXISTS knowledge_category") diff --git a/backend/app/api/knowledge.py b/backend/app/api/knowledge.py index 04f16f9..e755c26 100644 --- a/backend/app/api/knowledge.py +++ b/backend/app/api/knowledge.py @@ -21,15 +21,21 @@ from typing import Annotated, Any from fastapi import APIRouter, Query from pydantic import BaseModel, Field -from sqlalchemy import Select, and_, or_, select, update +from sqlalchemy import Select, and_, func, or_, select, update from sqlalchemy.ext.asyncio import AsyncSession from ..audit import record as audit_record from ..deps import MemberDep, SessionDep from ..domain.knowledge import can_publish_role, sidebar_fields, target_status from ..errors import AppError, ErrorCode -from ..models_knowledge import DOC_TYPES, KNOWLEDGE_STATUSES, KnowledgeDoc -from ..models_task import Activity, Item +from ..models_knowledge import ( + DOC_TYPES, + KNOWLEDGE_STATUSES, + MAX_CATEGORY_DEPTH, + KnowledgeCategory, + KnowledgeDoc, +) +from ..models_task import Activity, Item, ItemTag, Tag from ..schemas import clamp_limit, decode_cursor, encode_cursor, ok from ..serializers import item_out, tags_of from ..services.knowledge import create_draft_from_source, find_doc_by_source @@ -47,6 +53,7 @@ class KnowledgeIn(BaseModel): doc_type: str = "note" source_item_id: uuid.UUID | None = None tag_names: list[str] = Field(default_factory=list) + category_id: uuid.UUID | None = None class KnowledgePatch(BaseModel): @@ -55,6 +62,24 @@ class KnowledgePatch(BaseModel): summary: str | None = None doc_type: str | None = None tag_names: list[str] | None = None + category_id: uuid.UUID | None = None + # 单独一个开关:`category_id=None` 既可能表示"没改",也可能表示"清空分类", + # 靠 pydantic 的未传/传 null 区分太隐晦,不如显式给一个。 + clear_category: bool = False + version: int + + +class CategoryIn(BaseModel): + name: str = Field(min_length=1, max_length=40) + parent_id: uuid.UUID | None = None + sort_order: int = 0 + + +class CategoryPatch(BaseModel): + name: str | None = Field(default=None, min_length=1, max_length=40) + parent_id: uuid.UUID | None = None + move: bool = False + sort_order: int | None = None version: int @@ -112,6 +137,7 @@ def _doc_out(doc: KnowledgeDoc) -> dict[str, Any]: "status_hint": doc.status_hint, "source_kind": doc.source_kind, "source_item_id": str(doc.source_item_id) if doc.source_item_id else None, + "category_id": str(doc.category_id) if doc.category_id else None, "generated_by": doc.generated_by, "published_at": doc.published_at, "archived_at": doc.archived_at, @@ -120,12 +146,96 @@ def _doc_out(doc: KnowledgeDoc) -> dict[str, Any]: } +# ── 分类 ──────────────────────────────────────────────────────────────── +async def _categories( + session: AsyncSession, ws_id: uuid.UUID +) -> dict[uuid.UUID, KnowledgeCategory]: + """一次把分类全捞出来。分类是工作区级的小字典(几十条),比按需查祖先便宜。""" + rows = ( + await session.execute( + select(KnowledgeCategory).where(KnowledgeCategory.workspace_id == ws_id) + ) + ).scalars() + return {row.id: row for row in rows} + + +def _category_payload( + cat: KnowledgeCategory, by_id: dict[uuid.UUID, KnowledgeCategory] +) -> dict[str, Any]: + """分类带上完整路径:`研发 / 部署 / 回滚`。客户端不用自己去拼。""" + names: list[str] = [] + node: KnowledgeCategory | None = cat + while node is not None and len(names) < MAX_CATEGORY_DEPTH: + names.append(node.name) + node = by_id.get(node.parent_id) if node.parent_id else None + names.reverse() + return { + "id": str(cat.id), + "parent_id": str(cat.parent_id) if cat.parent_id else None, + "name": cat.name, + "depth": cat.depth, + "sort_order": cat.sort_order, + "version": cat.version, + "path": names, + "full_path": " / ".join(names), + } + + +def _subtree_ids(root_id: uuid.UUID, by_id: dict[uuid.UUID, KnowledgeCategory]) -> list[uuid.UUID]: + children: dict[uuid.UUID, list[uuid.UUID]] = {} + for cat in by_id.values(): + if cat.parent_id: + children.setdefault(cat.parent_id, []).append(cat.id) + out = [root_id] + stack = [root_id] + while stack: + for child in children.get(stack.pop(), []): + out.append(child) + stack.append(child) + return out + + +async def _resolve_category( + session: AsyncSession, ws_id: uuid.UUID, category_id: uuid.UUID | None +) -> KnowledgeCategory | None: + if category_id is None: + return None + cat = await session.get(KnowledgeCategory, category_id) + if cat is None or cat.workspace_id != ws_id: + raise AppError(ErrorCode.KNOWLEDGE_CATEGORY_NOT_FOUND, "这个分类不存在") + return cat + + +async def _category_name_taken( + session: AsyncSession, + ws_id: uuid.UUID, + parent_id: uuid.UUID | None, + name: str, + exclude: uuid.UUID | None = None, +) -> bool: + stmt = select(KnowledgeCategory.id).where( + KnowledgeCategory.workspace_id == ws_id, + func.lower(KnowledgeCategory.name) == name.strip().lower(), + ) + stmt = ( + stmt.where(KnowledgeCategory.parent_id.is_(None)) + if parent_id is None + else stmt.where(KnowledgeCategory.parent_id == parent_id) + ) + if exclude is not None: + stmt = stmt.where(KnowledgeCategory.id != exclude) + return (await session.execute(stmt)).first() is not None + + async def _knowledge_out( session: AsyncSession, ws_id: uuid.UUID, item: Item, doc: KnowledgeDoc ) -> dict[str, Any]: tags = (await tags_of(session, ws_id, [item.id])).get(item.id, []) data = item_out(item, tags) data["knowledge"] = _doc_out(doc) + by_id = await _categories(session, ws_id) + cat = by_id.get(doc.category_id) if doc.category_id else None + data["category"] = _category_payload(cat, by_id) if cat else None data["source"] = await _source_out(session, ws_id, doc.source_item_id) data["attachments"] = await list_for_item(session, ws_id, item.id) data["attachment_count"] = len(data["attachments"]) @@ -148,6 +258,9 @@ async def list_knowledge( status: Annotated[list[str] | None, Query()] = None, doc_type: Annotated[str | None, Query()] = None, source_item_id: Annotated[uuid.UUID | None, Query()] = None, + category_id: Annotated[uuid.UUID | None, Query(description="含子分类")] = None, + uncategorized: Annotated[bool, Query(description="只看未分类")] = False, + tag: Annotated[str | None, Query(max_length=32)] = None, q: Annotated[str | None, Query(max_length=100)] = None, limit: Annotated[int, Query(ge=1, le=100, description="每页条数")] = 30, cursor: Annotated[str | None, Query(description="上一页返回的 next_cursor")] = None, @@ -165,6 +278,21 @@ async def list_knowledge( stmt = stmt.where(KnowledgeDoc.doc_type == doc_type) if source_item_id: stmt = stmt.where(KnowledgeDoc.source_item_id == source_item_id) + by_id: dict[uuid.UUID, KnowledgeCategory] = {} + if uncategorized: + stmt = stmt.where(KnowledgeDoc.category_id.is_(None)) + elif category_id is not None: + by_id = await _categories(session, ws_id) + cat = by_id.get(category_id) + if cat is None: + raise AppError(ErrorCode.KNOWLEDGE_CATEGORY_NOT_FOUND, "这个分类不存在") + stmt = stmt.where(KnowledgeDoc.category_id.in_(_subtree_ids(category_id, by_id))) + if tag: + stmt = ( + stmt.join(ItemTag, ItemTag.item_id == Item.id) + .join(Tag, Tag.id == ItemTag.tag_id) + .where(Tag.workspace_id == ws_id, func.lower(Tag.name) == tag.strip().lower()) + ) if q: like = f"%{q.strip()}%" stmt = stmt.where( @@ -188,12 +316,16 @@ async def list_knowledge( has_more = len(rows) > limit rows = rows[:limit] tag_map = await tags_of(session, ws_id, [it.id for it, _ in rows]) + if not by_id: + by_id = await _categories(session, ws_id) items = [] counts = await count_for_items(session, ws_id, [it.id for it, _ in rows]) for item, doc in rows: data = item_out(item, tag_map.get(item.id, [])) data["knowledge"] = _doc_out(doc) + cat = by_id.get(doc.category_id) if doc.category_id else None + data["category"] = _category_payload(cat, by_id) if cat else None data["source"] = await _source_out(session, ws_id, doc.source_item_id) data["attachment_count"] = counts.get(str(item.id), 0) items.append(data) @@ -204,6 +336,207 @@ async def list_knowledge( return ok({"items": items, "next_cursor": next_cursor, "has_more": has_more}) +# ── 分类的增删改查 ────────────────────────────────────────────────────── +# 路由顺序:`/knowledge/categories` 必须排在 `/knowledge/{item_id}` 之前, +# 否则 "categories" 会被当成 uuid 解析,直接 422。 +@router.get("/knowledge/categories", summary="知识分类") +async def list_categories(ctx: MemberDep, session: SessionDep) -> dict: + by_id = await _categories(session, ctx.ws_id) + counts = dict( + ( + await session.execute( + select(KnowledgeDoc.category_id, func.count()) + .join(Item, Item.id == KnowledgeDoc.item_id) + .where( + KnowledgeDoc.workspace_id == ctx.ws_id, + KnowledgeDoc.category_id.is_not(None), + KnowledgeDoc.status_hint != "archived", + Item.deleted_at.is_(None), + ) + .group_by(KnowledgeDoc.category_id) + ) + ).all() + ) + items = [] + for cat in sorted(by_id.values(), key=lambda c: (c.depth, c.sort_order, c.name)): + data = _category_payload(cat, by_id) + data["doc_count"] = counts.get(cat.id, 0) + items.append(data) + return ok({"items": items, "max_depth": MAX_CATEGORY_DEPTH}) + + +@router.post("/knowledge/categories", summary="新建分类") +async def create_category(body: CategoryIn, ctx: MemberDep, session: SessionDep) -> dict: + ws_id = ctx.ws_id + depth = 1 + if body.parent_id is not None: + parent = await _resolve_category(session, ws_id, body.parent_id) + depth = parent.depth + 1 + if depth > MAX_CATEGORY_DEPTH: + raise AppError( + ErrorCode.KNOWLEDGE_CATEGORY_DEPTH, + f"分类最多 {MAX_CATEGORY_DEPTH} 级,「{parent.name}」下面不能再建了", + details={"parent_depth": parent.depth}, + ) + name = body.name.strip() + if await _category_name_taken(session, ws_id, body.parent_id, name): + raise AppError(ErrorCode.KNOWLEDGE_CATEGORY_CONFLICT, "同一层已经有同名的分类") + cat = KnowledgeCategory( + workspace_id=ws_id, + parent_id=body.parent_id, + name=name, + depth=depth, + sort_order=body.sort_order, + ) + session.add(cat) + await session.flush() + await audit_record( + session, + workspace_id=ws_id, + actor_user_id=ctx.user.id, + action="knowledge.category.create", + target_type="knowledge_category", + target_id=str(cat.id), + ) + by_id = await _categories(session, ws_id) + return ok(_category_payload(cat, by_id)) + + +@router.patch("/knowledge/categories/{category_id}", summary="改名或移动分类") +async def patch_category( + category_id: uuid.UUID, body: CategoryPatch, ctx: MemberDep, session: SessionDep +) -> dict: + ws_id = ctx.ws_id + cat = await session.get(KnowledgeCategory, category_id) + if cat is None or cat.workspace_id != ws_id: + raise AppError(ErrorCode.KNOWLEDGE_CATEGORY_NOT_FOUND, "这个分类不存在") + by_id = await _categories(session, ws_id) + now = datetime.now(timezone.utc) + + new_parent_id = cat.parent_id + new_depth = cat.depth + shift = 0 + subtree: list[uuid.UUID] = [] + if body.move: + new_parent_id = body.parent_id + if new_parent_id is not None and new_parent_id not in by_id: + raise AppError(ErrorCode.KNOWLEDGE_CATEGORY_NOT_FOUND, "这个分类不存在") + if new_parent_id == category_id: + raise AppError(ErrorCode.KNOWLEDGE_CATEGORY_DEPTH, "不能把分类挂到自己下面") + subtree = _subtree_ids(category_id, by_id) + if new_parent_id is not None and new_parent_id in subtree: + raise AppError(ErrorCode.KNOWLEDGE_CATEGORY_DEPTH, "不能把分类移到它自己的子分类下") + new_depth = by_id[new_parent_id].depth + 1 if new_parent_id else 1 + deepest = max(by_id[cid].depth for cid in subtree) + shift = new_depth - cat.depth + if deepest + shift > MAX_CATEGORY_DEPTH: + raise AppError( + ErrorCode.KNOWLEDGE_CATEGORY_DEPTH, + f"移动后子树最深到第 {deepest + shift} 级,超过 {MAX_CATEGORY_DEPTH} 级", + details={"deepest_after_move": deepest + shift}, + ) + + name = body.name.strip() if body.name is not None else cat.name + if name != cat.name and await _category_name_taken( + session, ws_id, new_parent_id, name, exclude=category_id + ): + raise AppError(ErrorCode.KNOWLEDGE_CATEGORY_CONFLICT, "同一层已经有同名的分类") + + values: dict[str, Any] = {"name": name, "updated_at": now} + if body.move: + values.update(parent_id=new_parent_id, depth=new_depth) + if body.sort_order is not None: + values["sort_order"] = body.sort_order + + result = await session.execute( + update(KnowledgeCategory) + .where( + KnowledgeCategory.id == category_id, + KnowledgeCategory.workspace_id == ws_id, + KnowledgeCategory.version == body.version, + ) + .values(version=KnowledgeCategory.version + 1, **values) + ) + if result.rowcount == 0: + await session.refresh(cat) + raise AppError( + ErrorCode.VERSION_CONFLICT, + "这个分类刚被改过", + details={"current_version": cat.version}, + ) + + # 子树跟着整体升降;只改被移动的那棵子树以外的行会留下 depth 不一致 + if body.move and shift: + for cid in subtree: + if cid == category_id: + continue + await session.execute( + update(KnowledgeCategory) + .where(KnowledgeCategory.id == cid, KnowledgeCategory.workspace_id == ws_id) + .values( + depth=KnowledgeCategory.depth + shift, + updated_at=now, + version=KnowledgeCategory.version + 1, + ) + ) + await session.flush() + fresh = await _categories(session, ws_id) + return ok(_category_payload(fresh[category_id], fresh)) + + +@router.delete("/knowledge/categories/{category_id}", summary="删除分类") +async def delete_category( + category_id: uuid.UUID, + ctx: MemberDep, + session: SessionDep, + force: Annotated[bool, Query(description="真删:里面的知识变成未分类")] = False, +) -> dict: + ws_id = ctx.ws_id + cat = await session.get(KnowledgeCategory, category_id) + if cat is None or cat.workspace_id != ws_id: + raise AppError(ErrorCode.KNOWLEDGE_CATEGORY_NOT_FOUND, "这个分类不存在") + children = ( + await session.execute( + select(func.count()) + .select_from(KnowledgeCategory) + .where( + KnowledgeCategory.workspace_id == ws_id, + KnowledgeCategory.parent_id == category_id, + ) + ) + ).scalar_one() + if children: + raise AppError( + ErrorCode.KNOWLEDGE_CATEGORY_CONFLICT, + f"「{cat.name}」下面还有 {children} 个子分类,先把它们移走或删掉", + details={"children": children}, + ) + docs = ( + await session.execute( + select(func.count()) + .select_from(KnowledgeDoc) + .where(KnowledgeDoc.workspace_id == ws_id, KnowledgeDoc.category_id == category_id) + ) + ).scalar_one() + if docs and not force: + raise AppError( + ErrorCode.KNOWLEDGE_CATEGORY_CONFLICT, + f"「{cat.name}」里还有 {docs} 条知识,先移走,或者带 force 删成未分类", + details={"doc_count": docs}, + ) + await session.delete(cat) + await audit_record( + session, + workspace_id=ws_id, + actor_user_id=ctx.user.id, + action="knowledge.category.delete", + target_type="knowledge_category", + target_id=str(category_id), + payload={"orphaned_docs": docs}, + ) + return ok({"deleted": str(category_id), "uncategorized_docs": docs}) + + # 注意路由顺序:`/knowledge/search` 必须排在 `/knowledge/{item_id}` 之前, # 否则 "search" 会被当成 uuid 解析,返回 422。 @router.get("/knowledge/search", summary="检索已发布知识") @@ -257,6 +590,7 @@ async def create_knowledge(body: KnowledgeIn, ctx: MemberDep, session: SessionDe ErrorCode.KNOWLEDGE_SOURCE_REQUIRED, "来源必须是与本知识同一工作区的任务或工单", ) + category = await _resolve_category(session, ws_id, body.category_id) item = Item( workspace_id=ws_id, @@ -278,6 +612,7 @@ async def create_knowledge(body: KnowledgeIn, ctx: MemberDep, session: SessionDe doc_type=body.doc_type, status_hint="draft", summary=body.summary, + category_id=category.id if category else None, generated_by="human", ) session.add(doc) @@ -320,7 +655,10 @@ async def patch_knowledge( if body.doc_type is not None: values["doc_type"] = body.doc_type - if values: + # 分类/标签也改的是这条知识的"样子",所以同样要推进 version: + # 否则只改了标签的知识,下一次保存还会拿着旧 version 撞 4009。 + meta_changed = body.clear_category or body.category_id is not None or body.tag_names is not None + if values or meta_changed: values["updated_at"] = datetime.now(timezone.utc) result = await session.execute( update(Item) @@ -344,6 +682,11 @@ async def patch_knowledge( doc.doc_type = body.doc_type if body.summary is not None: doc.summary = body.summary + if body.clear_category: + doc.category_id = None + elif body.category_id is not None: + category = await _resolve_category(session, ws_id, body.category_id) + doc.category_id = category.id if category else None if body.tag_names is not None: await _set_tags_for(session, ws_id, item, body.tag_names) await session.flush() diff --git a/backend/app/errors.py b/backend/app/errors.py index c67a9ec..0f9b2c3 100644 --- a/backend/app/errors.py +++ b/backend/app/errors.py @@ -56,6 +56,9 @@ class ErrorCode(IntEnum): KNOWLEDGE_STATUS_INVALID = 7003 KNOWLEDGE_FORBIDDEN = 7004 KNOWLEDGE_SOURCE_REQUIRED = 7005 + KNOWLEDGE_CATEGORY_NOT_FOUND = 7006 + KNOWLEDGE_CATEGORY_DEPTH = 7007 + KNOWLEDGE_CATEGORY_CONFLICT = 7008 # 8xxx 附件 ATTACHMENT_NOT_FOUND = 8001 @@ -101,6 +104,9 @@ _HTTP_STATUS: dict[ErrorCode, int] = { ErrorCode.KNOWLEDGE_STATUS_INVALID: 409, ErrorCode.KNOWLEDGE_FORBIDDEN: 403, ErrorCode.KNOWLEDGE_SOURCE_REQUIRED: 409, + ErrorCode.KNOWLEDGE_CATEGORY_NOT_FOUND: 404, + ErrorCode.KNOWLEDGE_CATEGORY_DEPTH: 422, + ErrorCode.KNOWLEDGE_CATEGORY_CONFLICT: 409, ErrorCode.ATTACHMENT_NOT_FOUND: 404, ErrorCode.ATTACHMENT_TOO_LARGE: 413, ErrorCode.ATTACHMENT_FORBIDDEN: 403, diff --git a/backend/app/models_knowledge.py b/backend/app/models_knowledge.py index 9b54dda..3771ef9 100644 --- a/backend/app/models_knowledge.py +++ b/backend/app/models_knowledge.py @@ -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") diff --git a/backend/tests/test_knowledge_categories.py b/backend/tests/test_knowledge_categories.py new file mode 100644 index 0000000..c3f0bf8 --- /dev/null +++ b/backend/tests/test_knowledge_categories.py @@ -0,0 +1,280 @@ +"""知识库的"备忘录"形态:分类树、正文、标签、筛选。 + +知识最容易做坏的地方,是把它做成"只填标题的任务"——那样的库三个月后没人会打开。 +所以这里断言的是:一条知识能装下正文和标签、能被归到三级分类里、能按分类和标签找回来。 + +三条边界单独盯着: + +1. **三级到底**。第四级不是"更细",是"找不到";越界必须当场拒绝(7007)。 +2. **移动会带着子树一起动**。把一棵三层树挪到第二层下面,结果会是第五层, + 这种操作要么重算层级、要么拒绝——这里选择拒绝,因为静默改层级同样会让人找不到。 +3. **删分类不删知识**。有内容时必须显式 `force`,否则容易点一下就把别人的整理工作打散。 +""" + +from __future__ import annotations + +from typing import Any + +import pytest +from httpx import AsyncClient + +from .conftest import create_workspace, login, unwrap + +V1 = "/api/v1" + + +def _code(resp: Any) -> int: + return int(resp.json().get("code", 0)) + + +async def _ws(client: AsyncClient) -> tuple[dict[str, Any], str]: + user = await login(client) + ws = await create_workspace(client, user, "知识库") + return user, ws["id"] + + +async def _category( + client: AsyncClient, user: dict[str, Any], ws_id: str, name: str, parent_id: str | None = None +) -> dict[str, Any]: + return unwrap( + await client.post( + f"{V1}/workspaces/{ws_id}/knowledge/categories", + json={"name": name, "parent_id": parent_id}, + headers=user["headers"], + ) + ) + + +async def _note( + client: AsyncClient, + user: dict[str, Any], + ws_id: str, + title: str, + *, + body: str = "", + tags: list[str] | None = None, + category_id: str | None = None, +) -> dict[str, Any]: + return unwrap( + await client.post( + f"{V1}/workspaces/{ws_id}/knowledge", + json={ + "title": title, + "body_md": body, + "doc_type": "note", + "tag_names": tags or [], + "category_id": category_id, + }, + headers=user["headers"], + ) + ) + + +async def _list( + client: AsyncClient, user: dict[str, Any], ws_id: str, **params: Any +) -> list[dict[str, Any]]: + return unwrap( + await client.get( + f"{V1}/workspaces/{ws_id}/knowledge", params=params, headers=user["headers"] + ) + )["items"] + + +@pytest.mark.asyncio +async def test_note_keeps_body_tags_and_category_path(client: AsyncClient) -> None: + user, ws_id = await _ws(client) + root = await _category(client, user, ws_id, "研发") + child = await _category(client, user, ws_id, "部署", root["id"]) + leaf = await _category(client, user, ws_id, "回滚", child["id"]) + assert leaf["depth"] == 3 + assert leaf["full_path"] == "研发 / 部署 / 回滚" + + doc = await _note( + client, + user, + ws_id, + "回滚前先停掉灰度", + body="# 步骤\n\n1. 停灰度\n2. 回滚镜像\n", + tags=["发布", "事故"], + category_id=leaf["id"], + ) + assert doc["category"]["id"] == leaf["id"] + assert doc["category"]["full_path"] == "研发 / 部署 / 回滚" + assert {t["name"] for t in doc["tags"]} == {"发布", "事故"} + + detail = unwrap( + await client.get(f"{V1}/workspaces/{ws_id}/knowledge/{doc['id']}", headers=user["headers"]) + ) + assert "停灰度" in detail["body_md"] + assert detail["category"]["full_path"] == "研发 / 部署 / 回滚" + + +@pytest.mark.asyncio +async def test_category_depth_stops_at_three(client: AsyncClient) -> None: + user, ws_id = await _ws(client) + l1 = await _category(client, user, ws_id, "一级") + l2 = await _category(client, user, ws_id, "二级", l1["id"]) + l3 = await _category(client, user, ws_id, "三级", l2["id"]) + resp = await client.post( + f"{V1}/workspaces/{ws_id}/knowledge/categories", + json={"name": "四级", "parent_id": l3["id"]}, + headers=user["headers"], + ) + assert resp.status_code == 422, resp.text + assert _code(resp) == 7007 + + +@pytest.mark.asyncio +async def test_move_cannot_exceed_depth_or_loop(client: AsyncClient) -> None: + user, ws_id = await _ws(client) + a = await _category(client, user, ws_id, "A") + b = await _category(client, user, ws_id, "B", a["id"]) + c = await _category(client, user, ws_id, "C", b["id"]) + other = await _category(client, user, ws_id, "X") + + # 把 A(下面还有 B、C)移到 C 下面:自己是自己的祖先 + resp = await client.patch( + f"{V1}/workspaces/{ws_id}/knowledge/categories/{a['id']}", + json={"move": True, "parent_id": c["id"], "version": a["version"]}, + headers=user["headers"], + ) + assert resp.status_code in (409, 422), resp.text + assert _code(resp) == 7007 + + # 把 A 移到 X 下面:A 现在是一级,B、C 跟着下沉,C 会到第四级 -> 拒绝 + resp = await client.patch( + f"{V1}/workspaces/{ws_id}/knowledge/categories/{a['id']}", + json={"move": True, "parent_id": other["id"], "version": a["version"]}, + headers=user["headers"], + ) + assert _code(resp) == 7007 + del c, b + + +@pytest.mark.asyncio +async def test_rename_conflict_and_move_to_root(client: AsyncClient) -> None: + user, ws_id = await _ws(client) + a = await _category(client, user, ws_id, "研发") + b = await _category(client, user, ws_id, "市场") + dup = await client.post( + f"{V1}/workspaces/{ws_id}/knowledge/categories", + json={"name": "研发"}, + headers=user["headers"], + ) + assert dup.status_code == 409, dup.text + assert _code(dup) == 7008 + + child = await _category(client, user, ws_id, "部署", a["id"]) + moved = unwrap( + await client.patch( + f"{V1}/workspaces/{ws_id}/knowledge/categories/{child['id']}", + json={"move": True, "parent_id": None, "version": child["version"]}, + headers=user["headers"], + ) + ) + assert moved["parent_id"] is None + assert moved["depth"] == 1 + assert moved["full_path"] == "部署" + del b + + +@pytest.mark.asyncio +async def test_list_filters_by_category_subtree_tag_and_search(client: AsyncClient) -> None: + user, ws_id = await _ws(client) + root = await _category(client, user, ws_id, "研发") + sub = await _category(client, user, ws_id, "部署", root["id"]) + other = await _category(client, user, ws_id, "市场") + + deep = await _note( + client, + user, + ws_id, + "灰度发布流程", + body="先停灰度再回滚", + tags=["发布"], + category_id=sub["id"], + ) + await _note( + client, user, ws_id, "季度预算", body="市场预算表", tags=["财务"], category_id=other["id"] + ) + loose = await _note(client, user, ws_id, "散记", body="还没归类", tags=["草稿"]) + + by_category = await _list(client, user, ws_id, category_id=root["id"]) + assert {d["id"] for d in by_category} == {deep["id"]} + by_none = await _list(client, user, ws_id, uncategorized="true") + assert {d["id"] for d in by_none} == {loose["id"]} + assert {d["id"] for d in await _list(client, user, ws_id, tag="发布")} == {deep["id"]} + assert {d["id"] for d in await _list(client, user, ws_id, q="回滚")} == {deep["id"]} + + +@pytest.mark.asyncio +async def test_delete_category_needs_force_when_docs_exist(client: AsyncClient) -> None: + user, ws_id = await _ws(client) + cat = await _category(client, user, ws_id, "待清理") + doc = await _note(client, user, ws_id, "有主的东西", category_id=cat["id"]) + + blocked = await client.delete( + f"{V1}/workspaces/{ws_id}/knowledge/categories/{cat['id']}", headers=user["headers"] + ) + assert blocked.status_code == 409, blocked.text + assert _code(blocked) == 7008 + + forced = unwrap( + await client.delete( + f"{V1}/workspaces/{ws_id}/knowledge/categories/{cat['id']}?force=true", + headers=user["headers"], + ) + ) + assert forced["uncategorized_docs"] == 1 + detail = unwrap( + await client.get(f"{V1}/workspaces/{ws_id}/knowledge/{doc['id']}", headers=user["headers"]) + ) + assert detail["category"] is None + + +@pytest.mark.asyncio +async def test_category_is_tenant_scoped(client: AsyncClient) -> None: + owner, ws_id = await _ws(client) + intruder = await login(client) + other_ws = await create_workspace(client, intruder, "别人的工作区") + foreign = await _category(client, intruder, other_ws["id"], "他们的分类") + + listed = unwrap( + await client.get(f"{V1}/workspaces/{ws_id}/knowledge/categories", headers=owner["headers"]) + ) + assert listed["items"] == [] + + # 用别的工作区的分类 id 建知识:应被当作"不存在" + resp = await client.post( + f"{V1}/workspaces/{ws_id}/knowledge", + json={"title": "越界", "category_id": foreign["id"]}, + headers=owner["headers"], + ) + assert resp.status_code == 404, resp.text + assert _code(resp) == 7006 + + +@pytest.mark.asyncio +async def test_patch_knowledge_moves_and_clears_category(client: AsyncClient) -> None: + user, ws_id = await _ws(client) + cat = await _category(client, user, ws_id, "研发") + doc = await _note(client, user, ws_id, "一篇知识", category_id=cat["id"]) + + moved = unwrap( + await client.patch( + f"{V1}/workspaces/{ws_id}/knowledge/{doc['id']}", + json={"category_id": cat["id"], "tag_names": ["运维"], "version": doc["version"]}, + headers=user["headers"], + ) + ) + assert moved["category"]["name"] == "研发" + assert {t["name"] for t in moved["tags"]} == {"运维"} + + cleared = unwrap( + await client.patch( + f"{V1}/workspaces/{ws_id}/knowledge/{doc['id']}", + json={"clear_category": True, "version": moved["version"]}, + headers=user["headers"], + ) + ) + assert cleared["category"] is None diff --git a/web/e2e/knowledge-notebook.mjs b/web/e2e/knowledge-notebook.mjs new file mode 100644 index 0000000..769d94d --- /dev/null +++ b/web/e2e/knowledge-notebook.mjs @@ -0,0 +1,201 @@ +/** + * 知识备忘录形态 e2e:分类树 + 新建时一次填全(标题/类型/分类/标签/正文)+ 按分类筛。 + * + * 与 knowledge.mjs 的分工:那个脚本验"沉淀 → 发布"闭环,这个只验"知识是最像备忘录的形态"。 + * 前面造数据走接口,界面部分点真实按钮。 + */ +import { chromium } from "playwright-core"; + +const WEB = process.env.WEB ?? "http://127.0.0.1:5180"; +const API = process.env.API ?? "http://127.0.0.1:8100/api/v1"; +const stamp = String(Date.now()).slice(-7); +const phone = `137${stamp}0`; + +let failures = 0; +function check(name, ok, extra = "") { + console.log(`${ok ? "PASS" : "FAIL"} ${name}${extra ? ` — ${extra}` : ""}`); + if (!ok) failures += 1; +} + +async function call(method, path, body, token) { + const res = await fetch(`${API}${path}`, { + method, + headers: { + "content-type": "application/json", + ...(token ? { authorization: `Bearer ${token}` } : {}), + ...(method === "GET" ? {} : { "idempotency-key": `e2e-${Math.random()}` }), + }, + body: body ? JSON.stringify(body) : undefined, + }); + const text = await res.text(); + const json = text ? JSON.parse(text) : {}; + if (json.code !== 0) throw new Error(`${method} ${path} -> ${json.code} ${json.message}`); + return json.data; +} + +const browser = await chromium.launch({ + executablePath: + process.env.CHROME ?? + `${process.env.LOCALAPPDATA}\\ms-playwright\\chromium-1223\\chrome-win64\\chrome.exe`, +}); +const consoleErrors = []; +const context = await browser.newContext({ viewport: { width: 1280, height: 900 } }); +const page = await context.newPage(); +page.on("console", (m) => { + if (m.type() === "error") consoleErrors.push(m.text()); +}); +page.on("response", (r) => { + if (r.status() >= 400) consoleErrors.push(`${r.status()} ${r.url()}`); +}); + +try { + await call("POST", "/auth/sms/send", { phone }); + const login = await call("POST", "/auth/login", { phone, code: "123456" }); + const token = login.tokens.access_token; + const ws = await call("POST", "/workspaces", { name: `知识备忘 ${stamp}` }, token); + + await page.addInitScript( + ([access, refresh]) => { + localStorage.setItem("dadao.access", access); + localStorage.setItem("dadao.refresh", refresh); + }, + [token, login.tokens.refresh_token], + ); + await page.goto(`${WEB}/w/${ws.id}/knowledge`, { waitUntil: "networkidle" }); + await page.getByRole("heading", { name: "知识" }).waitFor({ timeout: 10_000 }); + + // ① 建两级分类:研发 / 部署 + await page.getByRole("button", { name: "+ 新建", exact: true }).click(); + await page.getByLabel("新分类名").fill("研发"); + await page.getByRole("button", { name: "创建", exact: true }).click(); + await page.getByRole("button", { name: /^研发/, exact: false }).first().waitFor(); + await page.getByRole("button", { name: "+ 新建", exact: true }).click(); + await page.getByLabel("新分类名").fill("部署"); + await page.getByLabel("父分类").selectOption({ label: "研发 下" }); + await page.getByRole("button", { name: "创建", exact: true }).click(); + await page.getByRole("button", { name: /^部署/, exact: false }).first().waitFor(); + check("能在侧栏建出「研发 / 部署」两级分类", true); + + // ② 记一条:一个面板里填完标题/类型/分类/标签/正文 + await page.getByRole("button", { name: "记一条", exact: true }).click(); + await page.getByLabel("标题").fill("回滚前先停灰度"); + await page.getByLabel("类型").selectOption("howto"); + await page.getByLabel("分类", { exact: true }).selectOption({ label: "研发 / 部署" }); + await page.getByLabel("标签").fill("发布 事故"); + await page + .getByLabel("正文") + .fill("## 步骤\n1. 停掉灰度流量\n2. 回滚镜像到上一个 tag\n3. 验证关键接口"); + await page.getByRole("button", { name: "保存并继续写", exact: true }).click(); + + // 保存后直接打开详情:标题、分类路径、标签都在 + await page.getByText("回滚前先停灰度", { exact: false }).first().waitFor({ timeout: 10_000 }); + const drawerText = (await page.textContent("body")) ?? ""; + check( + "新建面板能把分类与标签一起存下来", + drawerText.includes("研发 / 部署") && drawerText.includes("发布"), + ); + await page.screenshot({ path: "e2e/artifacts/knowledge-composer-saved.png", fullPage: false }); + + // ③ 列表卡片上能看到分类路径与标签(不用点开) + await page.getByRole("button", { name: "关闭", exact: true }).click(); + const card = page.locator("li").filter({ hasText: "回滚前先停灰度" }).first(); + await card.waitFor({ timeout: 10_000 }); + const cardText = (await card.textContent()) ?? ""; + check("列表卡片带分类路径与标签", cardText.includes("研发 / 部署") && cardText.includes("#发布")); + + // ④ 侧栏按分类筛:部署里有,未分类里没有 + await page.getByRole("button", { name: /^部署/, exact: false }).first().click(); + await page.waitForTimeout(400); + check( + "点「部署」能看到归属该分类的知识", + await page.getByText("回滚前先停灰度", { exact: false }).first().isVisible(), + ); + await page.getByRole("button", { name: "未分类", exact: true }).click(); + await page.waitForTimeout(400); + const uncategorizedHasCard = await page + .getByText("回滚前先停灰度", { exact: false }) + .first() + .isVisible() + .catch(() => false); + check("「未分类」里不出现已归类的知识", !uncategorizedHasCard); + await page.screenshot({ path: "e2e/artifacts/knowledge-category-filter.png", fullPage: false }); + + // ⑤ 抽屉里能改标签与分类,改完接口里就是新的 + await page.getByRole("button", { name: "全部知识", exact: true }).click(); + // 先等抽屉自己把详情拉回来再填:GET 比手快一步的话,刚填的字会被它覆盖掉 + await Promise.all([ + page.waitForResponse( + (r) => + r.url().includes("/knowledge/") && + r.request().method() === "GET" && + !r.url().endsWith("/knowledge/categories") && + r.status() === 200, + ), + page.getByRole("button", { name: "打开", exact: true }).first().click(), + ]); + const tagInput = page.getByLabel("标签", { exact: true }).last(); + await tagInput.waitFor({ timeout: 10_000 }); + // 先等抽屉把自己拉回来的旧标签填上——那一刻之后才没有"迟到的响应"来覆盖输入 + await page + .waitForFunction( + () => { + const el = document.querySelector('input[aria-label="标签"]'); + return !!el && el.value.includes("事故"); + }, + null, + { timeout: 10_000 }, + ) + .catch(() => {}); + // 用真实键盘输入而不是 fill:fill 走原生 setter,React 的受控 input 有时收不到这次变更 + await tagInput.click(); + await tagInput.press("Control+A"); + await page.keyboard.type("发布 回滚", { delay: 20 }); + check("抽屉里的标签输入框拿到新值", (await tagInput.inputValue()) === "发布 回滚"); + const [patchResp] = await Promise.all([ + page.waitForResponse( + (r) => + r.url().includes("/knowledge/") && + r.request().method() === "PATCH" && + r.status() === 200, + ), + page.getByRole("button", { name: "保存", exact: true }).last().click(), + ]); + check("抽屉里保存标签/分类成功", patchResp.ok()); + + const list = await call( + "GET", + `/workspaces/${ws.id}/knowledge?status=draft&status=in_review`, + undefined, + token, + ); + const doc = list.items.find((d) => d.title === "回滚前先停灰度"); + check( + "接口里标签已更新、分类是「研发 / 部署」", + String((doc?.tags ?? []).map((t) => t.name).join(" ")).includes("回滚") && + doc?.category?.full_path === "研发 / 部署", + `tags=${JSON.stringify((doc?.tags ?? []).map((t) => t.name))} category=${doc?.category?.full_path}`, + ); + check("正文按 Markdown 存下来了", String(doc?.body_md ?? "").includes("停掉灰度流量")); + + // ⑥ 分类重名与三级上限:接口层面确认约束还在 + const dup = await fetch( + `${API}/workspaces/${ws.id}/knowledge/categories`, + { + method: "POST", + headers: { "content-type": "application/json", authorization: `Bearer ${token}` }, + body: JSON.stringify({ name: "部署", parent_id: doc.category.parent_id }), + }, + ).then((r) => r.json()); + check("同层重名被拒(7008)", dup.code === 7008, `code=${dup.code}`); + + check("浏览器控制台与网络无报错", consoleErrors.length === 0, consoleErrors.slice(0, 3).join(" | ")); +} catch (err) { + failures += 1; + console.log(`FAIL 脚本异常 — ${err.message}`); + await page.screenshot({ path: "e2e/artifacts/knowledge-notebook-failure.png" }).catch(() => {}); + console.log((await page.textContent("body"))?.slice(0, 600)); +} finally { + await browser.close(); + console.log(failures === 0 ? "\nALL PASS" : `\n${failures} FAILED`); + process.exit(failures === 0 ? 0 : 1); +} diff --git a/web/src/components/TaskDetail.tsx b/web/src/components/TaskDetail.tsx index 34b181a..21d8f7b 100644 --- a/web/src/components/TaskDetail.tsx +++ b/web/src/components/TaskDetail.tsx @@ -353,7 +353,7 @@ export function TaskDetail({ ( ); +/** 放大到整屏 / 收回原来的宽度:长文和图片需要有地方放 */ +export const Expand = (p: IconProps) => ( + + + +); + +export const Collapse = (p: IconProps) => ( + + + +); + export const More = (p: IconProps) => ( diff --git a/web/src/components/ui.tsx b/web/src/components/ui.tsx index f8a61f2..63347b8 100644 --- a/web/src/components/ui.tsx +++ b/web/src/components/ui.tsx @@ -8,7 +8,17 @@ import { useState, type ReactNode, } from "react"; -import { Check, X } from "./icons"; +import { Check, Collapse, Expand, X } from "./icons"; + +/** + * 抽屉宽度档位。以前的抽屉默认 `max-w-xl`(576px),写长文像在门缝里写字; + * 知识、工单这类要贴图的内容统一走 xl,上面还有"展开整屏"。 + */ +const SHEET_SIZE = { + md: "max-w-2xl", + lg: "max-w-4xl", + xl: "max-w-6xl", +} as const; /* ——————————————————————————— 按钮 ——————————————————————————— */ @@ -267,7 +277,8 @@ export function Sheet({ subtitle, children, footer, - width = "max-w-xl", + size = "lg", + width, }: { open: boolean; onClose: () => void; @@ -275,8 +286,14 @@ export function Sheet({ subtitle?: ReactNode; children: ReactNode; footer?: ReactNode; + /** 宽度档位;长文/多图用 xl,右上方还能一键放满整屏 */ + size?: "md" | "lg" | "xl"; + /** 兜底:直接给 Tailwind 宽度类 */ width?: string; }) { + const [expanded, setExpanded] = useState(false); + const sizeClass = width ?? SHEET_SIZE[size]; + useEffect(() => { if (!open) return; const onKey = (e: KeyboardEvent) => { @@ -298,17 +315,31 @@ export function Sheet({ role="dialog" aria-modal="true" aria-label={typeof title === "string" ? title : "详情"} - className={`relative flex h-full w-full ${width} animate-slide-in-right flex-col border-l border-line-200 bg-white shadow-pop`} + className={`relative flex h-full w-full ${ + expanded ? "max-w-none" : sizeClass + } animate-slide-in-right flex-col border-l border-line-200 bg-white shadow-pop`} >
{title}
{subtitle ?
{subtitle}
: null}
- +
+ + +
{children}
{footer ? ( diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index 3fc5eca..6ed7e02 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -150,8 +150,25 @@ export interface KnowledgeSource { status: string; } +export interface KnowledgeCategory { + id: string; + parent_id: string | null; + name: string; + depth: number; + sort_order: number; + version: number; + /** 从根到自己的名字链,后端拼好了,前端不用自己爬树 */ + path: string[]; + full_path: string; +} + +export interface KnowledgeCategoryNode extends KnowledgeCategory { + doc_count: number; +} + export interface Knowledge extends Item { knowledge: KnowledgeInfo; + category: KnowledgeCategory | null; source: KnowledgeSource | null; attachments?: Attachment[]; attachment_count?: number; diff --git a/web/src/pages/Knowledge.tsx b/web/src/pages/Knowledge.tsx index 930ce5c..cbf45c1 100644 --- a/web/src/pages/Knowledge.tsx +++ b/web/src/pages/Knowledge.tsx @@ -3,7 +3,7 @@ import { useParams } from "react-router-dom"; import { AppShell } from "../components/AppShell"; import { AttachmentPanel } from "../components/AttachmentPanel"; import { RichTextEditor } from "../components/RichTextEditor"; -import { Book, Link2, Plus, Search, Sparkle } from "../components/icons"; +import { Link2, Plus, Search, Sparkle } from "../components/icons"; import { Button, EmptyState, @@ -14,7 +14,13 @@ import { StatusPill, } from "../components/ui"; import { ApiError, api } from "../lib/client"; -import type { Attachment, Knowledge, KnowledgeStatus, SearchHit } from "../lib/types"; +import type { + Attachment, + Knowledge, + KnowledgeCategoryNode, + KnowledgeStatus, + SearchHit, +} from "../lib/types"; const TABS = [ { key: "draft", label: "待核对", statuses: ["draft", "in_review"] }, @@ -61,12 +67,48 @@ export function KnowledgePage() { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [openId, setOpenId] = useState(null); - const [adding, setAdding] = useState(false); - const [draftTitle, setDraftTitle] = useState(""); + const [composing, setComposing] = useState(false); const [hits, setHits] = useState(null); const [searching, setSearching] = useState(false); + /** null = 不按分类筛(全部);"none" = 未分类;其它是分类 id(含子分类) */ + const [catFilter, setCatFilter] = useState(null); + const [categories, setCategories] = useState([]); + const [catBusy, setCatBusy] = useState(false); const reqId = useRef(0); + const loadCategories = useCallback(async () => { + try { + const page = await api.get<{ items: KnowledgeCategoryNode[] }>( + `/workspaces/${wsId}/knowledge/categories`, + ); + setCategories(page.items); + } catch (err) { + setError(err instanceof ApiError ? err.message : "分类加载失败"); + } + }, [wsId]); + + useEffect(() => { + void loadCategories(); + }, [loadCategories]); + + async function createCategory(name: string, parentId: string | null) { + setCatBusy(true); + setError(null); + try { + const created = await api.post( + `/workspaces/${wsId}/knowledge/categories`, + { name, parent_id: parentId }, + ); + await loadCategories(); + return created; + } catch (err) { + setError(err instanceof ApiError ? err.message : "分类创建失败"); + return null; + } finally { + setCatBusy(false); + } + } + const load = useCallback( async (nextCursor: string | null) => { // 切 tab / 改搜索会连发请求,慢的回来会盖掉新结果 @@ -78,6 +120,8 @@ export function KnowledgePage() { const statuses = TABS.find((t) => t.key === tab)!.statuses; for (const s of statuses) qs.append("status", s); if (q.trim()) qs.set("q", q.trim()); + if (catFilter === "none") qs.set("uncategorized", "true"); + else if (catFilter) qs.set("category_id", catFilter); if (nextCursor) qs.set("cursor", nextCursor); const page = await api.get<{ items: Knowledge[]; @@ -94,7 +138,7 @@ export function KnowledgePage() { if (mine === reqId.current) setLoading(false); } }, - [wsId, tab, q], + [wsId, tab, q, catFilter], ); useEffect(() => { @@ -127,22 +171,13 @@ export function KnowledgePage() { } } - async function addNote() { - const title = draftTitle.trim(); - if (!title) return; - try { - const doc = await api.post(`/workspaces/${wsId}/knowledge`, { - title, - doc_type: "note", - }); - setDraftTitle(""); - setAdding(false); - setTab("draft"); - setOpenId(doc.id); - void load(null); - } catch (err) { - setError(err instanceof ApiError ? err.message : "保存失败"); - } + function onCreated(doc: Knowledge) { + setComposing(false); + setTab("draft"); + setCatFilter(null); + setOpenId(doc.id); + void load(null); + void loadCategories(); } const pendingCount = docs.filter((d) => d.knowledge.status_hint !== "published").length; @@ -176,7 +211,7 @@ export function KnowledgePage() { {searching ? "检索中…" : "检索"} - @@ -184,6 +219,18 @@ export function KnowledgePage() { } /> +
+ { + setCatFilter(value); + setHits(null); + }} + busy={catBusy} + onCreate={(name, parentId) => void createCategory(name, parentId)} + /> +
{TABS.map((t) => { const active = tab === t.key; @@ -208,34 +255,6 @@ export function KnowledgePage() { ) : null}
- {adding && ( -
-
- - - -

- 手记的知识没有来源,它是你自己的笔记;想沉淀某件事,去那件事的详情里点「沉淀成知识」。 -

-
-
- setDraftTitle(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") void addNote(); - }} - /> - -
-
- )} - {error && (
{error} @@ -327,6 +346,11 @@ export function KnowledgePage() { 自动整理 ) : null} + {d.category ? ( + + {d.category.full_path} + + ) : null}

{d.title} @@ -339,6 +363,16 @@ export function KnowledgePage() { {d.source ? 「{d.source.title}」 : null} {new Date(d.updated_at).toLocaleString("zh-CN")}

+ {/* 标签是备忘录的检索入口,卡片上就露出来,不用点开才知道归在哪 */} + {d.tags.length > 0 ? ( +
+ {d.tags.slice(0, 5).map((t) => ( + + #{t.name} + + ))} +
+ ) : null}
)} +
+ + + {composing && ( + setComposing(false)} + onCreated={onCreated} + /> + )} {openId && ( setOpenId(null)} - onChanged={() => void load(null)} + onChanged={() => { + void load(null); + void loadCategories(); + }} /> )} ); } +/** 左侧分类树:一层一层缩进,点一下就把列表收窄到该分类(含子分类)。 */ +function CategorySidebar({ + categories, + selected, + onSelect, + busy, + onCreate, +}: { + categories: KnowledgeCategoryNode[]; + selected: string | null; + onSelect: (value: string | null) => void; + busy: boolean; + onCreate: (name: string, parentId: string | null) => void; +}) { + const [adding, setAdding] = useState(false); + const [name, setName] = useState(""); + const [parentId, setParentId] = useState(""); + + return ( + + ); +} + +function CategoryButton({ + active, + indent = 0, + onClick, + children, +}: { + active: boolean; + indent?: number; + onClick: () => void; + children: React.ReactNode; +}) { + return ( + + ); +} + +/** 新建知识:标题、类型、分类、标签、正文一起填,不再是「输入标题就算完」。 */ +function KnowledgeComposer({ + wsId, + categories, + catBusy, + onCreateCategory, + onClose, + onCreated, +}: { + wsId: string; + categories: KnowledgeCategoryNode[]; + catBusy: boolean; + onCreateCategory: (name: string, parentId: string | null) => Promise; + onClose: () => void; + onCreated: (doc: Knowledge) => void; +}) { + const [title, setTitle] = useState(""); + const [docType, setDocType] = useState("note"); + const [body, setBody] = useState(""); + const [tags, setTags] = useState(""); + const [categoryId, setCategoryId] = useState(""); + const [newCat, setNewCat] = useState(""); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + + const parentDepth = categories.find((c) => c.id === categoryId)?.depth ?? 0; + const canAddCategory = parentDepth < 3 && Boolean(newCat.trim()); + const canSave = Boolean(title.trim()) && !busy; + + async function addCategory() { + const name = newCat.trim(); + if (!name || !canAddCategory) return; + const created = await onCreateCategory(name, categoryId || null); + if (created) { + setCategoryId(created.id); + setNewCat(""); + } + } + + async function save() { + if (!canSave) return; + setBusy(true); + setError(null); + try { + const doc = await api.post(`/workspaces/${wsId}/knowledge`, { + title: title.trim(), + body_md: body, + doc_type: docType, + tag_names: tags.split(/[\s,,]+/).filter(Boolean), + category_id: categoryId || null, + }); + onCreated(doc); + } catch (err) { + setError(err instanceof ApiError ? err.message : "保存失败"); + } finally { + setBusy(false); + } + } + + return ( + + + + + 先存成草稿,核对发布后才进团队检索 + + + } + > +
+ + +
+ + +
+ +
+ 在当前分类下新建子分类(最多三级) +
+ = 3 ? "已经到第三级了" : "子分类名"} + value={newCat} + disabled={parentDepth >= 3} + onChange={(e) => setNewCat(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + void addCategory(); + } + }} + aria-label="新建子分类" + /> + +
+
+ + + +