@@ -401,6 +460,131 @@ export function MemoryV2Page() {
)}
+
+
{BACKENDS.map((backend) => {
const section = draft[backend.key];
const currentSection = current[backend.key];
diff --git a/src/admin/pages/SystemTestsPage.tsx b/src/admin/pages/SystemTestsPage.tsx
new file mode 100644
index 0000000..3725ecd
--- /dev/null
+++ b/src/admin/pages/SystemTestsPage.tsx
@@ -0,0 +1,421 @@
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import {
+ createSystemTestAccount,
+ deleteSystemTestAccount,
+ listSkillCatalog,
+ listSystemTestAccounts,
+ runSystemSkillValidation,
+} from '../../api/client';
+import type { AdminSystemTestAccount, AdminSystemTestReport, SkillDefinition } from '../../types';
+import { formatTime } from '../utils/format';
+
+const DEFAULT_SKILL = 'service-integration-smoke';
+
+function statusLabel(status: 'passed' | 'warning' | 'failed') {
+ if (status === 'passed') return '通过';
+ if (status === 'warning') return '待处理';
+ return '失败';
+}
+
+function AddAccountModal({
+ onClose,
+ onSave,
+}: {
+ onClose: () => void;
+ onSave: (payload: { label: string; username: string; password: string }) => Promise
;
+}) {
+ const [label, setLabel] = useState('');
+ const [username, setUsername] = useState('');
+ const [password, setPassword] = useState('');
+ const [saving, setSaving] = useState(false);
+ const [error, setError] = useState(null);
+
+ const handleSubmit = async (event: React.FormEvent) => {
+ event.preventDefault();
+ const normalizedUsername = username.trim();
+ const normalizedLabel = label.trim() || normalizedUsername;
+ if (!normalizedUsername || !password) return;
+ setSaving(true);
+ setError(null);
+ try {
+ await onSave({
+ label: normalizedLabel,
+ username: normalizedUsername,
+ password,
+ });
+ } catch (err) {
+ setError(err instanceof Error ? err.message : '保存测试账号失败');
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ return (
+ event.target === event.currentTarget && onClose()}>
+
+
+
+
添加测试账号
+
保存到后台配置,后续管理员都可以直接复用。
+
+
+
+ {error &&
{error}
}
+
+
+
+ );
+}
+
+export function SystemTestsPage() {
+ const [catalog, setCatalog] = useState([]);
+ const [skillName, setSkillName] = useState(DEFAULT_SKILL);
+ const [accounts, setAccounts] = useState([]);
+ const [selectedAccountId, setSelectedAccountId] = useState('');
+ const [loading, setLoading] = useState(true);
+ const [running, setRunning] = useState(false);
+ const [accountsLoading, setAccountsLoading] = useState(true);
+ const [accountBusyId, setAccountBusyId] = useState(null);
+ const [showAddAccount, setShowAddAccount] = useState(false);
+ const [error, setError] = useState(null);
+ const [report, setReport] = useState(null);
+
+ const loadCatalog = useCallback(async () => {
+ setLoading(true);
+ setError(null);
+ try {
+ const nextCatalog = await listSkillCatalog();
+ setCatalog(nextCatalog);
+ if (nextCatalog.some((item) => item.name === DEFAULT_SKILL)) {
+ setSkillName(DEFAULT_SKILL);
+ } else if (nextCatalog[0]?.name) {
+ setSkillName(nextCatalog[0].name);
+ }
+ } catch (err) {
+ setError(err instanceof Error ? err.message : '加载 skill 列表失败');
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ void loadCatalog();
+ }, [loadCatalog]);
+
+ const loadAccounts = useCallback(async () => {
+ setAccountsLoading(true);
+ try {
+ const nextAccounts = await listSystemTestAccounts();
+ setAccounts(nextAccounts);
+ setSelectedAccountId((current) =>
+ nextAccounts.some((item) => item.id === current) ? current : (nextAccounts[0]?.id ?? ''),
+ );
+ } catch (err) {
+ setError(err instanceof Error ? err.message : '加载测试账号失败');
+ } finally {
+ setAccountsLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ void loadAccounts();
+ }, [loadAccounts]);
+
+ const selectedSkill = useMemo(
+ () => catalog.find((item) => item.name === skillName) ?? null,
+ [catalog, skillName],
+ );
+
+ const selectedAccount = useMemo(
+ () => accounts.find((item) => item.id === selectedAccountId) ?? null,
+ [accounts, selectedAccountId],
+ );
+
+ const handleSaveAccount = async (payload: { label: string; username: string; password: string }) => {
+ const account = await createSystemTestAccount(payload);
+ setAccounts((current) => [account, ...current]);
+ setSelectedAccountId(account.id);
+ setShowAddAccount(false);
+ };
+
+ const handleRemoveAccount = async () => {
+ if (!selectedAccount) return;
+ setAccountBusyId(selectedAccount.id);
+ setError(null);
+ try {
+ await deleteSystemTestAccount(selectedAccount.id);
+ const nextAccounts = accounts.filter((item) => item.id !== selectedAccount.id);
+ setAccounts(nextAccounts);
+ setSelectedAccountId(nextAccounts[0]?.id ?? '');
+ } catch (err) {
+ setError(err instanceof Error ? err.message : '删除测试账号失败');
+ } finally {
+ setAccountBusyId(null);
+ }
+ };
+
+ const handleSubmit = async (event: React.FormEvent) => {
+ event.preventDefault();
+ if (!selectedAccount) {
+ setError('请先选择一个测试账号,或先添加账号');
+ return;
+ }
+ setRunning(true);
+ setError(null);
+ try {
+ const nextReport = await runSystemSkillValidation({
+ accountId: selectedAccount.id,
+ username: '',
+ password: '',
+ skillName,
+ });
+ setReport(nextReport);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : '系统测试执行失败');
+ } finally {
+ setRunning(false);
+ }
+ };
+
+ return (
+
+
+
系统测试验证
+
选择测试账号后,按选定 skill 执行整条服务联调,并汇总异常反馈。
+
+
+ {error &&
{error}
}
+
+
+
+
+
执行参数
+
默认推荐使用 service-integration-smoke,对真实账号跑登录、直连聊天、记忆读取和 skill 输出验证。
+
+
+ {loading ? (
+ 加载 skill 列表中…
+ ) : (
+
+ )}
+
+
+ {showAddAccount && (
+
setShowAddAccount(false)}
+ onSave={handleSaveAccount}
+ />
+ )}
+
+ {report && (
+ <>
+
+
+
+
验证摘要
+
+ 账号 @{report.account.username} · skill {report.selectedSkill} · Portal {report.portalBaseUrl}
+
+
+
+
+
+
总体结果
+
{report.ok ? '通过' : '有问题'}
+
+ 开始 {formatTime(report.startedAt)} / 完成 {formatTime(report.finishedAt)}
+
+
+
+
通过
+
{report.summary.passed}
+
+
+
待处理
+
{report.summary.warnings}
+
+
+
失败
+
{report.summary.failed}
+
+
+
+
+
+
+
步骤结果
+
+
+
+
+
+ | 步骤 |
+ 状态 |
+ 结果 |
+
+
+
+ {report.steps.map((step) => (
+
+ | {step.label} |
+ {statusLabel(step.status)} |
+
+ {step.message}
+ {step.details && (
+
+ 查看详情
+
+ {JSON.stringify(step.details, null, 2)}
+
+
+ )}
+ |
+
+ ))}
+
+
+
+
+
+
+
+
问题反馈
+
+ {report.issues.length === 0 ? (
+ 本轮未发现额外问题。
+ ) : (
+
+
+
+
+ | 级别 |
+ 步骤 |
+ 问题 |
+
+
+
+ {report.issues.map((issue, index) => (
+
+ | {issue.severity === 'error' ? '错误' : '提醒'} |
+ {issue.stepKey} |
+
+ {issue.title}
+ {issue.detail}
+ |
+
+ ))}
+
+
+
+ )}
+
+ >
+ )}
+
+ );
+}
diff --git a/src/api/client.ts b/src/api/client.ts
index 99b39ec..efbcf9d 100644
--- a/src/api/client.ts
+++ b/src/api/client.ts
@@ -2,6 +2,8 @@ import type {
AdminDashboardSummary,
AdminServiceRestartAction,
AdminServiceRestartResult,
+ AdminSystemTestReport,
+ AdminSystemTestAccount,
AdminSubscription,
AdminUserRow,
AuthStatus,
@@ -310,6 +312,41 @@ export async function listMemoryV2ModelOptions(): Promise<{
};
}
+export async function runSystemSkillValidation(payload: {
+ accountId?: string;
+ username: string;
+ password: string;
+ skillName: string;
+}): Promise {
+ return portalFetch('/admin-api/system-tests/skill-validation', {
+ method: 'POST',
+ body: JSON.stringify(payload),
+ });
+}
+
+export async function listSystemTestAccounts(): Promise {
+ const result = await portalFetch<{ accounts: AdminSystemTestAccount[] }>('/admin-api/system-tests/accounts');
+ return result.accounts ?? [];
+}
+
+export async function createSystemTestAccount(payload: {
+ label?: string;
+ username: string;
+ password: string;
+}): Promise {
+ const result = await portalFetch<{ account: AdminSystemTestAccount }>('/admin-api/system-tests/accounts', {
+ method: 'POST',
+ body: JSON.stringify(payload),
+ });
+ return result.account;
+}
+
+export async function deleteSystemTestAccount(accountId: string): Promise {
+ await portalFetch(`/admin-api/system-tests/accounts/${encodeURIComponent(accountId)}`, {
+ method: 'DELETE',
+ });
+}
+
export async function listWechatBindings(params?: {
search?: string;
limit?: number;
diff --git a/src/index.css b/src/index.css
index 5df3677..1377c83 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1649,6 +1649,30 @@ body,
margin-top: 4px;
}
+.system-test-account-panel {
+ display: grid;
+ gap: 10px;
+ padding: 14px;
+ border: 1px solid var(--color-border-input);
+ border-radius: var(--radius-md);
+ background: var(--color-bg-subtle, rgba(0, 0, 0, 0.02));
+}
+
+.system-test-account-select {
+ display: grid;
+ gap: 8px;
+}
+
+.system-test-account-actions {
+ display: flex;
+ gap: 10px;
+ flex-wrap: wrap;
+}
+
+.system-test-account-summary {
+ font-size: 13px;
+}
+
/* ── Plan catalog table extras ──────────────────────────────────── */
.plan-desc {
font-size: 11px;
diff --git a/src/types.ts b/src/types.ts
index 6fc31f5..26f8976 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -267,6 +267,7 @@ export type MemoryV2AdminConfig = {
vectorEnabled?: boolean;
failOpen?: boolean;
};
+ chatIntentRouter: MemoryV2AdminSection;
pgvector: MemoryV2AdminSection;
qdrant: MemoryV2AdminSection;
weaviate: MemoryV2AdminSection;
@@ -283,6 +284,53 @@ export type MemoryV2AdminConfigResponse = {
updatedBy: string | null;
};
+export type AdminSystemTestStepStatus = 'passed' | 'warning' | 'failed';
+
+export type AdminSystemTestStep = {
+ key: string;
+ label: string;
+ status: AdminSystemTestStepStatus;
+ message: string;
+ details?: Record | null;
+};
+
+export type AdminSystemTestIssue = {
+ stepKey: string;
+ severity: 'warning' | 'error';
+ title: string;
+ detail: string;
+};
+
+export type AdminSystemTestReport = {
+ ok: boolean;
+ startedAt: number;
+ finishedAt: number;
+ portalBaseUrl: string;
+ selectedSkill: string;
+ account: {
+ userId?: string;
+ username: string;
+ displayName?: string | null;
+ };
+ summary: {
+ passed: number;
+ warnings: number;
+ failed: number;
+ };
+ steps: AdminSystemTestStep[];
+ issues: AdminSystemTestIssue[];
+};
+
+export type AdminSystemTestAccount = {
+ id: string;
+ label: string;
+ username: string;
+ passwordMasked: string;
+ createdAt: number;
+ updatedAt: number;
+ updatedBy?: string | null;
+};
+
export type WechatBinding = {
userId: string;
username: string;