feat(moderation): add user-facing reasons for rejected or failed content

Prompt AI models to provide short explanations for rejected content
Store reasons in database and broadcast via WebSocket
Display moderation details in UI for authors and admins
This commit is contained in:
2026-05-04 11:18:54 +08:00
parent 3d6188748d
commit 07698e063d
12 changed files with 352 additions and 50 deletions

View File

@@ -43,6 +43,7 @@ type NotificationRow = {
entityId: number | null;
reactionType: LifeReactionType | null;
moderationStatus: NotificationModerationStatus | null;
moderationReason: string | null;
readAt: Date | null;
createdAt: Date;
createdAtCursor: string;
@@ -67,6 +68,7 @@ export type NotificationItem = {
target: NotificationTarget;
reactionType: LifeReactionType | null;
moderationStatus: NotificationModerationStatus | null;
moderationReason: string | null;
readAt: Date | null;
createdAt: Date;
updatedAt: Date;
@@ -88,6 +90,7 @@ type NotificationWsMessage =
target: NotificationTarget;
moderationStatus: NotificationModerationStatus;
moderationLanguageCode: string | null;
moderationReason: string | null;
};
const defaultNotificationLimit = 15;
@@ -152,6 +155,7 @@ function notificationProjection(): string {
n.entity_id AS "entityId",
n.reaction_type AS "reactionType",
n.moderation_status AS "moderationStatus",
n.moderation_reason AS "moderationReason",
n.read_at AS "readAt",
n.created_at AS "createdAt",
n.created_at::text AS "createdAtCursor",
@@ -216,6 +220,7 @@ function toNotificationItem(row: NotificationRow): NotificationItem {
},
reactionType: row.reactionType,
moderationStatus: row.moderationStatus,
moderationReason: row.moderationReason,
readAt: row.readAt,
createdAt: row.createdAt,
updatedAt: row.updatedAt
@@ -277,13 +282,15 @@ async function publishModerationUpdate(
userId: number,
target: NotificationTarget,
moderationStatus: NotificationModerationStatus,
moderationLanguageCode: string | null
moderationLanguageCode: string | null,
moderationReason: string | null
): Promise<void> {
broadcastNotificationMessage(userId, {
type: 'moderation.updated',
target,
moderationStatus,
moderationLanguageCode
moderationLanguageCode,
moderationReason
});
}
@@ -563,6 +570,7 @@ export async function createModerationResultNotification(
id: number;
recipientUserId: number;
moderationLanguageCode: string | null;
moderationReason: string | null;
lifePostId: number;
}>(
`
@@ -571,9 +579,10 @@ export async function createModerationResultNotification(
actor_user_id,
type,
life_post_id,
moderation_status
moderation_status,
moderation_reason
)
SELECT created_by_user_id, NULL, 'moderation_result', id, $2
SELECT created_by_user_id, NULL, 'moderation_result', id, $2, ai_moderation_reason
FROM life_posts
WHERE id = $1
AND deleted_at IS NULL
@@ -586,6 +595,11 @@ export async function createModerationResultNotification(
FROM life_posts
WHERE id = $1
) AS "moderationLanguageCode",
(
SELECT ai_moderation_reason
FROM life_posts
WHERE id = $1
) AS "moderationReason",
life_post_id AS "lifePostId"
`,
[target.id, status]
@@ -605,7 +619,8 @@ export async function createModerationResultNotification(
entityId: null
},
status,
row.moderationLanguageCode
row.moderationLanguageCode,
row.moderationReason
);
}
return;
@@ -616,6 +631,7 @@ export async function createModerationResultNotification(
id: number;
recipientUserId: number;
moderationLanguageCode: string | null;
moderationReason: string | null;
lifePostId: number;
lifeCommentId: number;
}>(
@@ -627,7 +643,8 @@ export async function createModerationResultNotification(
life_post_id,
life_comment_id,
parent_life_comment_id,
moderation_status
moderation_status,
moderation_reason
)
SELECT
lc.created_by_user_id,
@@ -636,7 +653,8 @@ export async function createModerationResultNotification(
lc.post_id,
lc.id,
lc.parent_comment_id,
$2
$2,
lc.ai_moderation_reason
FROM life_post_comments lc
JOIN life_posts lp ON lp.id = lc.post_id
WHERE lc.id = $1
@@ -651,6 +669,11 @@ export async function createModerationResultNotification(
FROM life_post_comments
WHERE id = $1
) AS "moderationLanguageCode",
(
SELECT ai_moderation_reason
FROM life_post_comments
WHERE id = $1
) AS "moderationReason",
life_post_id AS "lifePostId",
life_comment_id AS "lifeCommentId"
`,
@@ -671,7 +694,8 @@ export async function createModerationResultNotification(
entityId: null
},
status,
row.moderationLanguageCode
row.moderationLanguageCode,
row.moderationReason
);
}
return;
@@ -681,6 +705,7 @@ export async function createModerationResultNotification(
id: number;
recipientUserId: number;
moderationLanguageCode: string | null;
moderationReason: string | null;
discussionCommentId: number;
entityType: DiscussionEntityType;
entityId: number;
@@ -694,7 +719,8 @@ export async function createModerationResultNotification(
parent_discussion_comment_id,
entity_type,
entity_id,
moderation_status
moderation_status,
moderation_reason
)
SELECT
created_by_user_id,
@@ -704,7 +730,8 @@ export async function createModerationResultNotification(
parent_comment_id,
entity_type,
entity_id,
$2
$2,
ai_moderation_reason
FROM entity_discussion_comments
WHERE id = $1
AND deleted_at IS NULL
@@ -717,6 +744,11 @@ export async function createModerationResultNotification(
FROM entity_discussion_comments
WHERE id = $1
) AS "moderationLanguageCode",
(
SELECT ai_moderation_reason
FROM entity_discussion_comments
WHERE id = $1
) AS "moderationReason",
discussion_comment_id AS "discussionCommentId",
entity_type AS "entityType",
entity_id AS "entityId"
@@ -738,7 +770,8 @@ export async function createModerationResultNotification(
entityId: row.entityId
},
status,
row.moderationLanguageCode
row.moderationLanguageCode,
row.moderationReason
);
}
}