吸收数字员工审批历史订阅策略
This commit is contained in:
@@ -879,6 +879,65 @@ describe("digital employee state service", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("persists approval subscription preferences and records runtime events", async () => {
|
||||
const { readDigitalEmployeeUiState, saveDigitalEmployeeUiState } = await import("./stateService");
|
||||
|
||||
saveDigitalEmployeeUiState({
|
||||
action: "update_approval_subscriptions",
|
||||
metadata: { source: "approval-settings" },
|
||||
state: {
|
||||
selectedDutyIdsByDate: {},
|
||||
confirmedDates: {},
|
||||
reportScheduleTime: "18:00",
|
||||
reportGeneratedAtByDate: {},
|
||||
decisionResultsByDate: {},
|
||||
notificationStateById: {},
|
||||
notificationPreferences: {
|
||||
enabled: true,
|
||||
muted_kinds: [],
|
||||
muted_levels: [],
|
||||
updated_at: null,
|
||||
},
|
||||
approvalSubscriptionPreferences: {
|
||||
enabled: true,
|
||||
subscribed_actions: ["delegate", "mark_risk", "decision"],
|
||||
subscribed_risk_levels: ["high", "critical"],
|
||||
notify_on_timeout: false,
|
||||
updated_at: "2026-06-07T08:05:00.000Z",
|
||||
},
|
||||
consoleRole: "sales",
|
||||
rolePreferences: {},
|
||||
profile: {
|
||||
name: "飞天数字员工",
|
||||
company: "qimingclaw 客户端",
|
||||
position: "客户端任务执行员",
|
||||
currentStatus: "working",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(readDigitalEmployeeUiState()).toMatchObject({
|
||||
approvalSubscriptionPreferences: {
|
||||
enabled: true,
|
||||
subscribed_actions: ["delegate", "mark_risk", "decision"],
|
||||
subscribed_risk_levels: ["high", "critical"],
|
||||
notify_on_timeout: false,
|
||||
updated_at: "2026-06-07T08:05:00.000Z",
|
||||
},
|
||||
});
|
||||
const preferenceEvent = Array.from(mockState.db?.events.values() ?? [])
|
||||
.find((event) => event.kind === "digital_workday_update_approval_subscriptions");
|
||||
expect(preferenceEvent).toMatchObject({
|
||||
kind: "digital_workday_update_approval_subscriptions",
|
||||
message: "数字员工审批订阅已更新",
|
||||
});
|
||||
expect(mockState.db?.outbox.get(`event:insert:${preferenceEvent?.id}`)).toMatchObject({
|
||||
entity_type: "event",
|
||||
operation: "insert",
|
||||
status: "pending",
|
||||
});
|
||||
});
|
||||
|
||||
it("records governance commands as formal runtime records", async () => {
|
||||
const { recordDigitalEmployeeGovernanceCommand } = await import("./stateService");
|
||||
|
||||
|
||||
@@ -473,6 +473,13 @@ export interface DigitalEmployeeUiState {
|
||||
muted_levels: string[];
|
||||
updated_at: string | null;
|
||||
};
|
||||
approvalSubscriptionPreferences?: {
|
||||
enabled: boolean;
|
||||
subscribed_actions: string[];
|
||||
subscribed_risk_levels: string[];
|
||||
notify_on_timeout: boolean;
|
||||
updated_at: string | null;
|
||||
};
|
||||
consoleRole?: string;
|
||||
rolePreferences?: Record<string, unknown>;
|
||||
profile: {
|
||||
@@ -649,6 +656,7 @@ function defaultUiState(): DigitalEmployeeUiState {
|
||||
decisionResultsByDate: {},
|
||||
notificationStateById: {},
|
||||
notificationPreferences: defaultNotificationPreferences(),
|
||||
approvalSubscriptionPreferences: defaultApprovalSubscriptionPreferences(),
|
||||
consoleRole: "sales",
|
||||
rolePreferences: {},
|
||||
profile: {
|
||||
@@ -711,6 +719,16 @@ function defaultNotificationPreferences(): NonNullable<DigitalEmployeeUiState["n
|
||||
};
|
||||
}
|
||||
|
||||
function defaultApprovalSubscriptionPreferences(): NonNullable<DigitalEmployeeUiState["approvalSubscriptionPreferences"]> {
|
||||
return {
|
||||
enabled: true,
|
||||
subscribed_actions: ["comment", "delegate", "mark_timeout", "mark_risk", "clear_risk", "decision"],
|
||||
subscribed_risk_levels: ["high", "critical"],
|
||||
notify_on_timeout: true,
|
||||
updated_at: null,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeStringArray(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return [...new Set(value.filter((item): item is string => typeof item === "string" && Boolean(item.trim())))];
|
||||
@@ -730,6 +748,26 @@ function normalizeNotificationPreferences(
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeApprovalSubscriptionPreferences(
|
||||
value: unknown,
|
||||
): NonNullable<DigitalEmployeeUiState["approvalSubscriptionPreferences"]> {
|
||||
const fallback = defaultApprovalSubscriptionPreferences();
|
||||
const record = value && typeof value === "object" && !Array.isArray(value)
|
||||
? value as Record<string, unknown>
|
||||
: {};
|
||||
return {
|
||||
enabled: record.enabled !== false,
|
||||
subscribed_actions: normalizeStringArray(record.subscribed_actions).length > 0
|
||||
? normalizeStringArray(record.subscribed_actions)
|
||||
: fallback.subscribed_actions,
|
||||
subscribed_risk_levels: normalizeStringArray(record.subscribed_risk_levels).length > 0
|
||||
? normalizeStringArray(record.subscribed_risk_levels)
|
||||
: fallback.subscribed_risk_levels,
|
||||
notify_on_timeout: record.notify_on_timeout !== false,
|
||||
updated_at: typeof record.updated_at === "string" ? record.updated_at : null,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeUiState(
|
||||
value: Partial<DigitalEmployeeUiState>,
|
||||
): DigitalEmployeeUiState {
|
||||
@@ -745,6 +783,7 @@ function normalizeUiState(
|
||||
decisionResultsByDate: normalizeNestedStringMap(value.decisionResultsByDate),
|
||||
notificationStateById: normalizeRecord(value.notificationStateById),
|
||||
notificationPreferences: normalizeNotificationPreferences(value.notificationPreferences),
|
||||
approvalSubscriptionPreferences: normalizeApprovalSubscriptionPreferences(value.approvalSubscriptionPreferences),
|
||||
consoleRole: typeof value.consoleRole === "string" && value.consoleRole.trim()
|
||||
? value.consoleRole
|
||||
: fallback.consoleRole,
|
||||
@@ -785,6 +824,8 @@ function digitalEmployeeUiActionMessage(action: string, date?: string): string {
|
||||
return "数字员工运营偏好已更新";
|
||||
case "update_notification_preferences":
|
||||
return "数字员工通知订阅已更新";
|
||||
case "update_approval_subscriptions":
|
||||
return "数字员工审批订阅已更新";
|
||||
default:
|
||||
return `数字员工页面状态已更新${suffix}`;
|
||||
}
|
||||
|
||||
@@ -511,6 +511,17 @@ describe("digital employee sync service", () => {
|
||||
sla_summary: { due_at: "2026-06-08T09:00:00.000Z", sla_status: "tracked" },
|
||||
risk_summary: { risk_level: "high" },
|
||||
});
|
||||
insertEventEntity("event-approval-decision", "approval_decision", {
|
||||
approvalId: "approval-1",
|
||||
decision: "approved",
|
||||
previousStatus: "pending",
|
||||
nextStatus: "approved",
|
||||
actor: "operator",
|
||||
decidedAt: "2026-06-07T08:05:00.000Z",
|
||||
planId: "plan-1",
|
||||
taskId: "task-1",
|
||||
runId: "run-1",
|
||||
});
|
||||
insertEventEntity("event-artifact", "artifact_retention_marked", {
|
||||
artifact_id: "artifact-1",
|
||||
action: "mark_keep",
|
||||
@@ -616,12 +627,34 @@ describe("digital employee sync service", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
insertEventEntity("event-approval-subscriptions", "digital_workday_update_approval_subscriptions", {
|
||||
metadata: {
|
||||
action: "update_approval_subscriptions",
|
||||
approval_subscriptions: {
|
||||
enabled: false,
|
||||
subscribed_actions: ["mark_risk", "decision"],
|
||||
subscribed_risk_levels: ["high", "critical"],
|
||||
notify_on_timeout: false,
|
||||
updated_at: "2026-06-07T08:06:00.000Z",
|
||||
},
|
||||
},
|
||||
state: {
|
||||
approvalSubscriptionPreferences: {
|
||||
enabled: false,
|
||||
subscribed_actions: ["mark_risk", "decision"],
|
||||
subscribed_risk_levels: ["high", "critical"],
|
||||
notify_on_timeout: false,
|
||||
updated_at: "2026-06-07T08:06:00.000Z",
|
||||
},
|
||||
},
|
||||
});
|
||||
mockState.fetch.mockResolvedValue(
|
||||
jsonResponse({
|
||||
success: true,
|
||||
acked: [
|
||||
{ entity_type: "event", entity_id: "event-route" },
|
||||
{ entity_type: "event", entity_id: "event-approval" },
|
||||
{ entity_type: "event", entity_id: "event-approval-decision" },
|
||||
{ entity_type: "event", entity_id: "event-artifact" },
|
||||
{ entity_type: "event", entity_id: "event-skill" },
|
||||
{ entity_type: "event", entity_id: "event-dispatch" },
|
||||
@@ -630,6 +663,7 @@ describe("digital employee sync service", () => {
|
||||
{ entity_type: "event", entity_id: "event-notification-dismiss" },
|
||||
{ entity_type: "event", entity_id: "event-notification-reply" },
|
||||
{ entity_type: "event", entity_id: "event-notification-preferences" },
|
||||
{ entity_type: "event", entity_id: "event-approval-subscriptions" },
|
||||
],
|
||||
}),
|
||||
);
|
||||
@@ -665,6 +699,21 @@ describe("digital employee sync service", () => {
|
||||
delegate_to: "manager-1",
|
||||
comment_length: 4,
|
||||
});
|
||||
expect(businessView("event-approval")).not.toHaveProperty("comment");
|
||||
expect(businessView("event-approval")).not.toHaveProperty("comment_preview");
|
||||
expect(businessView("event-approval-decision")).toMatchObject({
|
||||
category: "approval",
|
||||
approval_id: "approval-1",
|
||||
action: "decision",
|
||||
decision: "approved",
|
||||
previous_status: "pending",
|
||||
next_status: "approved",
|
||||
actor: "operator",
|
||||
decided_at: "2026-06-07T08:05:00.000Z",
|
||||
plan_id: "plan-1",
|
||||
task_id: "task-1",
|
||||
run_id: "run-1",
|
||||
});
|
||||
expect(businessView("event-artifact")).toMatchObject({
|
||||
category: "artifact",
|
||||
artifact_id: "artifact-1",
|
||||
@@ -730,6 +779,17 @@ describe("digital employee sync service", () => {
|
||||
muted_kinds: ["task_finished", "need_input"],
|
||||
muted_levels: ["info"],
|
||||
});
|
||||
expect(businessView("event-approval-subscriptions")).toMatchObject({
|
||||
category: "approval",
|
||||
action: "update_approval_subscriptions",
|
||||
preferences_enabled: false,
|
||||
subscribed_actions: ["mark_risk", "decision"],
|
||||
subscribed_risk_levels: ["high", "critical"],
|
||||
notify_on_timeout: false,
|
||||
updated_at: "2026-06-07T08:06:00.000Z",
|
||||
});
|
||||
expect(businessView("event-approval-subscriptions")).not.toHaveProperty("state");
|
||||
expect(businessView("event-approval-subscriptions")).not.toHaveProperty("approvalSubscriptionPreferences");
|
||||
expect(readEntity("event", "event-route")).toMatchObject({
|
||||
sync_status: "synced",
|
||||
sync_error: null,
|
||||
|
||||
@@ -562,7 +562,9 @@ function eventSpecificBusinessView(
|
||||
eventId: string,
|
||||
): Record<string, unknown> {
|
||||
if (kind === "route_decision") return routeDecisionBusinessView(payload, eventId);
|
||||
if (kind === "approval_decision") return approvalDecisionBusinessView(payload);
|
||||
if (kind.startsWith("approval_action_")) return approvalActionBusinessView(payload);
|
||||
if (isApprovalSubscriptionEvent(kind)) return approvalSubscriptionBusinessView(payload);
|
||||
if (kind.startsWith("artifact_access_") || kind.startsWith("artifact_retention_") || kind === "artifact_version_observed") {
|
||||
return artifactEventBusinessView(payload);
|
||||
}
|
||||
@@ -607,6 +609,37 @@ function approvalActionBusinessView(payload: Record<string, unknown>): Record<st
|
||||
};
|
||||
}
|
||||
|
||||
function approvalDecisionBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
|
||||
return {
|
||||
approval_id: stringField(payload.approvalId ?? payload.approval_id) || null,
|
||||
action: "decision",
|
||||
decision: stringField(payload.decision) || null,
|
||||
previous_status: stringField(payload.previousStatus ?? payload.previous_status) || null,
|
||||
next_status: stringField(payload.nextStatus ?? payload.next_status) || null,
|
||||
actor: stringField(payload.actor ?? payload.source) || null,
|
||||
decided_at: stringField(payload.decidedAt ?? payload.decided_at) || null,
|
||||
plan_id: stringField(payload.planId ?? payload.plan_id) || null,
|
||||
task_id: stringField(payload.taskId ?? payload.task_id) || null,
|
||||
run_id: stringField(payload.runId ?? payload.run_id) || null,
|
||||
};
|
||||
}
|
||||
|
||||
function approvalSubscriptionBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
|
||||
const metadata = objectRecord(payload.metadata) ?? {};
|
||||
const state = objectRecord(payload.state) ?? {};
|
||||
const preferences = objectRecord(metadata.approval_subscriptions)
|
||||
?? objectRecord(state.approvalSubscriptionPreferences)
|
||||
?? {};
|
||||
return {
|
||||
action: stringField(metadata.action ?? payload.action) || "update_approval_subscriptions",
|
||||
preferences_enabled: typeof preferences.enabled === "boolean" ? preferences.enabled : null,
|
||||
subscribed_actions: stringArrayField(preferences.subscribed_actions ?? preferences.subscribedActions),
|
||||
subscribed_risk_levels: stringArrayField(preferences.subscribed_risk_levels ?? preferences.subscribedRiskLevels),
|
||||
notify_on_timeout: typeof preferences.notify_on_timeout === "boolean" ? preferences.notify_on_timeout : null,
|
||||
updated_at: stringField(preferences.updated_at ?? preferences.updatedAt) || null,
|
||||
};
|
||||
}
|
||||
|
||||
function artifactEventBusinessView(payload: Record<string, unknown>): Record<string, unknown> {
|
||||
const retention = objectRecord(payload.retention_summary);
|
||||
return {
|
||||
@@ -678,6 +711,7 @@ function notificationBusinessView(payload: Record<string, unknown>): Record<stri
|
||||
function eventBusinessCategory(kind: string): string {
|
||||
if (kind === "route_decision") return "route";
|
||||
if (kind.startsWith("approval_")) return "approval";
|
||||
if (isApprovalSubscriptionEvent(kind)) return "approval";
|
||||
if (kind.startsWith("artifact_")) return "artifact";
|
||||
if (kind.startsWith("skill_call_")) return "skill";
|
||||
if (kind.startsWith("plan_step_dispatch_")) return "dispatch";
|
||||
@@ -686,6 +720,10 @@ function eventBusinessCategory(kind: string): string {
|
||||
return "runtime";
|
||||
}
|
||||
|
||||
function isApprovalSubscriptionEvent(kind: string): boolean {
|
||||
return kind === "digital_workday_update_approval_subscriptions";
|
||||
}
|
||||
|
||||
function isNotificationEvent(kind: string): boolean {
|
||||
return kind === "digital_workday_update_notification_preferences"
|
||||
|| kind === "digital_workday_notification_read"
|
||||
|
||||
Reference in New Issue
Block a user