import { useCallback, useEffect, useMemo, useState } from 'react'; import { Link } from 'react-router-dom'; import { cancelWechatDigest, clearWechatRoute, createWechatWebNotification, getWechatAdminSummary, getWechatIntentRouterConfig, getWechatIntentRouterRuntime, listAdminUsers, listLlmProviderKeys, listWechatBindings, listWechatDeliveries, listWechatDigests, listWechatMessages, listWechatWebNotifications, patchWechatIntentRouterConfig, resumeWechatDigest, updateWechatScheduleLlmConfig, } from '../../api/client'; import type { AdminUserRow, LlmProviderKeyRow, WechatAdminSummary, WechatBinding, WechatDeliveryLog, WechatDigestSubscription, WechatIntentRouterAdminConfig, WechatIntentRouterRuntimeState, WechatMessage, WechatWebNotification, } from '../../types'; import { formatTime } from '../utils/format'; function count(record: Record, key: string) { return record[key] ?? 0; } function safeSummary(summary: WechatAdminSummary | null) { return { config: summary?.config ?? { mpEnabled: false, scheduleEnabled: false, reminderWorkerEnabled: false, appId: null, publicBaseUrl: null, bindPath: null, tokenEndpointConfigured: false, customerServiceEndpointConfigured: false, scheduleLlmEnabled: false, }, counts: summary?.counts ?? { boundUsers: 0, routes: { total: 0, active: 0 }, recentMessages: {}, digests: {}, recentDeliveries: {}, }, }; } function statusLabel(status?: string | null) { if (!status) return '—'; return status; } function statusClass(status?: string | null) { if (status === 'failed') return 'text-error'; if (status === 'active' || status === 'success' || status === 'done') return 'wechat-ok'; return 'muted'; } function userLabel(row: { displayName?: string | null; username?: string | null }) { return row.displayName || row.username || '—'; } function dateLabel(value?: number | null) { return value ? formatTime(value) : '—'; } type IntentRouterForm = { enabled: boolean; shadowMode: boolean; modelProviderKeyId: string; model: string; minConfidence: string; timeoutMs: string; canaryOpenids: string; }; function intentRouterToForm( config: WechatIntentRouterAdminConfig | undefined, keys: LlmProviderKeyRow[], ): IntentRouterForm { const keyId = String(config?.modelProviderKeyId ?? '').trim(); const selectedKey = keys.find((item) => item.id === keyId) ?? null; const model = String(config?.model ?? '').trim() || selectedKey?.defaultModel || selectedKey?.models?.[0] || ''; return { enabled: Boolean(config?.enabled), shadowMode: config?.shadowMode !== false, modelProviderKeyId: keyId, model, minConfidence: String(config?.minConfidence ?? 0.65), timeoutMs: String(config?.timeoutMs ?? 4000), canaryOpenids: Array.isArray(config?.canaryOpenids) ? config.canaryOpenids.join('\n') : '', }; } function intentRouterRuntimeMode(overrides: Record) { const enabled = ['1', 'true', 'yes', 'on'].includes( String(overrides.MEMIND_WECHAT_INTENT_LLM_ENABLED ?? '').toLowerCase(), ); const shadow = ['1', 'true', 'yes', 'on'].includes( String(overrides.MEMIND_WECHAT_INTENT_LLM_SHADOW ?? '').toLowerCase(), ); if (!enabled) return '关闭(仅规则意图)'; if (shadow) return 'Shadow 观测(行为不变)'; return '已激活(chat.general 可升级为 page.generate)'; } export function WechatPage() { const [summary, setSummary] = useState(null); const [bindings, setBindings] = useState([]); const [messages, setMessages] = useState([]); const [digests, setDigests] = useState([]); const [deliveries, setDeliveries] = useState([]); const [webNotifications, setWebNotifications] = useState([]); const [users, setUsers] = useState([]); const [search, setSearch] = useState(''); const [messageStatus, setMessageStatus] = useState(''); const [digestStatus, setDigestStatus] = useState(''); const [deliveryStatus, setDeliveryStatus] = useState(''); const [notificationStatus, setNotificationStatus] = useState(''); const [notifyAudience, setNotifyAudience] = useState<'single' | 'multi' | 'all'>('single'); const [notifyUserId, setNotifyUserId] = useState(''); const [notifyUserIds, setNotifyUserIds] = useState([]); const [notifyTitle, setNotifyTitle] = useState(''); const [notifyBody, setNotifyBody] = useState(''); const [notifyType, setNotifyType] = useState('manual'); const [notifyChannels, setNotifyChannels] = useState>(['web']); const [scheduleLlmEnabled, setScheduleLlmEnabled] = useState(false); const [llmKeys, setLlmKeys] = useState([]); const [intentForm, setIntentForm] = useState(null); const [savedIntentForm, setSavedIntentForm] = useState(null); const [intentRuntime, setIntentRuntime] = useState(null); const [intentSaving, setIntentSaving] = useState(false); const [loading, setLoading] = useState(true); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const [notice, setNotice] = useState(null); const safe = safeSummary(summary); const intentDirty = useMemo(() => { if (!intentForm || !savedIntentForm) return false; return JSON.stringify(intentForm) !== JSON.stringify(savedIntentForm); }, [intentForm, savedIntentForm]); const selectedIntentKey = useMemo( () => llmKeys.find((item) => item.id === intentForm?.modelProviderKeyId) ?? null, [llmKeys, intentForm?.modelProviderKeyId], ); const load = useCallback(async () => { setLoading(true); setError(null); try { const [ nextSummary, nextBindings, nextMessages, nextDigests, nextDeliveries, nextNotifications, nextUsers, nextLlmKeys, nextIntentConfig, nextIntentRuntime, ] = await Promise.all([ getWechatAdminSummary(), listWechatBindings({ search, limit: 80 }), listWechatMessages({ status: messageStatus || undefined, limit: 80 }), listWechatDigests({ status: digestStatus || undefined, limit: 80 }), listWechatDeliveries({ status: deliveryStatus || undefined, limit: 80 }), listWechatWebNotifications({ status: notificationStatus || undefined, limit: 80 }), listAdminUsers({ page: 1, pageSize: 200, status: 'active' }), listLlmProviderKeys().catch(() => [] as LlmProviderKeyRow[]), getWechatIntentRouterConfig().catch(() => null), getWechatIntentRouterRuntime().catch(() => null), ]); setSummary(nextSummary); setScheduleLlmEnabled(nextSummary.config.scheduleLlmEnabled ?? false); setBindings(nextBindings); setMessages(nextMessages); setDigests(nextDigests); setDeliveries(nextDeliveries); setWebNotifications(nextNotifications); setUsers(nextUsers.items); setNotifyUserId((current) => current || nextUsers.items[0]?.id || ''); setLlmKeys(nextLlmKeys); if (nextIntentConfig) { const nextForm = intentRouterToForm(nextIntentConfig, nextLlmKeys); setIntentForm(nextForm); setSavedIntentForm(nextForm); } setIntentRuntime(nextIntentRuntime); } catch (err) { setError(err instanceof Error ? err.message : '加载服务号管理失败'); } finally { setLoading(false); } }, [deliveryStatus, digestStatus, messageStatus, notificationStatus, search]); useEffect(() => { void load(); }, [load]); const runAction = async (action: () => Promise) => { setBusy(true); setError(null); try { await action(); await load(); } catch (err) { setError(err instanceof Error ? err.message : '操作失败'); } finally { setBusy(false); } }; const toggleChannel = (channel: 'web' | 'wechat') => { setNotifyChannels((current) => { if (current.includes(channel)) { if (current.length === 1) return current; return current.filter((item) => item !== channel); } return [...current, channel]; }); }; const handleSendNotification = async () => { const title = notifyTitle.trim(); const body = notifyBody.trim(); if (!title) { setError('请填写通知标题'); return; } if (!body) { setError('请填写通知内容'); return; } if (notifyAudience === 'single' && !notifyUserId) { setError('请选择目标用户'); return; } if (notifyAudience === 'multi' && notifyUserIds.length === 0) { setError('请至少选择一个用户'); return; } setBusy(true); setError(null); setNotice(null); try { const result = await createWechatWebNotification( notifyAudience === 'all' ? { audience: 'all', allUsers: true, title, body, notificationType: notifyType.trim() || 'manual', channels: notifyChannels, } : notifyAudience === 'multi' ? { userIds: notifyUserIds, title, body, notificationType: notifyType.trim() || 'manual', channels: notifyChannels, } : { userId: notifyUserId, title, body, notificationType: notifyType.trim() || 'manual', channels: notifyChannels, }, ); const failed = result.wechatFailures?.length ?? 0; setNotice( `发送完成:目标 ${result.targets} 人,网页通知 ${result.created} 条,公众号成功 ${result.wechatSent} 条${ failed ? `,失败 ${failed} 条` : '' }。`, ); setNotifyTitle(''); setNotifyBody(''); if (notifyAudience === 'multi') setNotifyUserIds([]); await load(); } catch (err) { setError(err instanceof Error ? err.message : '发送失败'); } finally { setBusy(false); } }; const handleSaveScheduleLlm = async () => { await runAction(async () => { await updateWechatScheduleLlmConfig({ scheduleLlmEnabled }); setNotice(`提醒待办 LLM 已${scheduleLlmEnabled ? '开启' : '关闭'}。`); }); }; const handleSaveIntentRouter = async () => { if (!intentForm) return; setIntentSaving(true); setError(null); setNotice(null); try { const result = await patchWechatIntentRouterConfig({ enabled: intentForm.enabled, shadowMode: intentForm.shadowMode, modelProviderKeyId: intentForm.modelProviderKeyId || null, model: intentForm.model || null, minConfidence: Number(intentForm.minConfidence) || 0.65, timeoutMs: Number(intentForm.timeoutMs) || 4000, canaryOpenids: intentForm.canaryOpenids .split(/[\s,]+/u) .map((item) => item.trim()) .filter(Boolean), }); const nextForm = intentRouterToForm(result, llmKeys); setIntentForm(nextForm); setSavedIntentForm(nextForm); setIntentRuntime(await getWechatIntentRouterRuntime().catch(() => null)); setNotice('微信 LLM 意图路由配置已保存。Portal 会在下次请求时自动热加载。'); } catch (err) { setError(err instanceof Error ? err.message : '保存失败'); } finally { setIntentSaving(false); } }; return (

服务号管理

绑定、路由、每日待办推送和投递记录

{error &&

{error}

} {notice &&

{notice}

}
{summary && (

配置状态

AppID
{summary.config.appId ?? '未配置'}
公网地址
{summary.config.publicBaseUrl ?? '未配置'}
绑定路径
{summary.config.bindPath ?? '未配置'}
定时服务
{summary.config.scheduleEnabled ? '已启用' : '未启用'}
推送 worker
{summary.config.reminderWorkerEnabled ? '已启用' : '未启用'}
提醒待办 LLM
{summary.config.scheduleLlmEnabled ? '已开启' : '已关闭'}

默认关闭。开启后仅在规则未识别出提醒意图时,才调用后台已选中的 LLM 做补充解析;复杂定时提醒仍走原有会话链路。

)} {intentForm ? (

微信 LLM 意图路由

独立于 H5 Router。仅对规则判为 chat.general 的消息做 LLM 二次判定(page.generate vs chat.general)。