From 1af5b1dd8224af1ab7c4ca010424401750f86fb4 Mon Sep 17 00:00:00 2001 From: john Date: Sat, 1 Aug 2026 21:16:35 +0800 Subject: [PATCH] feat(wechat): add admin UI for WeChat LLM intent router toggles. Expose memindadm controls for the service-account-only intent refinement layer so operators can enable, shadow, and canary the router without touching H5 chatIntentRouter settings. Co-authored-by: Cursor --- src/admin/pages/WechatPage.tsx | 245 ++++++++++++++++++++++++++++++++- src/api/client.ts | 22 +++ src/types.ts | 25 ++++ 3 files changed, 291 insertions(+), 1 deletion(-) diff --git a/src/admin/pages/WechatPage.tsx b/src/admin/pages/WechatPage.tsx index 23a3625..95b4777 100644 --- a/src/admin/pages/WechatPage.tsx +++ b/src/admin/pages/WechatPage.tsx @@ -1,24 +1,31 @@ -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; 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'; @@ -70,6 +77,49 @@ 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([]); @@ -91,12 +141,27 @@ export function WechatPage() { 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); @@ -109,6 +174,9 @@ export function WechatPage() { nextDeliveries, nextNotifications, nextUsers, + nextLlmKeys, + nextIntentConfig, + nextIntentRuntime, ] = await Promise.all([ getWechatAdminSummary(), @@ -118,6 +186,9 @@ export function WechatPage() { 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); @@ -128,6 +199,13 @@ export function WechatPage() { 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 { @@ -235,6 +313,36 @@ export function WechatPage() { }); }; + 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 (
@@ -316,6 +424,141 @@ export function WechatPage() { )} + {intentForm ? ( +
+
+
+

微信 LLM 意图路由

+

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

+
+ +
+
+ + + + + + +