新增智能对话菜单

main
wangyunfei 2 days ago
parent d7e002783e
commit b875f5e1ab

@ -223,6 +223,12 @@ export default [
name: 'systemApplicationStatistics', name: 'systemApplicationStatistics',
component: './systemApplicationStatistics/TimeSheetList', component: './systemApplicationStatistics/TimeSheetList',
}, },
{
path: '/topnavbar00/hrefficiency/ChatConversation',
// icon: 'bank',
name: 'chatConversation',
component: './chatConversation/ChatConversation',
},
], ],
}, },
// 知识库管理 // 知识库管理

@ -25,7 +25,9 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-contexify": "^5.0.0", "react-contexify": "^5.0.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-split-pane": "^0.1.92" "react-markdown": "^10.1.0",
"react-split-pane": "^0.1.92",
"remark-gfm": "^4.0.1"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.0.0", "@types/react": "^18.0.0",

@ -0,0 +1,111 @@
// API配置文件
export const API_CONFIG = {
// DeepSeek API配置
DEEPSEEK: {
BASE_URL: 'https://api.deepseek.com/v1',
MODEL: 'deepseek-chat',
API_KEY:
process.env.UMI_APP_DEEPSEEK_API_KEY ||
process.env.REACT_APP_DEEPSEEK_API_KEY ||
'your-api-key-here',
MAX_TOKENS: 2000,
TEMPERATURE: 0.7,
SYSTEM_PROMPT: '你是一个智能AI助手请用简洁、专业、友好的方式回答用户的问题。'
},
// 其他API配置
OPENAI: {
BASE_URL: 'https://api.openai.com/v1',
MODEL: 'gpt-3.5-turbo',
API_KEY: process.env.UMI_APP_OPENAI_API_KEY || '',
MAX_TOKENS: 2000,
TEMPERATURE: 0.7
}
}
// 调试:开发阶段临时查看是否拿到
if (process.env.NODE_ENV === 'development') {
// 只显示是否存在,不打印具体 key
// eslint-disable-next-line no-console
console.log(
'DeepSeek key loaded =',
!!process.env.UMI_APP_DEEPSEEK_API_KEY
)
}
// DeepSeek 调用
export const callDeepSeekAPI = async (messages, options = {}) => {
const cfg = API_CONFIG.DEEPSEEK
const apiKey = cfg.API_KEY
if (!apiKey || apiKey === 'your-api-key-here') {
throw new Error('NO_API_KEY')
}
// 组装消息(如果历史里已经含 system 就不重复)
const hasSystem = messages.some(m => m.role === 'system')
const finalMessages = hasSystem
? messages
: [{ role: 'system', content: cfg.SYSTEM_PROMPT }, ...messages]
const payload = {
model: cfg.MODEL,
messages: finalMessages.map(m => ({ role: m.role, content: m.content })),
temperature: options.temperature ?? cfg.TEMPERATURE,
max_tokens: options.maxTokens ?? cfg.MAX_TOKENS,
stream: false
}
const resp = await fetch(`${cfg.BASE_URL}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`
},
body: JSON.stringify(payload)
})
const data = await resp.json().catch(() => ({}))
if (!resp.ok) {
// 统一抛出,交给 handleAPIError
const msg = data?.error?.message || resp.statusText || 'API_ERROR'
const err = new Error(msg)
err.status = resp.status
err.code = data?.error?.code
throw err
}
const content = data?.choices?.[0]?.message?.content?.trim()
if (!content) throw new Error('EMPTY_RESPONSE')
return content
}
// DeepSeek 流式调用
// 错误处理函数
export const handleAPIError = (error) => {
const raw = error?.message || ''
const status = error?.status
const code = error?.code
if (raw === 'NO_API_KEY') return '未配置 API Key请在 .env 里加 UMI_APP_DEEPSEEK_API_KEY 并重启。'
if (raw === 'EMPTY_RESPONSE') return '接口返回空结果,请重试。'
// 典型鉴权问题
if (status === 401 || /unauthorized|invalid api key/i.test(raw) || code === 'invalid_api_key') {
return 'API密钥无效请检查是否正确。'
}
if (status === 429 || /rate limit/i.test(raw)) return '请求过多,被限流。'
if (status === 500) return '服务端错误,请稍后再试。'
if (/timeout/i.test(raw)) return '请求超时,请检查网络。'
if (/certificate|SSL/i.test(raw)) return '证书/网络问题,请更换网络或代理。'
return `调用失败:${raw || '未知错误'}`
}

@ -0,0 +1,413 @@
import React, { useState, useRef, useEffect } from 'react'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import {
Input,
Button,
Typography,
Spin,
message,
Layout,
Tooltip,
Input as AntInput,
Modal,
Upload,
Dropdown,
Space,
Select,
} from 'antd'
import {
ArrowUpOutlined,
LinuxOutlined,
RobotOutlined,
LoadingOutlined,
CopyOutlined,
ReloadOutlined,
PaperClipOutlined,
} from '@ant-design/icons'
import { callDeepSeekAPI, handleAPIError } from './models/api'
import { conversationStore } from '@/utils/pageConversationStore'
import './ChatConversation.less'
const { Text } = Typography
const { TextArea } = Input
const { Sider, Content } = Layout
function ChatConversation() {
// 会话相关状态 - 使用独立的存储
const [conversations, setConversations] = useState(conversationStore.getConversations())
const [currentConversationId, setCurrentConversationId] = useState(conversationStore.getCurrentConversationId())
const [inputValue, setInputValue] = useState('')
const [loading, setLoading] = useState(false)
const [sidebarExpanded, setSidebarExpanded] = useState(false)
const [editModalVisible, setEditModalVisible] = useState(false)
const [editingConversation, setEditingConversation] = useState(null)
const [newTitle, setNewTitle] = useState('')
const messagesEndRef = useRef(null)
const inputRef = useRef(null)
// 获取当前会话
const currentConversation = conversations.find(conv => conv.id === currentConversationId)
const currentMessages = currentConversation?.messages || []
// 自动滚动到底部
const chatScrollRef = useRef(null);
useEffect(() => {
if (chatScrollRef.current) {
chatScrollRef.current.scrollTop = chatScrollRef.current.scrollHeight;
}
}, [currentMessages, loading]); // currentMessages 是你的消息数组
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
}
const handleChange = value => {
console.log(`selected ${value}`);
}
// 订阅对话状态变化
useEffect(() => {
const unsubscribe = conversationStore.subscribe(({ conversations, currentConversationId }) => {
setConversations(conversations)
setCurrentConversationId(currentConversationId)
})
return unsubscribe
}, [])
useEffect(() => {
scrollToBottom()
}, [currentMessages])
// 创建新对话
const createNewConversation = () => {
conversationStore.createNewConversation()
setInputValue('')
}
// 切换对话
const switchConversation = (conversationId) => {
conversationStore.switchConversation(conversationId)
setInputValue('')
}
// 删除对话
const deleteConversation = (conversationId) => {
if (conversations.length <= 1) {
message.warning('至少需要保留一个对话')
return
}
conversationStore.deleteConversation(conversationId)
}
// 编辑对话标题
const editConversationTitle = (conversation) => {
setEditingConversation(conversation)
setNewTitle(conversation.title)
setEditModalVisible(true)
}
// 保存对话标题
const saveConversationTitle = () => {
if (!newTitle.trim()) {
message.warning('标题不能为空')
return
}
conversationStore.updateConversationTitle(editingConversation.id, newTitle.trim())
setEditModalVisible(false)
setEditingConversation(null)
setNewTitle('')
}
// 发送消息
const sendMessage = async () => {
const messageText = inputValue.trim()
if (!messageText || loading) return
// 添加用户消息到独立存储
conversationStore.addMessage('user', messageText)
setInputValue('')
setLoading(true)
try {
// 准备发送给API的消息历史
const apiMessages = currentMessages.map(msg => ({
role: msg.role,
content: msg.content
})).concat({
role: 'user',
content: messageText
})
// 调用DeepSeek API
const assistantResponse = await callDeepSeekAPI(apiMessages)
// 添加助手回复到独立存储
conversationStore.addMessage('assistant', assistantResponse)
} catch (error) {
const errorMessage = handleAPIError(error)
message.error(errorMessage)
// 添加错误消息到独立存储
conversationStore.addMessage('assistant', `抱歉,我遇到了一个问题:${errorMessage}`)
} finally {
setLoading(false)
}
}
// 处理回车键发送
const handleKeyPress = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
sendMessage()
}
}
// 清空当前对话
const clearCurrentChat = () => {
conversationStore.clearCurrentConversation()
}
// 兼容剪贴板
const copyToClipboard = async (text) => {
if (!text) return Promise.reject(new Error("exmpty text"))
// 现代 API需 HTTPS 或 localhostwindow.isSecureContext
if (navigator.clipboard && window.isSecureContext) {
try {
await navigator.clipboard.writeText(text)
return
} catch (e) {
// 若失败,继续使用回退方案
console.warn('navigator.clipboard failed, fallback to execCommand', e)
}
}
// 回退方案
return new Promise((resolve, reject) => {
try {
const ta = document.createElement('textarea')
ta.value = text
ta.setAttribute('readonly', '')
ta.style.position = 'fixed'
ta.style.left = '-9999px'
document.body.appendChild(ta)
ta.select()
ta.setSelectionRange(0, ta.value.length)
const ok = document.execCommand('copy')
document.body.removeChild(ta)
if (ok) resolve()
else reject(new Error('execCommand failed'))
} catch (err) {
reject(err)
}
})
}
// 渲染单条消息 ==================================
const renderMessage = (msg, index) => {
const isUser = msg.role === 'user'
return (
<div
key={index}
className={`ds-message-row ${isUser ? 'from-user' : 'from-assistant'}`}
>
{!isUser && (
<div className='ds-avatar'>
<RobotOutlined />
</div>
)}
{isUser && <div className='ds-avatar user'><LinuxOutlined /></div>}
<div className='ds-message-body'>
<div className='ds-message-meta'>
<span className='role'>{isUser ? '' : 'AI对话'}</span>
<span className='time'>
{msg.timestamp
? (typeof msg.timestamp === 'string'
? msg.timestamp
: msg.timestamp.toLocaleString())
: ''}
</span>
{!isUser && (
<span className='actions'>
<Tooltip title='复制'>
<Button
size='small'
type='text'
icon={<CopyOutlined />}
onClick={async () => {
try {
await copyToClipboard(msg.content || '')
message.success('已复制')
} catch (err) {
console.error('copy failed', err)
message.error('复制失败,请在 HTTPS / localhost 或允许剪贴板权限的环境下重试')
}
}}
/>
</Tooltip>
<Tooltip title='重新生成(示例)'>
<Button
size='small'
type='text'
icon={<ReloadOutlined />}
disabled={loading}
onClick={() => {
// 可复用最后一条用户消息再请求
const lastUser = [...currentMessages].reverse().find(m => m.role === 'user')
if (lastUser) {
setInputValue(lastUser.content)
sendMessage()
}
}}
/>
</Tooltip>
</span>
)}
</div>
{/* 回答框 */}
<div className='ds-message-content'>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
>
{msg.content || ''}
</ReactMarkdown>
</div>
</div>
</div>
)
}
// =================================================
return (
<div className='ds-chat-page'>
<div className='ds-chat-scroll' ref={chatScrollRef}>
<div className='ds-chat-inner'>
{currentMessages.length === 0 && !loading && (
// 已有默认初始化对话,无需此处提示
<div className='ds-empty'>
<h3>开始对话吧</h3>
<p>提出一个问题比如请帮我总结一段文本</p>
</div>
)}
{/* 第一句话 */}
{currentMessages.map(renderMessage)}
{loading && (
<div className='ds-message-row from-assistant thinking'>
<div className='ds-avatar'>
<RobotOutlined />
</div>
<div className='ds-message-body'>
<div className='ds-message-meta'>
<span className='role'>AI</span>
<span className='time'>正在思考</span>
</div>
<div className='ds-message-content typing'>
<Spin
indicator={<LoadingOutlined style={{ fontSize: 16 }} spin />}
size='small'
/>
<span className='typing-dots'>
<i /><i /><i />
</span>
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
</div>
{/* 问答输入框 */}
<div className='ds-input-bar'>
<div className='ds-input-inner'>
<div className='ds-textarea-wrap'>
<TextArea
ref={inputRef}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
sendMessage()
}
}}
placeholder='给AI助手发送消息'
autoSize={{ minRows: 3, maxRows: 8 }}
disabled={loading}
bordered={false}
/>
</div>
{/* 模型切换 */}
<Select
defaultValue="DeepSeek-3.1"
style={{
minWidth: 120, maxWidth: 200, marginRight: 8
}}
onchange={handleChange}
options={[
{ value: "DeepSeek-3.1", label: "DeepSeek-3.1" },
{ value: "GPT-5 mini", label: "GPT-5 mini" },
{ value: "Claude Sonnet 4", label: "Claude Sonnet 4" },
{ value: "Kimi Senior", label: "Kimi Senior" },
]}>
</Select>
{/* 上传文件 */}
<Upload className='ds-uploading'>
<Button
shape='circle'
icon={<PaperClipOutlined />}
/>
</Upload>
{/* 发送按钮 */}
<div className='ds-input-actions'>
<Button
type='primary'
shape='circle'
icon={<ArrowUpOutlined />}
onClick={sendMessage}
disabled={!inputValue.trim() || loading}
>
</Button>
</div>
</div>
<div className='ds-input-hint'>
Enter 发送 · Shift+Enter 换行
<span className='sep'>|</span>
<a onClick={clearCurrentChat}>清空当前对话</a>
<Select
className='ds-select'
dropdownClassName="ds-select-dropdown"
defaultValue="模型反馈"
style={{
minWidth: 120, maxWidth: 200, marginLeft: 'auto', marginRight: 8
}}
onchange={handleChange}
options={[
{ value: "正面反馈", label: "正面反馈" },
{ value: "负面反馈", label: "负面反馈" },
]}>
</Select>
</div>
</div>
</div>
)
}
export default ChatConversation

@ -0,0 +1,477 @@
// // 滚动条样式
// .chat-list::-webkit-scrollbar {
// width: 6px;
// }
// .chat-list::-webkit-scrollbar-track {
// background: #f1f1f1;
// border-radius: 3px;
// }
// .chat-list::-webkit-scrollbar-thumb {
// background: #c1c1c1;
// border-radius: 3px;
// }
// .chat-list::-webkit-scrollbar-thumb:hover {
// background: #a8a8a8;
// }
// // 响应式设计
// @media (max-width: 768px) {
// .sidebar-column {
// position: absolute !important;
// z-index: 1000;
// height: 100vh !important;
// box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
// }
// .content-column {
// padding: 8px;
// }
// .chat-conversation-card {
// .chat-messages {
// margin: 8px;
// }
// .chat-list {
// padding: 12px;
// }
// .message-bubble {
// max-width: 85% !important;
// }
// .chat-input {
// padding: 12px;
// }
// }
// .chat-sidebar {
// padding: 12px;
// .conversation-item {
// padding: 8px;
// .conversation-content .conversation-title .title-text {
// max-width: 100px;
// }
// }
// }
// }
// @keyframes fadeIn {
// from {
// opacity: 0;
// transform: translateY(10px);
// }
// to {
// opacity: 1;
// transform: translateY(0);
// }
// }
// @keyframes slideIn {
// from {
// opacity: 0;
// transform: translateX(-10px);
// }
// to {
// opacity: 1;
// transform: translateX(0);
// }
// }
/* DeepSeek 风格仿制样式 */
.ds-chat-page {
position: relative;
height: 100%;
min-height: 100vh;
// background: #f5f7fb;
display: flex;
overflow-y: hidden;
flex-direction: column;
padding-bottom: 140px;
/* 给输入区预留空间 */
color: #1f2329;
// background: #f5f6fa;
border-radius: 8px;
// padding: 16px 20px 5px 20px;
padding: 0px 20px 0px 20px;
min-height: 100%;
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
}
.ds-chat-scroll {
flex: 1;
overflow-y: auto;
scrollbar-width: thin;
}
.ds-chat-scroll::-webkit-scrollbar {
width: 8px;
}
.ds-chat-scroll::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, .15);
border-radius: 4px;
}
.ds-chat-inner {
max-width: 860px;
margin: 0 auto;
padding: 40px 24px 24px;
box-sizing: border-box;
}
.ds-empty {
text-align: center;
margin-top: 80px;
h3 {
font-weight: 600;
font-size: 20px;
margin-bottom: 12px;
}
p {
color: #667085;
font-size: 14px;
}
}
.ds-message-row {
display: flex;
align-items: flex-start;
gap: 14px;
margin-bottom: 28px;
line-height: 1.6;
&.from-user {
flex-direction: row-reverse;
.ds-message-body {
align-items: flex-end;
}
.ds-message-content {
background: #f4f4f4;
color: #111;
border-radius: 20px;
font-size: 17px;
// border: 1px solid #3478f6;
// box-shadow: 0 2px 6px rgba(52, 120, 246, .3);
p {
color: #111;
}
}
.ds-message-meta {
justify-content: flex-end;
.role {
color: #3478f6;
}
}
}
&.thinking .ds-message-content {
display: flex;
align-items: center;
gap: 10px;
}
}
.ds-avatar {
width: 34px;
height: 34px;
flex-shrink: 0;
border-radius: 8px;
background: #e9eef5;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: #355070;
&.user {
background: #e9eef5; // 用户头像也用这个颜色
color: #4f5fa3;
}
}
.ds-message-body {
display: flex;
flex-direction: column;
max-width: 100%;
// flex: 1;
position: relative;
}
.ds-message-meta {
font-size: 12px;
color: #86909c;
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 4px;
.role {
font-weight: 600;
color: #1f2329;
}
.actions {
margin-left: auto;
opacity: 0;
transition: opacity .2s;
display: flex;
gap: 4px;
.ant-btn {
padding: 0 4px;
height: 24px;
}
}
}
.ds-message-row:hover .ds-message-meta .actions {
opacity: 1;
}
.ds-message-content {
display: inline-block;
max-width: 100%;
min-width: 48px;
background: #fff;
border: 1px solid #e3e8ef;
border-radius: 14px;
padding: 6px 16px;
font-size: 17px;
color: #1f2329;
position: relative;
overflow: hidden;
word-break: break-word;
box-shadow: 0 2px 4px rgba(0, 0, 0, .04);
p {
margin: 0 0 8px;
&:last-child {
margin-bottom: 0;
}
}
code {
background: #f2f4f8;
padding: 2px 6px;
border-radius: 4px;
font-family: ui-monospace, Menlo, Consolas, monospace;
font-size: 13px;
}
pre {
background: #0f1115;
color: #e9edf2;
padding: 14px 16px;
border-radius: 12px;
overflow: auto;
font-size: 13px;
line-height: 1.5;
code {
background: transparent;
padding: 0;
color: inherit;
}
}
}
.typing-dots {
display: inline-flex;
gap: 4px;
margin-left: 6px;
i {
width: 6px;
height: 6px;
background: #3478f6;
border-radius: 50%;
display: block;
animation: blink 1s infinite ease-in-out;
}
i:nth-child(2) {
animation-delay: .2s;
}
i:nth-child(3) {
animation-delay: .4s;
}
}
@keyframes blink {
0%,
80%,
100% {
opacity: .2;
transform: translateY(0);
}
40% {
opacity: 1;
transform: translateY(-3px);
}
}
.ds-input-bar {
// display: flex;
// align-items: flex-end;
// position: fixed;
left: 0;
right: 0;
// bottom: -10vh;
// background: linear-gradient(to top, #f5f7fb 40%, rgba(245, 247, 251, 0));
// padding: 32px 0 28px;
z-index: 20;
}
.ds-input-inner {
display: flex;
align-items: flex-end;
width: 900px;
margin: 0 auto;
background: #fff;
border: 1px solid #d9dde3;
border-radius: 20px;
padding: 12px 14px 12px 16px;
gap: 12px;
box-shadow: 0 6px 28px -8px rgba(33, 53, 79, .18), 0 4px 12px -4px rgba(33, 53, 79, .08);
}
.ds-textarea-wrap {
display: flex;
flex: 1;
.ant-input {
font-size: 19px;
line-height: 1.5;
padding: 0;
background: transparent;
}
.ant-input:focus {
box-shadow: none;
}
}
.ds-uploading {
.ant-btn.ant-btn-icon-only .anticon {
font-size: 18px;
}
.ant-btn {
border: none;
background: transparent;
box-shadow: none;
color: #111;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
&:hover {
background: #f2f3f4;
color: #111;
box-shadow: none;
}
}
}
.ds-input-actions {
display: flex;
align-items: flex-end;
.ant-btn {
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: 500;
padding: 0 0px;
margin: 0;
height: 36px;
width: 36px;
background-color: #b0c1ff;
border: none;
color: #fff;
}
}
.ds-input-hint {
max-width: 860px;
margin: 6px auto 0;
font-size: 12px;
color: #808791;
display: flex;
gap: 8px;
a {
color: #3478f6;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
.sep {
opacity: .3;
}
}
.ds-select,
.ds-select .ant-select-selector,
.ds-select .ant-select-selection-item,
.ds-select .ant-select-dropdown {
font-size: 12px !important;
color: #808791 !important;
}
.ds-select-dropdown .ant-select-item {
font-size: 12px !important;
color: #808791 !important;
}
@media (max-width: 1000px) {
.ds-chat-inner,
.ds-input-inner,
.ds-input-hint {
max-width: 100%;
padding-left: 16px;
padding-right: 16px;
}
.ds-input-inner {
border-radius: 16px;
}
}

@ -0,0 +1,183 @@
// API配置文件
export const API_CONFIG = {
// DeepSeek API配置
DEEPSEEK: {
BASE_URL: 'https://api.deepseek.com/v1',
MODEL: 'deepseek-chat',
API_KEY:
process.env.UMI_APP_DEEPSEEK_API_KEY ||
process.env.REACT_APP_DEEPSEEK_API_KEY ||
'your-api-key-here',
MAX_TOKENS: 2000,
TEMPERATURE: 0.7,
SYSTEM_PROMPT: '你是一个智能AI助手请用简洁、专业、友好的方式回答用户的问题。'
},
// 其他API配置
OPENAI: {
BASE_URL: 'https://api.openai.com/v1',
MODEL: 'gpt-3.5-turbo',
API_KEY: process.env.UMI_APP_OPENAI_API_KEY || '',
MAX_TOKENS: 2000,
TEMPERATURE: 0.7
}
}
// 调试:开发阶段临时查看是否拿到
if (process.env.NODE_ENV === 'development') {
// 只显示是否存在,不打印具体 key
// eslint-disable-next-line no-console
console.log(
'DeepSeek key loaded =',
!!process.env.UMI_APP_DEEPSEEK_API_KEY
)
}
// DeepSeek 调用
export const callDeepSeekAPI = async (messages, options = {}) => {
const cfg = API_CONFIG.DEEPSEEK
const apiKey = cfg.API_KEY
if (!apiKey || apiKey === 'your-api-key-here') {
throw new Error('NO_API_KEY')
}
// 组装消息(如果历史里已经含 system 就不重复)
const hasSystem = messages.some(m => m.role === 'system')
const finalMessages = hasSystem
? messages
: [{ role: 'system', content: cfg.SYSTEM_PROMPT }, ...messages]
const payload = {
model: cfg.MODEL,
messages: finalMessages.map(m => ({ role: m.role, content: m.content })),
temperature: options.temperature ?? cfg.TEMPERATURE,
max_tokens: options.maxTokens ?? cfg.MAX_TOKENS,
stream: false
}
const resp = await fetch(`${cfg.BASE_URL}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`
},
body: JSON.stringify(payload)
})
const data = await resp.json().catch(() => ({}))
if (!resp.ok) {
// 统一抛出,交给 handleAPIError
const msg = data?.error?.message || resp.statusText || 'API_ERROR'
const err = new Error(msg)
err.status = resp.status
err.code = data?.error?.code
throw err
}
const content = data?.choices?.[0]?.message?.content?.trim()
if (!content) throw new Error('EMPTY_RESPONSE')
return content
}
// 本地服务调用(调试版:打印详细日志,方便逐步排查)
export const callLocalChatAPI = async (prompt) => {
const base = '/api/chat/stream'
const url = `${base}?prompt=${encodeURIComponent(prompt)}`
// 逐步调试日志
// eslint-disable-next-line no-console
console.debug('[callLocalChatAPI] url:', url)
try {
// eslint-disable-next-line no-console
console.debug('[callLocalChatAPI] fetch start')
const resp = await fetch(url, { method: 'GET' })
// eslint-disable-next-line no-console
console.debug('[callLocalChatAPI] fetch done', {
ok: resp.ok,
status: resp.status,
statusText: resp.statusText,
headers: Object.fromEntries(resp.headers.entries())
})
if (!resp.ok) {
const txt = await resp.text().catch(() => null)
// eslint-disable-next-line no-console
console.error('[callLocalChatAPI] non-OK response body:', txt)
const msg = txt || resp.statusText || `HTTP_${resp.status}`
const err = new Error(msg)
err.status = resp.status
throw err
}
// 最简单:一次性读取完整响应文本(非流式),然后从 SSE 格式中提取第一个 event:message 的 data
const raw = await resp.text().catch(e => {
// eslint-disable-next-line no-console
console.error('[callLocalChatAPI] text read error:', e)
return ''
})
// eslint-disable-next-line no-console
console.debug('[callLocalChatAPI] raw length:', raw?.length || 0, 'preview:', (raw || '').slice(0, 500))
if (!raw) throw new Error('EMPTY_RESPONSE')
// 按空行分块,优先找到 event:message 的 data
const blocks = raw.split(/\r?\n\r?\n/).map(b => b.trim()).filter(Boolean)
for (const block of blocks) {
const lines = block.split(/\r?\n/).map(l => l.trim())
let eventType = ''
const dataLines = []
for (const line of lines) {
if (/^event\s*:/i.test(line)) {
eventType = line.split(':').slice(1).join(':').trim()
} else if (/^data\s*:/i.test(line)) {
dataLines.push(line.split(':').slice(1).join(':'))
}
}
if (eventType === 'message' && dataLines.length > 0) {
const message = dataLines.join('\n').trim()
// eslint-disable-next-line no-console
console.debug('[callLocalChatAPI] parsed message data preview:', message.slice(0, 300))
if (!message) throw new Error('EMPTY_RESPONSE')
return message
}
}
// 回退:取第一个 data: 行的内容
const firstDataMatch = raw.match(/^[ \t]*data\s*:(.*)$/im)
if (firstDataMatch && firstDataMatch[1]) {
const fallback = firstDataMatch[1].trim()
// eslint-disable-next-line no-console
console.debug('[callLocalChatAPI] fallback data:', fallback.slice(0, 300))
return fallback
}
// 最后回退:返回原始文本
return raw
} catch (err) {
// eslint-disable-next-line no-console
console.error('[callLocalChatAPI] ERROR:', err)
throw err
}
}
// 错误处理函数
export const handleAPIError = (error) => {
const raw = error?.message || ''
const status = error?.status
const code = error?.code
if (raw === 'NO_API_KEY') return '未配置 API Key请在 .env 里加 UMI_APP_DEEPSEEK_API_KEY 并重启。'
if (raw === 'EMPTY_RESPONSE') return '接口返回空结果,请重试。'
// 典型鉴权问题
if (status === 401 || /unauthorized|invalid api key/i.test(raw) || code === 'invalid_api_key') {
return 'API密钥无效请检查是否正确。'
}
if (status === 429 || /rate limit/i.test(raw)) return '请求过多,被限流。'
if (status === 500) return '服务端错误,请稍后再试。'
if (/timeout/i.test(raw)) return '请求超时,请检查网络。'
if (/certificate|SSL/i.test(raw)) return '证书/网络问题,请更换网络或代理。'
return `调用失败:${raw || '未知错误'}`
}

@ -48,6 +48,10 @@ const menuItem = [
key: '/topnavbar00/hrefficiency/backgroundManagement', key: '/topnavbar00/hrefficiency/backgroundManagement',
// icon: <SettingOutlined />, // icon: <SettingOutlined />,
}, },
{
label: '智能对话',
path: '/topnavbar00/hrefficiency/ChatConversation',
},
// { // {
// label: '智能体配置1', // label: '智能体配置1',
// key: '/topnavbar00/hrefficiency/staffuph', // key: '/topnavbar00/hrefficiency/staffuph',

@ -0,0 +1,179 @@
// 全局对话状态管理
class ConversationStore {
constructor() {
this.conversations = [
{
id: '1',
title: '新对话',
messages: [
{
role: 'assistant',
content: '你好我是AI助手有什么可以帮助你的吗',
timestamp: new Date().toLocaleTimeString()
}
],
lastUpdate: new Date(),
isActive: true
}
]
this.currentConversationId = '1'
this.listeners = []
}
// 获取所有对话
getConversations() {
return this.conversations
}
// 获取当前对话
getCurrentConversation() {
return this.conversations.find(conv => conv.id === this.currentConversationId)
}
// 获取当前对话ID
getCurrentConversationId() {
return this.currentConversationId
}
// 创建新对话
createNewConversation() {
const newId = Date.now().toString()
const newConversation = {
id: newId,
title: '新对话',
messages: [
{
role: 'assistant',
content: '你好我是AI助手有什么可以帮助你的吗',
timestamp: new Date().toLocaleTimeString()
}
],
lastUpdate: new Date(),
isActive: true
}
// 将之前的对话设为非活跃
this.conversations = this.conversations.map(conv => ({ ...conv, isActive: false }))
// 添加新对话
this.conversations.push(newConversation)
this.currentConversationId = newId
this.notifyListeners()
return newId
}
// 切换对话
switchConversation(conversationId) {
this.conversations = this.conversations.map(conv => ({
...conv,
isActive: conv.id === conversationId
}))
this.currentConversationId = conversationId
this.notifyListeners()
}
// 删除对话
deleteConversation(conversationId) {
if (this.conversations.length <= 1) {
return false
}
this.conversations = this.conversations.filter(conv => conv.id !== conversationId)
// 如果删除的是当前对话,切换到第一个对话
if (conversationId === this.currentConversationId) {
if (this.conversations.length > 0) {
this.currentConversationId = this.conversations[0].id
this.conversations = this.conversations.map((conv, index) =>
index === 0 ? { ...conv, isActive: true } : { ...conv, isActive: false }
)
}
}
this.notifyListeners()
return true
}
// 添加消息到当前对话
addMessage(role, content) {
const currentConversation = this.getCurrentConversation()
if (!currentConversation) return
const message = {
role,
content,
timestamp: new Date().toLocaleTimeString()
}
currentConversation.messages.push(message)
currentConversation.lastUpdate = new Date()
// 如果是用户的第一条消息,更新对话标题
if (role === 'user' && currentConversation.title === '新对话') {
currentConversation.title = content.slice(0, 20) + '...'
}
this.notifyListeners()
}
// 更新对话标题
updateConversationTitle(conversationId, newTitle) {
const conversation = this.conversations.find(conv => conv.id === conversationId)
if (conversation) {
conversation.title = newTitle
this.notifyListeners()
}
}
// 清空当前对话
clearCurrentConversation() {
const currentConversation = this.getCurrentConversation()
if (currentConversation) {
currentConversation.messages = [
{
role: 'assistant',
content: '你好我是AI助手有什么可以帮助你的吗',
timestamp: new Date().toLocaleTimeString()
}
]
currentConversation.title = '新对话'
currentConversation.lastUpdate = new Date()
this.notifyListeners()
}
}
// 订阅状态变化
subscribe(listener) {
this.listeners.push(listener)
// 返回取消订阅函数
return () => {
const index = this.listeners.indexOf(listener)
if (index > -1) {
this.listeners.splice(index, 1)
}
}
}
// 通知所有监听器
notifyListeners() {
this.listeners.forEach(listener => {
listener({
conversations: this.conversations,
currentConversationId: this.currentConversationId,
currentConversation: this.getCurrentConversation()
})
})
}
}
// 创建全局单例实例
const conversationStore = new ConversationStore()
// 导出实例和类
export default conversationStore
export { ConversationStore }

@ -0,0 +1,202 @@
// 每个页面独立的对话存储工具
class PageConversationStore {
constructor(pageName) {
this.pageName = pageName
this.storageKey = `page_conversations_${pageName}`
console.log('Initializing store for page:', this.storageKey)
this.conversations = this.loadFromStorage()
this.currentConversationId = this.conversations.length > 0 ? this.conversations[0].id : null
this.subscribers = []
// 如果没有对话,创建默认对话
if (this.conversations.length === 0) {
this.createNewConversation()
}
}
// 从本地存储加载对话
loadFromStorage() {
try {
const stored = localStorage.getItem(this.storageKey)
if (stored) {
const parsed = JSON.parse(stored)
// 恢复日期对象
return parsed.map(conv => ({
...conv,
lastUpdate: new Date(conv.lastUpdate),
messages: conv.messages.map(msg => ({
...msg,
timestamp: new Date(msg.timestamp)
}))
}))
}
} catch (error) {
console.error(`Error loading conversations for ${this.pageName}:`, error)
}
return []
}
// 保存到本地存储
saveToStorage() {
try {
localStorage.setItem(this.storageKey, JSON.stringify(this.conversations))
} catch (error) {
console.error(`Error saving conversations for ${this.pageName}:`, error)
}
}
// 获取所有对话
getConversations() {
return this.conversations
}
// 获取当前对话ID
getCurrentConversationId() {
return this.currentConversationId
}
// 获取当前对话
getCurrentConversation() {
return this.conversations.find(conv => conv.id === this.currentConversationId)
}
// 创建新对话
createNewConversation() {
const newConversation = {
id: `${this.pageName}_${Date.now()}`,
title: `新对话 ${this.conversations.length + 1}`,
messages: [
{
role: 'assistant',
content: `欢迎使用${this.pageName}我是您的AI助手有什么可以帮助您的吗`,
timestamp: new Date()
}
],
lastUpdate: new Date(),
isActive: true
}
// 将之前的对话设为非活跃
this.conversations.forEach(conv => conv.isActive = false)
this.conversations.unshift(newConversation)
this.currentConversationId = newConversation.id
this.saveToStorage()
this.notifySubscribers()
}
// 切换对话
switchConversation(conversationId) {
this.conversations.forEach(conv => conv.isActive = false)
const targetConversation = this.conversations.find(conv => conv.id === conversationId)
if (targetConversation) {
targetConversation.isActive = true
this.currentConversationId = conversationId
this.saveToStorage()
this.notifySubscribers()
}
}
// 添加消息
addMessage(role, content) {
const currentConversation = this.getCurrentConversation()
if (currentConversation) {
const newMessage = {
role,
content,
timestamp: new Date()
}
currentConversation.messages.push(newMessage)
currentConversation.lastUpdate = new Date()
// 如果是用户消息,更新标题
if (role === 'user') {
const words = content.split(' ').slice(0, 5)
currentConversation.title = words.join(' ') + (content.length > 30 ? '...' : '')
}
this.saveToStorage()
this.notifySubscribers()
}
}
// 更新对话标题
updateConversationTitle(conversationId, newTitle) {
const conversation = this.conversations.find(conv => conv.id === conversationId)
if (conversation) {
conversation.title = newTitle
this.saveToStorage()
this.notifySubscribers()
}
}
// 删除对话
deleteConversation(conversationId) {
const index = this.conversations.findIndex(conv => conv.id === conversationId)
if (index !== -1) {
this.conversations.splice(index, 1)
// 如果删除的是当前对话,切换到第一个对话
if (conversationId === this.currentConversationId) {
if (this.conversations.length > 0) {
this.currentConversationId = this.conversations[0].id
this.conversations[0].isActive = true
} else {
this.currentConversationId = null
}
}
this.saveToStorage()
this.notifySubscribers()
}
}
// 清空当前对话
clearCurrentConversation() {
const currentConversation = this.getCurrentConversation()
if (currentConversation) {
currentConversation.messages = [
{
role: 'assistant',
content: `对话已清空,有什么可以帮助您的吗?`,
timestamp: new Date()
}
]
currentConversation.lastUpdate = new Date()
this.saveToStorage()
this.notifySubscribers()
}
}
// 订阅状态变化
subscribe(callback) {
this.subscribers.push(callback)
return () => {
const index = this.subscribers.indexOf(callback)
if (index !== -1) {
this.subscribers.splice(index, 1)
}
}
}
// 通知订阅者
notifySubscribers() {
this.subscribers.forEach(callback => {
callback({
conversations: this.conversations,
currentConversationId: this.currentConversationId
})
})
}
}
// 为每个页面创建独立的存储实例
export const dataAnalysisStore = new PageConversationStore('数据分析')
export const conversationStore = new PageConversationStore('智能对话')
export const writingStore = new PageConversationStore('智能写作')
export const mobileStore = new PageConversationStore('问答移动端')
export const assistantStore = new PageConversationStore('智能助理')
export default PageConversationStore
Loading…
Cancel
Save