feat(wechat): add admin-pluggable LLM intent router separate from H5.
Memind CI / Test, build, and release guards (push) Failing after 8s

Give WeChat MP its own chat.general→page.generate LLM refinement layer with memindadm toggles, shadow mode, and canary openids so service account routing stays independent of the H5 chatIntentRouter.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-08-01 20:57:41 +08:00
parent cdc265d7e0
commit 91e140d402
14 changed files with 1145 additions and 4 deletions
+42
View File
@@ -738,6 +738,48 @@ export async function resumeWechatDigest(id: string) {
});
}
export type WechatIntentRouterAdminConfig = {
enabled: boolean;
shadowMode: boolean;
modelProviderKeyId: string | null;
model: string | null;
minConfidence: number;
timeoutMs: number;
canaryOpenids: string[];
updatedAt?: number | null;
updatedBy?: string | null;
};
export type WechatIntentRouterConfigState = {
config: WechatIntentRouterAdminConfig;
};
export type WechatIntentRouterRuntimeState = {
source: string;
updatedAt: number | null;
updatedBy: string | null;
fingerprint?: string;
overrides: Record<string, string>;
config: WechatIntentRouterAdminConfig;
};
export async function fetchWechatIntentRouterConfig() {
return adminFetch<WechatIntentRouterConfigState>('/admin-api/wechat/intent-router/config');
}
export async function patchWechatIntentRouterConfig(
patch: Partial<Omit<WechatIntentRouterAdminConfig, 'updatedAt' | 'updatedBy'>>,
) {
return adminFetch<WechatIntentRouterConfigState>('/admin-api/wechat/intent-router/config', {
method: 'PATCH',
body: JSON.stringify(patch),
});
}
export async function fetchWechatIntentRouterRuntime() {
return adminFetch<WechatIntentRouterRuntimeState>('/admin-api/wechat/intent-router/runtime');
}
// ─── Agent Code Run ───────────────────────────────────────────────────────────
export type AgentCodeRunConfigShape = {
+247 -1
View File
@@ -1,21 +1,28 @@
import { useEffect, useState, type CSSProperties, type ReactNode } from 'react';
import { useEffect, useMemo, useState, type CSSProperties, type ReactNode } from 'react';
import {
cancelWechatDigest,
createWechatWebNotification,
clearWechatRoute,
fetchAdminUsers,
fetchLlmProviderKeys,
fetchWechatBindings,
fetchWechatDeliveries,
fetchWechatDigests,
fetchWechatIntentRouterConfig,
fetchWechatIntentRouterRuntime,
fetchWechatMessages,
fetchWechatSummary,
fetchWechatWebNotifications,
patchWechatIntentRouterConfig,
resumeWechatDigest,
type AdminUser,
type LlmProviderKeyRow,
type WechatAdminSummary,
type WechatBinding,
type WechatDeliveryLog,
type WechatDigestSubscription,
type WechatIntentRouterAdminConfig,
type WechatIntentRouterRuntimeState,
type WechatMessage,
type WechatWebNotification,
} from '../../api/admin';
@@ -44,6 +51,49 @@ function userName(row: { displayName?: string | null; username?: string | null }
return row.displayName || row.username || '—';
}
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<string, string>) {
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';
}
const tableStyle = {
width: '100%',
borderCollapse: 'collapse',
@@ -88,6 +138,21 @@ export function WechatPage() {
const [error, setError] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
const [notice, setNotice] = useState<string | null>(null);
const [llmKeys, setLlmKeys] = useState<LlmProviderKeyRow[]>([]);
const [intentForm, setIntentForm] = useState<IntentRouterForm | null>(null);
const [savedIntentForm, setSavedIntentForm] = useState<IntentRouterForm | null>(null);
const [intentRuntime, setIntentRuntime] = useState<WechatIntentRouterRuntimeState | null>(null);
const [intentSaving, setIntentSaving] = useState(false);
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 = async () => {
setError(null);
@@ -100,6 +165,9 @@ export function WechatPage() {
deliveryResult,
notificationResult,
userResult,
llmKeyResult,
intentConfigResult,
intentRuntimeResult,
] =
await Promise.all([
fetchWechatSummary(),
@@ -109,6 +177,9 @@ export function WechatPage() {
fetchWechatDeliveries({ status: deliveryStatus || undefined, limit: 80 }),
fetchWechatWebNotifications({ status: notificationStatus || undefined, limit: 80 }),
fetchAdminUsers({ page: 1, pageSize: 200, status: 'active' }),
fetchLlmProviderKeys().catch(() => ({ keys: [] as LlmProviderKeyRow[] })),
fetchWechatIntentRouterConfig().catch(() => null),
fetchWechatIntentRouterRuntime().catch(() => null),
]);
setSummary(summaryResult);
setBindings(bindingResult.bindings);
@@ -120,6 +191,13 @@ export function WechatPage() {
setNotifyUserId((current) =>
current || userResult.users[0]?.id || '',
);
setLlmKeys(llmKeyResult.keys);
if (intentConfigResult?.config) {
const nextForm = intentRouterToForm(intentConfigResult.config, llmKeyResult.keys);
setIntentForm(nextForm);
setSavedIntentForm(nextForm);
}
setIntentRuntime(intentRuntimeResult);
} catch (err) {
setError(err instanceof Error ? err.message : '加载失败');
}
@@ -235,6 +313,37 @@ 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.config, llmKeys);
setIntentForm(nextForm);
setSavedIntentForm(nextForm);
const runtime = await fetchWechatIntentRouterRuntime().catch(() => null);
setIntentRuntime(runtime);
setNotice('微信 LLM 意图路由配置已保存。Portal 进程会在下次请求时自动热加载。');
} catch (err) {
setError(err instanceof Error ? err.message : '保存失败');
} finally {
setIntentSaving(false);
}
};
if (!summary && !error) return <p>...</p>;
return (
@@ -260,6 +369,143 @@ export function WechatPage() {
</div>
) : null}
{intentForm ? (
<section className="card grid">
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap' }}>
<div>
<h3 style={{ margin: 0 }}> LLM </h3>
<p style={{ margin: '6px 0 0', color: '#68716c', fontSize: 13 }}>
H5 Router chat.general LLM page.generate vs chat.general
</p>
</div>
<button
type="button"
className="btn"
onClick={() => void handleSaveIntentRouter()}
disabled={busy || intentSaving || !intentDirty}
>
{intentSaving ? '保存中…' : intentDirty ? '保存配置' : '已保存'}
</button>
</div>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))',
gap: 12,
}}
>
<Field label="启用 LLM 意图层">
<label style={{ display: 'flex', gap: 8, alignItems: 'center', minHeight: 38 }}>
<input
type="checkbox"
checked={intentForm.enabled}
onChange={(event) =>
setIntentForm((current) => current && { ...current, enabled: event.target.checked })
}
/>
<span>{intentForm.enabled ? '已启用' : '关闭'}</span>
</label>
</Field>
<Field label="Shadow 模式">
<label style={{ display: 'flex', gap: 8, alignItems: 'center', minHeight: 38 }}>
<input
type="checkbox"
checked={intentForm.shadowMode}
disabled={!intentForm.enabled}
onChange={(event) =>
setIntentForm((current) => current && { ...current, shadowMode: event.target.checked })
}
/>
<span>{intentForm.shadowMode ? '只观测不改行为' : 'Active 生效'}</span>
</label>
</Field>
<Field label="LLM Provider Key">
<select
value={intentForm.modelProviderKeyId}
onChange={(event) => {
const keyId = event.target.value;
const key = llmKeys.find((item) => item.id === keyId) ?? null;
setIntentForm((current) =>
current
? {
...current,
modelProviderKeyId: keyId,
model: key?.defaultModel || key?.models?.[0] || current.model,
}
: current,
);
}}
>
<option value="">使 Key</option>
{llmKeys.map((key) => (
<option key={key.id} value={key.id}>
{key.name} ({key.providerLabel})
</option>
))}
</select>
</Field>
<Field label="模型">
<input
list="wechat-intent-models"
value={intentForm.model}
onChange={(event) =>
setIntentForm((current) => current && { ...current, model: event.target.value })
}
placeholder={selectedIntentKey?.defaultModel || 'deepseek-v4-pro'}
/>
<datalist id="wechat-intent-models">
{(selectedIntentKey?.models ?? []).map((model) => (
<option key={model} value={model} />
))}
</datalist>
</Field>
<Field label="最低置信度">
<input
type="number"
min={0}
max={1}
step={0.05}
value={intentForm.minConfidence}
onChange={(event) =>
setIntentForm((current) => current && { ...current, minConfidence: event.target.value })
}
/>
</Field>
<Field label="超时 (ms)">
<input
type="number"
min={500}
max={30000}
step={100}
value={intentForm.timeoutMs}
onChange={(event) =>
setIntentForm((current) => current && { ...current, timeoutMs: event.target.value })
}
/>
</Field>
</div>
<Field label="Canary OpenID(每行或逗号分隔,留空=全员)">
<textarea
value={intentForm.canaryOpenids}
onChange={(event) =>
setIntentForm((current) => current && { ...current, canaryOpenids: event.target.value })
}
rows={3}
placeholder="oXXXX..."
/>
</Field>
{intentRuntime ? (
<p style={{ margin: 0, color: '#68716c', fontSize: 12 }}>
{intentRouterRuntimeMode(intentRuntime.overrides)} · {intentRuntime.source}
{intentRuntime.updatedAt ? ` · 更新 ${time(intentRuntime.updatedAt)}` : ''}
</p>
) : null}
</section>
) : null}
<section className="card grid">
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap' }}>
<h3 style={{ margin: 0 }}></h3>