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
+10
View File
@@ -168,6 +168,16 @@ H5_PUBLIC_BASE_URL=http://127.0.0.1:5173
# MEMIND_CHAT_ROUTER_MODEL_PROVIDER_KEY_ID=<h5_llm_provider_keys.id> # MEMIND_CHAT_ROUTER_MODEL_PROVIDER_KEY_ID=<h5_llm_provider_keys.id>
# MEMIND_CHAT_ROUTER_MODEL=deepseek-v4-pro # MEMIND_CHAT_ROUTER_MODEL=deepseek-v4-pro
# 微信服务号专用 LLM 意图层(独立于 H5 chatIntentRoutermemindadm 可插拔)
# 默认关闭;Shadow=1 时只记日志不改行为;canary 留空=全员
# MEMIND_WECHAT_INTENT_LLM_ENABLED=0
# MEMIND_WECHAT_INTENT_LLM_SHADOW=1
# MEMIND_WECHAT_INTENT_MODEL_PROVIDER_KEY_ID=
# MEMIND_WECHAT_INTENT_MODEL=
# MEMIND_WECHAT_INTENT_MIN_CONFIDENCE=0.65
# MEMIND_WECHAT_INTENT_TIMEOUT_MS=4000
# MEMIND_WECHAT_INTENT_CANARY_OPENIDS=
# H5 Session stream replaydocs/h5-session-architecture-20260706.md Patch 4c # H5 Session stream replaydocs/h5-session-architecture-20260706.md Patch 4c
# 默认 0session SSE 仍纯透传 goosed;设 1 时 Portal 持久化 session 事件并支持 Last-Event-ID 重连补发。 # 默认 0session SSE 仍纯透传 goosed;设 1 时 Portal 持久化 session 事件并支持 Last-Event-ID 重连补发。
# MEMIND_SESSION_STREAM_REPLAY=0 # MEMIND_SESSION_STREAM_REPLAY=0
+3
View File
@@ -28,6 +28,7 @@ import { createSystemDisclosurePolicyService } from './system-disclosure-policy.
import { createAgentCodeRunAdminConfigService } from './agent-code-run-admin-config.mjs'; import { createAgentCodeRunAdminConfigService } from './agent-code-run-admin-config.mjs';
import { createMindSearchConfigService } from './mindsearch-config.mjs'; import { createMindSearchConfigService } from './mindsearch-config.mjs';
import { createWechatScheduleLlmConfigService } from './wechat-schedule-llm-config.mjs'; import { createWechatScheduleLlmConfigService } from './wechat-schedule-llm-config.mjs';
import { createWechatIntentRouterConfigService } from './wechat-intent-router-config.mjs';
import { createPlazaPostService, formatPostRow } from './plaza-posts.mjs'; import { createPlazaPostService, formatPostRow } from './plaza-posts.mjs';
import { createPlazaInteractionService } from './plaza-interactions.mjs'; import { createPlazaInteractionService } from './plaza-interactions.mjs';
import { createPlazaOpsService } from './plaza-ops.mjs'; import { createPlazaOpsService } from './plaza-ops.mjs';
@@ -131,6 +132,7 @@ export async function createAdminServices(env = {}) {
await systemDisclosurePolicyService.initialize(); await systemDisclosurePolicyService.initialize();
const agentCodeRunPolicyService = createAgentCodeRunAdminConfigService(pool, { env: process.env }); const agentCodeRunPolicyService = createAgentCodeRunAdminConfigService(pool, { env: process.env });
const wechatScheduleLlmConfigService = createWechatScheduleLlmConfigService(pool); const wechatScheduleLlmConfigService = createWechatScheduleLlmConfigService(pool);
const wechatIntentRouterConfigService = createWechatIntentRouterConfigService(pool);
const adminSystemTestService = createAdminSystemTestService({ const adminSystemTestService = createAdminSystemTestService({
pool, pool,
userAuth, userAuth,
@@ -178,6 +180,7 @@ export async function createAdminServices(env = {}) {
systemDisclosurePolicyService, systemDisclosurePolicyService,
agentCodeRunPolicyService, agentCodeRunPolicyService,
wechatScheduleLlmConfigService, wechatScheduleLlmConfigService,
wechatIntentRouterConfigService,
adminSystemTestService, adminSystemTestService,
plazaPosts, plazaPosts,
plazaOps, plazaOps,
+30
View File
@@ -42,6 +42,7 @@ function plazaRouteError(res, req, error) {
* @param {object|null} deps.plazaPosts * @param {object|null} deps.plazaPosts
* @param {object|null} deps.plazaOps * @param {object|null} deps.plazaOps
* @param {object|null} deps.wechatAdmin * @param {object|null} deps.wechatAdmin
* @param {object|null} deps.wechatIntentRouterConfigService
*/ */
export function createAdminApi({ export function createAdminApi({
jsonBody, jsonBody,
@@ -64,6 +65,7 @@ export function createAdminApi({
plazaPosts, plazaPosts,
plazaOps, plazaOps,
wechatAdmin, wechatAdmin,
wechatIntentRouterConfigService,
subscriptionService, subscriptionService,
}) { }) {
function requireAdmin(req, res, next) { function requireAdmin(req, res, next) {
@@ -838,6 +840,34 @@ export function createAdminApi({
res.json(result); res.json(result);
}); });
adminApi.get('/wechat/intent-router/config', requireAdmin, async (_req, res) => {
if (!wechatIntentRouterConfigService?.getConfig) {
return res.status(503).json({ message: '微信意图路由配置未启用' });
}
const config = await wechatIntentRouterConfigService.getConfig();
return res.json({ config });
});
adminApi.get('/wechat/intent-router/runtime', requireAdmin, async (_req, res) => {
if (!wechatIntentRouterConfigService?.getRuntimeState) {
return res.status(503).json({ message: '微信意图路由配置未启用' });
}
return res.json(await wechatIntentRouterConfigService.getRuntimeState());
});
const updateWechatIntentRouterConfig = async (req, res) => {
if (!wechatIntentRouterConfigService?.updateConfig) {
return res.status(503).json({ message: '微信意图路由配置未启用' });
}
const config = await wechatIntentRouterConfigService.updateConfig(req.body ?? {}, {
updatedBy: req.currentUser.id,
});
return res.json({ config });
};
adminApi.put('/wechat/intent-router/config', requireAdmin, updateWechatIntentRouterConfig);
adminApi.patch('/wechat/intent-router/config', requireAdmin, updateWechatIntentRouterConfig);
adminApi.get('/llm-providers/catalog', requireAdmin, (_req, res) => { adminApi.get('/llm-providers/catalog', requireAdmin, (_req, res) => {
if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' }); if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' });
res.json({ catalog: llmProviderService.catalog }); res.json({ catalog: llmProviderService.catalog });
+91
View File
@@ -702,3 +702,94 @@ test('admin system test route executes shared validation service', async () => {
await server.close(); await server.close();
} }
}); });
test('admin wechat intent router config routes', async () => {
const updates = [];
const router = createAdminApi({
jsonBody: express.json(),
getToken() {
return 'token-admin';
},
userAuth: {
async getMe(token) {
if (token !== 'token-admin') return null;
return { id: 'admin-1', role: 'admin' };
},
},
wechatIntentRouterConfigService: {
async getConfig() {
return {
enabled: true,
shadowMode: true,
modelProviderKeyId: 'key-1',
model: 'deepseek-v4-pro',
minConfidence: 0.65,
timeoutMs: 4000,
canaryOpenids: ['openid-a'],
updatedAt: 123,
updatedBy: 'admin-1',
};
},
async updateConfig(patch, { updatedBy }) {
updates.push({ patch, updatedBy });
return {
enabled: false,
shadowMode: true,
modelProviderKeyId: null,
model: null,
minConfidence: 0.65,
timeoutMs: 4000,
canaryOpenids: [],
updatedAt: 456,
updatedBy,
};
},
async getRuntimeState() {
return {
source: 'admin-db',
updatedAt: 123,
updatedBy: 'admin-1',
fingerprint: 'fp-1',
overrides: { MEMIND_WECHAT_INTENT_LLM_ENABLED: '1' },
config: { enabled: true, shadowMode: true },
};
},
},
plazaPosts: null,
plazaOps: null,
wechatAdmin: null,
subscriptionService: null,
});
const server = await startTestServer(router);
try {
const configRes = await fetch(`${server.baseUrl}/admin-api/wechat/intent-router/config`, {
headers: { cookie: 'h5_user_session=token-admin' },
});
assert.equal(configRes.status, 200);
const configBody = await configRes.json();
assert.equal(configBody.config.enabled, true);
assert.equal(configBody.config.model, 'deepseek-v4-pro');
const updateRes = await fetch(`${server.baseUrl}/admin-api/wechat/intent-router/config`, {
method: 'PATCH',
headers: {
'content-type': 'application/json',
cookie: 'h5_user_session=token-admin',
},
body: JSON.stringify({ enabled: false }),
});
assert.equal(updateRes.status, 200);
assert.deepEqual(updates, [{ patch: { enabled: false }, updatedBy: 'admin-1' }]);
const runtimeRes = await fetch(`${server.baseUrl}/admin-api/wechat/intent-router/runtime`, {
headers: { cookie: 'h5_user_session=token-admin' },
});
assert.equal(runtimeRes.status, 200);
assert.deepEqual((await runtimeRes.json()).overrides, {
MEMIND_WECHAT_INTENT_LLM_ENABLED: '1',
});
} finally {
await server.close();
}
});
+1
View File
@@ -91,6 +91,7 @@ const CONSOLES = {
plazaPosts: services.plazaPosts, plazaPosts: services.plazaPosts,
plazaOps: services.plazaOps, plazaOps: services.plazaOps,
wechatAdmin: services.wechatAdmin, wechatAdmin: services.wechatAdmin,
wechatIntentRouterConfigService: services.wechatIntentRouterConfigService,
subscriptionService: services.subscriptionService, subscriptionService: services.subscriptionService,
}), }),
}, },
+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 ─────────────────────────────────────────────────────────── // ─── Agent Code Run ───────────────────────────────────────────────────────────
export type AgentCodeRunConfigShape = { 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 { import {
cancelWechatDigest, cancelWechatDigest,
createWechatWebNotification, createWechatWebNotification,
clearWechatRoute, clearWechatRoute,
fetchAdminUsers, fetchAdminUsers,
fetchLlmProviderKeys,
fetchWechatBindings, fetchWechatBindings,
fetchWechatDeliveries, fetchWechatDeliveries,
fetchWechatDigests, fetchWechatDigests,
fetchWechatIntentRouterConfig,
fetchWechatIntentRouterRuntime,
fetchWechatMessages, fetchWechatMessages,
fetchWechatSummary, fetchWechatSummary,
fetchWechatWebNotifications, fetchWechatWebNotifications,
patchWechatIntentRouterConfig,
resumeWechatDigest, resumeWechatDigest,
type AdminUser, type AdminUser,
type LlmProviderKeyRow,
type WechatAdminSummary, type WechatAdminSummary,
type WechatBinding, type WechatBinding,
type WechatDeliveryLog, type WechatDeliveryLog,
type WechatDigestSubscription, type WechatDigestSubscription,
type WechatIntentRouterAdminConfig,
type WechatIntentRouterRuntimeState,
type WechatMessage, type WechatMessage,
type WechatWebNotification, type WechatWebNotification,
} from '../../api/admin'; } from '../../api/admin';
@@ -44,6 +51,49 @@ function userName(row: { displayName?: string | null; username?: string | null }
return row.displayName || row.username || '—'; 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 = { const tableStyle = {
width: '100%', width: '100%',
borderCollapse: 'collapse', borderCollapse: 'collapse',
@@ -88,6 +138,21 @@ export function WechatPage() {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
const [notice, setNotice] = useState<string | null>(null); 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 () => { const load = async () => {
setError(null); setError(null);
@@ -100,6 +165,9 @@ export function WechatPage() {
deliveryResult, deliveryResult,
notificationResult, notificationResult,
userResult, userResult,
llmKeyResult,
intentConfigResult,
intentRuntimeResult,
] = ] =
await Promise.all([ await Promise.all([
fetchWechatSummary(), fetchWechatSummary(),
@@ -109,6 +177,9 @@ export function WechatPage() {
fetchWechatDeliveries({ status: deliveryStatus || undefined, limit: 80 }), fetchWechatDeliveries({ status: deliveryStatus || undefined, limit: 80 }),
fetchWechatWebNotifications({ status: notificationStatus || undefined, limit: 80 }), fetchWechatWebNotifications({ status: notificationStatus || undefined, limit: 80 }),
fetchAdminUsers({ page: 1, pageSize: 200, status: 'active' }), fetchAdminUsers({ page: 1, pageSize: 200, status: 'active' }),
fetchLlmProviderKeys().catch(() => ({ keys: [] as LlmProviderKeyRow[] })),
fetchWechatIntentRouterConfig().catch(() => null),
fetchWechatIntentRouterRuntime().catch(() => null),
]); ]);
setSummary(summaryResult); setSummary(summaryResult);
setBindings(bindingResult.bindings); setBindings(bindingResult.bindings);
@@ -120,6 +191,13 @@ export function WechatPage() {
setNotifyUserId((current) => setNotifyUserId((current) =>
current || userResult.users[0]?.id || '', 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) { } catch (err) {
setError(err instanceof Error ? err.message : '加载失败'); 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>; if (!summary && !error) return <p>...</p>;
return ( return (
@@ -260,6 +369,143 @@ export function WechatPage() {
</div> </div>
) : null} ) : 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"> <section className="card grid">
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap' }}> <div style={{ display: 'flex', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap' }}>
<h3 style={{ margin: 0 }}></h3> <h3 style={{ margin: 0 }}></h3>
+4
View File
@@ -280,6 +280,7 @@ let skillRuntimeConfigService = null;
let systemDisclosurePolicyService = null; let systemDisclosurePolicyService = null;
let agentCodeRunPolicyService = null; let agentCodeRunPolicyService = null;
let wechatScheduleLlmConfigService = null; let wechatScheduleLlmConfigService = null;
let wechatIntentRouter = null;
let mindSpace = null; let mindSpace = null;
let mindSpaceAssets = null; let mindSpaceAssets = null;
let mindSpaceAudit = null; let mindSpaceAudit = null;
@@ -471,6 +472,8 @@ async function bootstrapUserAuth() {
memorySessionServices.agentCodeRunPolicyService; memorySessionServices.agentCodeRunPolicyService;
wechatScheduleLlmConfigService = wechatScheduleLlmConfigService =
memorySessionServices.wechatScheduleLlmConfigService; memorySessionServices.wechatScheduleLlmConfigService;
wechatIntentRouter =
memorySessionServices.wechatIntentRouter;
conversationMemoryService = conversationMemoryService =
memorySessionServices.conversationMemoryService; memorySessionServices.conversationMemoryService;
memoryV2 = memorySessionServices.memoryV2; memoryV2 = memorySessionServices.memoryV2;
@@ -530,6 +533,7 @@ async function bootstrapUserAuth() {
scheduledTaskService, scheduledTaskService,
sessionSnapshotService, sessionSnapshotService,
wechatScheduleLlmConfigService, wechatScheduleLlmConfigService,
wechatIntentRouter,
llmProviderService, llmProviderService,
chatIntentRouter, chatIntentRouter,
systemDisclosurePolicyService, systemDisclosurePolicyService,
@@ -33,6 +33,7 @@ export async function bootstrapPortalIntegrationServices({
scheduledTaskService = null, scheduledTaskService = null,
sessionSnapshotService = null, sessionSnapshotService = null,
wechatScheduleLlmConfigService, wechatScheduleLlmConfigService,
wechatIntentRouter = null,
llmProviderService, llmProviderService,
chatIntentRouter, chatIntentRouter,
systemDisclosurePolicyService, systemDisclosurePolicyService,
@@ -165,6 +166,7 @@ export async function bootstrapPortalIntegrationServices({
wechatScheduleLlmConfigService, wechatScheduleLlmConfigService,
llmProviderService, llmProviderService,
chatIntentRouter, chatIntentRouter,
wechatIntentRouter,
systemDisclosurePolicyService, systemDisclosurePolicyService,
sessionIntentClassifier: ({ text }) => sessionIntentClassifier: ({ text }) =>
chatIntentRouter?.classifySessionAction({ chatIntentRouter?.classifySessionAction({
@@ -11,6 +11,8 @@ import { isSessionStreamReplayEnabled } from '../session-stream.mjs';
import { createSkillRuntimeAdminConfigService } from '../skill-runtime-admin-config.mjs'; import { createSkillRuntimeAdminConfigService } from '../skill-runtime-admin-config.mjs';
import { createSystemDisclosurePolicyService } from '../system-disclosure-policy.mjs'; import { createSystemDisclosurePolicyService } from '../system-disclosure-policy.mjs';
import { createWechatScheduleLlmConfigService } from '../wechat-schedule-llm-config.mjs'; import { createWechatScheduleLlmConfigService } from '../wechat-schedule-llm-config.mjs';
import { createWechatIntentRouterConfigService } from '../wechat-intent-router-config.mjs';
import { createManagedWechatIntentRouter } from '../wechat-intent-router.mjs';
export async function bootstrapPortalMemorySessionServices({ export async function bootstrapPortalMemorySessionServices({
pool, pool,
@@ -30,6 +32,10 @@ export async function bootstrapPortalMemorySessionServices({
createAgentCodeRunAdminConfigService, createAgentCodeRunAdminConfigService,
createWechatScheduleLlmConfigServiceFn = createWechatScheduleLlmConfigServiceFn =
createWechatScheduleLlmConfigService, createWechatScheduleLlmConfigService,
createWechatIntentRouterConfigServiceFn =
createWechatIntentRouterConfigService,
createManagedWechatIntentRouterFn =
createManagedWechatIntentRouter,
createConversationMemoryServiceFn = createConversationMemoryServiceFn =
createConversationMemoryService, createConversationMemoryService,
createManagedMemoryV2RuntimeFn = createManagedMemoryV2RuntimeFn =
@@ -71,6 +77,15 @@ export async function bootstrapPortalMemorySessionServices({
createAgentCodeRunAdminConfigServiceFn(pool, { env }); createAgentCodeRunAdminConfigServiceFn(pool, { env });
const wechatScheduleLlmConfigService = const wechatScheduleLlmConfigService =
createWechatScheduleLlmConfigServiceFn(pool); createWechatScheduleLlmConfigServiceFn(pool);
const wechatIntentRouterConfigService =
createWechatIntentRouterConfigServiceFn(pool, { env });
const wechatIntentRouter =
createManagedWechatIntentRouterFn({
llmProviderService,
configService: wechatIntentRouterConfigService,
env,
logger,
});
const getEffectiveEnv = async () => { const getEffectiveEnv = async () => {
const state = await memoryV2ConfigService const state = await memoryV2ConfigService
@@ -140,6 +155,8 @@ export async function bootstrapPortalMemorySessionServices({
systemDisclosurePolicyService, systemDisclosurePolicyService,
agentCodeRunPolicyService, agentCodeRunPolicyService,
wechatScheduleLlmConfigService, wechatScheduleLlmConfigService,
wechatIntentRouterConfigService,
wechatIntentRouter,
conversationMemoryService, conversationMemoryService,
memoryV2, memoryV2,
episodicMemoryService, episodicMemoryService,
+175
View File
@@ -0,0 +1,175 @@
const CONFIG_TABLE = 'h5_wechat_admin_config';
const CONFIG_KEY = 'intent_router';
function normalizeBoolean(value, fallback = false) {
if (value == null || value === '') return fallback;
if (typeof value === 'boolean') return value;
const normalized = String(value).trim().toLowerCase();
if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
return fallback;
}
function normalizeString(value) {
if (value == null) return '';
return String(value).trim();
}
function normalizeNumber(value, fallback) {
if (value == null || value === '') return fallback;
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : fallback;
}
function parseConfigJson(value) {
if (!value) return {};
if (typeof value === 'object') return value;
try {
return JSON.parse(String(value));
} catch {
return {};
}
}
function normalizeOpenidList(value) {
return [...new Set(String(value ?? '')
.split(/[\s,]+/u)
.map((item) => item.trim())
.filter(Boolean))].slice(0, 1000);
}
async function ensureConfigTable(pool) {
await pool.query(`
CREATE TABLE IF NOT EXISTS ${CONFIG_TABLE} (
config_key VARCHAR(64) PRIMARY KEY,
config_json JSON NOT NULL,
updated_by CHAR(36) NULL,
updated_at BIGINT NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`);
}
export function defaultsFromEnv(env = process.env) {
return {
enabled: normalizeBoolean(env.MEMIND_WECHAT_INTENT_LLM_ENABLED, false),
shadowMode: normalizeBoolean(env.MEMIND_WECHAT_INTENT_LLM_SHADOW, true),
modelProviderKeyId: normalizeString(env.MEMIND_WECHAT_INTENT_MODEL_PROVIDER_KEY_ID),
model: normalizeString(env.MEMIND_WECHAT_INTENT_MODEL),
minConfidence: normalizeNumber(env.MEMIND_WECHAT_INTENT_MIN_CONFIDENCE, 0.65),
timeoutMs: normalizeNumber(env.MEMIND_WECHAT_INTENT_TIMEOUT_MS, 4000),
canaryOpenids: normalizeOpenidList(env.MEMIND_WECHAT_INTENT_CANARY_OPENIDS),
};
}
export function createWechatIntentRouterConfigService(pool, { env = process.env } = {}) {
let ensurePromise = null;
async function ensureReady() {
if (!ensurePromise) ensurePromise = ensureConfigTable(pool);
await ensurePromise;
}
async function readRow() {
await ensureReady();
const [rows] = await pool.query(
`SELECT config_json, updated_by, updated_at
FROM ${CONFIG_TABLE}
WHERE config_key = ?
LIMIT 1`,
[CONFIG_KEY],
);
return rows[0] ?? null;
}
function mergeConfig(row) {
const defaults = defaultsFromEnv(env);
const stored = parseConfigJson(row?.config_json);
return {
enabled: normalizeBoolean(stored.enabled, defaults.enabled),
shadowMode: normalizeBoolean(stored.shadowMode, defaults.shadowMode),
modelProviderKeyId: normalizeString(stored.modelProviderKeyId) || defaults.modelProviderKeyId || null,
model: normalizeString(stored.model) || defaults.model || null,
minConfidence: normalizeNumber(stored.minConfidence, defaults.minConfidence),
timeoutMs: normalizeNumber(stored.timeoutMs, defaults.timeoutMs),
canaryOpenids: stored.canaryOpenids != null
? normalizeOpenidList(stored.canaryOpenids)
: defaults.canaryOpenids,
};
}
return {
async getConfig() {
const row = await readRow();
return {
...mergeConfig(row),
updatedAt: row?.updated_at ? Number(row.updated_at) : null,
updatedBy: row?.updated_by ?? null,
};
},
async getRuntimeState() {
const config = await this.getConfig();
const overrides = {
MEMIND_WECHAT_INTENT_LLM_ENABLED: config.enabled ? '1' : '0',
MEMIND_WECHAT_INTENT_LLM_SHADOW: config.shadowMode ? '1' : '0',
MEMIND_WECHAT_INTENT_MODEL_PROVIDER_KEY_ID: config.modelProviderKeyId ?? '',
MEMIND_WECHAT_INTENT_MODEL: config.model ?? '',
MEMIND_WECHAT_INTENT_MIN_CONFIDENCE: String(config.minConfidence),
MEMIND_WECHAT_INTENT_TIMEOUT_MS: String(config.timeoutMs),
MEMIND_WECHAT_INTENT_CANARY_OPENIDS: config.canaryOpenids.join(','),
};
return {
source: 'admin-db',
updatedAt: config.updatedAt,
updatedBy: config.updatedBy,
fingerprint: JSON.stringify(config),
overrides,
config,
};
},
async updateConfig(payload = {}, { updatedBy = null } = {}) {
const current = await this.getConfig();
const next = {
enabled: payload.enabled === undefined
? current.enabled
: normalizeBoolean(payload.enabled, current.enabled),
shadowMode: payload.shadowMode === undefined
? current.shadowMode
: normalizeBoolean(payload.shadowMode, current.shadowMode),
modelProviderKeyId: payload.modelProviderKeyId === undefined
? (current.modelProviderKeyId ?? '')
: normalizeString(payload.modelProviderKeyId),
model: payload.model === undefined
? (current.model ?? '')
: normalizeString(payload.model),
minConfidence: payload.minConfidence === undefined
? current.minConfidence
: normalizeNumber(payload.minConfidence, current.minConfidence),
timeoutMs: payload.timeoutMs === undefined
? current.timeoutMs
: normalizeNumber(payload.timeoutMs, current.timeoutMs),
canaryOpenids: payload.canaryOpenids === undefined
? current.canaryOpenids
: normalizeOpenidList(payload.canaryOpenids),
};
const now = Date.now();
await pool.query(
`INSERT INTO ${CONFIG_TABLE} (config_key, config_json, updated_by, updated_at)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
config_json = VALUES(config_json),
updated_by = VALUES(updated_by),
updated_at = VALUES(updated_at)`,
[CONFIG_KEY, JSON.stringify(next), updatedBy, now],
);
return this.getConfig();
},
};
}
export const wechatIntentRouterConfigInternals = {
normalizeBoolean,
normalizeOpenidList,
defaultsFromEnv,
};
+345
View File
@@ -0,0 +1,345 @@
import { wantsDocxDownload } from './wechat/intent/patterns.mjs';
import { defaultsFromEnv } from './wechat-intent-router-config.mjs';
export const WECHAT_LLM_REFINABLE_KINDS = new Set(['chat.general']);
const DEFAULT_MIN_CONFIDENCE = 0.65;
const DEFAULT_TIMEOUT_MS = 4000;
function envFlag(value, fallback = false) {
const raw = String(value ?? '').trim().toLowerCase();
if (!raw) return fallback;
return ['1', 'true', 'yes', 'on'].includes(raw);
}
function boundedNumber(value, fallback, { min = 0, max = Number.POSITIVE_INFINITY } = {}) {
if (value == null || value === '') return fallback;
const parsed = Number(value);
if (!Number.isFinite(parsed)) return fallback;
return Math.min(max, Math.max(min, parsed));
}
function normalizeOpenidList(value) {
return [...new Set(String(value ?? '')
.split(/[\s,]+/u)
.map((item) => item.trim())
.filter(Boolean))].slice(0, 1000);
}
export function resolveWechatIntentRouterPolicy(env = process.env, overrides = {}) {
const merged = { ...env, ...overrides };
return {
enabled: envFlag(merged.MEMIND_WECHAT_INTENT_LLM_ENABLED, false),
shadowMode: envFlag(merged.MEMIND_WECHAT_INTENT_LLM_SHADOW, true),
modelProviderKeyId: String(merged.MEMIND_WECHAT_INTENT_MODEL_PROVIDER_KEY_ID ?? '').trim() || null,
model: String(merged.MEMIND_WECHAT_INTENT_MODEL ?? '').trim() || null,
minConfidence: boundedNumber(
merged.MEMIND_WECHAT_INTENT_MIN_CONFIDENCE,
DEFAULT_MIN_CONFIDENCE,
{ min: 0, max: 1 },
),
timeoutMs: Math.round(boundedNumber(
merged.MEMIND_WECHAT_INTENT_TIMEOUT_MS,
DEFAULT_TIMEOUT_MS,
{ min: 500, max: 30_000 },
)),
canaryOpenids: normalizeOpenidList(merged.MEMIND_WECHAT_INTENT_CANARY_OPENIDS),
};
}
export function isWechatIntentLlmEligible({ policy, openid = null } = {}) {
if (!policy?.enabled) return false;
if (!Array.isArray(policy.canaryOpenids) || policy.canaryOpenids.length === 0) return true;
const normalized = String(openid ?? '').trim();
return normalized.length > 0 && policy.canaryOpenids.includes(normalized);
}
export function shouldRefineWechatIntent(ruleIntent, text = '') {
const kind = String(ruleIntent?.kind ?? '').trim();
if (!WECHAT_LLM_REFINABLE_KINDS.has(kind)) return false;
return String(text ?? '').trim().length >= 2;
}
function buildWechatIntentSystemPrompt() {
return [
'你是微信服务号意图分类器,只判断用户消息更适合哪种处理通道。',
'只输出 JSON,不要 markdown',
'{"kind":"page.generate|chat.general","confidence":0.0,"reason":"一句话"}',
'page.generate:用户希望生成/整理/排版 HTML 页面、图文、推文、专题、报告页,或明确要把内容做成可发布/可转发的页面。',
'chat.general:普通问答、闲聊、解释、建议、查询,不需要产出 HTML 页面。',
'如果用户只是口头提到“页面/文章”但没有生成或交付页面意图,仍判 chat.general。',
].join('\n');
}
function buildWechatIntentUserPrompt(text) {
return ['[User]', String(text ?? '').trim() || '(empty)'].join('\n');
}
export function parseWechatIntentJson(reply) {
const text = String(reply ?? '').trim();
if (!text) return null;
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
const candidate = (fenced?.[1] ?? text).trim();
try {
return JSON.parse(candidate);
} catch {
const start = candidate.indexOf('{');
const end = candidate.lastIndexOf('}');
if (start < 0 || end <= start) return null;
try {
return JSON.parse(candidate.slice(start, end + 1));
} catch {
return null;
}
}
}
export function normalizeWechatLlmIntent(raw) {
const kindRaw = String(raw?.kind ?? '').trim().toLowerCase();
const kind = kindRaw === 'page.generate' ? 'page.generate' : 'chat.general';
const confidenceRaw = Number(raw?.confidence);
const confidence = Number.isFinite(confidenceRaw)
? Math.min(1, Math.max(0, confidenceRaw))
: 0.7;
const reason = String(raw?.reason ?? '').trim() || '微信 LLM 意图判定';
return { kind, confidence, reason, source: 'llm' };
}
async function withTimeout(promise, timeoutMs, label) {
if (!timeoutMs || timeoutMs <= 0) return promise;
let timer = null;
try {
return await Promise.race([
promise,
new Promise((_, reject) => {
timer = setTimeout(() => {
const err = new Error(`${label} timed out after ${timeoutMs}ms`);
err.code = 'WECHAT_INTENT_ROUTER_TIMEOUT';
reject(err);
}, timeoutMs);
timer.unref?.();
}),
]);
} finally {
if (timer) clearTimeout(timer);
}
}
export function createWechatIntentRouter({
llmProviderService,
env = process.env,
logger = console,
policy: policyOverride = null,
} = {}) {
const policy = policyOverride ?? resolveWechatIntentRouterPolicy(env);
async function classifyWithLlm(text) {
if (typeof llmProviderService?.createChatCompletion !== 'function') return null;
try {
const completion = await withTimeout(
llmProviderService.createChatCompletion({
providerKeyId: policy.modelProviderKeyId || undefined,
model: policy.model || undefined,
temperature: 0,
messages: [
{ role: 'system', content: buildWechatIntentSystemPrompt() },
{ role: 'user', content: buildWechatIntentUserPrompt(text) },
],
}),
policy.timeoutMs,
'WeChat intent router',
);
if (!completion?.ok) return null;
const parsed = parseWechatIntentJson(completion.reply);
if (!parsed) return null;
return normalizeWechatLlmIntent({
...parsed,
model: completion.model ?? policy.model,
providerKeyId: completion.providerKeyId ?? policy.modelProviderKeyId,
});
} catch (err) {
logger?.warn?.(
`[wechat-intent-router] LLM classify skipped: ${err instanceof Error ? err.message : err}`,
);
return null;
}
}
function getStatus() {
return {
enabled: policy.enabled,
shadowMode: policy.shadowMode,
llmRoutingEnabled: Boolean(policy.enabled && !policy.shadowMode),
llmRoutingShadow: Boolean(policy.enabled && policy.shadowMode),
modelProviderKeyId: policy.modelProviderKeyId,
model: policy.model,
minConfidence: policy.minConfidence,
timeoutMs: policy.timeoutMs,
canaryOpenidCount: policy.canaryOpenids.length,
};
}
async function refineIntent({
ruleIntent,
text = '',
openid = null,
} = {}) {
const baseline = ruleIntent ?? { kind: 'chat.general' };
if (!shouldRefineWechatIntent(baseline, text)) {
return { ...baseline, intentSource: 'rules' };
}
if (!isWechatIntentLlmEligible({ policy, openid })) {
return { ...baseline, intentSource: 'rules' };
}
const llmResult = await classifyWithLlm(text);
const shadowPayload = llmResult
? {
kind: llmResult.kind,
confidence: llmResult.confidence,
reason: llmResult.reason,
wouldChangeKind: llmResult.kind !== baseline.kind,
}
: {
skipped: true,
reason: 'llm_unavailable_or_low_signal',
wouldChangeKind: false,
};
if (policy.shadowMode) {
logger?.info?.('[wechat-intent-router-shadow]', {
openid: openid ?? null,
baselineKind: baseline.kind,
...shadowPayload,
});
return {
...baseline,
intentSource: 'rules',
llmShadow: shadowPayload,
};
}
if (!llmResult || llmResult.confidence < policy.minConfidence) {
return {
...baseline,
intentSource: 'rules',
llmSuggestion: llmResult,
};
}
if (llmResult.kind === baseline.kind) {
return {
...baseline,
intentSource: 'rules',
llmSuggestion: llmResult,
};
}
if (llmResult.kind === 'page.generate') {
return {
kind: 'page.generate',
topic: String(text ?? '').trim(),
wantsDocx: wantsDocxDownload(text),
intentSource: 'llm',
llmReason: llmResult.reason,
llmConfidence: llmResult.confidence,
sessionActionResult: baseline.sessionActionResult,
};
}
return {
kind: 'chat.general',
text: String(text ?? '').trim(),
intentSource: 'llm',
llmReason: llmResult.reason,
llmConfidence: llmResult.confidence,
sessionActionResult: baseline.sessionActionResult,
};
}
return {
getStatus,
refineIntent,
};
}
export function createManagedWechatIntentRouter({
llmProviderService,
configService = null,
env = process.env,
logger = console,
} = {}) {
let activeRouter = null;
let activeFingerprint = null;
let activeMeta = { source: 'env', updatedAt: null, updatedBy: null, configError: null };
async function loadRouterState() {
if (!configService?.getRuntimeState) {
return {
source: 'env',
updatedAt: null,
updatedBy: null,
fingerprint: 'env-only',
effectiveEnv: env,
configError: null,
};
}
try {
const state = await configService.getRuntimeState();
return {
source: state?.source ?? 'admin-db',
updatedAt: state?.updatedAt ?? null,
updatedBy: state?.updatedBy ?? null,
fingerprint: state?.fingerprint ?? `admin-db:${Date.now()}`,
effectiveEnv: { ...env, ...(state?.overrides ?? {}) },
configError: null,
};
} catch (err) {
logger?.warn?.(
`[wechat-intent-router] admin config unavailable, using process env: ${err instanceof Error ? err.message : err}`,
);
return {
source: 'env-fallback',
updatedAt: null,
updatedBy: null,
fingerprint: 'env-fallback',
effectiveEnv: { ...env },
configError: err instanceof Error ? err.message : String(err),
};
}
}
async function ensureRouter() {
const state = await loadRouterState();
if (activeRouter && activeFingerprint === state.fingerprint) {
activeMeta = state;
return activeRouter;
}
activeRouter = createWechatIntentRouter({
llmProviderService,
env: state.effectiveEnv,
logger,
policy: resolveWechatIntentRouterPolicy(state.effectiveEnv),
});
activeFingerprint = state.fingerprint;
activeMeta = state;
return activeRouter;
}
return {
async getStatus() {
const router = await ensureRouter();
return {
...router.getStatus(),
configSource: activeMeta.source,
configUpdatedAt: activeMeta.updatedAt,
configUpdatedBy: activeMeta.updatedBy,
configError: activeMeta.configError,
};
},
async refineIntent(input = {}) {
const router = await ensureRouter();
return router.refineIntent(input);
},
};
}
+155
View File
@@ -0,0 +1,155 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
createWechatIntentRouter,
isWechatIntentLlmEligible,
normalizeWechatLlmIntent,
parseWechatIntentJson,
resolveWechatIntentRouterPolicy,
shouldRefineWechatIntent,
} from './wechat-intent-router.mjs';
import {
createWechatIntentRouterConfigService,
defaultsFromEnv,
} from './wechat-intent-router-config.mjs';
test('resolveWechatIntentRouterPolicy reads env defaults', () => {
const policy = resolveWechatIntentRouterPolicy({
MEMIND_WECHAT_INTENT_LLM_ENABLED: '1',
MEMIND_WECHAT_INTENT_LLM_SHADOW: '0',
MEMIND_WECHAT_INTENT_MODEL: 'deepseek-v4-pro',
MEMIND_WECHAT_INTENT_MIN_CONFIDENCE: '0.7',
MEMIND_WECHAT_INTENT_TIMEOUT_MS: '5000',
MEMIND_WECHAT_INTENT_CANARY_OPENIDS: 'openid-a, openid-b',
});
assert.equal(policy.enabled, true);
assert.equal(policy.shadowMode, false);
assert.equal(policy.model, 'deepseek-v4-pro');
assert.equal(policy.minConfidence, 0.7);
assert.equal(policy.timeoutMs, 5000);
assert.deepEqual(policy.canaryOpenids, ['openid-a', 'openid-b']);
});
test('shouldRefineWechatIntent only refines chat.general', () => {
assert.equal(shouldRefineWechatIntent({ kind: 'chat.general' }, '整理成一篇给读者看的文章'), true);
assert.equal(shouldRefineWechatIntent({ kind: 'page.generate' }, '生成 HTML 页面'), false);
assert.equal(shouldRefineWechatIntent({ kind: 'greeting' }, '你好'), false);
});
test('isWechatIntentLlmEligible respects canary openids', () => {
const policy = { enabled: true, canaryOpenids: ['openid-a'] };
assert.equal(isWechatIntentLlmEligible({ policy, openid: 'openid-a' }), true);
assert.equal(isWechatIntentLlmEligible({ policy, openid: 'openid-b' }), false);
assert.equal(isWechatIntentLlmEligible({ policy: { enabled: true, canaryOpenids: [] }, openid: 'x' }), true);
});
test('createWechatIntentRouter upgrades chat.general to page.generate in active mode', async () => {
const router = createWechatIntentRouter({
policy: resolveWechatIntentRouterPolicy({
MEMIND_WECHAT_INTENT_LLM_ENABLED: '1',
MEMIND_WECHAT_INTENT_LLM_SHADOW: '0',
MEMIND_WECHAT_INTENT_MIN_CONFIDENCE: '0.6',
}),
llmProviderService: {
async createChatCompletion() {
return {
ok: true,
reply: JSON.stringify({
kind: 'page.generate',
confidence: 0.92,
reason: '用户希望整理成可发布页面',
}),
model: 'deepseek-v4-pro',
};
},
},
});
const result = await router.refineIntent({
ruleIntent: { kind: 'chat.general', text: '帮我整理成一篇给读者看的专题页' },
text: '帮我整理成一篇给读者看的专题页',
openid: 'openid-a',
});
assert.equal(result.kind, 'page.generate');
assert.equal(result.intentSource, 'llm');
assert.equal(result.wantsDocx, false);
});
test('createWechatIntentRouter keeps baseline in shadow mode', async () => {
const router = createWechatIntentRouter({
policy: resolveWechatIntentRouterPolicy({
MEMIND_WECHAT_INTENT_LLM_ENABLED: '1',
MEMIND_WECHAT_INTENT_LLM_SHADOW: '1',
}),
llmProviderService: {
async createChatCompletion() {
return {
ok: true,
reply: JSON.stringify({
kind: 'page.generate',
confidence: 0.95,
reason: 'shadow',
}),
};
},
},
});
const result = await router.refineIntent({
ruleIntent: { kind: 'chat.general', text: '整理成文章' },
text: '整理成文章',
});
assert.equal(result.kind, 'chat.general');
assert.equal(result.intentSource, 'rules');
assert.equal(result.llmShadow?.wouldChangeKind, true);
});
test('wechat intent router config service persists admin toggles', async () => {
const rows = new Map();
const pool = {
async query(sql, params = []) {
if (String(sql).includes('CREATE TABLE')) return [[]];
if (String(sql).includes('SELECT config_json')) {
const row = rows.get(params[0]);
return [row ? [row] : []];
}
if (String(sql).includes('INSERT INTO')) {
rows.set(params[0], {
config_json: params[1],
updated_by: params[2],
updated_at: params[3],
});
return [{ affectedRows: 1 }];
}
throw new Error(`unexpected sql: ${sql}`);
},
};
const service = createWechatIntentRouterConfigService(pool, {
env: defaultsFromEnv({}),
});
const saved = await service.updateConfig({
enabled: true,
shadowMode: true,
model: 'deepseek-v4-flash',
canaryOpenids: 'openid-test',
}, { updatedBy: 'admin-1' });
assert.equal(saved.enabled, true);
assert.equal(saved.shadowMode, true);
assert.equal(saved.model, 'deepseek-v4-flash');
assert.deepEqual(saved.canaryOpenids, ['openid-test']);
const runtime = await service.getRuntimeState();
assert.equal(runtime.overrides.MEMIND_WECHAT_INTENT_LLM_ENABLED, '1');
assert.equal(runtime.overrides.MEMIND_WECHAT_INTENT_LLM_SHADOW, '1');
assert.equal(runtime.overrides.MEMIND_WECHAT_INTENT_MODEL, 'deepseek-v4-flash');
});
test('parseWechatIntentJson accepts fenced JSON', () => {
const parsed = parseWechatIntentJson('```json\n{"kind":"page.generate","confidence":0.8}\n```');
assert.equal(parsed.kind, 'page.generate');
assert.equal(normalizeWechatLlmIntent(parsed).kind, 'page.generate');
});
+23 -3
View File
@@ -1487,6 +1487,7 @@ export function createWechatMpService({
wechatScheduleLlmConfigService = null, wechatScheduleLlmConfigService = null,
llmProviderService = null, llmProviderService = null,
chatIntentRouter = null, chatIntentRouter = null,
wechatIntentRouter = null,
systemDisclosurePolicyService = null, systemDisclosurePolicyService = null,
onPageGenerated = null, onPageGenerated = null,
applySessionLlmProvider = null, applySessionLlmProvider = null,
@@ -2433,7 +2434,7 @@ export function createWechatMpService({
}; };
const runIntentMessage = async ({ inbound, intent, user }) => { const runIntentMessage = async ({ inbound, intent, user }) => {
const wechatIntent = await resolveWechatIntent(intent); const wechatIntent = await resolveWechatIntent(intent, { openid: inbound.fromUserName });
const mediaAnalysisEnabled = isWechatMediaGrayUser(user, config.mediaAnalysisGrayUsers); const mediaAnalysisEnabled = isWechatMediaGrayUser(user, config.mediaAnalysisGrayUsers);
const reliabilityEnabled = isWechatMediaGrayUser(user, config.reliabilityGrayUsers); const reliabilityEnabled = isWechatMediaGrayUser(user, config.reliabilityGrayUsers);
const agentReplyTimeoutMs = reliabilityEnabled ? config.agentReplyTimeoutMs : 0; const agentReplyTimeoutMs = reliabilityEnabled ? config.agentReplyTimeoutMs : 0;
@@ -3066,7 +3067,7 @@ export function createWechatMpService({
} }
}; };
const resolveWechatIntent = async (intent) => { const resolveWechatIntent = async (intent, { openid = null } = {}) => {
const wechatIntent = classifyWechatIntent(intent); const wechatIntent = classifyWechatIntent(intent);
if (intent.msgType !== 'text' && intent.msgType !== 'voice') return wechatIntent; if (intent.msgType !== 'text' && intent.msgType !== 'voice') return wechatIntent;
const sessionActionResult = await resolveWechatSessionAction(intent.agentText, { const sessionActionResult = await resolveWechatSessionAction(intent.agentText, {
@@ -3080,7 +3081,26 @@ export function createWechatMpService({
if (sessionActionResult.action === 'ignore_previous') { if (sessionActionResult.action === 'ignore_previous') {
intent.sessionAction = sessionActionResult.action; intent.sessionAction = sessionActionResult.action;
} }
return { ...wechatIntent, sessionActionResult }; const withSession = {
...wechatIntent,
sessionActionResult,
};
if (!wechatIntentRouter?.refineIntent) {
return withSession;
}
try {
return await wechatIntentRouter.refineIntent({
ruleIntent: withSession,
text: intent.agentText,
openid,
});
} catch (err) {
logger.warn?.(
'WeChat MP intent router refine skipped:',
err instanceof Error ? err.message : err,
);
return withSession;
}
}; };
const handleInboundMessage = async (bodyText, query = {}) => { const handleInboundMessage = async (bodyText, query = {}) => {