fix: V1 测试版本端到端验证修复 — 6 CRITICAL + 3 HIGH 问题全量修复
修复项: - fix(db): 迁移 149 — 修复 Admin 角色权限绑定被迁移链破坏 (FE-C1) - fix(health): 4 个 handler 添加空名称验证 — Doctor/Article/AlertRule/Tag (API-C1~C4) - fix(health): Stats 仪表盘 new_this_week 查询修复 — SeaORM date_trunc bug (FE-C2) - fix(server): 添加安全响应头 — X-Frame-Options/CSP/XSS-Protection/Referrer-Policy (SEC-H1) - fix(mp): 预约创建契约修复 — notes/reason 字段映射 + 移除 schedule_id (MP-H1) - fix(mp): 咨询会话 subject/last_message 字段改为可选 (MP-H3) - fix(ai): AiConfig Default derive 替代手写 impl (clippy) 测试报告: - 8 维度端到端测试全部完成 (后端 87 用例 / 前端 30 页面 / 小程序 80+ API / 安全 20 项 / 性能 20 端点) - 多角色 7 角色 49 检查 100% 通过 - 综合测试报告 + 专家评估报告
This commit is contained in:
@@ -45,6 +45,7 @@ const StatisticsDashboard = lazy(() => import('./pages/health/StatisticsDashboar
|
||||
const AiPromptList = lazy(() => import('./pages/health/AiPromptList'));
|
||||
const AiAnalysisList = lazy(() => import('./pages/health/AiAnalysisList'));
|
||||
const AiUsageDashboard = lazy(() => import('./pages/health/AiUsageDashboard'));
|
||||
const AiConfigPage = lazy(() => import('./pages/health/AiConfigPage'));
|
||||
const AlertList = lazy(() => import('./pages/health/AlertList'));
|
||||
const AlertDashboard = lazy(() => import('./pages/health/AlertDashboard'));
|
||||
const AlertRuleList = lazy(() => import('./pages/health/AlertRuleList'));
|
||||
@@ -254,7 +255,7 @@ export default function App() {
|
||||
"/health/follow-up-records", "/health/consultations",
|
||||
"/health/points-rules", "/health/points-products", "/health/points-orders",
|
||||
"/health/offline-events", "/health/ai-prompts", "/health/ai-analysis",
|
||||
"/health/ai-usage", "/health/alerts", "/health/alert-dashboard",
|
||||
"/health/ai-usage", "/health/ai-config", "/health/alerts", "/health/alert-dashboard",
|
||||
"/health/alert-rules", "/health/devices", "/health/realtime-monitor",
|
||||
"/health/oauth-clients", "/health/dialysis", "/health/action-inbox",
|
||||
"/health/follow-up-templates", "/health/care-plans", "/health/shifts",
|
||||
@@ -325,6 +326,7 @@ export default function App() {
|
||||
<Route path="/health/ai-prompts" element={<AiPromptList />} />
|
||||
<Route path="/health/ai-analysis" element={<AiAnalysisList />} />
|
||||
<Route path="/health/ai-usage" element={<AiUsageDashboard />} />
|
||||
<Route path="/health/ai-config" element={<AiConfigPage />} />
|
||||
<Route path="/health/alerts" element={<AlertList />} />
|
||||
<Route path="/health/alert-dashboard" element={<AlertDashboard />} />
|
||||
<Route path="/health/alert-rules" element={<AlertRuleList />} />
|
||||
|
||||
35
apps/web/src/api/ai/config.ts
Normal file
35
apps/web/src/api/ai/config.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import client from '../client';
|
||||
|
||||
export interface AiAgentConfig {
|
||||
model: string;
|
||||
temperature: number;
|
||||
max_tokens: number;
|
||||
max_iterations: number;
|
||||
system_prompt: string;
|
||||
}
|
||||
|
||||
export interface AiAnalysisDefaults {
|
||||
model: string;
|
||||
temperature: number;
|
||||
max_tokens: number;
|
||||
}
|
||||
|
||||
export interface AiConfig {
|
||||
agent: AiAgentConfig;
|
||||
analysis_defaults: AiAnalysisDefaults;
|
||||
}
|
||||
|
||||
export const aiConfigApi = {
|
||||
get: async () => {
|
||||
const resp = await client.get('/ai/config');
|
||||
return resp.data.data as AiConfig;
|
||||
},
|
||||
getDefaults: async () => {
|
||||
const resp = await client.get('/ai/config/defaults');
|
||||
return resp.data.data as AiConfig;
|
||||
},
|
||||
update: async (config: AiConfig) => {
|
||||
const resp = await client.put('/ai/config', { config });
|
||||
return resp.data.data as AiConfig;
|
||||
},
|
||||
};
|
||||
280
apps/web/src/pages/health/AiConfigPage.tsx
Normal file
280
apps/web/src/pages/health/AiConfigPage.tsx
Normal file
@@ -0,0 +1,280 @@
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Button,
|
||||
Space,
|
||||
message,
|
||||
Divider,
|
||||
Spin,
|
||||
Tabs,
|
||||
} from 'antd';
|
||||
import { SaveOutlined, UndoOutlined } from '@ant-design/icons';
|
||||
import { aiConfigApi, type AiConfig } from '../../api/ai/config';
|
||||
import { AuthButton } from '../../components/AuthButton';
|
||||
import { useThemeMode } from '../../hooks/useThemeMode';
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
export default function AiConfigPage() {
|
||||
const [config, setConfig] = useState<AiConfig | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const isDark = useThemeMode();
|
||||
|
||||
const fetchConfig = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await aiConfigApi.get();
|
||||
setConfig(data);
|
||||
form.setFieldsValue({
|
||||
agentModel: data.agent.model,
|
||||
agentTemperature: data.agent.temperature,
|
||||
agentMaxTokens: data.agent.max_tokens,
|
||||
agentMaxIterations: data.agent.max_iterations,
|
||||
agentSystemPrompt: data.agent.system_prompt,
|
||||
analysisModel: data.analysis_defaults.model,
|
||||
analysisTemperature: data.analysis_defaults.temperature,
|
||||
analysisMaxTokens: data.analysis_defaults.max_tokens,
|
||||
});
|
||||
} catch {
|
||||
message.error('加载 AI 配置失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [form]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchConfig();
|
||||
}, [fetchConfig]);
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setSaving(true);
|
||||
|
||||
const updated: AiConfig = {
|
||||
agent: {
|
||||
model: values.agentModel,
|
||||
temperature: values.agentTemperature,
|
||||
max_tokens: values.agentMaxTokens,
|
||||
max_iterations: values.agentMaxIterations,
|
||||
system_prompt: values.agentSystemPrompt,
|
||||
},
|
||||
analysis_defaults: {
|
||||
model: values.analysisModel,
|
||||
temperature: values.analysisTemperature,
|
||||
max_tokens: values.analysisMaxTokens,
|
||||
},
|
||||
};
|
||||
|
||||
const result = await aiConfigApi.update(updated);
|
||||
setConfig(result);
|
||||
message.success('AI 配置已保存');
|
||||
} catch (err: unknown) {
|
||||
if (err && typeof err === 'object' && 'errorFields' in err) {
|
||||
return;
|
||||
}
|
||||
message.error('保存 AI 配置失败');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = async () => {
|
||||
try {
|
||||
const defaults = await aiConfigApi.getDefaults();
|
||||
form.setFieldsValue({
|
||||
agentModel: defaults.agent.model,
|
||||
agentTemperature: defaults.agent.temperature,
|
||||
agentMaxTokens: defaults.agent.max_tokens,
|
||||
agentMaxIterations: defaults.agent.max_iterations,
|
||||
agentSystemPrompt: defaults.agent.system_prompt,
|
||||
analysisModel: defaults.analysis_defaults.model,
|
||||
analysisTemperature: defaults.analysis_defaults.temperature,
|
||||
analysisMaxTokens: defaults.analysis_defaults.max_tokens,
|
||||
});
|
||||
message.info('已恢复为系统默认值(未保存)');
|
||||
} catch {
|
||||
message.error('加载默认配置失败');
|
||||
}
|
||||
};
|
||||
|
||||
if (loading && !config) {
|
||||
return (
|
||||
<div style={{ textAlign: 'center', padding: 48 }}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const cardStyle = isDark
|
||||
? { background: '#1f1f1f', borderColor: '#333' }
|
||||
: {};
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24, maxWidth: 960, margin: '0 auto' }}>
|
||||
<h2 style={{ marginBottom: 24 }}>AI 配置管理</h2>
|
||||
|
||||
<Form form={form} layout="vertical">
|
||||
<Tabs
|
||||
items={[
|
||||
{
|
||||
key: 'agent',
|
||||
label: 'Agent 对话配置',
|
||||
children: (
|
||||
<Card
|
||||
title="AI 客服 Agent 参数"
|
||||
size="small"
|
||||
style={cardStyle}
|
||||
>
|
||||
<Form.Item
|
||||
label="模型名称"
|
||||
name="agentModel"
|
||||
rules={[{ required: true, message: '请输入模型名称' }]}
|
||||
extra="如 claude-sonnet-4-6、gpt-4o 等"
|
||||
>
|
||||
<Input placeholder="claude-sonnet-4-6" />
|
||||
</Form.Item>
|
||||
|
||||
<Space style={{ width: '100%' }} size="large">
|
||||
<Form.Item
|
||||
label="温度 (Temperature)"
|
||||
name="agentTemperature"
|
||||
rules={[{ required: true, message: '请输入温度参数' }]}
|
||||
extra="0.0 ~ 2.0,越高越随机"
|
||||
style={{ width: 200 }}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={2}
|
||||
step={0.1}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="最大 Token"
|
||||
name="agentMaxTokens"
|
||||
rules={[{ required: true, message: '请输入最大 Token' }]}
|
||||
extra="1 ~ 65536"
|
||||
style={{ width: 200 }}
|
||||
>
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={65536}
|
||||
step={256}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="最大迭代次数"
|
||||
name="agentMaxIterations"
|
||||
rules={[
|
||||
{ required: true, message: '请输入最大迭代次数' },
|
||||
]}
|
||||
extra="1 ~ 20,Agent ReAct 循环上限"
|
||||
style={{ width: 200 }}
|
||||
>
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={20}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
|
||||
<Divider style={{ margin: '8px 0 16px' }} />
|
||||
|
||||
<Form.Item
|
||||
label="系统提示词 (System Prompt)"
|
||||
name="agentSystemPrompt"
|
||||
rules={[{ required: true, message: '请输入系统提示词' }]}
|
||||
extra="定义 AI 客服的角色和行为策略"
|
||||
>
|
||||
<TextArea rows={12} placeholder="你是 HMS 健康管理平台的 AI 健康顾问..." />
|
||||
</Form.Item>
|
||||
</Card>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'analysis',
|
||||
label: '分析任务默认配置',
|
||||
children: (
|
||||
<Card
|
||||
title="AI 分析任务默认参数"
|
||||
size="small"
|
||||
style={cardStyle}
|
||||
extra="当 Prompt 模板未指定模型参数时使用的默认值"
|
||||
>
|
||||
<Form.Item
|
||||
label="默认模型"
|
||||
name="analysisModel"
|
||||
rules={[{ required: true, message: '请输入默认模型' }]}
|
||||
extra="化验单解读、趋势分析等分析任务的默认模型"
|
||||
>
|
||||
<Input placeholder="claude-sonnet-4-6" />
|
||||
</Form.Item>
|
||||
|
||||
<Space style={{ width: '100%' }} size="large">
|
||||
<Form.Item
|
||||
label="默认温度"
|
||||
name="analysisTemperature"
|
||||
rules={[{ required: true, message: '请输入默认温度' }]}
|
||||
extra="分析任务建议用较低温度"
|
||||
style={{ width: 200 }}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={2}
|
||||
step={0.1}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="默认最大 Token"
|
||||
name="analysisMaxTokens"
|
||||
rules={[
|
||||
{ required: true, message: '请输入默认最大 Token' },
|
||||
]}
|
||||
style={{ width: 200 }}
|
||||
>
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={65536}
|
||||
step={256}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</Card>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<div style={{ marginTop: 16, textAlign: 'right' }}>
|
||||
<Space>
|
||||
<Button icon={<UndoOutlined />} onClick={handleReset}>
|
||||
恢复默认值
|
||||
</Button>
|
||||
<AuthButton
|
||||
permission="ai.config.manage"
|
||||
type="primary"
|
||||
icon={<SaveOutlined />}
|
||||
loading={saving}
|
||||
onClick={handleSave}
|
||||
>
|
||||
保存配置
|
||||
</AuthButton>
|
||||
</Space>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -143,6 +143,10 @@ const ENTRIES: RoutePermissionEntry[] = [
|
||||
permissions: ["ai.analysis.list", "ai.analysis.manage"],
|
||||
},
|
||||
{ path: "/health/ai-usage", permissions: ["ai.usage.list"] },
|
||||
{
|
||||
path: "/health/ai-config",
|
||||
permissions: ["ai.config.read", "ai.config.manage"],
|
||||
},
|
||||
|
||||
// ===== 健康管理 — 积分商城 =====
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user