pull/710/merge
yanyiwen0929 2025-05-16 00:07:11 +08:00 committed by GitHub
commit 9590d26dfb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 199 additions and 15 deletions

View File

@ -15,4 +15,5 @@ urlpatterns = [
url(r'^about/$', get_about), url(r'^about/$', get_about),
url(r'^push/bind/$', handle_push_bind), url(r'^push/bind/$', handle_push_bind),
url(r'^push/balance/$', handle_push_balance), url(r'^push/balance/$', handle_push_balance),
url(r'^ai_assistant/$', ai_assistant), # 新增 AI 助理接口
] ]

View File

@ -14,6 +14,9 @@ from apps.setting.models import Setting, KEYS_DEFAULT
from copy import deepcopy from copy import deepcopy
import platform import platform
import ldap import ldap
from django.http import StreamingHttpResponse
from openai import OpenAI
import json
class SettingView(AdminView): class SettingView(AdminView):
@ -146,3 +149,66 @@ def handle_push_balance(request):
return json_response(error='请先配置推送服务绑定账户') return json_response(error='请先配置推送服务绑定账户')
res = get_balance(token) res = get_balance(token)
return json_response(res) return json_response(res)
@auth('admin')
def ai_assistant(request):
"""
使用 DashScope 接入大模型通过 openai 库流式返回生成结果支持上下文对话
"""
print(request.body)
try:
# 解析请求
form = json.loads(request.body)
question = form.get('question')
context = form.get('context', [])
if not question:
return JsonResponse({"error": "请输入问题"}, status=400)
except Exception as e:
return JsonResponse({"error": f"请求解析失败:{e}"}, status=400)
api_key = "sk-d4f98b80a3064eed843aa670eee486b4"
if not api_key:
return JsonResponse({"error": "未配置 DashScope API Key请在系统设置中配置。"}, status=400)
try:
# 初始化 OpenAI 客户端
client = OpenAI(
api_key=api_key,
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1"
)
# 构建消息列表
messages = context + [
{"role": "user", "content": question}
]
# 调用 DashScope API
completion = client.chat.completions.create(
model="qwen-plus",
messages=messages,
stream=True,
stream_options={"include_usage": True}
)
# 流式返回
def stream_response():
try:
for chunk in completion:
# 参考阿里云代码,跳过 usage chunk
if chunk.choices:
delta_content = chunk.choices[0].delta.content
if delta_content: # 确保内容非空
yield delta_content
# else: usage chunk忽略
except Exception as e:
yield json.dumps({"error": f"流式响应错误:{str(e)}"})
finally:
completion.close() # 释放资源
return StreamingHttpResponse(
stream_response(),
content_type="text/plain; charset=utf-8"
)
except Exception as e:
return JsonResponse({"error": f"调用 DashScope API 失败:{e}"}, status=500)

View File

@ -5,40 +5,106 @@
*/ */
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Switch, Route } from 'react-router-dom'; import { Switch, Route } from 'react-router-dom';
import { Layout, message } from 'antd'; import { Layout, message, Drawer, Button, Input } from 'antd';
import { NotFound } from 'components'; import { NotFound } from 'components';
import Sider from './Sider'; import Sider from './Sider';
import Header from './Header'; import Header from './Header';
import Footer from './Footer' import Footer from './Footer';
import routes from '../routes'; import routes from '../routes';
import { hasPermission, isMobile } from 'libs'; import { hasPermission, isMobile } from 'libs';
import styles from './layout.module.less'; import styles from './layout.module.less';
import { RobotOutlined } from '@ant-design/icons'; // AI 助理图标
import Markdown from 'markdown-to-jsx'; // 引入 markdown-to-jsx
function initRoutes(Routes, routes) { function initRoutes(Routes, routes) {
for (let route of routes) { for (let route of routes) {
if (route.component) { if (route.component) {
if (!route.auth || hasPermission(route.auth)) { if (!route.auth || hasPermission(route.auth)) {
Routes.push(<Route exact key={route.path} path={route.path} component={route.component}/>) Routes.push(<Route exact key={route.path} path={route.path} component={route.component} />);
} }
} else if (route.child) { } else if (route.child) {
initRoutes(Routes, route.child) initRoutes(Routes, route.child);
} }
} }
} }
export default function () { export default function () {
const [collapsed, setCollapsed] = useState(false) const [collapsed, setCollapsed] = useState(false);
const [Routes, setRoutes] = useState([]); const [Routes, setRoutes] = useState([]);
const [drawerVisible, setDrawerVisible] = useState(false); // 控制抽屉显示
const [messages, setMessages] = useState([]); // 存储对话消息
const [loading, setLoading] = useState(false); // 控制加载状态
const [context, setContext] = useState([]); // 新增上下文状态
useEffect(() => { useEffect(() => {
if (isMobile) { if (isMobile) {
setCollapsed(true); setCollapsed(true);
message.warn('检测到您在移动设备上访问,请使用横屏模式。', 5) message.warn('检测到您在移动设备上访问,请使用横屏模式。', 5);
} }
const Routes = []; const Routes = [];
initRoutes(Routes, routes); initRoutes(Routes, routes);
setRoutes(Routes) setRoutes(Routes);
}, []) }, []);
const toggleDrawer = () => {
setDrawerVisible(!drawerVisible);
};
const handleSendMessage = async (value) => {
if (!value.trim()) return;
const userMessage = { role: 'user', content: value };
setMessages((prev) => [...prev, userMessage]); // 添加用户消息
setContext((prev) => [...prev, userMessage]); // 更新上下文
setLoading(true);
try {
const X_TOKEN = localStorage.getItem('token');
const response = await fetch('/api/setting/ai_assistant/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Token': X_TOKEN,
},
body: JSON.stringify({ question: value, context }),
});
if (!response.body) {
throw new Error('No response body');
}
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let done = false;
let assistantMessage = { role: 'assistant', content: '' };
while (!done) {
const { value, done: readerDone } = await reader.read();
done = readerDone;
if (value) {
const chunk = decoder.decode(value, { stream: true });
assistantMessage.content += chunk;
setMessages((prev) => {
const updatedMessages = [...prev];
const lastMessage = updatedMessages[updatedMessages.length - 1];
if (lastMessage?.role === 'assistant') {
lastMessage.content += chunk;
} else {
updatedMessages.push(assistantMessage);
}
return updatedMessages;
});
}
}
// 更新上下文
setContext((prev) => [...prev, assistantMessage]);
} catch (error) {
console.error('Error fetching AI response:', error);
message.error('AI 助理接口调用失败,请稍后重试。');
} finally {
setLoading(false);
}
};
return ( return (
<Layout> <Layout>
@ -50,9 +116,60 @@ export default function () {
{Routes} {Routes}
<Route component={NotFound} /> <Route component={NotFound} />
</Switch> </Switch>
{/* AI 助理图标 */}
<Button
type="primary"
shape="circle"
icon={<RobotOutlined />}
style={{
position: 'fixed',
bottom: 24,
right: 24,
zIndex: 1000,
}}
onClick={toggleDrawer}
/>
{/* 抽屉对话框 */}
<Drawer
title="AI 助理"
placement="right"
onClose={toggleDrawer}
visible={drawerVisible}
width={400}
>
<div style={{ height: 'calc(100% - 60px)', overflowY: 'auto', marginBottom: 16 }}>
{/* AI 对话区域 */}
{messages.map((msg, index) => (
<div
key={index}
style={{
padding: 16,
background: msg.role === 'user' ? '#e6f7ff' : '#f5f5f5',
borderRadius: 4,
marginBottom: 8,
}}
>
<p>{msg.role === 'user' ? '用户:' : 'AI 助理:'}</p>
<Markdown>{msg.content}</Markdown> {/* 使用 Markdown 渲染内容 */}
</div>
))}
{loading && (
<div style={{ padding: 16, background: '#f5f5f5', borderRadius: 4 }}>
<p>AI 助理正在输入...</p>
</div>
)}
</div>
{/* 输入框 */}
<Input.Search
placeholder="请输入内容..."
enterButton="发送"
onSearch={handleSendMessage}
disabled={loading}
/>
</Drawer>
</Layout.Content> </Layout.Content>
<Footer /> <Footer />
</Layout> </Layout>
</Layout> </Layout>
) );
} }