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:
+166
-4
@@ -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),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user