import { apiRequest } from "@/api/base"; import { getAuthToken } from "@/lib/api"; export type AccountLanguage = "zh-CN" | "en-US"; export type AccountAnswerDepth = "concise" | "balanced" | "deep"; export type AccountEvidenceDisplay = "summary" | "inline" | "full"; export type AccountReminderIntensity = "silent" | "normal" | "prominent"; export type AccountNotificationEvidenceDisplay = "compact" | "expanded"; export type AccountNotificationKey = "pendingConfirmation" | "executionWriteback" | "releaseGate" | "failureAlert"; export type AccountSettingsSaveScope = "local" | "account"; export type AccountReminderConsumptionEvent = | "change_review_high_risk" | "iteration_draft_ready" | "release_blocked" | "execution_stalled"; export type AccountReviewDecisionNotificationEvent = | "review_requested" | "review_approved" | "review_changes_requested" | "review_rejected" | "high_risk_approval_required"; export type AccountPostReleaseNotificationEvent = | "feedback_received" | "revenue_signal" | "compliance_blocked" | "iteration_recommendation" | "next_change_review"; export type AccountReleaseNotificationEvent = | "release_gate_blocked" | "release_gate_ready" | "release_validation_blocked" | "release_validation_needs_confirmation" | "failed_event"; export interface AccountProfileSettings { displayName: string; roleTitle: string; email: string; defaultLanguage: AccountLanguage; } export interface AccountAiPreferenceSettings { answerDepth: AccountAnswerDepth; evidenceDisplay: AccountEvidenceDisplay; } export interface AccountNotificationPreference { enabled: boolean; desktop: boolean; emailDigest: boolean; reminderIntensity?: AccountReminderIntensity; evidenceDisplay?: AccountNotificationEvidenceDisplay; } export type AccountNotificationSettings = Record; export type AccountNotificationConsumptionSurface = "change_review" | "execution_bridge" | "release_growth" | "system_failure"; export interface AccountNotificationConsumption { key: AccountNotificationKey; surface: AccountNotificationConsumptionSurface; title: string; productArea: string; trigger: string; enabled: boolean; reminderIntensity: AccountReminderIntensity; evidenceDisplay: AccountNotificationEvidenceDisplay; showInAppReminder: boolean; showProminentReminder: boolean; desktopPreference: boolean; emailDigestPreference: boolean; statusLabel: string; detail: string; } export interface AccountReminderConsumptionContract { event: AccountReminderConsumptionEvent; notificationKey: AccountNotificationKey; surface: Exclude; productArea: string; title: string; reason: string; reminderIntensity: AccountReminderIntensity; evidenceDisplay: AccountNotificationEvidenceDisplay; showInAppReminder: boolean; showProminentReminder: boolean; desktopPreference: boolean; emailDigestPreference: boolean; notificationTransport: "in_app_only"; evidencePolicy: "preserve_business_evidence"; auditPolicy: "preserve_audit_state"; mutedDetail: string; } export interface AccountReviewDecisionNotificationPreference { event: AccountReviewDecisionNotificationEvent; notificationKey: AccountNotificationKey; surface: "change_review"; productArea: "Change Review"; title: string; reason: string; reminderIntensity: AccountReminderIntensity; evidenceDisplay: AccountNotificationEvidenceDisplay; showInAppReminder: boolean; showProminentReminder: boolean; desktopPreference: boolean; emailDigestPreference: boolean; notificationTransport: "in_app_only"; evidencePolicy: "preserve_business_evidence"; auditPolicy: "preserve_audit_state"; reviewerRoutingPolicy: "preserve_reviewer_routing"; approvalPolicy: "preserve_approval_state"; mutedDetail: string; } export interface AccountReleaseGateNotificationPreference { event: AccountReleaseNotificationEvent; notificationKey: AccountNotificationKey; surface: "release_growth" | "system_failure"; inAppEnabled: boolean; reminderIntensity: AccountReminderIntensity; evidenceDisplay: AccountNotificationEvidenceDisplay; showProminentReminder: boolean; desktopPreference: boolean; emailDigestPreference: boolean; notificationTransport: "in_app_only"; title: string; reason: string; } export interface AccountPostReleaseNotificationPreference { event: AccountPostReleaseNotificationEvent; notificationKey: AccountNotificationKey; surface: "release_growth" | "change_review"; spaceReadable: boolean; inAppEnabled: boolean; showInAppReminder: boolean; reminderIntensity: AccountReminderIntensity; evidenceDisplay: AccountNotificationEvidenceDisplay; showProminentReminder: boolean; desktopPreference: boolean; emailDigestPreference: boolean; notificationTransport: "in_app_only"; title: string; reason: string; evidencePolicy: "preserve_business_evidence"; reminderPolicy: "page_reminder_only"; mutedDetail: string; } export interface AccountSettings { profile: AccountProfileSettings; aiPreferences: AccountAiPreferenceSettings; notifications: AccountNotificationSettings; saveScope: AccountSettingsSaveScope; updatedAt: string; } export interface AccountSettingsSeed { displayName?: string | null; roleTitle?: string | null; email?: string | null; } const STORAGE_KEY = "pmflow:account-settings:v1"; const ACCOUNT_SETTINGS_ENDPOINT = "/account-settings/me"; interface ApiEnvelope { code: number; message: string; data: T | null; } export const accountNotificationLabels: Record = { pendingConfirmation: { title: "待确认", detail: "AI 生成草案、review request、review decision 或高风险 approval 需要你拍板时提醒。", }, executionWriteback: { title: "执行回写", detail: "Agent、IDE 或集成系统回传开发进度、分支、PR 与测试结果时提醒。", }, releaseGate: { title: "发布门禁", detail: "发布校验、合规检查、验收项阻塞或通过时提醒。", }, failureAlert: { title: "失败提醒", detail: "AI 调用、同步任务、执行回写或外部集成失败时提醒。", }, }; export const accountNotificationKeys: AccountNotificationKey[] = [ "pendingConfirmation", "executionWriteback", "releaseGate", "failureAlert", ]; const accountNotificationConsumptionMeta: Record< AccountNotificationKey, { surface: AccountNotificationConsumptionSurface; productArea: string; trigger: string } > = { pendingConfirmation: { surface: "change_review", productArea: "待确认中心", trigger: "AI 生成草案、ChangeSet 待审、review request / decision、高风险 approval 的页面内提醒。", }, executionWriteback: { surface: "execution_bridge", productArea: "Execution Bridge", trigger: "branch、commit、PR、测试进展或执行回写事件进入 dev-sync 时的页面内提醒。", }, releaseGate: { surface: "release_growth", productArea: "Release / Growth", trigger: "发布门禁阻塞、校验通过、风险项需要处理时的页面内提醒。", }, failureAlert: { surface: "system_failure", productArea: "全局失败提醒", trigger: "AI 调用、同步任务、执行回写或外部集成失败时的页面内失败提示。", }, }; const reminderConsumptionEventMeta: Record< AccountReminderConsumptionEvent, { notificationKey: AccountNotificationKey; surface: Exclude; productArea: string; reason: string } > = { change_review_high_risk: { notificationKey: "pendingConfirmation", surface: "change_review", productArea: "Change Review", reason: "高风险 ChangeSet 或 ChangeItem 进入待审时使用待确认偏好控制页面内提醒表达。", }, iteration_draft_ready: { notificationKey: "pendingConfirmation", surface: "change_review", productArea: "Change Review", reason: "下一轮迭代建议草案准备好、需要人工拍板时使用待确认偏好控制页面内提醒表达。", }, release_blocked: { notificationKey: "releaseGate", surface: "release_growth", productArea: "Release / Growth", reason: "发布、合规或增长验证受阻时使用发布门禁偏好控制页面内提醒表达。", }, execution_stalled: { notificationKey: "executionWriteback", surface: "execution_bridge", productArea: "Execution Bridge", reason: "执行链路停滞、长时间无进展或等待运行端处理时使用执行回写偏好控制页面内提醒表达。", }, }; const reviewDecisionNotificationEventMeta: Record< AccountReviewDecisionNotificationEvent, { notificationKey: AccountNotificationKey; reason: string } > = { review_requested: { notificationKey: "pendingConfirmation", reason: "Reviewer request 已创建、需要审查人进入 Change Review 时使用待确认偏好控制页面内提醒;reviewerRouting 仍按业务页面展示。", }, review_approved: { notificationKey: "pendingConfirmation", reason: "Reviewer 已批准 ChangeSet 或审查项时使用待确认偏好控制页面内提醒;approval 审计状态仍必须保留。", }, review_changes_requested: { notificationKey: "pendingConfirmation", reason: "Reviewer 要求修改 ChangeSet 或审查项时使用待确认偏好控制页面内提醒;变更请求、证据和审计状态不得被偏好隐藏。", }, review_rejected: { notificationKey: "pendingConfirmation", reason: "Reviewer 拒绝 ChangeSet 或审查项时使用待确认偏好控制页面内提醒;拒绝原因、evidence 和 audit state 必须展示。", }, high_risk_approval_required: { notificationKey: "pendingConfirmation", reason: "高风险 ChangeSet 需要 approval gate 时使用待确认偏好控制页面内提醒;approval 状态和业务证据仍由 Change Review 真源控制。", }, }; const releaseNotificationEventMeta: Record< AccountReleaseNotificationEvent, { notificationKey: AccountNotificationKey; surface: "release_growth" | "system_failure"; reason: string } > = { release_gate_blocked: { notificationKey: "releaseGate", surface: "release_growth", reason: "Release/Growth 发布门禁阻塞、缺少证据或需要人工处理时使用发布门禁偏好。", }, release_gate_ready: { notificationKey: "releaseGate", surface: "release_growth", reason: "Release/Growth 形成可发布候选或需要人工确认时使用发布门禁偏好。", }, release_validation_blocked: { notificationKey: "releaseGate", surface: "release_growth", reason: "真实发布验证记录存在未通过检查项时使用发布门禁偏好。", }, release_validation_needs_confirmation: { notificationKey: "releaseGate", surface: "release_growth", reason: "真实发布验证记录已生成、需要人工确认时使用发布门禁偏好。", }, failed_event: { notificationKey: "failureAlert", surface: "system_failure", reason: "Release/Growth 捕获 failed event、验证运行失败或执行链路失败时使用失败提醒偏好。", }, }; const postReleaseNotificationEventMeta: Record< AccountPostReleaseNotificationEvent, { notificationKey: AccountNotificationKey; surface: "release_growth" | "change_review"; reason: string } > = { feedback_received: { notificationKey: "releaseGate", surface: "release_growth", reason: "Release/Growth 收到发布后用户反馈时使用发布门禁偏好控制页面内提醒;反馈证据本身不受偏好隐藏。", }, revenue_signal: { notificationKey: "releaseGate", surface: "release_growth", reason: "Release/Growth 收到收入、转化或增长信号时使用发布门禁偏好控制页面内提醒;收入证据本身不受偏好隐藏。", }, compliance_blocked: { notificationKey: "releaseGate", surface: "release_growth", reason: "合规、风控或发布后审核阻塞下一步时使用发布门禁偏好控制页面内提醒;阻塞状态与证据仍必须展示在业务页面。", }, iteration_recommendation: { notificationKey: "pendingConfirmation", surface: "change_review", reason: "AI 生成下一轮迭代建议、需要人类拍板时使用待确认偏好控制页面内提醒;建议对象和证据仍保留。", }, next_change_review: { notificationKey: "pendingConfirmation", surface: "change_review", reason: "下一轮 Change Review 或 ChangeSet 待审需要人类拍板时使用待确认偏好控制页面内提醒;ChangeSet 与 recommendation 不受偏好隐藏。", }, }; export function createDefaultAccountSettings(seed: AccountSettingsSeed = {}): AccountSettings { return { profile: { displayName: seed.displayName?.trim() || "张子墨", roleTitle: seed.roleTitle?.trim() || "产品负责人", email: seed.email?.trim() || "", defaultLanguage: "zh-CN", }, aiPreferences: { answerDepth: "balanced", evidenceDisplay: "inline", }, notifications: { pendingConfirmation: { enabled: true, desktop: true, emailDigest: false, reminderIntensity: "normal", evidenceDisplay: "compact", }, executionWriteback: { enabled: true, desktop: true, emailDigest: false, reminderIntensity: "normal", evidenceDisplay: "compact", }, releaseGate: { enabled: true, desktop: true, emailDigest: true, reminderIntensity: "normal", evidenceDisplay: "compact", }, failureAlert: { enabled: true, desktop: true, emailDigest: true, reminderIntensity: "normal", evidenceDisplay: "compact", }, }, saveScope: "local", updatedAt: new Date().toISOString(), }; } function isBrowserStorageAvailable(): boolean { return typeof window !== "undefined" && typeof window.localStorage !== "undefined"; } function canUseAccountBackend(): boolean { if (typeof window === "undefined") return false; return Boolean(getAuthToken()); } function readStoredAccountSettings(): Partial | null { if (!isBrowserStorageAvailable()) return null; const raw = window.localStorage.getItem(STORAGE_KEY); if (!raw) return null; try { const parsed = JSON.parse(raw) as Partial; return parsed && typeof parsed === "object" ? parsed : null; } catch { return null; } } function persistAccountSettings(settings: AccountSettings): void { if (!isBrowserStorageAvailable()) { throw new Error("当前环境不支持本机暂存"); } window.localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); } function cacheAccountSettingsSnapshot(settings: AccountSettings): void { try { persistAccountSettings(settings); } catch { // Backend account save is the source of truth; local snapshot failure should not downgrade it. } } function mergeAccountSettings(base: AccountSettings, stored: Partial | null): AccountSettings { if (!stored) return base; const mergedNotifications = accountNotificationKeys.reduce((result, key) => { result[key] = normalizeNotificationPreference(base.notifications[key], stored.notifications?.[key]); return result; }, {} as AccountNotificationSettings); return { ...base, ...stored, profile: { ...base.profile, ...stored.profile, }, aiPreferences: { ...base.aiPreferences, ...stored.aiPreferences, }, notifications: mergedNotifications, }; } function normalizeNotificationPreference( base: AccountNotificationPreference, stored?: Partial ): AccountNotificationPreference { const raw = { ...base, ...(stored || {}), }; const storedIntensity = stored?.reminderIntensity; const reminderIntensity: AccountReminderIntensity = storedIntensity === "silent" || storedIntensity === "normal" || storedIntensity === "prominent" ? storedIntensity : raw.enabled ? "normal" : "silent"; const evidenceDisplay: AccountNotificationEvidenceDisplay = raw.evidenceDisplay === "expanded" ? "expanded" : "compact"; return { ...raw, enabled: reminderIntensity !== "silent", desktop: reminderIntensity !== "silent" && Boolean(raw.desktop), emailDigest: reminderIntensity !== "silent" && Boolean(raw.emailDigest), reminderIntensity, evidenceDisplay, }; } function getNotificationReminderIntensity(preference: AccountNotificationPreference): AccountReminderIntensity { return preference.reminderIntensity === "silent" || preference.reminderIntensity === "normal" || preference.reminderIntensity === "prominent" ? preference.reminderIntensity : preference.enabled ? "normal" : "silent"; } function getNotificationEvidenceDisplay( preference: AccountNotificationPreference ): AccountNotificationEvidenceDisplay { return preference.evidenceDisplay === "expanded" ? "expanded" : "compact"; } export function shouldShowAccountNotification( settings: AccountSettings | Partial | null | undefined, key: AccountNotificationKey ): boolean { const normalized = mergeAccountSettings(createDefaultAccountSettings(), settings || null); return Boolean(normalized.notifications[key].enabled); } export function getAccountNotificationConsumption( settings: AccountSettings | Partial | null | undefined ): AccountNotificationConsumption[] { const normalized = mergeAccountSettings(createDefaultAccountSettings(), settings || null); return accountNotificationKeys.map((key) => { const preference = normalized.notifications[key]; const label = accountNotificationLabels[key]; const meta = accountNotificationConsumptionMeta[key]; const reminderIntensity = getNotificationReminderIntensity(preference); const evidenceDisplay = getNotificationEvidenceDisplay(preference); const enabled = reminderIntensity !== "silent" && Boolean(preference.enabled); return { key, surface: meta.surface, title: label.title, productArea: meta.productArea, trigger: meta.trigger, enabled, reminderIntensity, evidenceDisplay, showInAppReminder: enabled, showProminentReminder: enabled && reminderIntensity === "prominent", desktopPreference: enabled && Boolean(preference.desktop), emailDigestPreference: enabled && Boolean(preference.emailDigest), statusLabel: enabled ? reminderIntensity === "prominent" ? "页面内强提醒" : "页面内提醒显示" : "页面内提醒静默", detail: enabled ? `${meta.productArea} 会按 ${reminderIntensity === "prominent" ? "强提醒" : "普通提醒"} 展示页面内提醒,证据以 ${evidenceDisplay === "expanded" ? "展开" : "紧凑"} 模式呈现;桌面/邮件只记录偏好,不代表系统级推送已接入。` : `${meta.productArea} 不显示该类页面内提醒;底层对象、状态和审计记录仍保留。`, }; }); } export function getAccountReminderConsumption( settings: AccountSettings | Partial | null | undefined, event: AccountReminderConsumptionEvent ): AccountReminderConsumptionContract { const normalized = mergeAccountSettings(createDefaultAccountSettings(), settings || null); const meta = reminderConsumptionEventMeta[event]; const preference = normalized.notifications[meta.notificationKey]; const reminderIntensity = getNotificationReminderIntensity(preference); const evidenceDisplay = getNotificationEvidenceDisplay(preference); const enabled = reminderIntensity !== "silent" && Boolean(preference.enabled); return { event, notificationKey: meta.notificationKey, surface: meta.surface, productArea: meta.productArea, title: accountNotificationLabels[meta.notificationKey].title, reason: meta.reason, reminderIntensity, evidenceDisplay, showInAppReminder: enabled, showProminentReminder: enabled && reminderIntensity === "prominent", desktopPreference: enabled && Boolean(preference.desktop), emailDigestPreference: enabled && Boolean(preference.emailDigest), notificationTransport: "in_app_only", evidencePolicy: "preserve_business_evidence", auditPolicy: "preserve_audit_state", mutedDetail: enabled ? "仅影响页面内提醒强度和证据展开方式;不接真实邮件、桌面推送或系统通知中心。" : "仅静默页面内提醒;真实证据、ChangeSet、recommendation、执行状态和审计状态仍必须按业务页面展示。", }; } export function getReviewDecisionNotificationPreference( settings: AccountSettings | Partial | null | undefined, event: AccountReviewDecisionNotificationEvent ): AccountReviewDecisionNotificationPreference { const normalized = mergeAccountSettings(createDefaultAccountSettings(), settings || null); const meta = reviewDecisionNotificationEventMeta[event]; const preference = normalized.notifications[meta.notificationKey]; const reminderIntensity = getNotificationReminderIntensity(preference); const evidenceDisplay = getNotificationEvidenceDisplay(preference); const enabled = reminderIntensity !== "silent" && Boolean(preference.enabled); return { event, notificationKey: meta.notificationKey, surface: "change_review", productArea: "Change Review", title: accountNotificationLabels[meta.notificationKey].title, reason: meta.reason, reminderIntensity, evidenceDisplay, showInAppReminder: enabled, showProminentReminder: enabled && reminderIntensity === "prominent", desktopPreference: enabled && Boolean(preference.desktop), emailDigestPreference: enabled && Boolean(preference.emailDigest), notificationTransport: "in_app_only", evidencePolicy: "preserve_business_evidence", auditPolicy: "preserve_audit_state", reviewerRoutingPolicy: "preserve_reviewer_routing", approvalPolicy: "preserve_approval_state", mutedDetail: enabled ? "仅影响 Change Review 页面内提醒强度和证据展开方式;不接真实邮件、桌面推送或系统通知中心。" : "仅静默页面内提醒;真实 reviewerRouting、approval、evidence 和 audit state 仍必须按业务页面展示。", }; } export function getReleaseGateNotificationPreference( settings: AccountSettings | Partial | null | undefined, event: AccountReleaseNotificationEvent = "release_gate_blocked" ): AccountReleaseGateNotificationPreference { const normalized = mergeAccountSettings(createDefaultAccountSettings(), settings || null); const meta = releaseNotificationEventMeta[event]; const preference = normalized.notifications[meta.notificationKey]; const reminderIntensity = getNotificationReminderIntensity(preference); const evidenceDisplay = getNotificationEvidenceDisplay(preference); const enabled = reminderIntensity !== "silent" && Boolean(preference.enabled); return { event, notificationKey: meta.notificationKey, surface: meta.surface, inAppEnabled: enabled, reminderIntensity, evidenceDisplay, showProminentReminder: enabled && reminderIntensity === "prominent", desktopPreference: enabled && Boolean(preference.desktop), emailDigestPreference: enabled && Boolean(preference.emailDigest), notificationTransport: "in_app_only", title: accountNotificationLabels[meta.notificationKey].title, reason: meta.reason, }; } export function getPostReleaseNotificationPreference( settings: AccountSettings | Partial | null | undefined, event: AccountPostReleaseNotificationEvent ): AccountPostReleaseNotificationPreference { const normalized = mergeAccountSettings(createDefaultAccountSettings(), settings || null); const meta = postReleaseNotificationEventMeta[event]; const preference = normalized.notifications[meta.notificationKey]; const reminderIntensity = getNotificationReminderIntensity(preference); const evidenceDisplay = getNotificationEvidenceDisplay(preference); const enabled = reminderIntensity !== "silent" && Boolean(preference.enabled); return { event, notificationKey: meta.notificationKey, surface: meta.surface, spaceReadable: true, inAppEnabled: enabled, showInAppReminder: enabled, reminderIntensity, evidenceDisplay, showProminentReminder: enabled && reminderIntensity === "prominent", desktopPreference: enabled && Boolean(preference.desktop), emailDigestPreference: enabled && Boolean(preference.emailDigest), notificationTransport: "in_app_only", title: accountNotificationLabels[meta.notificationKey].title, reason: meta.reason, evidencePolicy: "preserve_business_evidence", reminderPolicy: "page_reminder_only", mutedDetail: enabled ? "Release/Growth 或 Space 可显示页面内提醒;桌面/邮件只记录偏好,不代表系统级推送已接入。" : "仅静默页面内提醒;反馈、收入、合规、ChangeSet、迭代建议及其证据仍必须按业务页面展示。", }; } export async function getAccountSettings(seed?: AccountSettingsSeed): Promise { const defaults = createDefaultAccountSettings(seed); const localSettings = mergeAccountSettings(defaults, readStoredAccountSettings()); if (!canUseAccountBackend()) return localSettings; try { const response = await apiRequest>(ACCOUNT_SETTINGS_ENDPOINT, { method: "GET" }); if (response.data) { const accountSettings = mergeAccountSettings(defaults, response.data); cacheAccountSettingsSnapshot(accountSettings); return accountSettings; } } catch { return localSettings; } return localSettings; } export async function updateAccountSettings(nextSettings: AccountSettings): Promise { if (canUseAccountBackend()) { try { const response = await apiRequest>(ACCOUNT_SETTINGS_ENDPOINT, { method: "PUT", body: JSON.stringify({ profile: nextSettings.profile, aiPreferences: nextSettings.aiPreferences, notifications: nextSettings.notifications, }), }); if (response.data) { const saved = { ...response.data, saveScope: "account" as const, }; cacheAccountSettingsSnapshot(saved); return saved; } } catch { // Fall back to honest local-only persistence below. } } const persisted = { ...nextSettings, saveScope: "local" as const, updatedAt: new Date().toISOString(), }; persistAccountSettings(persisted); return persisted; }