feat(awooop): record recurrence handoff proposals

This commit is contained in:
Your Name
2026-05-19 00:13:40 +08:00
parent 0028993851
commit fb9b0b3b7c
6 changed files with 424 additions and 19 deletions

View File

@@ -1846,13 +1846,17 @@
"previewing": "Previewing",
"dryRun": "Dry-run",
"dryRunning": "Dry-running",
"failed": "The safe preview / dry-run API did not respond, so the next step cannot be claimed.",
"handoff": "Handoff",
"handoffing": "Handing off",
"failed": "The safe preview / dry-run / handoff API did not respond, so the next step cannot be claimed.",
"allowed": "Safety gate passed",
"blocked": "Safety gate blocked",
"mode": "Mode: {mode}",
"previewResult": "Result: {result}",
"writes": "Writes: incident={incident}; autoRepair={autoRepair}; ticket={ticket}",
"history": "Dry-run stored: {recorded}",
"handoffStatus": "Handoff: {kind} / {status}",
"externalTicket": "External ticket created: {created}",
"ticket": "Ticket preview: {title}",
"modes": {
"auto": "Auto select",
@@ -1862,6 +1866,18 @@
"observe": "Observe",
"unknown": "Unknown"
},
"handoffKinds": {
"ticket_proposal": "Ticket proposal",
"manual_review": "Manual review",
"unknown": "Unknown"
},
"handoffStatuses": {
"ready_to_record": "Ready to record",
"recorded": "Recorded",
"record_failed": "Record failed",
"blocked": "Blocked",
"unknown": "Unknown"
},
"previews": {
"ticket_preview_ready": "Ticket preview ready",
"reverify_preview_ready": "Reverify preview ready",

View File

@@ -1847,13 +1847,17 @@
"previewing": "預覽中",
"dryRun": "乾跑",
"dryRunning": "乾跑中",
"failed": "安全預覽 / 乾跑 API 未回應,不能判定下一步。",
"handoff": "交接",
"handoffing": "交接中",
"failed": "安全預覽 / 乾跑 / 交接 API 未回應,不能判定下一步。",
"allowed": "安全閘門通過",
"blocked": "安全閘門阻塞",
"mode": "模式:{mode}",
"previewResult": "結果:{result}",
"writes": "寫入incident={incident}autoRepair={autoRepair}ticket={ticket}",
"history": "試跑入庫:{recorded}",
"handoffStatus": "交接:{kind} / {status}",
"externalTicket": "外部 Ticket 建立:{created}",
"ticket": "Ticket 預覽:{title}",
"modes": {
"auto": "自動選擇",
@@ -1863,6 +1867,18 @@
"observe": "觀察",
"unknown": "未知"
},
"handoffKinds": {
"ticket_proposal": "Ticket 提案",
"manual_review": "人工覆核",
"unknown": "未知"
},
"handoffStatuses": {
"ready_to_record": "待寫入歷史",
"recorded": "已寫入歷史",
"record_failed": "寫入失敗",
"blocked": "已阻塞",
"unknown": "未知"
},
"previews": {
"ticket_preview_ready": "Ticket 預覽已就緒",
"reverify_preview_ready": "重新驗證預覽已就緒",

View File

@@ -110,12 +110,16 @@ type RecurrenceWorkItemActionResult = {
incident_id?: string | null;
mode?: string | null;
requested_mode?: string | null;
handoff_kind?: string | null;
handoff_status?: string | null;
handoff_owner?: string | null;
allowed?: boolean | null;
executed?: boolean | null;
safety_level?: string | null;
writes_incident_state?: boolean | null;
writes_auto_repair_result?: boolean | null;
writes_ticket?: boolean | null;
creates_external_ticket?: boolean | null;
verification_result_preview?: string | null;
next_step?: string | null;
checks?: Array<{ name?: string | null; passed?: boolean | null; detail?: string | null }>;
@@ -153,7 +157,7 @@ type RecurrenceWorkItemActionResult = {
};
type RecurrenceWorkItemActionState = {
loading?: "preview" | "dryRun" | null;
loading?: "preview" | "dryRun" | "handoff" | null;
result?: RecurrenceWorkItemActionResult | null;
error?: string | null;
};
@@ -331,6 +335,25 @@ function recurrencePreviewKey(preview?: string | null) {
return "unknown";
}
function recurrenceHandoffStatusKey(status?: string | null) {
if (
status === "ready_to_record" ||
status === "recorded" ||
status === "record_failed" ||
status === "blocked"
) {
return status;
}
return "unknown";
}
function recurrenceHandoffKindKey(kind?: string | null) {
if (kind === "ticket_proposal" || kind === "manual_review") {
return kind;
}
return "unknown";
}
function buildWorkItems(
telemetry: Telemetry,
t: ReturnType<typeof useTranslations>
@@ -653,7 +676,7 @@ function RecurrenceWorkQueuePanel({
const summary = recurrence?.summary;
const runWorkItemAction = useCallback(async (
workItemId: string,
action: "preview" | "dryRun"
action: "preview" | "dryRun" | "handoff"
) => {
setActionState((current) => ({
...current,
@@ -662,21 +685,36 @@ function RecurrenceWorkQueuePanel({
const encodedProjectId = encodeURIComponent(projectId);
const encodedWorkItemId = encodeURIComponent(workItemId);
const result = action === "preview"
? await fetchJson<RecurrenceWorkItemActionResult>(
`${API_BASE}/api/v1/platform/events/dossier/recurrence/work-item/preview?project_id=${encodedProjectId}&work_item_id=${encodedWorkItemId}`,
12000
)
: await postJson<RecurrenceWorkItemActionResult>(
`${API_BASE}/api/v1/platform/events/dossier/recurrence/work-item/dry-run`,
{
project_id: projectId,
work_item_id: workItemId,
mode: "auto",
limit: 300,
},
15000
);
let result: RecurrenceWorkItemActionResult | null = null;
if (action === "preview") {
result = await fetchJson<RecurrenceWorkItemActionResult>(
`${API_BASE}/api/v1/platform/events/dossier/recurrence/work-item/preview?project_id=${encodedProjectId}&work_item_id=${encodedWorkItemId}`,
12000
);
} else if (action === "dryRun") {
result = await postJson<RecurrenceWorkItemActionResult>(
`${API_BASE}/api/v1/platform/events/dossier/recurrence/work-item/dry-run`,
{
project_id: projectId,
work_item_id: workItemId,
mode: "auto",
limit: 300,
},
15000
);
} else {
result = await postJson<RecurrenceWorkItemActionResult>(
`${API_BASE}/api/v1/platform/events/dossier/recurrence/work-item/handoff`,
{
project_id: projectId,
work_item_id: workItemId,
mode: "auto",
handoff_kind: "ticket_proposal",
limit: 300,
},
15000
);
}
setActionState((current) => ({
...current,
@@ -736,6 +774,8 @@ function RecurrenceWorkQueuePanel({
const actionAllowed = actionResult?.allowed === true;
const actionModeKey = recurrenceActionModeKey(actionResult?.mode);
const previewKey = recurrencePreviewKey(actionResult?.verification_result_preview);
const handoffStatusKey = recurrenceHandoffStatusKey(actionResult?.handoff_status);
const handoffKindKey = recurrenceHandoffKindKey(actionResult?.handoff_kind);
return (
<article
@@ -802,6 +842,17 @@ function RecurrenceWorkQueuePanel({
? t("actions.dryRunning")
: t("actions.dryRun")}
</button>
<button
type="button"
onClick={() => runWorkItemAction(workItemId, "handoff")}
disabled={currentAction?.loading === "handoff"}
className="inline-flex items-center gap-1.5 border border-[#d8d3c7] bg-white px-2 py-1 text-xs font-semibold text-[#2e2b26] hover:border-[#9bc7a4] hover:bg-[#f0faf2] hover:text-[#17602a] disabled:cursor-not-allowed disabled:opacity-60"
>
<GitBranch className="h-3.5 w-3.5" aria-hidden="true" />
{currentAction?.loading === "handoff"
? t("actions.handoffing")
: t("actions.handoff")}
</button>
</>
) : null}
{runHref ? (
@@ -863,6 +914,21 @@ function RecurrenceWorkQueuePanel({
recorded: String(actionResult.history?.recorded ?? false),
})}
</p>
{actionResult.handoff_status ? (
<p>
{t("actions.handoffStatus", {
status: t(`actions.handoffStatuses.${handoffStatusKey}` as never),
kind: t(`actions.handoffKinds.${handoffKindKey}` as never),
})}
</p>
) : null}
{actionResult.creates_external_ticket !== undefined ? (
<p>
{t("actions.externalTicket", {
created: String(actionResult.creates_external_ticket ?? false),
})}
</p>
) : null}
{actionResult.ticket_preview?.title ? (
<p className="truncate">
{t("actions.ticket", {