Add smart ACK provider for WeChat MP replies

Replace fixed ackText with a rule-based AckProvider that picks
response templates by message type and intent (translate, summary,
rewrite, poster, ppt, mindmap, code, search, schedule). Pure sync,
zero I/O, auto-falls back to config.ackText on any error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
john
2026-06-26 15:19:03 +08:00
parent 9ed4fd48d7
commit 9b4a25799f
162 changed files with 17276 additions and 2054 deletions
+166 -4
View File
@@ -42,6 +42,9 @@ import type {
SessionListResponse,
UsageRecord,
BalanceUpdate,
UserNotification,
PlanDefinition,
ActiveSubscription,
} from '../types';
import { CHAT_IMAGE_UPLOAD_MAX_OUTPUT_BYTES } from '../utils/imageUpload';
import { normalizeConversationMessages, normalizeUserMessageForApi } from '../utils/message';
@@ -209,7 +212,15 @@ async function portalFetch<T>(path: string, init?: RequestInit): Promise<T> {
}
if (res.status === 204) return undefined as T;
return res.json() as Promise<T>;
const text = await res.text().catch(() => '');
if (text.trimStart().startsWith('<')) {
throw new ApiError(res.status, '服务器返回了意外的页面,请确认后端服务已启动并已重启');
}
try {
return JSON.parse(text) as T;
} catch {
throw new ApiError(res.status, '服务器响应格式错误');
}
}
function formatNetworkError(err: unknown) {
@@ -259,7 +270,15 @@ async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
}
if (res.status === 204) return undefined as T;
return res.json() as Promise<T>;
const rawText = await res.text().catch(() => '');
if (rawText.trimStart().startsWith('<')) {
throw new ApiError(res.status, '服务器返回了意外的页面,请确认后端服务已启动并已重启');
}
try {
return JSON.parse(rawText) as T;
} catch {
throw new ApiError(res.status, '服务器响应格式错误');
}
}
export async function startSession(): Promise<Session> {
@@ -407,6 +426,31 @@ export async function getBillingConfig(): Promise<BillingConfig> {
return portalFetch('/auth/billing/config');
}
export async function getAvailablePlans(): Promise<{
plans: PlanDefinition[];
subscription: ActiveSubscription | null;
balanceCents: number;
}> {
return portalFetch('/auth/billing/plans');
}
export async function purchaseSubscription(planType: string, autoRenew = false): Promise<{
subscription: ActiveSubscription;
balanceCents: number;
}> {
return portalFetch('/auth/billing/subscribe', {
method: 'POST',
body: JSON.stringify({ planType, autoRenew }),
});
}
export async function setAutoRenew(enabled: boolean): Promise<{ ok: boolean; updated: boolean }> {
return portalFetch('/auth/billing/auto-renew', {
method: 'POST',
body: JSON.stringify({ enabled }),
});
}
export async function createRechargeOrder(input: {
amountCents: number;
payScene: 'native' | 'h5' | 'jsapi';
@@ -425,6 +469,18 @@ export async function getRechargeOrder(orderId: string): Promise<{
return portalFetch(`/auth/billing/recharge-orders/${orderId}`);
}
export async function purchaseSpaceQuota(sizeMb: number): Promise<{
quota: MindSpaceQuota;
balanceCents: number;
purchasedMb: number;
costCents: number;
}> {
return portalFetch('/auth/billing/space-purchase', {
method: 'POST',
body: JSON.stringify({ sizeMb }),
});
}
export async function getMindSpace(): Promise<MindSpace> {
const result = await apiFetch<{ data: MindSpace }>('/mindspace/v1/space');
return result.data;
@@ -965,6 +1021,32 @@ export async function redactMindSpacePage(
return result.data;
}
export async function fixMindSpacePagePublication(
pageId: string,
input: {
pageVersionId: string;
expectedVersion: number;
title: string;
summary: string;
content: string;
},
): Promise<MindSpaceRedactedCopyResult> {
const result = await apiFetch<{ data: MindSpaceRedactedCopyResult }>(
`/mindspace/v1/pages/${encodeURIComponent(pageId)}/publish-fix`,
{
method: 'POST',
body: JSON.stringify({
page_version_id: input.pageVersionId,
expected_version: input.expectedVersion,
title: input.title,
summary: input.summary,
content: input.content,
}),
},
);
return result.data;
}
/** @deprecated use redactMindSpacePage */
export async function createMindSpaceRedactedCopy(
pageId: string,
@@ -1418,6 +1500,79 @@ export async function getWechatBindingStatus(): Promise<WechatBindingStatus> {
}
}
export async function listNotifications(status = 'unread', limit = 20): Promise<UserNotification[]> {
const params = new URLSearchParams();
if (status && status !== 'all') params.set('status', status);
params.set('limit', String(limit));
const response = await fetch(`/auth/notifications?${params}`);
if (!response.ok) {
throw new ApiError(response.status, '读取通知失败');
}
const body = (await response.json()) as { notifications?: UserNotification[] };
return body.notifications ?? [];
}
export function subscribeNotificationEvents({
onNotification,
onSync,
}: {
onNotification: (notification: UserNotification) => void;
onSync?: () => void;
}): () => void {
const source = new EventSource('/auth/notifications/events', { withCredentials: true });
source.addEventListener('notification', (event) => {
try {
const payload = JSON.parse((event as MessageEvent).data) as {
notification?: UserNotification;
};
if (payload.notification) onNotification(payload.notification);
} catch {
onSync?.();
}
});
source.addEventListener('sync', () => onSync?.());
source.onerror = () => {
// EventSource reconnects automatically; consumers can keep their last state.
};
return () => source.close();
}
export async function markNotificationRead(notificationId: string): Promise<void> {
const response = await fetch(`/auth/notifications/${encodeURIComponent(notificationId)}/read`, {
method: 'POST',
});
if (!response.ok) {
throw new ApiError(response.status, '通知状态更新失败');
}
}
export async function markAllNotificationsRead(): Promise<void> {
const response = await fetch('/auth/notifications/read-all', {
method: 'POST',
});
if (!response.ok) {
throw new ApiError(response.status, '全部已读失败');
}
}
export async function deleteNotification(notificationId: string): Promise<void> {
const response = await fetch(`/auth/notifications/${encodeURIComponent(notificationId)}`, {
method: 'DELETE',
});
if (!response.ok && response.status !== 204) {
throw new ApiError(response.status, '删除通知失败');
}
}
export async function clearNotifications(status = 'all'): Promise<void> {
const response = await fetch(`/auth/notifications?status=${encodeURIComponent(status)}`, {
method: 'DELETE',
});
if (!response.ok) {
throw new ApiError(response.status, '清空通知失败');
}
}
export async function getWechatAgentRouteStatus(): Promise<WechatAgentRouteStatus> {
try {
const response = await fetch('/auth/wechat/agent-route');
@@ -1820,11 +1975,18 @@ export async function deleteChatSession(sessionId: string): Promise<void> {
await apiFetch(`/sessions/${sessionId}`, { method: 'DELETE' });
}
export async function loadSessionDetail(sessionId: string): Promise<{
export async function loadSessionDetail(
sessionId: string,
hints?: { messageCount?: number; updatedAt?: string },
): Promise<{
session: Session;
messages: Message[];
}> {
const detail = await getSession(sessionId);
const params = new URLSearchParams();
if (hints?.messageCount != null) params.set('hint_mc', String(hints.messageCount));
if (hints?.updatedAt) params.set('hint_ua', hints.updatedAt);
const qs = params.size ? `?${params.toString()}` : '';
const detail = await apiFetch<Session>(`/sessions/${sessionId}${qs}`);
const messages = normalizeConversationMessages(
(detail.conversation ?? []).filter((m) => m.metadata?.userVisible),
);