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,
|
||||
useApproveExperience,
|
||||
useRejectExperience,
|
||||
useCreateExperience,
|
||||
useRunEvolution,
|
||||
EXPERIENCE_QUERY_KEY,
|
||||
EXPERIENCE_STATS_KEY,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
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_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() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,14 @@ export interface ExperienceStatistics {
|
|||
byType: Record<string, number>;
|
||||
}
|
||||
|
||||
export interface CreateExperienceDto {
|
||||
experienceType: string;
|
||||
content: string;
|
||||
scenario: string;
|
||||
relatedCategory?: string;
|
||||
confidence?: number;
|
||||
}
|
||||
|
||||
export const experienceApi = {
|
||||
getPendingExperiences: async (type?: string): Promise<ExperienceListResponse> => {
|
||||
const params = new URLSearchParams();
|
||||
|
|
@ -48,6 +56,23 @@ export const experienceApi = {
|
|||
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<{
|
||||
conversationsAnalyzed: number;
|
||||
experiencesExtracted: number;
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
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,
|
||||
Row,
|
||||
Col,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
} from 'antd';
|
||||
import {
|
||||
CheckOutlined,
|
||||
CloseOutlined,
|
||||
EyeOutlined,
|
||||
PlayCircleOutlined,
|
||||
PlusOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useAuthStore } from '../../../auth/application';
|
||||
import {
|
||||
|
|
@ -25,11 +29,13 @@ import {
|
|||
useExperienceStatistics,
|
||||
useApproveExperience,
|
||||
useRejectExperience,
|
||||
useCreateExperience,
|
||||
useRunEvolution,
|
||||
} from '../../application';
|
||||
import type { Experience } from '../../infrastructure';
|
||||
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
const { TextArea } = Input;
|
||||
|
||||
const EXPERIENCE_TYPES = [
|
||||
{ value: 'COMMON_QUESTION', label: '常见问题' },
|
||||
|
|
@ -47,6 +53,8 @@ export function ExperiencePage() {
|
|||
const [typeFilter, setTypeFilter] = useState<string>();
|
||||
const [selectedExperience, setSelectedExperience] = useState<Experience | null>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||
const [createForm] = Form.useForm();
|
||||
const admin = useAuthStore((state) => state.admin);
|
||||
|
||||
const { data: pendingData, isLoading: pendingLoading } = usePendingExperiences(
|
||||
|
|
@ -56,6 +64,7 @@ export function ExperiencePage() {
|
|||
const { data: stats } = useExperienceStatistics();
|
||||
const approveMutation = useApproveExperience();
|
||||
const rejectMutation = useRejectExperience();
|
||||
const createMutation = useCreateExperience();
|
||||
const runEvolutionMutation = useRunEvolution();
|
||||
|
||||
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) => {
|
||||
return EXPERIENCE_TYPES.find((t) => t.value === type)?.label || type;
|
||||
};
|
||||
|
|
@ -181,14 +204,22 @@ export function ExperiencePage() {
|
|||
<div className="p-6">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<Title level={4} className="mb-0">系统经验管理</Title>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlayCircleOutlined />}
|
||||
onClick={() => runEvolutionMutation.mutate({})}
|
||||
loading={runEvolutionMutation.isPending}
|
||||
>
|
||||
运行进化任务
|
||||
</Button>
|
||||
<Space>
|
||||
<Button
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setIsCreateModalOpen(true)}
|
||||
>
|
||||
新建经验
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlayCircleOutlined />}
|
||||
onClick={() => runEvolutionMutation.mutate({})}
|
||||
loading={runEvolutionMutation.isPending}
|
||||
>
|
||||
运行进化任务
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
|
|
@ -346,6 +377,77 @@ export function ExperiencePage() {
|
|||
</div>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue