吸收数字员工通知订阅策略

This commit is contained in:
baiyanyun
2026-06-10 15:56:34 +08:00
parent 13e48df63d
commit 5b2f14123e
14 changed files with 597 additions and 195 deletions

View File

@@ -823,6 +823,62 @@ describe("digital employee state service", () => {
});
});
it("persists notification preferences and keeps notification overlays in UI state", async () => {
const { readDigitalEmployeeUiState, saveDigitalEmployeeUiState } = await import("./stateService");
saveDigitalEmployeeUiState({
action: "update_notification_preferences",
metadata: { source: "notification-popover" },
state: {
selectedDutyIdsByDate: {},
confirmedDates: {},
reportScheduleTime: "18:00",
reportGeneratedAtByDate: {},
decisionResultsByDate: {},
notificationStateById: {
"task:task-1": { status: "read", read_at: "2026-06-07T07:00:00.000Z" },
},
notificationPreferences: {
enabled: true,
muted_kinds: ["task_finished", "need_input"],
muted_levels: ["info"],
updated_at: "2026-06-07T08:00:00.000Z",
},
consoleRole: "sales",
rolePreferences: {},
profile: {
name: "飞天数字员工",
company: "qimingclaw 客户端",
position: "客户端任务执行员",
currentStatus: "working",
},
},
});
expect(readDigitalEmployeeUiState()).toMatchObject({
notificationStateById: {
"task:task-1": { status: "read", read_at: "2026-06-07T07:00:00.000Z" },
},
notificationPreferences: {
enabled: true,
muted_kinds: ["task_finished", "need_input"],
muted_levels: ["info"],
updated_at: "2026-06-07T08:00:00.000Z",
},
});
const preferenceEvent = Array.from(mockState.db?.events.values() ?? [])
.find((event) => event.kind === "digital_workday_update_notification_preferences");
expect(preferenceEvent).toMatchObject({
kind: "digital_workday_update_notification_preferences",
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");

View File

@@ -466,6 +466,13 @@ export interface DigitalEmployeeUiState {
reportScheduleTime: string;
reportGeneratedAtByDate: Record<string, string>;
decisionResultsByDate: Record<string, Record<string, string>>;
notificationStateById?: Record<string, unknown>;
notificationPreferences?: {
enabled: boolean;
muted_kinds: string[];
muted_levels: string[];
updated_at: string | null;
};
consoleRole?: string;
rolePreferences?: Record<string, unknown>;
profile: {
@@ -640,6 +647,8 @@ function defaultUiState(): DigitalEmployeeUiState {
reportScheduleTime: "18:00",
reportGeneratedAtByDate: {},
decisionResultsByDate: {},
notificationStateById: {},
notificationPreferences: defaultNotificationPreferences(),
consoleRole: "sales",
rolePreferences: {},
profile: {
@@ -693,6 +702,34 @@ function normalizeRecord(value: unknown): Record<string, unknown> {
return { ...value as Record<string, unknown> };
}
function defaultNotificationPreferences(): NonNullable<DigitalEmployeeUiState["notificationPreferences"]> {
return {
enabled: true,
muted_kinds: [],
muted_levels: [],
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())))];
}
function normalizeNotificationPreferences(
value: unknown,
): NonNullable<DigitalEmployeeUiState["notificationPreferences"]> {
const record = value && typeof value === "object" && !Array.isArray(value)
? value as Record<string, unknown>
: {};
return {
enabled: record.enabled !== false,
muted_kinds: normalizeStringArray(record.muted_kinds),
muted_levels: normalizeStringArray(record.muted_levels),
updated_at: typeof record.updated_at === "string" ? record.updated_at : null,
};
}
function normalizeUiState(
value: Partial<DigitalEmployeeUiState>,
): DigitalEmployeeUiState {
@@ -706,6 +743,8 @@ function normalizeUiState(
: fallback.reportScheduleTime,
reportGeneratedAtByDate: normalizeStringMap(value.reportGeneratedAtByDate),
decisionResultsByDate: normalizeNestedStringMap(value.decisionResultsByDate),
notificationStateById: normalizeRecord(value.notificationStateById),
notificationPreferences: normalizeNotificationPreferences(value.notificationPreferences),
consoleRole: typeof value.consoleRole === "string" && value.consoleRole.trim()
? value.consoleRole
: fallback.consoleRole,
@@ -744,6 +783,8 @@ function digitalEmployeeUiActionMessage(action: string, date?: string): string {
return "数字员工角色已更新";
case "update_role_preferences":
return "数字员工运营偏好已更新";
case "update_notification_preferences":
return "数字员工通知订阅已更新";
default:
return `数字员工页面状态已更新${suffix}`;
}