+58
-10
@@ -205,6 +205,48 @@ async def _apply_update(
|
||||
return item
|
||||
|
||||
|
||||
async def _notify_status_change(
|
||||
session: AsyncSession,
|
||||
*,
|
||||
ws_id: uuid.UUID,
|
||||
item: Item,
|
||||
previous: str,
|
||||
to_status: str,
|
||||
actor_id: uuid.UUID,
|
||||
actor_name: str,
|
||||
) -> None:
|
||||
"""状态变化的通知。
|
||||
|
||||
"完成"单独一种 kind:收件人一眼能看出是结项,而不是又一次普通的状态流转;
|
||||
其余状态仍走 `task_status_changed`,正文里写清从什么改成了什么。
|
||||
"""
|
||||
if to_status == "done":
|
||||
await notify(
|
||||
session,
|
||||
ws_id=ws_id,
|
||||
user_ids=(item.creator_id, item.assignee_id),
|
||||
kind="task_completed",
|
||||
item_id=item.id,
|
||||
title=f"「{item.title}」已完成",
|
||||
body=f"{actor_name} 把它标记为完成。",
|
||||
actor_id=actor_id,
|
||||
)
|
||||
return
|
||||
await notify(
|
||||
session,
|
||||
ws_id=ws_id,
|
||||
user_ids=(item.creator_id, item.assignee_id),
|
||||
kind="task_status_changed",
|
||||
item_id=item.id,
|
||||
title=f"「{item.title}」变成{TASK_STATUS_CN.get(to_status, to_status)}了",
|
||||
body=(
|
||||
f"{actor_name} 把它从 {TASK_STATUS_CN.get(previous, previous)} "
|
||||
f"改成了 {TASK_STATUS_CN.get(to_status, to_status)}"
|
||||
),
|
||||
actor_id=actor_id,
|
||||
)
|
||||
|
||||
|
||||
# ── 列表 ────────────────────────────────────────────────────────────────
|
||||
def _apply_filters(
|
||||
stmt: Select,
|
||||
@@ -581,6 +623,17 @@ async def batch_status(body: BatchStatusIn, ctx: MemberDep, session: SessionDep)
|
||||
}
|
||||
)
|
||||
continue
|
||||
# 批量改状态以前是"悄悄改完":被指派人/创建人收不到任何通知,
|
||||
# 和单条改状态的行为不一致。这里补上同一条通知。
|
||||
await _notify_status_change(
|
||||
session,
|
||||
ws_id=ctx.ws_id,
|
||||
item=item,
|
||||
previous=previous,
|
||||
to_status=body.status,
|
||||
actor_id=ctx.user.id,
|
||||
actor_name=ctx.user.nickname or "同事",
|
||||
)
|
||||
updated.append({"id": str(item.id), "version": item.version, "unchanged": False})
|
||||
|
||||
return ok(
|
||||
@@ -682,19 +735,14 @@ async def change_status(
|
||||
payload={"from": previous, "to": body.status},
|
||||
)
|
||||
tags = await _tags_of(session, ctx.ws_id, [item.id])
|
||||
await notify(
|
||||
await _notify_status_change(
|
||||
session,
|
||||
ws_id=ctx.ws_id,
|
||||
user_ids=(item.creator_id, item.assignee_id),
|
||||
kind="task_status_changed",
|
||||
item_id=item.id,
|
||||
title=f"「{item.title}」变成{TASK_STATUS_CN.get(body.status, body.status)}了",
|
||||
body=(
|
||||
f"{ctx.user.nickname or '同事'} 把它从 "
|
||||
f"{TASK_STATUS_CN.get(previous, previous)} 改成了 "
|
||||
f"{TASK_STATUS_CN.get(body.status, body.status)}"
|
||||
),
|
||||
item=item,
|
||||
previous=previous,
|
||||
to_status=body.status,
|
||||
actor_id=ctx.user.id,
|
||||
actor_name=ctx.user.nickname or "同事",
|
||||
)
|
||||
return ok(_item_out(item, tags.get(item.id)), message="状态已更新")
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ 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
|
||||
from ..services.knowledge_index import reindex_document, search_knowledge
|
||||
from ..services.notify import notify, workspace_admins
|
||||
from .attachments import count_for_items, list_for_item
|
||||
|
||||
router = APIRouter(prefix="/workspaces/{ws_id}", tags=["知识库"])
|
||||
@@ -1025,6 +1026,17 @@ async def create_knowledge(body: KnowledgeIn, ctx: MemberDep, session: SessionDe
|
||||
await session.flush()
|
||||
await _log(session, item, ctx.user.id, "knowledge_created", {"generated_by": "human"})
|
||||
await _set_tags_for(session, ws_id, item, body.tag_names)
|
||||
# 知识是团队资产:新建草稿先告诉能发布的人,别让草稿在列表里躺到没人发现。
|
||||
await notify(
|
||||
session,
|
||||
ws_id=ws_id,
|
||||
user_ids=await workspace_admins(session, ws_id),
|
||||
kind="knowledge_draft_created",
|
||||
item_id=item.id,
|
||||
title=f"新的知识草稿:{item.title}",
|
||||
body=f"{ctx.user.nickname or '同事'} 新建了一条知识,核对后可以发布。",
|
||||
actor_id=ctx.user.id,
|
||||
)
|
||||
await audit_record(
|
||||
session,
|
||||
workspace_id=ws_id,
|
||||
@@ -1204,6 +1216,42 @@ async def change_status(
|
||||
target_id=str(item.id),
|
||||
payload={"to": to_status},
|
||||
)
|
||||
# 知识的状态变化要让相关的人知道:提交审议找管理员核对,
|
||||
# 发布/归档则回告作者——否则作者不知道自己的草稿已经被团队采纳。
|
||||
actor_name = ctx.user.nickname or "同事"
|
||||
if body.action == "submit_review":
|
||||
await notify(
|
||||
session,
|
||||
ws_id=ws_id,
|
||||
user_ids=await workspace_admins(session, ws_id),
|
||||
kind="knowledge_review_requested",
|
||||
item_id=item.id,
|
||||
title=f"待核对:{item.title}",
|
||||
body=f"{actor_name} 提交了一份知识,核对后可以发布。",
|
||||
actor_id=ctx.user.id,
|
||||
)
|
||||
elif body.action == "publish":
|
||||
await notify(
|
||||
session,
|
||||
ws_id=ws_id,
|
||||
user_ids=(doc.author_id,),
|
||||
kind="knowledge_published",
|
||||
item_id=item.id,
|
||||
title=f"「{item.title}」已发布",
|
||||
body=f"{actor_name} 发布了这份知识,现在可以被团队检索到。",
|
||||
actor_id=ctx.user.id,
|
||||
)
|
||||
elif body.action == "archive":
|
||||
await notify(
|
||||
session,
|
||||
ws_id=ws_id,
|
||||
user_ids=(doc.author_id,),
|
||||
kind="knowledge_archived",
|
||||
item_id=item.id,
|
||||
title=f"「{item.title}」已归档",
|
||||
body=f"{actor_name} 把它归档了,检索不再返回它。",
|
||||
actor_id=ctx.user.id,
|
||||
)
|
||||
await session.flush()
|
||||
if body.action == "publish":
|
||||
# 发布后立刻建索引:检索只认 published,不在这里建就要等定时任务,
|
||||
@@ -1270,6 +1318,16 @@ async def promote_to_knowledge(
|
||||
await _log(
|
||||
session, doc_item, ctx.user.id, "knowledge_created", {"source_item_id": str(source.id)}
|
||||
)
|
||||
await notify(
|
||||
session,
|
||||
ws_id=ws_id,
|
||||
user_ids=await workspace_admins(session, ws_id),
|
||||
kind="knowledge_draft_created",
|
||||
item_id=doc_item.id,
|
||||
title=f"从事项沉淀了知识:{doc_item.title}",
|
||||
body=f"{ctx.user.nickname or '同事'} 把「{source.title}」沉淀成草稿,核对后可以发布。",
|
||||
actor_id=ctx.user.id,
|
||||
)
|
||||
await audit_record(
|
||||
session,
|
||||
workspace_id=ws_id,
|
||||
|
||||
@@ -767,11 +767,14 @@ async def delete_link(
|
||||
|
||||
|
||||
# -- 通知 ---------------------------------------------------------------
|
||||
def _notification_out(n: Notification, item_title: str | None = None) -> dict[str, Any]:
|
||||
def _notification_out(
|
||||
n: Notification, item_title: str | None = None, item_kind: str | None = None
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"id": str(n.id),
|
||||
"kind": n.kind,
|
||||
"item_id": str(n.item_id) if n.item_id else None,
|
||||
"item_kind": item_kind,
|
||||
"item_title": item_title,
|
||||
"title": n.title,
|
||||
"body": n.body,
|
||||
@@ -813,18 +816,27 @@ async def list_notifications(
|
||||
rows = list(rows[:limit])
|
||||
|
||||
titles: dict[uuid.UUID, str] = {}
|
||||
kinds: dict[uuid.UUID, str] = {}
|
||||
item_ids = [n.item_id for n in rows if n.item_id]
|
||||
if item_ids:
|
||||
for item_id, title in (
|
||||
await session.execute(select(Item.id, Item.title).where(Item.id.in_(item_ids)))
|
||||
for item_id, title, kind in (
|
||||
await session.execute(
|
||||
select(Item.id, Item.title, Item.kind).where(Item.id.in_(item_ids))
|
||||
)
|
||||
).all():
|
||||
titles[item_id] = title
|
||||
kinds[item_id] = kind
|
||||
|
||||
next_cursor = encode_cursor(rows[-1].created_at, rows[-1].id) if has_more and rows else None
|
||||
return ok(
|
||||
{
|
||||
"notifications": [
|
||||
_notification_out(n, titles.get(n.item_id) if n.item_id else None) for n in rows
|
||||
_notification_out(
|
||||
n,
|
||||
titles.get(n.item_id) if n.item_id else None,
|
||||
kinds.get(n.item_id) if n.item_id else None,
|
||||
)
|
||||
for n in rows
|
||||
],
|
||||
"next_cursor": next_cursor,
|
||||
"has_more": has_more,
|
||||
|
||||
Reference in New Issue
Block a user