feat(chat): migrate chat protocol to lock detail

This commit is contained in:
2026-06-24 14:06:30 +08:00
parent c280a3c1ea
commit 42c03f8901
34 changed files with 609 additions and 373 deletions
+83 -47
View File
@@ -31,13 +31,20 @@ export const chatRepo: IChatRepository = chatRepository;
export function localMessagesToUi(
records: ReadonlyArray<{
id?: string;
type?: string;
content: string;
role: string;
createdAt: string;
imageUrl?: string | null;
isPrivate?: boolean | null;
privateLocked?: boolean | null;
privateHint?: string | null;
audioUrl?: string | null;
image?: { type: string | null; url: string | null };
lockDetail?: {
locked: boolean;
showContent: boolean;
showUpgrade: boolean;
reason: string | null;
hint: string | null;
detail: Record<string, unknown> | null;
};
}>,
): UiMessage[] {
return records.map((m) => ({
@@ -45,14 +52,13 @@ export function localMessagesToUi(
content: getAiMessageDisplayContent({
content: m.content,
isFromAI: m.role === "assistant",
imageUrl: m.imageUrl,
hasImage: Boolean(m.image?.url),
lockDetail: m.lockDetail,
}),
isFromAI: m.role === "assistant",
date: messageDateFromCreatedAt(m.createdAt),
...(m.imageUrl ? { imageUrl: m.imageUrl } : {}),
...(m.isPrivate != null ? { isPrivate: m.isPrivate } : {}),
...(m.privateLocked != null ? { privateLocked: m.privateLocked } : {}),
...(m.privateHint != null ? { privateHint: m.privateHint } : {}),
...(m.audioUrl ? { audioUrl: m.audioUrl } : {}),
...deriveUiLockFields(m.lockDetail, m.image?.url),
}));
}
@@ -65,9 +71,14 @@ function messageDateFromCreatedAt(createdAt: string): string {
function getAiMessageDisplayContent(input: {
content: string;
isFromAI: boolean;
imageUrl?: string | null;
hasImage?: boolean;
lockDetail?: {
showContent: boolean;
reason: string | null;
};
}): string {
return input.isFromAI && input.imageUrl ? "" : input.content;
if (input.lockDetail?.showContent === false) return "";
return input.isFromAI && input.hasImage ? "" : input.content;
}
/**
@@ -81,20 +92,38 @@ export function sendResponseToUiMessage(response: ChatSendResponse): UiMessage {
content: getAiMessageDisplayContent({
content: response.reply,
isFromAI: true,
imageUrl: response.imageUrl,
hasImage: Boolean(response.image.url),
lockDetail: response.lockDetail,
}),
isFromAI: true,
date: todayString(new Date(response.timestamp)),
...(response.imageUrl ? { imageUrl: response.imageUrl } : {}),
...(response.showUpgrade && response.imageUrl
? { imagePaywalled: true }
...(response.audioUrl ? { audioUrl: response.audioUrl } : {}),
...deriveUiLockFields(response.lockDetail, response.image.url),
};
}
function deriveUiLockFields(
lockDetail:
| {
locked: boolean;
showContent: boolean;
showUpgrade: boolean;
reason: string | null;
hint: string | null;
}
| undefined,
imageUrl?: string | null,
): Partial<UiMessage> {
const isPrivateMessage = lockDetail?.reason === "private_message";
return {
...(imageUrl ? { imageUrl } : {}),
...(imageUrl && lockDetail?.showUpgrade ? { imagePaywalled: true } : {}),
...(isPrivateMessage ? { isPrivate: true } : {}),
...(isPrivateMessage && lockDetail?.locked
? { lockedPrivate: true }
: {}),
...(response.isPrivate != null ? { isPrivate: response.isPrivate } : {}),
...(response.privateLocked != null
? { privateLocked: response.privateLocked }
: {}),
...(response.privateHint != null
? { privateHint: response.privateHint }
...(isPrivateMessage && lockDetail?.hint
? { privateMessageHint: lockDetail.hint }
: {}),
};
}
@@ -110,13 +139,13 @@ export function applyHttpSendOutput(
): Partial<ChatState> {
const { response, reply } = output;
const lockDetail = response.lockDetail;
const isMessageLimitBlocked =
response.blocked === true &&
(response.blockReason === "daily_limit" ||
response.blockReason === "total_limit");
lockDetail.locked &&
lockDetail.showUpgrade &&
lockDetail.reason === "daily_limit";
if (isMessageLimitBlocked) {
const detail = response.blockDetail;
const lastMessage = context.messages[context.messages.length - 1];
const messages =
lastMessage && !lastMessage.isFromAI
@@ -126,21 +155,10 @@ export function applyHttpSendOutput(
return {
messages,
isReplyingAI: false,
paywallTriggered: true,
paywallReason:
response.blockReason === "total_limit" ? "total_limit" : "daily_limit",
paywallDetail: detail
? {
type: detail.type,
...(detail.usedToday != null
? { usedToday: detail.usedToday }
: {}),
...(detail.usedTotal != null
? { usedTotal: detail.usedTotal }
: {}),
limit: detail.limit,
}
: null,
upgradePromptVisible: true,
upgradeReason: "daily_limit",
upgradeHint: lockDetail.hint,
upgradeDetail: normalizeLockDetail(lockDetail.detail),
};
}
@@ -150,22 +168,40 @@ export function applyHttpSendOutput(
};
}
if (response.showUpgrade) {
if (lockDetail.showUpgrade && lockDetail.reason === "private_message") {
return {
messages: [...context.messages, reply],
isReplyingAI: false,
paywallTriggered: false,
paywallReason: null,
paywallDetail: null,
upgradePromptVisible: false,
upgradeReason: null,
upgradeHint: null,
upgradeDetail: null,
};
}
return {
messages: [...context.messages, reply],
isReplyingAI: false,
paywallTriggered: false,
paywallReason: null,
paywallDetail: null,
upgradePromptVisible: false,
upgradeReason: null,
upgradeHint: null,
upgradeDetail: null,
};
}
export function normalizeLockDetail(
detail: Record<string, unknown> | null,
): ChatState["upgradeDetail"] {
if (!detail) return null;
return {
...(typeof detail.type === "string" ? { type: detail.type } : {}),
...(typeof detail.usedToday === "number"
? { usedToday: detail.usedToday }
: {}),
...(typeof detail.usedTotal === "number"
? { usedTotal: detail.usedTotal }
: {}),
...(typeof detail.limit === "number" ? { limit: detail.limit } : {}),
};
}