feat(account): define post-release notification preferences

This commit is contained in:
Albert 2026-04-26 14:00:47 +08:00
parent d2c92a980f
commit 66ec4c7d1c
2 changed files with 107 additions and 0 deletions

View File

@ -6,6 +6,11 @@ export type AccountAnswerDepth = "concise" | "balanced" | "deep";
export type AccountEvidenceDisplay = "summary" | "inline" | "full"; export type AccountEvidenceDisplay = "summary" | "inline" | "full";
export type AccountNotificationKey = "pendingConfirmation" | "executionWriteback" | "releaseGate" | "failureAlert"; export type AccountNotificationKey = "pendingConfirmation" | "executionWriteback" | "releaseGate" | "failureAlert";
export type AccountSettingsSaveScope = "local" | "account"; export type AccountSettingsSaveScope = "local" | "account";
export type AccountPostReleaseNotificationEvent =
| "feedback_received"
| "revenue_signal"
| "compliance_blocked"
| "iteration_recommendation";
export type AccountReleaseNotificationEvent = export type AccountReleaseNotificationEvent =
| "release_gate_blocked" | "release_gate_blocked"
| "release_gate_ready" | "release_gate_ready"
@ -59,6 +64,22 @@ export interface AccountReleaseGateNotificationPreference {
reason: string; reason: string;
} }
export interface AccountPostReleaseNotificationPreference {
event: AccountPostReleaseNotificationEvent;
notificationKey: AccountNotificationKey;
surface: "release_growth";
spaceReadable: boolean;
inAppEnabled: boolean;
showInAppReminder: boolean;
desktopPreference: boolean;
emailDigestPreference: boolean;
title: string;
reason: string;
evidencePolicy: "preserve_business_evidence";
reminderPolicy: "page_reminder_only";
mutedDetail: string;
}
export interface AccountSettings { export interface AccountSettings {
profile: AccountProfileSettings; profile: AccountProfileSettings;
aiPreferences: AccountAiPreferenceSettings; aiPreferences: AccountAiPreferenceSettings;
@ -165,6 +186,28 @@ const releaseNotificationEventMeta: Record<
}, },
}; };
const postReleaseNotificationEventMeta: Record<
AccountPostReleaseNotificationEvent,
{ notificationKey: AccountNotificationKey; reason: string }
> = {
feedback_received: {
notificationKey: "releaseGate",
reason: "Release/Growth 收到发布后用户反馈时使用发布门禁偏好控制页面内提醒;反馈证据本身不受偏好隐藏。",
},
revenue_signal: {
notificationKey: "releaseGate",
reason: "Release/Growth 收到收入、转化或增长信号时使用发布门禁偏好控制页面内提醒;收入证据本身不受偏好隐藏。",
},
compliance_blocked: {
notificationKey: "releaseGate",
reason: "合规、风控或发布后审核阻塞下一步时使用发布门禁偏好控制页面内提醒;阻塞状态与证据仍必须展示在业务页面。",
},
iteration_recommendation: {
notificationKey: "pendingConfirmation",
reason: "AI 生成下一轮迭代建议、需要人类拍板时使用待确认偏好控制页面内提醒;建议对象和证据仍保留。",
},
};
export function createDefaultAccountSettings(seed: AccountSettingsSeed = {}): AccountSettings { export function createDefaultAccountSettings(seed: AccountSettingsSeed = {}): AccountSettings {
return { return {
profile: { profile: {
@ -303,6 +346,33 @@ export function getReleaseGateNotificationPreference(
}; };
} }
export function getPostReleaseNotificationPreference(
settings: AccountSettings | Partial<AccountSettings> | null | undefined,
event: AccountPostReleaseNotificationEvent
): AccountPostReleaseNotificationPreference {
const normalized = mergeAccountSettings(createDefaultAccountSettings(), settings || null);
const meta = postReleaseNotificationEventMeta[event];
const preference = normalized.notifications[meta.notificationKey];
const enabled = Boolean(preference.enabled);
return {
event,
notificationKey: meta.notificationKey,
surface: "release_growth",
spaceReadable: true,
inAppEnabled: enabled,
showInAppReminder: enabled,
desktopPreference: enabled && Boolean(preference.desktop),
emailDigestPreference: enabled && Boolean(preference.emailDigest),
title: accountNotificationLabels[meta.notificationKey].title,
reason: meta.reason,
evidencePolicy: "preserve_business_evidence",
reminderPolicy: "page_reminder_only",
mutedDetail: enabled
? "Release/Growth 或 Space 可显示页面内提醒;桌面/邮件只记录偏好,不代表系统级推送已接入。"
: "仅静默页面内提醒;反馈、收入、合规、迭代建议及其证据仍必须按业务页面展示。",
};
}
export async function getAccountSettings(seed?: AccountSettingsSeed): Promise<AccountSettings> { export async function getAccountSettings(seed?: AccountSettingsSeed): Promise<AccountSettings> {
const defaults = createDefaultAccountSettings(seed); const defaults = createDefaultAccountSettings(seed);
const localSettings = mergeAccountSettings(defaults, readStoredAccountSettings()); const localSettings = mergeAccountSettings(defaults, readStoredAccountSettings());

View File

@ -3,6 +3,7 @@ import {
createDefaultAccountSettings, createDefaultAccountSettings,
getAccountNotificationConsumption, getAccountNotificationConsumption,
getAccountSettings, getAccountSettings,
getPostReleaseNotificationPreference,
getReleaseGateNotificationPreference, getReleaseGateNotificationPreference,
shouldShowAccountNotification, shouldShowAccountNotification,
updateAccountSettings, updateAccountSettings,
@ -132,6 +133,42 @@ describe("account settings api", () => {
expect(failedEvent.reason).toContain("failed event"); expect(failedEvent.reason).toContain("failed event");
}); });
it("maps post-release feedback, revenue, compliance, and iteration events to local reminder preferences", () => {
const defaults = createDefaultAccountSettings();
const settings = {
...defaults,
notifications: {
...defaults.notifications,
releaseGate: { enabled: true, desktop: false, emailDigest: true },
pendingConfirmation: { enabled: false, desktop: true, emailDigest: true },
},
};
const feedback = getPostReleaseNotificationPreference(settings, "feedback_received");
const revenue = getPostReleaseNotificationPreference(settings, "revenue_signal");
const compliance = getPostReleaseNotificationPreference(settings, "compliance_blocked");
const iteration = getPostReleaseNotificationPreference(settings, "iteration_recommendation");
expect(feedback.notificationKey).toBe("releaseGate");
expect(feedback.surface).toBe("release_growth");
expect(feedback.spaceReadable).toBe(true);
expect(feedback.showInAppReminder).toBe(true);
expect(feedback.desktopPreference).toBe(false);
expect(feedback.emailDigestPreference).toBe(true);
expect(feedback.reminderPolicy).toBe("page_reminder_only");
expect(feedback.evidencePolicy).toBe("preserve_business_evidence");
expect(feedback.mutedDetail).toContain("不代表系统级推送已接入");
expect(revenue.notificationKey).toBe("releaseGate");
expect(revenue.reason).toContain("收入");
expect(compliance.notificationKey).toBe("releaseGate");
expect(compliance.reason).toContain("合规");
expect(iteration.notificationKey).toBe("pendingConfirmation");
expect(iteration.showInAppReminder).toBe(false);
expect(iteration.desktopPreference).toBe(false);
expect(iteration.emailDigestPreference).toBe(false);
expect(iteration.mutedDetail).toContain("证据仍必须");
});
it("falls back missing notification fields to safe defaults", async () => { it("falls back missing notification fields to safe defaults", async () => {
window.localStorage.setItem( window.localStorage.setItem(
"pmflow:account-settings:v1", "pmflow:account-settings:v1",