Files
DadaoTask/backend/app/services/knowledge.py
T
dadao 0919c8ecd3
CI / backend (push) Canceled after 0s
fix(knowledge): 草稿的过程段写「收件箱 → 待办」,而不是两句「状态变更」
自动生成的草稿里,时间线只查了 activity.action,没带 payload,
于是每条状态变化都变成一模一样的「状态变更」,读者看不出事情怎么走的。

- services/knowledge.snapshot_source 多取 Activity.payload 一起交给渲染
- domain/knowledge 加 STATUS_LABEL 与 _describe(),把 payload 的 from/to 翻成人话
- 新增用例断言草稿里有「待办 → 进行中」「进行中 → 已完成」

66 passed。
2026-10-28 14:14:50 +08:00

137 lines
4.2 KiB
Python

"""知识域的数据库配套动作(M3)。
放在 services 而不是 api:两个入口都要用——用户手动点"沉淀成知识",
以及工单验收通过时自动落一份草稿。逻辑只有一份,行为才不会分叉。
"""
from __future__ import annotations
import uuid
from typing import Any
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from ..domain.knowledge import render_draft
from ..errors import AppError, ErrorCode
from ..models import AppUser
from ..models_knowledge import KnowledgeDoc
from ..models_task import Activity, Comment, Item
async def snapshot_source(session: AsyncSession, ws_id: uuid.UUID, item: Item) -> dict[str, Any]:
"""把来源事项的"人、时间线、讨论"取成一份快照,供 render_draft 使用。"""
nicknames = dict(
(await session.execute(select(AppUser.id, AppUser.nickname))).all() # type: ignore[arg-type]
)
timeline = (
await session.execute(
select(Activity.action, Activity.created_at, Activity.payload)
.where(Activity.workspace_id == ws_id, Activity.item_id == item.id)
.order_by(Activity.created_at)
)
).all()
comments = (
await session.execute(
select(Comment.body_md, Comment.author_id, Comment.created_at)
.where(
Comment.workspace_id == ws_id,
Comment.item_id == item.id,
Comment.deleted_at.is_(None),
)
.order_by(Comment.created_at)
)
).all()
return {
"people": {
"提出人": nicknames.get(item.creator_id, ""),
"办理人": nicknames.get(item.assignee_id, "") if item.assignee_id else "",
},
"timeline": [{"action": a, "at": at, "payload": p or {}} for a, at, p in timeline],
"comments": [
{"body": body, "author": nicknames.get(author_id, ""), "at": at}
for body, author_id, at in comments
],
}
async def find_doc_by_source(
session: AsyncSession, ws_id: uuid.UUID, source_item_id: uuid.UUID
) -> KnowledgeDoc | None:
return (
await session.execute(
select(KnowledgeDoc).where(
KnowledgeDoc.workspace_id == ws_id,
KnowledgeDoc.source_item_id == source_item_id,
)
)
).scalar_one_or_none()
async def create_draft_from_source(
session: AsyncSession,
*,
ws_id: uuid.UUID,
source: Item,
author_id: uuid.UUID,
generated_by: str = "rule",
resolution_md: str = "",
doc_type: str = "note",
title: str | None = None,
) -> tuple[Item, KnowledgeDoc]:
"""从一条任务/工单生成知识草稿(draft)。同一个来源只沉淀一次。"""
existing = await find_doc_by_source(session, ws_id, source.id)
if existing is not None:
raise AppError(
ErrorCode.KNOWLEDGE_ALREADY_EXISTS,
message="这件事已经沉淀过一份知识了",
details={"knowledge_id": str(existing.item_id)},
)
snap = await snapshot_source(session, ws_id, source)
body = render_draft(
source_kind=source.kind,
title=source.title,
body_md=source.body_md,
resolution_md=resolution_md,
created_at=source.created_at,
closed_at=source.completed_at,
people=snap["people"],
timeline=snap["timeline"],
comments=snap["comments"],
)
doc_item = Item(
workspace_id=ws_id,
kind="knowledge",
title=title or source.title,
body_md=body,
status="draft",
priority=0,
creator_id=author_id,
)
session.add(doc_item)
await session.flush()
doc = KnowledgeDoc(
item_id=doc_item.id,
workspace_id=ws_id,
author_id=author_id,
source_item_id=source.id,
source_kind="ticket" if source.kind == "ticket" else "task",
doc_type=doc_type,
status_hint="draft",
summary=_first_line(resolution_md) or source.title,
generated_by=generated_by,
)
session.add(doc)
await session.flush()
return doc_item, doc
def _first_line(text: str) -> str:
for line in text.strip().splitlines():
if line.strip():
return line.strip()[:200]
return ""