feat(knowledge): 工单验收自动生成草稿,核对后发布,来源可追

- knowledge_doc 与 item 1:1,除 source_kind=manual 外必须有来源(库级约束)
- 状态机 draft/in_review/published/archived,发布要 owner/admin;非法动作回 allowed_from 与 allowed_actions
- 工单验收通过自动落 postmortem 草稿(generated_by=rule),同一来源只生成一次
- 接口:列表/详情/手记/改内容/状态动作/从事项沉淀;已发布禁改,改内容带乐观锁
- Web 知识页 + 详情抽屉;任务与工单详情里可「沉淀成知识」
- 8 个后端用例 + e2e 7 项断言(沉淀 → 待核对 → 发布)
This commit is contained in:
dadao
2026-10-28 14:02:35 +08:00
parent 3471b07ac1
commit e9addeed25
17 changed files with 1957 additions and 1 deletions
+33
View File
@@ -201,6 +201,25 @@ export function TaskDetail({
}
}
/** 把这件事整理成知识草稿。草稿默认待核对,不直接发布。 */
async function toKnowledge() {
if (!item) return;
setBusy(true);
setError(null);
try {
await api.post(`${base}/knowledge?doc_type=${item.kind === "ticket" ? "postmortem" : "howto"}`);
setNotice("已生成知识草稿。它现在在「知识 → 待核对」里,核对无误后再发布,才会进入团队检索。");
} catch (err) {
if (err instanceof ApiError && err.code === 7002) {
setNotice("这件事已经有一份知识草稿了,去「知识」页找它。");
} else {
setError(err instanceof ApiError ? err.message : "生成失败");
}
} finally {
setBusy(false);
}
}
async function ticketAction(action: TicketAction, extra: Record<string, unknown> = {}) {
if (!item) return;
setBusy(true);
@@ -412,6 +431,20 @@ export function TaskDetail({
</div>
)}
{item.kind !== "knowledge" && item.status !== "canceled" && (
<div className="mt-4 rounded-lg border border-ink-100 p-3">
<p className="text-sm font-medium">这件事值得留下来?沉淀成知识</p>
<p className="mt-1 text-xs text-ink-500">
会把标题、备注、时间线和评论整理成一份草稿,默认进「待核对」,不会直接对外发布。
</p>
<div className="mt-2">
<button className="btn-ghost px-3 py-1 text-sm" disabled={busy} onClick={() => void toKnowledge()}>
{item.kind === "ticket" ? "沉淀成复盘" : "沉淀成知识"}
</button>
</div>
</div>
)}
{notice && <p className="mt-3 text-sm text-emerald-700">{notice}</p>}
{error && <p className="mt-3 text-sm text-red-600">{error}</p>}
{busy && <p className="mt-3 text-xs text-ink-500">处理中…</p>}
+1
View File
@@ -6,6 +6,7 @@ const TABS = [
{ path: "", label: "任务" },
{ path: "/board", label: "看板" },
{ path: "/tickets", label: "工单" },
{ path: "/knowledge", label: "知识" },
{ path: "/members", label: "成员" },
];