Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4c33f210da | |||
| 1af5b1dd82 | |||
| 9933bfb89c |
@@ -16,6 +16,7 @@ import type {
|
||||
MemoryV2RuntimeStatusResponse,
|
||||
PersonalMemoryCandidateListResponse,
|
||||
} from '../../types';
|
||||
import { isMemoryV2SectionEnabled } from './memory-v2-config';
|
||||
|
||||
type BackendKey =
|
||||
| 'pgvector'
|
||||
@@ -923,7 +924,8 @@ export function MemoryV2Page() {
|
||||
{CAPABILITIES.map((capability, index) => {
|
||||
const section = draft[capability.key];
|
||||
const currentSection = current[capability.key];
|
||||
const enabled = Boolean(section.enabled);
|
||||
const enabled = isMemoryV2SectionEnabled(capability.key, section);
|
||||
const savedEnabled = isMemoryV2SectionEnabled(capability.key, currentSection);
|
||||
const accent = BACKEND_ACCENTS[index % BACKEND_ACCENTS.length];
|
||||
return (
|
||||
<section
|
||||
@@ -1015,8 +1017,8 @@ export function MemoryV2Page() {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<span className={`asset-status ${Boolean(currentSection.enabled) ? 'is-on' : ''}`}>
|
||||
当前保存:{Boolean(currentSection.enabled) ? '启用' : '关闭'}
|
||||
<span className={`asset-status ${savedEnabled ? 'is-on' : ''}`}>
|
||||
当前保存:{savedEnabled ? '启用' : '关闭'}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -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<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)';
|
||||
}
|
||||
|
||||
export function WechatPage() {
|
||||
const [summary, setSummary] = useState<WechatAdminSummary | null>(null);
|
||||
const [bindings, setBindings] = useState<WechatBinding[]>([]);
|
||||
@@ -91,12 +141,27 @@ export function WechatPage() {
|
||||
const [notifyType, setNotifyType] = useState('manual');
|
||||
const [notifyChannels, setNotifyChannels] = useState<Array<'web' | 'wechat'>>(['web']);
|
||||
const [scheduleLlmEnabled, setScheduleLlmEnabled] = useState(false);
|
||||
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 [loading, setLoading] = useState(true);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [notice, setNotice] = useState<string | null>(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 (
|
||||
<div className="admin-page">
|
||||
<div className="admin-page-head">
|
||||
@@ -316,6 +424,141 @@ export function WechatPage() {
|
||||
</section>
|
||||
)}
|
||||
|
||||
{intentForm ? (
|
||||
<section className="admin-card">
|
||||
<div className="admin-card-head">
|
||||
<div>
|
||||
<h2>微信 LLM 意图路由</h2>
|
||||
<p className="muted" style={{ marginTop: 6 }}>
|
||||
独立于 H5 Router。仅对规则判为 chat.general 的消息做 LLM 二次判定(page.generate vs chat.general)。
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="ghost-btn"
|
||||
onClick={() => void handleSaveIntentRouter()}
|
||||
disabled={busy || intentSaving || !intentDirty}
|
||||
>
|
||||
{intentSaving ? '保存中…' : intentDirty ? '保存配置' : '已保存'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="admin-form" style={{ marginTop: 16 }}>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={intentForm.enabled}
|
||||
disabled={busy || intentSaving}
|
||||
onChange={(event) =>
|
||||
setIntentForm((current) => current && { ...current, enabled: event.target.checked })
|
||||
}
|
||||
/>{' '}
|
||||
启用 LLM 意图层
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={intentForm.shadowMode}
|
||||
disabled={busy || intentSaving || !intentForm.enabled}
|
||||
onChange={(event) =>
|
||||
setIntentForm((current) => current && { ...current, shadowMode: event.target.checked })
|
||||
}
|
||||
/>{' '}
|
||||
Shadow 模式(只观测不改行为)
|
||||
</label>
|
||||
<label>
|
||||
LLM Provider Key
|
||||
<select
|
||||
value={intentForm.modelProviderKeyId}
|
||||
disabled={busy || intentSaving}
|
||||
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>
|
||||
</label>
|
||||
<label>
|
||||
模型
|
||||
<input
|
||||
list="wechat-intent-models"
|
||||
value={intentForm.model}
|
||||
disabled={busy || intentSaving}
|
||||
placeholder={selectedIntentKey?.defaultModel || 'deepseek-v4-pro'}
|
||||
onChange={(event) =>
|
||||
setIntentForm((current) => current && { ...current, model: event.target.value })
|
||||
}
|
||||
/>
|
||||
<datalist id="wechat-intent-models">
|
||||
{(selectedIntentKey?.models ?? []).map((model) => (
|
||||
<option key={model} value={model} />
|
||||
))}
|
||||
</datalist>
|
||||
</label>
|
||||
<label>
|
||||
最低置信度
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.05}
|
||||
value={intentForm.minConfidence}
|
||||
disabled={busy || intentSaving}
|
||||
onChange={(event) =>
|
||||
setIntentForm((current) => current && { ...current, minConfidence: event.target.value })
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
超时 (ms)
|
||||
<input
|
||||
type="number"
|
||||
min={500}
|
||||
max={30000}
|
||||
step={100}
|
||||
value={intentForm.timeoutMs}
|
||||
disabled={busy || intentSaving}
|
||||
onChange={(event) =>
|
||||
setIntentForm((current) => current && { ...current, timeoutMs: event.target.value })
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Canary OpenID(每行或逗号分隔,留空=全员)
|
||||
<textarea
|
||||
value={intentForm.canaryOpenids}
|
||||
disabled={busy || intentSaving}
|
||||
rows={3}
|
||||
placeholder="oXXXX..."
|
||||
onChange={(event) =>
|
||||
setIntentForm((current) => current && { ...current, canaryOpenids: event.target.value })
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{intentRuntime ? (
|
||||
<p className="muted" style={{ marginTop: 12 }}>
|
||||
运行时:{intentRouterRuntimeMode(intentRuntime.overrides)} · 来源 {intentRuntime.source}
|
||||
{intentRuntime.updatedAt ? ` · 更新 ${dateLabel(intentRuntime.updatedAt)}` : ''}
|
||||
</p>
|
||||
) : null}
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<section className="admin-card">
|
||||
<div className="admin-card-head">
|
||||
<h2>通知平台</h2>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { MemoryV2AdminSection } from '../../types';
|
||||
|
||||
type CapabilityKey =
|
||||
| 'candidateMemory'
|
||||
| 'runtimeControl'
|
||||
| 'policy'
|
||||
| 'retriever'
|
||||
| 'lifecycle'
|
||||
| 'persona'
|
||||
| 'graph'
|
||||
| 'userMemory'
|
||||
| 'pluginHealth';
|
||||
|
||||
function modeEnabled(value: unknown) {
|
||||
const mode = String(value ?? 'off').trim().toLowerCase();
|
||||
return mode !== '' && mode !== 'off';
|
||||
}
|
||||
|
||||
export function isMemoryV2SectionEnabled(
|
||||
sectionKey: CapabilityKey,
|
||||
section: MemoryV2AdminSection | undefined,
|
||||
): boolean {
|
||||
const config = section ?? {};
|
||||
if (sectionKey === 'runtimeControl') {
|
||||
return Boolean(config.agentResolveEnabled)
|
||||
|| modeEnabled(config.agentInjectionMode)
|
||||
|| Boolean(config.promotionEnabled)
|
||||
|| Boolean(config.compactionV2Enabled)
|
||||
|| Boolean(config.reflectionEnabled)
|
||||
|| Boolean(config.lifecycleWorkerEnabled)
|
||||
|| modeEnabled(config.lifecycleRolloutMode);
|
||||
}
|
||||
return Boolean(config.enabled);
|
||||
}
|
||||
@@ -51,6 +51,9 @@ import type {
|
||||
WechatDeliveryLog,
|
||||
WechatDigestSubscription,
|
||||
WechatScheduleLlmConfig,
|
||||
WechatIntentRouterAdminConfig,
|
||||
WechatIntentRouterConfigState,
|
||||
WechatIntentRouterRuntimeState,
|
||||
WechatMessage,
|
||||
WechatWebNotification,
|
||||
MindSearchConfig,
|
||||
@@ -446,6 +449,25 @@ export async function updateWechatScheduleLlmConfig(
|
||||
});
|
||||
}
|
||||
|
||||
export async function getWechatIntentRouterConfig(): Promise<WechatIntentRouterAdminConfig> {
|
||||
const result = await portalFetch<WechatIntentRouterConfigState>('/admin-api/wechat/intent-router/config');
|
||||
return result.config;
|
||||
}
|
||||
|
||||
export async function patchWechatIntentRouterConfig(
|
||||
patch: Partial<Omit<WechatIntentRouterAdminConfig, 'updatedAt' | 'updatedBy'>>,
|
||||
): Promise<WechatIntentRouterAdminConfig> {
|
||||
const result = await portalFetch<WechatIntentRouterConfigState>('/admin-api/wechat/intent-router/config', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(patch),
|
||||
});
|
||||
return result.config;
|
||||
}
|
||||
|
||||
export async function getWechatIntentRouterRuntime(): Promise<WechatIntentRouterRuntimeState> {
|
||||
return portalFetch<WechatIntentRouterRuntimeState>('/admin-api/wechat/intent-router/runtime');
|
||||
}
|
||||
|
||||
export async function getAssetGatewayConfig(): Promise<AssetGatewayConfig> {
|
||||
return portalFetch('/admin-api/asset-gateway/config');
|
||||
}
|
||||
|
||||
@@ -317,6 +317,31 @@ export type WechatScheduleLlmConfig = {
|
||||
updatedBy: string | null;
|
||||
};
|
||||
|
||||
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 type MindSpaceAdminConfig = {
|
||||
publicPageLimit: number;
|
||||
analytics: {
|
||||
|
||||
Reference in New Issue
Block a user