feat(admin-client): add manual experience creation in ExperiencePage
管理员现在可以在"系统经验管理"页面手动创建经验,而不仅限于审核系统自动生成的经验。 实现细节: - experience.api.ts: 新增 CreateExperienceDto 类型和 createExperience() API 方法 - 调用 POST /memory/experience 创建经验 - 创建后自动调用 POST /memory/experience/:id/approve 激活 - 管理员手动创建的经验无需额外审核流程 - sourceConversationId 标记为 'admin-manual' 以区分来源 - 默认置信度 80%(高于系统自动生成的 45%) - useExperience.ts: 新增 useCreateExperience mutation hook - 创建成功后自动刷新经验列表和统计数据 - ExperiencePage.tsx: 新增"新建经验"按钮和创建表单弹窗 - 表单字段:经验类型、适用场景、经验内容、相关移民类别(可选)、置信度 - 移民类别下拉:QMAS/GEP/IANG/TTPS/CIES/TechTAS - 表单验证:类型、场景、内容为必填 这与方案A(评估门控失败自动沉淀经验)互补: - 自动路径:Gate failure → PENDING experience → 管理员审核 → 激活 - 手动路径:管理员直接创建 → 自动激活(无需审核) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
04dbc61131
commit
cc4b7d50e3
|
|
@ -3,6 +3,7 @@ export {
|
||||||
useExperienceStatistics,
|
useExperienceStatistics,
|
||||||
useApproveExperience,
|
useApproveExperience,
|
||||||
useRejectExperience,
|
useRejectExperience,
|
||||||
|
useCreateExperience,
|
||||||
useRunEvolution,
|
useRunEvolution,
|
||||||
EXPERIENCE_QUERY_KEY,
|
EXPERIENCE_QUERY_KEY,
|
||||||
EXPERIENCE_STATS_KEY,
|
EXPERIENCE_STATS_KEY,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { message } from 'antd';
|
import { message } from 'antd';
|
||||||
import { experienceApi } from '../infrastructure/experience.api';
|
import { experienceApi, type CreateExperienceDto } from '../infrastructure/experience.api';
|
||||||
|
|
||||||
export const EXPERIENCE_QUERY_KEY = 'pending-experiences';
|
export const EXPERIENCE_QUERY_KEY = 'pending-experiences';
|
||||||
export const EXPERIENCE_STATS_KEY = 'experience-stats';
|
export const EXPERIENCE_STATS_KEY = 'experience-stats';
|
||||||
|
|
@ -48,6 +48,23 @@ export function useRejectExperience() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useCreateExperience() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (dto: CreateExperienceDto) =>
|
||||||
|
experienceApi.createExperience(dto),
|
||||||
|
onSuccess: () => {
|
||||||
|
message.success('经验已创建并自动激活');
|
||||||
|
queryClient.invalidateQueries({ queryKey: [EXPERIENCE_QUERY_KEY] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: [EXPERIENCE_STATS_KEY] });
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
message.error('创建经验失败');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function useRunEvolution() {
|
export function useRunEvolution() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,14 @@ export interface ExperienceStatistics {
|
||||||
byType: Record<string, number>;
|
byType: Record<string, number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CreateExperienceDto {
|
||||||
|
experienceType: string;
|
||||||
|
content: string;
|
||||||
|
scenario: string;
|
||||||
|
relatedCategory?: string;
|
||||||
|
confidence?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export const experienceApi = {
|
export const experienceApi = {
|
||||||
getPendingExperiences: async (type?: string): Promise<ExperienceListResponse> => {
|
getPendingExperiences: async (type?: string): Promise<ExperienceListResponse> => {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
|
|
@ -48,6 +56,23 @@ export const experienceApi = {
|
||||||
await api.post(`/memory/experience/${id}/reject`, { adminId });
|
await api.post(`/memory/experience/${id}/reject`, { adminId });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
createExperience: async (dto: CreateExperienceDto): Promise<Experience> => {
|
||||||
|
// 管理员手动创建:先创建(PENDING),再自动批准(APPROVED + isActive)
|
||||||
|
const createResponse = await api.post('/memory/experience', {
|
||||||
|
...dto,
|
||||||
|
sourceConversationId: 'admin-manual',
|
||||||
|
confidence: dto.confidence ?? 80,
|
||||||
|
});
|
||||||
|
const created = createResponse.data.data as Experience;
|
||||||
|
|
||||||
|
// 自动批准 — 管理员手动创建的经验无需再审核
|
||||||
|
await api.post(`/memory/experience/${created.id}/approve`, {
|
||||||
|
adminId: 'admin-manual',
|
||||||
|
});
|
||||||
|
|
||||||
|
return created;
|
||||||
|
},
|
||||||
|
|
||||||
runEvolution: async (hoursBack: number = 24, limit: number = 50): Promise<{
|
runEvolution: async (hoursBack: number = 24, limit: number = 50): Promise<{
|
||||||
conversationsAnalyzed: number;
|
conversationsAnalyzed: number;
|
||||||
experiencesExtracted: number;
|
experiencesExtracted: number;
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
export { experienceApi } from './experience.api';
|
export { experienceApi } from './experience.api';
|
||||||
export type { Experience, ExperienceListResponse, ExperienceStatistics } from './experience.api';
|
export type { Experience, ExperienceListResponse, ExperienceStatistics, CreateExperienceDto } from './experience.api';
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,16 @@ import {
|
||||||
Statistic,
|
Statistic,
|
||||||
Row,
|
Row,
|
||||||
Col,
|
Col,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
InputNumber,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
CheckOutlined,
|
CheckOutlined,
|
||||||
CloseOutlined,
|
CloseOutlined,
|
||||||
EyeOutlined,
|
EyeOutlined,
|
||||||
PlayCircleOutlined,
|
PlayCircleOutlined,
|
||||||
|
PlusOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { useAuthStore } from '../../../auth/application';
|
import { useAuthStore } from '../../../auth/application';
|
||||||
import {
|
import {
|
||||||
|
|
@ -25,11 +29,13 @@ import {
|
||||||
useExperienceStatistics,
|
useExperienceStatistics,
|
||||||
useApproveExperience,
|
useApproveExperience,
|
||||||
useRejectExperience,
|
useRejectExperience,
|
||||||
|
useCreateExperience,
|
||||||
useRunEvolution,
|
useRunEvolution,
|
||||||
} from '../../application';
|
} from '../../application';
|
||||||
import type { Experience } from '../../infrastructure';
|
import type { Experience } from '../../infrastructure';
|
||||||
|
|
||||||
const { Title, Text, Paragraph } = Typography;
|
const { Title, Text, Paragraph } = Typography;
|
||||||
|
const { TextArea } = Input;
|
||||||
|
|
||||||
const EXPERIENCE_TYPES = [
|
const EXPERIENCE_TYPES = [
|
||||||
{ value: 'COMMON_QUESTION', label: '常见问题' },
|
{ value: 'COMMON_QUESTION', label: '常见问题' },
|
||||||
|
|
@ -47,6 +53,8 @@ export function ExperiencePage() {
|
||||||
const [typeFilter, setTypeFilter] = useState<string>();
|
const [typeFilter, setTypeFilter] = useState<string>();
|
||||||
const [selectedExperience, setSelectedExperience] = useState<Experience | null>(null);
|
const [selectedExperience, setSelectedExperience] = useState<Experience | null>(null);
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||||
|
const [createForm] = Form.useForm();
|
||||||
const admin = useAuthStore((state) => state.admin);
|
const admin = useAuthStore((state) => state.admin);
|
||||||
|
|
||||||
const { data: pendingData, isLoading: pendingLoading } = usePendingExperiences(
|
const { data: pendingData, isLoading: pendingLoading } = usePendingExperiences(
|
||||||
|
|
@ -56,6 +64,7 @@ export function ExperiencePage() {
|
||||||
const { data: stats } = useExperienceStatistics();
|
const { data: stats } = useExperienceStatistics();
|
||||||
const approveMutation = useApproveExperience();
|
const approveMutation = useApproveExperience();
|
||||||
const rejectMutation = useRejectExperience();
|
const rejectMutation = useRejectExperience();
|
||||||
|
const createMutation = useCreateExperience();
|
||||||
const runEvolutionMutation = useRunEvolution();
|
const runEvolutionMutation = useRunEvolution();
|
||||||
|
|
||||||
const handleView = (exp: Experience) => {
|
const handleView = (exp: Experience) => {
|
||||||
|
|
@ -75,6 +84,20 @@ export function ExperiencePage() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCreate = async () => {
|
||||||
|
try {
|
||||||
|
const values = await createForm.validateFields();
|
||||||
|
createMutation.mutate(values, {
|
||||||
|
onSuccess: () => {
|
||||||
|
setIsCreateModalOpen(false);
|
||||||
|
createForm.resetFields();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// validation error — form will show inline errors
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getTypeLabel = (type: string) => {
|
const getTypeLabel = (type: string) => {
|
||||||
return EXPERIENCE_TYPES.find((t) => t.value === type)?.label || type;
|
return EXPERIENCE_TYPES.find((t) => t.value === type)?.label || type;
|
||||||
};
|
};
|
||||||
|
|
@ -181,14 +204,22 @@ export function ExperiencePage() {
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<div className="flex justify-between items-center mb-6">
|
<div className="flex justify-between items-center mb-6">
|
||||||
<Title level={4} className="mb-0">系统经验管理</Title>
|
<Title level={4} className="mb-0">系统经验管理</Title>
|
||||||
<Button
|
<Space>
|
||||||
type="primary"
|
<Button
|
||||||
icon={<PlayCircleOutlined />}
|
icon={<PlusOutlined />}
|
||||||
onClick={() => runEvolutionMutation.mutate({})}
|
onClick={() => setIsCreateModalOpen(true)}
|
||||||
loading={runEvolutionMutation.isPending}
|
>
|
||||||
>
|
新建经验
|
||||||
运行进化任务
|
</Button>
|
||||||
</Button>
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<PlayCircleOutlined />}
|
||||||
|
onClick={() => runEvolutionMutation.mutate({})}
|
||||||
|
loading={runEvolutionMutation.isPending}
|
||||||
|
>
|
||||||
|
运行进化任务
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 统计卡片 */}
|
{/* 统计卡片 */}
|
||||||
|
|
@ -346,6 +377,77 @@ export function ExperiencePage() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
{/* 新建经验弹窗 */}
|
||||||
|
<Modal
|
||||||
|
title="新建经验"
|
||||||
|
open={isCreateModalOpen}
|
||||||
|
onCancel={() => {
|
||||||
|
setIsCreateModalOpen(false);
|
||||||
|
createForm.resetFields();
|
||||||
|
}}
|
||||||
|
onOk={handleCreate}
|
||||||
|
okText="创建"
|
||||||
|
cancelText="取消"
|
||||||
|
confirmLoading={createMutation.isPending}
|
||||||
|
width={600}
|
||||||
|
>
|
||||||
|
<Form form={createForm} layout="vertical">
|
||||||
|
<Form.Item
|
||||||
|
name="experienceType"
|
||||||
|
label="经验类型"
|
||||||
|
rules={[{ required: true, message: '请选择经验类型' }]}
|
||||||
|
>
|
||||||
|
<Select placeholder="选择类型" options={EXPERIENCE_TYPES} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="scenario"
|
||||||
|
label="适用场景"
|
||||||
|
rules={[{ required: true, message: '请描述适用场景' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="例如:用户咨询QMAS签证费用时" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="content"
|
||||||
|
label="经验内容"
|
||||||
|
rules={[{ required: true, message: '请填写经验内容' }]}
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
rows={4}
|
||||||
|
placeholder="例如:回答费用问题时应引用官方数据并标注更新日期,避免给出过期信息"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item name="relatedCategory" label="相关移民类别">
|
||||||
|
<Select
|
||||||
|
placeholder="可选"
|
||||||
|
allowClear
|
||||||
|
options={[
|
||||||
|
{ value: 'QMAS', label: '优才计划 (QMAS)' },
|
||||||
|
{ value: 'GEP', label: '一般就业政策 (GEP)' },
|
||||||
|
{ value: 'IANG', label: '非本地毕业生 (IANG)' },
|
||||||
|
{ value: 'TTPS', label: '科技人才 (TTPS)' },
|
||||||
|
{ value: 'CIES', label: '资本投资者 (CIES)' },
|
||||||
|
{ value: 'TECHTAS', label: '人才清单 (TechTAS)' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="confidence"
|
||||||
|
label="置信度"
|
||||||
|
initialValue={80}
|
||||||
|
>
|
||||||
|
<InputNumber min={0} max={100} addonAfter="%" style={{ width: '100%' }} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Text type="secondary">
|
||||||
|
管理员手动创建的经验将自动激活,无需额外审核。
|
||||||
|
</Text>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue