674 行
21 KiB
TypeScript
674 行
21 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import {
|
|
Card,
|
|
Form,
|
|
Input,
|
|
Select,
|
|
Slider,
|
|
Switch,
|
|
Radio,
|
|
Button,
|
|
Space,
|
|
Message,
|
|
Spin,
|
|
Grid,
|
|
Tag,
|
|
Divider,
|
|
} from '@arco-design/web-react';
|
|
import axios from 'axios';
|
|
import useLocale from '@/utils/useLocale';
|
|
import { lightColors, spacing, radius } from '@/style/tokens';
|
|
import type {
|
|
AiConciergeConfig,
|
|
ConciergePersonaType,
|
|
ConciergeToneStyle,
|
|
DeepSeekConfig,
|
|
DeepSeekTestConnectionResult,
|
|
} from '@/types/omni';
|
|
import locale from './locale';
|
|
|
|
const { Row, Col } = Grid;
|
|
|
|
const GET_CONFIG_API = '/api/admin/ai/concierge/getByCardId';
|
|
const SAVE_CONFIG_API = '/api/admin/ai/concierge/saveByCardId';
|
|
const CARD_PAGE_API = '/api/admin/card/info/page';
|
|
const DEEPSEEK_CONFIG_API = '/api/admin/ai/deepseek/config';
|
|
const DEEPSEEK_TEST_API = '/api/admin/ai/deepseek/test-connection';
|
|
|
|
interface CoolResponse<T> {
|
|
code: number;
|
|
message?: string;
|
|
data?: T;
|
|
}
|
|
|
|
interface CardOptionItem {
|
|
label: string;
|
|
value: number;
|
|
}
|
|
|
|
interface CardPageRecord {
|
|
id: number;
|
|
name: string;
|
|
title?: string;
|
|
}
|
|
|
|
interface CardPageResult {
|
|
list?: CardPageRecord[];
|
|
}
|
|
|
|
interface PersonaPresetDetail {
|
|
welcomeMsg: string;
|
|
promptTemplate: string;
|
|
toneStyle: ConciergeToneStyle;
|
|
triggerLeadThreshold: number;
|
|
}
|
|
|
|
const PRESET_DATA: Record<ConciergePersonaType, PersonaPresetDetail> = {
|
|
trade_expert: {
|
|
welcomeMsg:
|
|
'您好!我是您的专属出海业务顾问。请问有什么全球贸易或合作需求可以协助您?',
|
|
promptTemplate:
|
|
'你是一名深耕国际贸易与跨国商务的出海业务专家。用专业、沉稳的商务语调为访客解答产品出海、跨境结算、供应链合作等咨询。引导有明确意向的客商提供联系方式。',
|
|
toneStyle: 'professional',
|
|
triggerLeadThreshold: 70,
|
|
},
|
|
business_rep: {
|
|
welcomeMsg: '欢迎光临!我是商务联络代表,很高兴为您服务。',
|
|
promptTemplate:
|
|
'你是一名高效热情的商务拓展代表。重点介绍公司业务优势与成功案例,积极促成商业联络,敏锐捕捉商机。',
|
|
toneStyle: 'enthusiastic',
|
|
triggerLeadThreshold: 60,
|
|
},
|
|
tech_advisor: {
|
|
welcomeMsg: '您好,我是技术顾问,随时解答系统对接与技术架构问题。',
|
|
promptTemplate:
|
|
'你是一名资深解决方案架构师与技术顾问。精确、客观、条理清晰地回答技术规格、API 集成与实施流程问题。',
|
|
toneStyle: 'direct',
|
|
triggerLeadThreshold: 80,
|
|
},
|
|
custom: {
|
|
welcomeMsg: '',
|
|
promptTemplate: '',
|
|
toneStyle: 'professional',
|
|
triggerLeadThreshold: 70,
|
|
},
|
|
};
|
|
|
|
const DEFAULT_CARD_ID = 1;
|
|
|
|
export default function AiConciergePage(): JSX.Element {
|
|
const t = useLocale(locale);
|
|
const [form] = Form.useForm<AiConciergeConfig>();
|
|
const [loading, setLoading] = useState<boolean>(false);
|
|
const [saving, setSaving] = useState<boolean>(false);
|
|
const [selectedCardId, setSelectedCardId] = useState<number>(DEFAULT_CARD_ID);
|
|
const [cardOptions, setCardOptions] = useState<CardOptionItem[]>([]);
|
|
|
|
const [deepSeekForm] = Form.useForm<DeepSeekConfig>();
|
|
const [deepSeekLoading, setDeepSeekLoading] = useState<boolean>(false);
|
|
const [deepSeekSaving, setDeepSeekSaving] = useState<boolean>(false);
|
|
const [deepSeekTesting, setDeepSeekTesting] = useState<boolean>(false);
|
|
const [deepSeekConfig, setDeepSeekConfig] = useState<DeepSeekConfig | null>(null);
|
|
const [testResult, setTestResult] = useState<DeepSeekTestConnectionResult | null>(null);
|
|
|
|
const loadDeepSeekConfig = async (): Promise<void> => {
|
|
setDeepSeekLoading(true);
|
|
try {
|
|
const res = await axios.post<CoolResponse<DeepSeekConfig>>(
|
|
DEEPSEEK_CONFIG_API,
|
|
{}
|
|
);
|
|
if (res.data && res.data.code === 1000 && res.data.data) {
|
|
const d = res.data.data;
|
|
setDeepSeekConfig(d);
|
|
deepSeekForm.setFieldsValue({
|
|
apiKey: d.apiKey || '',
|
|
baseUrl: d.baseUrl || 'https://api.deepseek.com',
|
|
model: d.model || 'deepseek-chat',
|
|
temperature: d.temperature ?? 0.7,
|
|
maxTokens: d.maxTokens ?? 2048,
|
|
enabled: d.enabled !== false,
|
|
});
|
|
}
|
|
} catch {
|
|
// 保持静默优雅降级
|
|
} finally {
|
|
setDeepSeekLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleTestDeepSeek = async (): Promise<void> => {
|
|
setDeepSeekTesting(true);
|
|
setTestResult(null);
|
|
try {
|
|
const values = deepSeekForm.getFieldsValue();
|
|
const res = await axios.post<CoolResponse<DeepSeekTestConnectionResult>>(
|
|
DEEPSEEK_TEST_API,
|
|
{ apiKey: values.apiKey }
|
|
);
|
|
if (res.data && res.data.code === 1000 && res.data.data) {
|
|
setTestResult(res.data.data);
|
|
if (res.data.data.success) {
|
|
Message.success(
|
|
`${t['aiDeepSeek.test.success']} (${res.data.data.latencyMs ?? 0}ms)`
|
|
);
|
|
} else {
|
|
Message.error(res.data.data.message || t['aiDeepSeek.test.fail']);
|
|
}
|
|
} else {
|
|
Message.error(res.data?.message || t['aiDeepSeek.test.fail']);
|
|
}
|
|
} catch {
|
|
Message.error(t['aiDeepSeek.test.fail']);
|
|
} finally {
|
|
setDeepSeekTesting(false);
|
|
}
|
|
};
|
|
|
|
const handleSaveDeepSeek = async (values: DeepSeekConfig): Promise<void> => {
|
|
setDeepSeekSaving(true);
|
|
try {
|
|
const payload: Partial<DeepSeekConfig> = {
|
|
baseUrl: values.baseUrl,
|
|
model: values.model,
|
|
temperature: Number(values.temperature),
|
|
maxTokens: Number(values.maxTokens),
|
|
enabled: Boolean(values.enabled),
|
|
};
|
|
if (values.apiKey && !values.apiKey.includes('****')) {
|
|
payload.apiKey = values.apiKey;
|
|
}
|
|
const res = await axios.post<CoolResponse<DeepSeekConfig>>(
|
|
DEEPSEEK_CONFIG_API,
|
|
payload
|
|
);
|
|
if (res.data && res.data.code === 1000 && res.data.data) {
|
|
Message.success(t['aiDeepSeek.save.success']);
|
|
setDeepSeekConfig(res.data.data);
|
|
if (res.data.data.apiKey) {
|
|
deepSeekForm.setFieldValue('apiKey', res.data.data.apiKey);
|
|
}
|
|
} else {
|
|
Message.error(res.data?.message || t['aiDeepSeek.save.fail']);
|
|
}
|
|
} catch {
|
|
Message.error(t['aiDeepSeek.save.fail']);
|
|
} finally {
|
|
setDeepSeekSaving(false);
|
|
}
|
|
};
|
|
|
|
const fetchCardOptions = async (): Promise<void> => {
|
|
try {
|
|
const res = await axios.post<CoolResponse<CardPageResult>>(
|
|
CARD_PAGE_API,
|
|
{
|
|
page: 1,
|
|
size: 20,
|
|
}
|
|
);
|
|
if (res.data && res.data.code === 1000 && res.data.data?.list) {
|
|
const options = res.data.data.list.map((c) => ({
|
|
label: `${c.name || 'Card'} (#${c.id})`,
|
|
value: Number(c.id),
|
|
}));
|
|
setCardOptions(options);
|
|
}
|
|
} catch {
|
|
// 保持静默降级,允许用户手动输入或保留默认名片
|
|
}
|
|
};
|
|
|
|
const loadConciergeConfig = async (cardId: number): Promise<void> => {
|
|
setLoading(true);
|
|
try {
|
|
const res = await axios.post<CoolResponse<AiConciergeConfig>>(
|
|
GET_CONFIG_API,
|
|
{
|
|
cardId,
|
|
}
|
|
);
|
|
if (res.data && res.data.code === 1000 && res.data.data) {
|
|
const d = res.data.data;
|
|
form.setFieldsValue({
|
|
cardId: Number(d.cardId),
|
|
personaType: d.personaType || 'trade_expert',
|
|
welcomeMsg: d.welcomeMsg,
|
|
triggerLeadThreshold: d.triggerLeadThreshold ?? 70,
|
|
promptTemplate: d.promptTemplate,
|
|
toneStyle: d.toneStyle || 'professional',
|
|
isEnabled: d.isEnabled !== false,
|
|
});
|
|
} else {
|
|
const fallback = PRESET_DATA.trade_expert;
|
|
form.setFieldsValue({
|
|
cardId,
|
|
personaType: 'trade_expert',
|
|
welcomeMsg: fallback.welcomeMsg,
|
|
triggerLeadThreshold: fallback.triggerLeadThreshold,
|
|
promptTemplate: fallback.promptTemplate,
|
|
toneStyle: fallback.toneStyle,
|
|
isEnabled: true,
|
|
});
|
|
}
|
|
} catch {
|
|
Message.error(t['aiConcierge.request.fail']);
|
|
const fallback = PRESET_DATA.trade_expert;
|
|
form.setFieldsValue({
|
|
cardId,
|
|
personaType: 'trade_expert',
|
|
welcomeMsg: fallback.welcomeMsg,
|
|
triggerLeadThreshold: fallback.triggerLeadThreshold,
|
|
promptTemplate: fallback.promptTemplate,
|
|
toneStyle: fallback.toneStyle,
|
|
isEnabled: true,
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchCardOptions();
|
|
loadConciergeConfig(selectedCardId);
|
|
loadDeepSeekConfig();
|
|
}, []);
|
|
|
|
const handleCardChange = (cardId: number): void => {
|
|
setSelectedCardId(cardId);
|
|
loadConciergeConfig(cardId);
|
|
};
|
|
|
|
const handlePersonaChange = (type: ConciergePersonaType): void => {
|
|
if (type !== 'custom') {
|
|
const preset = PRESET_DATA[type];
|
|
form.setFieldsValue({
|
|
personaType: type,
|
|
welcomeMsg: preset.welcomeMsg,
|
|
promptTemplate: preset.promptTemplate,
|
|
toneStyle: preset.toneStyle,
|
|
triggerLeadThreshold: preset.triggerLeadThreshold,
|
|
});
|
|
} else {
|
|
form.setFieldsValue({ personaType: type });
|
|
}
|
|
};
|
|
|
|
const handleSave = async (values: AiConciergeConfig): Promise<void> => {
|
|
setSaving(true);
|
|
try {
|
|
const payload: Partial<AiConciergeConfig> = {
|
|
cardId: Number(values.cardId),
|
|
personaType: values.personaType,
|
|
welcomeMsg: values.welcomeMsg,
|
|
triggerLeadThreshold: Number(values.triggerLeadThreshold),
|
|
promptTemplate: values.promptTemplate,
|
|
toneStyle: values.toneStyle,
|
|
isEnabled: Boolean(values.isEnabled),
|
|
};
|
|
|
|
const res = await axios.post<CoolResponse<AiConciergeConfig>>(
|
|
SAVE_CONFIG_API,
|
|
payload
|
|
);
|
|
if (res.data && res.data.code === 1000) {
|
|
Message.success(t['aiConcierge.request.success']);
|
|
} else {
|
|
Message.error(res.data?.message || t['aiConcierge.request.saveFail']);
|
|
}
|
|
} catch {
|
|
Message.error(t['aiConcierge.request.saveFail']);
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
const sliderMarks = {
|
|
0: '0',
|
|
50: '50',
|
|
70: '70',
|
|
90: '90',
|
|
100: '100',
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<Card
|
|
title={t['aiConcierge.card.title']}
|
|
bordered={false}
|
|
style={{
|
|
borderRadius: radius.card,
|
|
border: `1px solid ${lightColors.border}`,
|
|
backgroundColor: lightColors.bg,
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
color: lightColors.text2,
|
|
marginBottom: spacing[24],
|
|
fontSize: '14px',
|
|
}}
|
|
>
|
|
{t['aiConcierge.card.desc']}
|
|
</div>
|
|
|
|
<Spin loading={loading} style={{ width: '100%' }}>
|
|
<Form
|
|
form={form}
|
|
layout="vertical"
|
|
onSubmit={handleSave}
|
|
initialValues={{
|
|
cardId: DEFAULT_CARD_ID,
|
|
personaType: 'trade_expert',
|
|
toneStyle: 'professional',
|
|
triggerLeadThreshold: 70,
|
|
isEnabled: true,
|
|
}}
|
|
>
|
|
<Row gutter={24}>
|
|
<Col span={12}>
|
|
<Form.Item
|
|
label={t['aiConcierge.form.cardSelect']}
|
|
field="cardId"
|
|
rules={[
|
|
{
|
|
required: true,
|
|
message: t['aiConcierge.validation.cardId'],
|
|
},
|
|
]}
|
|
>
|
|
<Select
|
|
placeholder={t['aiConcierge.form.cardSelect.placeholder']}
|
|
onChange={handleCardChange}
|
|
options={
|
|
cardOptions.length > 0
|
|
? cardOptions
|
|
: [{ label: `Default Card (#${DEFAULT_CARD_ID})`, value: DEFAULT_CARD_ID }]
|
|
}
|
|
/>
|
|
</Form.Item>
|
|
</Col>
|
|
<Col span={12}>
|
|
<Form.Item
|
|
label={t['aiConcierge.form.status']}
|
|
field="isEnabled"
|
|
triggerPropName="checked"
|
|
extra={t['aiConcierge.form.status.desc']}
|
|
>
|
|
<Switch />
|
|
</Form.Item>
|
|
</Col>
|
|
</Row>
|
|
|
|
<Form.Item
|
|
label={t['aiConcierge.form.persona']}
|
|
field="personaType"
|
|
>
|
|
<Radio.Group
|
|
type="button"
|
|
onChange={handlePersonaChange}
|
|
style={{ width: '100%' }}
|
|
>
|
|
<Radio value="trade_expert">
|
|
{t['aiConcierge.persona.trade_expert']}
|
|
</Radio>
|
|
<Radio value="business_rep">
|
|
{t['aiConcierge.persona.business_rep']}
|
|
</Radio>
|
|
<Radio value="tech_advisor">
|
|
{t['aiConcierge.persona.tech_advisor']}
|
|
</Radio>
|
|
<Radio value="custom">{t['aiConcierge.persona.custom']}</Radio>
|
|
</Radio.Group>
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
label={t['aiConcierge.form.tone']}
|
|
field="toneStyle"
|
|
>
|
|
<Select>
|
|
<Select.Option value="professional">
|
|
{t['aiConcierge.tone.professional']}
|
|
</Select.Option>
|
|
<Select.Option value="enthusiastic">
|
|
{t['aiConcierge.tone.enthusiastic']}
|
|
</Select.Option>
|
|
<Select.Option value="direct">
|
|
{t['aiConcierge.tone.direct']}
|
|
</Select.Option>
|
|
<Select.Option value="friendly">
|
|
{t['aiConcierge.tone.friendly']}
|
|
</Select.Option>
|
|
</Select>
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
label={t['aiConcierge.form.welcome']}
|
|
field="welcomeMsg"
|
|
rules={[
|
|
{
|
|
required: true,
|
|
message: t['aiConcierge.validation.welcome'],
|
|
},
|
|
]}
|
|
>
|
|
<Input.TextArea
|
|
rows={3}
|
|
placeholder={t['aiConcierge.form.welcome.placeholder']}
|
|
/>
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
label={t['aiConcierge.form.prompt']}
|
|
field="promptTemplate"
|
|
>
|
|
<Input.TextArea
|
|
rows={4}
|
|
placeholder={t['aiConcierge.form.prompt.placeholder']}
|
|
/>
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
label={t['aiConcierge.form.threshold']}
|
|
field="triggerLeadThreshold"
|
|
extra={t['aiConcierge.form.threshold.desc']}
|
|
>
|
|
<Slider
|
|
min={0}
|
|
max={100}
|
|
marks={sliderMarks}
|
|
style={{ width: '100%', maxWidth: '560px' }}
|
|
/>
|
|
</Form.Item>
|
|
|
|
<div style={{ marginTop: spacing[32] }}>
|
|
<Space>
|
|
<Button
|
|
type="primary"
|
|
htmlType="submit"
|
|
loading={saving}
|
|
style={{ borderRadius: radius.button }}
|
|
>
|
|
{t['aiConcierge.action.save']}
|
|
</Button>
|
|
</Space>
|
|
</div>
|
|
</Form>
|
|
</Spin>
|
|
</Card>
|
|
|
|
<Card
|
|
title={
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
width: '100%',
|
|
}}
|
|
>
|
|
<span>{t['aiDeepSeek.card.title']}</span>
|
|
<Tag color={deepSeekConfig?.isConfigured ? 'green' : 'gray'}>
|
|
{deepSeekConfig?.isConfigured
|
|
? t['aiDeepSeek.status.configured']
|
|
: t['aiDeepSeek.status.unconfigured']}
|
|
</Tag>
|
|
</div>
|
|
}
|
|
bordered={false}
|
|
style={{
|
|
marginTop: spacing[24],
|
|
borderRadius: radius.card,
|
|
border: `1px solid ${lightColors.border}`,
|
|
backgroundColor: lightColors.bg,
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
color: lightColors.text2,
|
|
marginBottom: spacing[24],
|
|
fontSize: '14px',
|
|
}}
|
|
>
|
|
{t['aiDeepSeek.card.desc']}
|
|
</div>
|
|
|
|
<Spin loading={deepSeekLoading} style={{ width: '100%' }}>
|
|
<Form
|
|
form={deepSeekForm}
|
|
layout="vertical"
|
|
onSubmit={handleSaveDeepSeek}
|
|
initialValues={{
|
|
baseUrl: 'https://api.deepseek.com',
|
|
model: 'deepseek-chat',
|
|
temperature: 0.7,
|
|
maxTokens: 2048,
|
|
enabled: true,
|
|
}}
|
|
>
|
|
<Row gutter={24}>
|
|
<Col span={12}>
|
|
<Form.Item
|
|
label={t['aiDeepSeek.form.apiKey']}
|
|
field="apiKey"
|
|
extra={
|
|
deepSeekConfig?.apiKey
|
|
? `已保存密钥: ${deepSeekConfig.apiKey}`
|
|
: undefined
|
|
}
|
|
>
|
|
<Input.Password
|
|
placeholder={t['aiDeepSeek.form.apiKey.placeholder']}
|
|
/>
|
|
</Form.Item>
|
|
</Col>
|
|
<Col span={12}>
|
|
<Form.Item
|
|
label={t['aiDeepSeek.form.status']}
|
|
field="enabled"
|
|
triggerPropName="checked"
|
|
extra={t['aiDeepSeek.form.status.desc']}
|
|
>
|
|
<Switch />
|
|
</Form.Item>
|
|
</Col>
|
|
</Row>
|
|
|
|
<Row gutter={24}>
|
|
<Col span={12}>
|
|
<Form.Item
|
|
label={t['aiDeepSeek.form.baseUrl']}
|
|
field="baseUrl"
|
|
rules={[{ required: true }]}
|
|
>
|
|
<Input placeholder="https://api.deepseek.com" />
|
|
</Form.Item>
|
|
</Col>
|
|
<Col span={12}>
|
|
<Form.Item
|
|
label={t['aiDeepSeek.form.model']}
|
|
field="model"
|
|
rules={[{ required: true }]}
|
|
>
|
|
<Select>
|
|
<Select.Option value="deepseek-chat">
|
|
{t['aiDeepSeek.model.chat']}
|
|
</Select.Option>
|
|
<Select.Option value="deepseek-reasoner">
|
|
{t['aiDeepSeek.model.reasoner']}
|
|
</Select.Option>
|
|
</Select>
|
|
</Form.Item>
|
|
</Col>
|
|
</Row>
|
|
|
|
<Row gutter={24}>
|
|
<Col span={12}>
|
|
<Form.Item
|
|
label={t['aiDeepSeek.form.temperature']}
|
|
field="temperature"
|
|
extra={t['aiDeepSeek.form.temperature.desc']}
|
|
>
|
|
<Slider
|
|
min={0}
|
|
max={1.5}
|
|
step={0.1}
|
|
marks={{ 0: '0', 0.7: '0.7', 1.0: '1.0', 1.5: '1.5' }}
|
|
style={{ width: '100%' }}
|
|
/>
|
|
</Form.Item>
|
|
</Col>
|
|
<Col span={12}>
|
|
<Form.Item
|
|
label="Max Tokens"
|
|
field="maxTokens"
|
|
>
|
|
<Select>
|
|
<Select.Option value={1024}>1024</Select.Option>
|
|
<Select.Option value={2048}>2048</Select.Option>
|
|
<Select.Option value={4096}>4096</Select.Option>
|
|
<Select.Option value={8192}>8192</Select.Option>
|
|
</Select>
|
|
</Form.Item>
|
|
</Col>
|
|
</Row>
|
|
|
|
{testResult && (
|
|
<div style={{ marginBottom: spacing[16] }}>
|
|
<Tag
|
|
color={testResult.success ? 'green' : 'red'}
|
|
style={{ padding: `${spacing[4]} ${spacing[12]}` }}
|
|
>
|
|
{testResult.message || (testResult.success ? '连通成功' : '连通失败')}
|
|
{testResult.latencyMs !== undefined && ` | ${testResult.latencyMs}ms`}
|
|
{testResult.model && ` | ${testResult.model}`}
|
|
</Tag>
|
|
</div>
|
|
)}
|
|
|
|
<div style={{ marginTop: spacing[24] }}>
|
|
<Space>
|
|
<Button
|
|
htmlType="submit"
|
|
loading={deepSeekSaving}
|
|
style={{ borderRadius: radius.button }}
|
|
>
|
|
{t['aiDeepSeek.action.save']}
|
|
</Button>
|
|
<Button
|
|
type="outline"
|
|
onClick={handleTestDeepSeek}
|
|
loading={deepSeekTesting}
|
|
style={{ borderRadius: radius.button }}
|
|
>
|
|
{t['aiDeepSeek.action.test']}
|
|
</Button>
|
|
</Space>
|
|
</div>
|
|
</Form>
|
|
</Spin>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|