聊天工具优化

This commit is contained in:
2025-12-26 13:39:58 +08:00
parent f1c7639ec1
commit a4e2542b23
9 changed files with 1381 additions and 327 deletions
+7 -7
View File
@@ -2,7 +2,7 @@
# 复制此文件为 .env 并根据实际情况修改
# 服务端口
PORT=5000
PORT=15000
# 调试模式 (True/False)
DEBUG=True
@@ -10,15 +10,15 @@ DEBUG=True
# Flask密钥(生产环境请使用随机字符串)
SECRET_KEY=ai-assistant-web-client-secret-key
# API基础URL(如果需要代理到其他服务器
# API_BASE_URL=http://localhost:8082
# API后端服务地址(api模块提供HTTP接口
# 本地开发时使用80端口(Apollo配置的默认端口)
API_BASE_URL=http://localhost:80
# 认证Token(从登录获取,或使用固定token)
# AUTH_TOKEN=your_jwt_token_here
# 日志级别 (DEBUG/INFO/WARNING/ERROR)
LOG_LEVEL=INFO
# 默认应用ID
DEFAULT_APP_ID=15
# CORS允许的源(多个用逗号分隔)
# CORS_ORIGINS=http://localhost:3000,http://localhost:8080
+154
View File
@@ -0,0 +1,154 @@
# 更新日志
## [2.0.0] - 2025-12-26
### 🎉 重大更新:对接im-api后端服务
#### ✨ 新增功能
1. **应用选择界面**
- 新增应用列表展示页面
- 支持应用搜索功能
- 应用按创建时间倒排显示
- 卡片式应用展示,包含头像、名称、描述、分类
2. **后端API集成**
- 对接im-api后端聊天服务
- 支持SSE流式对话响应
- 支持推荐问题加载
- 支持多应用切换
3. **代理层实现**
- Flask代理层转发请求到im-api
- SSE流式响应转发
- 统一错误处理
- CORS支持
#### 🔄 功能变更
1. **移除左侧面板**
- 移除用户信息面板
- 移除待办事项功能
- 移除提醒设置功能
- 简化界面,专注对话功能
2. **对话界面优化**
- 添加返回按钮,可返回应用列表
- 顶部显示当前应用信息
- 动态加载推荐问题
- 优化欢迎消息显示
3. **API接口调整**
- `/api/applications``/api/ai-assistant/chatapp`
- `/api/chat/send``/api/ai-assistant/chat/completions/message`
- 新增 `/api/ai-assistant/chatapp/{appId}/getRecommendQuestion`
#### 🛠️ 技术改进
1. **前端优化**
- 重构JavaScript代码结构
- 添加应用选择逻辑
- 实现SSE流式响应处理
- 优化错误处理和提示
2. **后端优化**
- 使用requests库进行HTTP代理
- 实现SSE流式转发
- 添加健康检查接口
- 优化日志记录
3. **配置管理**
- 新增IM_API_BASE_URL配置
- 新增AUTH_TOKEN配置
- 更新环境变量示例
- 优化启动脚本
#### 📝 文档更新
1. **新增文档**
- `INTEGRATION_GUIDE.md` - 后端集成指南
- `CHANGELOG.md` - 更新日志
2. **更新文档**
- `.env.example` - 环境配置示例
- `start.sh` - 启动脚本
- `README.md` - 项目说明
#### 🐛 Bug修复
- 修复消息发送后输入框未清空的问题
- 修复打字指示器未正确移除的问题
- 修复滚动到底部的时机问题
#### ⚠️ 破坏性变更
1. **API变更**
- 旧的API接口已废弃
- 需要配置im-api后端地址
- 可能需要配置认证token
2. **界面变更**
- 移除了左侧用户信息面板
- 初始界面改为应用选择
- 需要先选择应用才能对话
#### 🔧 迁移指南
从1.0版本升级到2.0版本:
1. **更新配置文件**
```bash
cp .env.example .env
# 编辑.env,配置IM_API_BASE_URL
```
2. **确保后端服务运行**
```bash
# 确保im-api服务在http://localhost:8080运行
curl http://localhost:8080/api/ai-assistant/chatapp
```
3. **重启服务**
```bash
./start.sh
```
4. **访问新界面**
```
打开 http://localhost:15001
首先选择一个应用,然后开始对话
```
#### 📊 性能改进
- SSE流式响应,实时显示AI回复
- 优化应用列表加载速度
- 减少不必要的API请求
#### 🎯 下一步计划
- [ ] 集成真实的登录认证系统
- [ ] 支持文件上传功能
- [ ] 支持语音输入
- [ ] 支持多会话管理
- [ ] 支持历史记录查看
- [ ] 添加请求缓存
- [ ] 优化SSE重连机制
---
## [1.0.0] - 2025-12-25
### 初始版本
- ✅ 100%还原原型设计
- ✅ 粒子背景动画
- ✅ 玻璃态设计
- ✅ 消息气泡动画
- ✅ 打字指示器
- ✅ 快捷回复
- ✅ 主题切换
- ✅ 字体选择
- ✅ 待办事项管理
- ✅ 模拟API响应
+244
View File
@@ -0,0 +1,244 @@
# AI助手Web客户端 - 后端集成指南
## 📋 概述
本Web客户端已成功对接im-api后端聊天服务,支持:
- ✅ 应用列表加载(支持搜索、按创建时间倒排)
- ✅ 应用选择
- ✅ SSE流式对话
- ✅ 推荐问题加载
- ✅ 完整的对话交互
## 🔧 配置说明
### 1. 环境变量配置
复制 `.env.example``.env` 并修改:
```bash
# im-api后端服务地址
IM_API_BASE_URL=http://localhost:8080
# 认证Token(可选,如果后端需要认证)
AUTH_TOKEN=your_jwt_token_here
# 服务端口
PORT=15000
```
### 2. 后端API要求
确保im-api后端服务已启动,并提供以下接口:
#### 2.1 获取应用列表
```
GET /api/ai-assistant/chatapp
```
响应格式:
```json
{
"code": 200,
"message": "success",
"data": [
{
"id": "app_id",
"appName": "应用名称",
"appDescription": "应用描述",
"appAvatar": "头像URL",
"category": "分类",
"sortNum": 1,
"enableRecommendation": true,
"conversationStarter": "欢迎语",
"reasoningEnable": false
}
]
}
```
#### 2.2 获取推荐问题
```
GET /api/ai-assistant/chatapp/{appId}/getRecommendQuestion?pageSize=3&current=1
```
响应格式:
```json
{
"code": 200,
"data": {
"records": [
{
"question": "推荐问题1"
}
]
}
}
```
#### 2.3 发送消息(SSE流式)
```
POST /api/ai-assistant/chat/completions/message
Content-Type: application/json
```
请求体:
```json
{
"chatId": "chat_id_or_null",
"appId": "app_id",
"equipment": "web",
"messageTag": "AI_TAG",
"body": {
"messages": [
{
"role": "user",
"content": "用户消息"
}
],
"channel": "web",
"attachmentIds": [],
"recommendQuestions": [],
"variables": {},
"reasoning": "false"
}
}
```
SSE响应格式:
```
event: MESSAGE_DETAIL
data: {
"detailId": "detail_id",
"chatId": "chat_id",
"question": {
"content": "用户问题",
"role": "user"
},
"answer": {
"content": "AI回答",
"role": "assistant"
},
"status": "PROCESSING|FINISH|ERROR"
}
```
## 🚀 启动步骤
### 1. 安装依赖
```bash
cd web_client
pip3 install -r requirements.txt
```
### 2. 配置环境变量
```bash
# 复制配置文件
cp .env.example .env
# 编辑配置
vim .env
```
### 3. 启动服务
```bash
# 使用启动脚本
./start.sh
# 或直接运行
python3 app.py
```
### 4. 访问应用
打开浏览器访问:http://localhost:15000
## 📝 使用流程
1. **选择应用**
- 页面加载后自动显示应用列表
- 可以使用搜索框搜索应用
- 点击应用卡片进入对话
2. **开始对话**
- 进入对话界面后显示欢迎消息
- 如果应用启用了推荐问题,会显示快捷回复按钮
- 在输入框输入消息,按Enter或点击发送按钮
3. **查看回复**
- AI回复以流式方式实时显示
- 支持打字指示器动画
- 自动滚动到最新消息
4. **返回应用列表**
- 点击左上角返回按钮
- 可以切换到其他应用
## 🔍 故障排查
### 问题1:应用列表加载失败
**原因**:后端服务未启动或地址配置错误
**解决**
1. 检查im-api服务是否启动
2. 检查 `.env` 中的 `IM_API_BASE_URL` 配置
3. 查看浏览器控制台和后端日志
### 问题2:消息发送失败
**原因**:认证失败或SSE连接问题
**解决**
1. 检查是否需要配置 `AUTH_TOKEN`
2. 检查浏览器是否支持SSE
3. 查看网络请求详情
### 问题3:推荐问题不显示
**原因**:应用未启用推荐或后端接口返回空
**解决**
1. 检查应用的 `enableRecommendation` 字段
2. 检查后端推荐问题接口是否正常
## 📊 技术架构
```
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ 浏览器 │ ───> │ Flask代理层 │ ───> │ im-api后端 │
│ (前端UI) │ <─── │ (web_client)│ <─── │ (聊天服务) │
└─────────────┘ └──────────────┘ └─────────────┘
```
### 代理层功能
1. **请求转发**:将前端请求代理到im-api后端
2. **SSE流式转发**:支持Server-Sent Events流式响应
3. **错误处理**:统一的错误处理和日志记录
4. **CORS支持**:解决跨域问题
## 🎯 下一步优化
1. **认证集成**
- 集成真实的登录系统
- 从登录获取JWT token
- 支持token刷新
2. **功能增强**
- 支持文件上传
- 支持语音输入
- 支持多会话管理
- 支持历史记录
3. **性能优化**
- 添加请求缓存
- 优化SSE连接管理
- 添加重连机制
## 📞 联系方式
如有问题,请联系:huazm@glodon.com
Binary file not shown.
+158 -87
View File
@@ -1,27 +1,17 @@
"""
AI助手Web客户端 - Flask应用
100%还原docs/ai-assistant.html原型设计
对接im-api后端聊天服务
@author huazm
"""
import os
import sys
import json
import logging
from flask import Flask, render_template, request, jsonify, Response
import requests
from flask import Flask, render_template, request, jsonify, Response, stream_with_context
from flask_cors import CORS
# 添加父目录到路径,以便导入api_client
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
try:
from api_client import AIAssistantClient
from config import APPLICATIONS, AI_ASSISTANT_CONFIG
except ImportError:
# 如果导入失败,使用模拟数据
AIAssistantClient = None
APPLICATIONS = {}
AI_ASSISTANT_CONFIG = {}
# 配置日志
logging.basicConfig(
level=logging.INFO,
@@ -34,14 +24,14 @@ app = Flask(__name__)
app.config['SECRET_KEY'] = 'ai-assistant-web-client-secret-key'
CORS(app) # 启用CORS
# 初始化API客户端
api_client = None
if AIAssistantClient:
try:
api_client = AIAssistantClient()
logger.info("API客户端初始化成功")
except Exception as e:
logger.error(f"API客户端初始化失败: {e}")
# API后端地址配置(api模块提供HTTP接口)
API_BASE_URL = os.environ.get('API_BASE_URL', 'http://localhost:18090')
# api模块的接口路径前缀是/api/ai-assistant
API_PREFIX = '/api/ai-assistant'
# 认证token(实际使用时应该从登录获取)
# 这里暂时使用环境变量或固定值
AUTH_TOKEN = os.environ.get('AUTH_TOKEN', '')
@app.route('/')
@@ -50,81 +40,148 @@ def index():
return render_template('index.html')
@app.route('/api/applications', methods=['GET'])
def get_auth_headers():
"""
获取认证请求头
从前端请求中获取Authorization头并传递给后端
"""
headers = {
'Content-Type': 'application/json'
}
# 优先使用前端传递的Authorization头
auth_header = request.headers.get('Authorization')
if auth_header:
headers['Authorization'] = auth_header
elif AUTH_TOKEN:
# 如果前端没有传递,使用环境变量中的token
headers['Authorization'] = f'Bearer {AUTH_TOKEN}'
return headers
@app.route('/api/ai-assistant/chatapp', methods=['GET'])
def get_applications():
"""获取应用列表"""
"""
代理获取应用列表请求到api后端
"""
try:
if api_client:
apps = api_client.get_app_list()
return jsonify({
'code': 200,
'message': 'success',
'data': apps
})
else:
# 返回模拟数据
return jsonify({
'code': 200,
'message': 'success',
'data': list(APPLICATIONS.values()) if APPLICATIONS else []
})
url = f'{API_BASE_URL}{API_PREFIX}/chatapp'
logger.info(f"代理请求: GET {url}")
response = requests.get(
url,
headers=get_auth_headers(),
timeout=30
)
logger.info(f"响应状态码: {response.status_code}")
# 直接返回后端响应
return Response(
response.content,
status=response.status_code,
content_type=response.headers.get('Content-Type', 'application/json')
)
except requests.exceptions.RequestException as e:
logger.error(f"代理请求失败: {e}")
return jsonify({
'code': 500,
'message': f'代理请求失败: {str(e)}',
'data': None
}), 500
except Exception as e:
logger.error(f"获取应用列表失败: {e}")
return jsonify({
'code': 500,
'message': str(e),
'data': []
'data': None
}), 500
@app.route('/api/chat/send', methods=['POST'])
@app.route('/api/ai-assistant/chatapp/<app_id>/getRecommendQuestion', methods=['GET'])
def get_recommend_questions(app_id):
"""
代理获取推荐问题请求到api后端
"""
try:
# 获取查询参数
page_size = request.args.get('pageSize', '10')
current = request.args.get('current', '1')
url = f'{API_BASE_URL}{API_PREFIX}/chatapp/{app_id}/getRecommendQuestion'
params = {
'pageSize': page_size,
'current': current
}
logger.info(f"代理请求: GET {url} with params {params}")
response = requests.get(
url,
params=params,
headers=get_auth_headers(),
timeout=30
)
return Response(
response.content,
status=response.status_code,
content_type=response.headers.get('Content-Type', 'application/json')
)
except Exception as e:
logger.error(f"获取推荐问题失败: {e}")
return jsonify({
'code': 500,
'message': str(e),
'data': None
}), 500
@app.route('/api/ai-assistant/chat/completions/message', methods=['POST'])
def send_message():
"""发送聊天消息"""
"""
代理发送消息请求到api后端(SSE流式响应)
"""
try:
data = request.get_json()
app_id = data.get('appId')
message = data.get('message')
chat_id = data.get('chatId')
stream = data.get('stream', False)
if not message:
return jsonify({
'code': 400,
'message': '消息内容不能为空',
'data': None
}), 400
if api_client and app_id:
# 调用真实API
response = api_client.send_message(
app_id=app_id,
message=message,
chat_id=chat_id,
stream=stream
)
if stream:
# 流式响应
def generate():
for chunk in response:
yield f"data: {json.dumps(chunk, ensure_ascii=False)}\n\n"
yield "data: [DONE]\n\n"
return Response(generate(), mimetype='text/event-stream')
else:
# 普通响应
return jsonify(response)
else:
# 返回模拟响应
return jsonify({
'code': 200,
'message': 'success',
'data': {
'answer': f'这是对"{message}"的模拟回复。实际使用时会调用真实的AI助手API。',
'chatId': chat_id or 'mock_chat_id_001',
'messageId': 'mock_message_id_001'
}
})
url = f'{API_BASE_URL}{API_PREFIX}/chat/completions/message'
logger.info(f"代理SSE请求: POST {url}")
logger.info(f"请求体: {json.dumps(data, ensure_ascii=False)}")
# 发送POST请求并流式读取响应
response = requests.post(
url,
json=data,
headers=get_auth_headers(),
stream=True,
timeout=300 # 5分钟超时
)
# 流式转发SSE响应
def generate():
try:
for line in response.iter_lines():
if line:
decoded_line = line.decode('utf-8')
logger.debug(f"SSE数据: {decoded_line}")
yield f"{decoded_line}\n"
except Exception as e:
logger.error(f"流式响应错误: {e}")
yield f"event: error\ndata: {json.dumps({'error': str(e)})}\n\n"
return Response(
stream_with_context(generate()),
mimetype='text/event-stream',
headers={
'Cache-Control': 'no-cache',
'X-Accel-Buffering': 'no'
}
)
except Exception as e:
logger.error(f"发送消息失败: {e}")
return jsonify({
@@ -136,19 +193,33 @@ def send_message():
@app.route('/api/health', methods=['GET'])
def health_check():
"""健康检查"""
"""
健康检查
"""
# 检查api后端是否可用
api_available = False
try:
response = requests.get(
f'{API_BASE_URL}/actuator/health',
timeout=5
)
api_available = response.status_code == 200
except:
pass
return jsonify({
'code': 200,
'message': 'OK',
'data': {
'status': 'healthy',
'api_client_available': api_client is not None
'api_available': api_available,
'api_url': API_BASE_URL
}
})
if __name__ == '__main__':
port = int(os.environ.get('PORT', 5000))
port = int(os.environ.get('PORT', 15000))
debug = os.environ.get('DEBUG', 'True').lower() == 'true'
logger.info(f"启动AI助手Web客户端,端口: {port}, 调试模式: {debug}")
+9 -2
View File
@@ -41,10 +41,17 @@ echo "========================================="
echo " 启动Web服务器..."
echo "========================================="
echo ""
echo "🌐 访问地址: http://localhost:5000"
# 读取配置
PORT=${PORT:-15000}
API_BASE_URL=${API_BASE_URL:-http://localhost:8080}
echo "🌐 访问地址: http://localhost:$PORT"
echo "🔗 后端API: $API_BASE_URL"
echo "📝 按 Ctrl+C 停止服务"
echo ""
# 启动Flask应用
export PORT=$PORT
export API_BASE_URL=$API_BASE_URL
python3 app.py
+550 -113
View File
@@ -1,15 +1,102 @@
/**
* AI助手Web客户端 - 前端交互逻辑
* 100%还原原型设计的交互效果
* 对接im-api后端聊天服务
* @author huazm
*/
// 全局变量
let currentChatId = null;
let currentAppId = 15; // 默认使用人岗匹配应用
let currentAppId = null;
let currentAppInfo = null;
let allApps = [];
let isTyping = false;
let abortController = null; // 用于取消fetch请求
let authToken = null; // 认证token
// API配置
const API_BASE_URL = ''; // 使用相对路径
const API_BASE_URL = '/api/ai-assistant'; // api后端地址
/**
* Token管理功能
*/
// 打开Token设置模态框
function openTokenModal() {
const modal = document.getElementById('token-modal');
const input = document.getElementById('token-input');
const status = document.getElementById('token-status-modal');
// 加载已保存的token
const savedToken = localStorage.getItem('authToken');
if (savedToken) {
input.value = savedToken;
status.textContent = '已设置';
status.className = 'font-medium text-green-400';
} else {
input.value = '';
status.textContent = '未设置';
status.className = 'font-medium text-slate-400';
}
modal.classList.remove('hidden');
}
// 关闭Token设置模态框
function closeTokenModal() {
const modal = document.getElementById('token-modal');
modal.classList.add('hidden');
}
// 保存Token
function saveToken() {
const input = document.getElementById('token-input');
const token = input.value.trim();
if (token) {
// 保存到localStorage
localStorage.setItem('authToken', token);
authToken = token;
// 更新状态显示
const statusModal = document.getElementById('token-status-modal');
statusModal.textContent = '已设置';
statusModal.className = 'font-medium text-green-400';
console.log('Token已保存');
// 关闭模态框
closeTokenModal();
// 重新加载应用列表
loadApplications();
} else {
alert('请输入有效的Token');
}
}
// 获取认证请求头
function getAuthHeaders() {
const headers = {
'Content-Type': 'application/json'
};
// 从localStorage加载token
const savedToken = localStorage.getItem('authToken');
if (savedToken) {
authToken = savedToken;
headers['Authorization'] = `Bearer ${savedToken}`;
}
return headers;
}
// 页面加载时初始化token
function initToken() {
const savedToken = localStorage.getItem('authToken');
if (savedToken) {
authToken = savedToken;
console.log('已加载保存的Token');
}
}
/**
* 创建动态粒子背景
@@ -17,34 +104,279 @@ const API_BASE_URL = ''; // 使用相对路径
function createParticles() {
const particlesContainer = document.getElementById('particles');
const particleCount = 50;
for (let i = 0; i < particleCount; i++) {
const particle = document.createElement('div');
particle.className = 'particle';
// 随机位置和大小
const size = Math.random() * 4 + 1;
const posX = Math.random() * 100;
const posY = Math.random() * 100;
particle.style.width = `${size}px`;
particle.style.height = `${size}px`;
particle.style.left = `${posX}%`;
particle.style.top = `${posY}%`;
// 随机动画延迟
particle.style.animationDelay = `${Math.random() * 5}s`;
particlesContainer.appendChild(particle);
}
}
/**
* 加载应用列表
*/
async function loadApplications() {
try {
const response = await fetch(`${API_BASE_URL}/chatapp`, {
method: 'GET',
headers: getAuthHeaders()
});
const result = await response.json();
console.log('API返回数据:', result);
// 兼容两种数据格式:result.data 或 result.result
const apps = result.data || result.result;
if (result.code === 200 && apps) {
allApps = apps;
// 按sortNum倒排
allApps.sort((a, b) => {
if (b.sortNum !== undefined && a.sortNum !== undefined) {
return a.sortNum - b.sortNum; // 从小到大排序
}
return 0;
});
renderApplications(allApps);
} else {
showError('加载应用列表失败');
}
} catch (error) {
console.error('加载应用列表失败:', error);
showError('加载应用列表失败: ' + error.message);
}
}
/**
* 渲染应用列表
*/
function renderApplications(apps) {
const appList = document.getElementById('app-list');
if (!apps || apps.length === 0) {
appList.innerHTML = `
<div class="text-center text-slate-400 py-12 col-span-full">
<i class="fas fa-inbox text-4xl mb-4"></i>
<p>暂无可用的智能体</p>
</div>
`;
return;
}
appList.innerHTML = apps.map(app => `
<div class="app-card glass-effect bg-slate-800/50 p-6 rounded-xl border border-slate-700 hover:border-primary transition-all duration-300 cursor-pointer group"
data-app-id="${app.id}"
onclick="selectApplication('${app.id}')">
<div class="flex items-start space-x-4">
<div class="w-12 h-12 rounded-full bg-gradient-to-r from-primary to-secondary flex items-center justify-center text-white flex-shrink-0">
${app.appAvatar ?
`<img src="${app.appAvatar}" alt="${app.appName}" class="w-full h-full rounded-full object-cover">` :
`<i class="fas fa-robot"></i>`
}
</div>
<div class="flex-1 min-w-0">
<h3 class="text-white font-semibold text-lg mb-1 truncate group-hover:text-primary transition-colors">
${app.appName || '未命名应用'}
</h3>
<p class="text-slate-400 text-sm line-clamp-2 mb-2">
${app.appDescription || app.scopeDescription || '暂无描述'}
</p>
${app.category ? `
<span class="inline-block px-2 py-1 bg-primary/20 text-primary text-xs rounded-full">
${app.category}
</span>
` : ''}
</div>
</div>
</div>
`).join('');
}
/**
* 搜索应用
*/
function searchApplications(keyword) {
if (!keyword.trim()) {
renderApplications(allApps);
return;
}
const filtered = allApps.filter(app => {
const searchText = keyword.toLowerCase();
return (app.appName && app.appName.toLowerCase().includes(searchText)) ||
(app.appDescription && app.appDescription.toLowerCase().includes(searchText)) ||
(app.scopeDescription && app.scopeDescription.toLowerCase().includes(searchText)) ||
(app.category && app.category.toLowerCase().includes(searchText));
});
renderApplications(filtered);
}
/**
* 选择应用并进入对话
*/
async function selectApplication(appId) {
try {
// 查找应用信息
currentAppInfo = allApps.find(app => app.id === appId);
if (!currentAppInfo) {
showError('应用信息不存在');
return;
}
currentAppId = appId;
currentChatId = null; // 重置聊天ID,开始新对话
// 隐藏应用选择面板,显示对话面板
document.getElementById('app-selector-panel').classList.add('hidden');
document.getElementById('chat-panel').classList.remove('hidden');
// 更新顶部应用信息
updateCurrentAppInfo();
// 清空对话内容
clearChatContainer();
// 显示欢迎消息
showWelcomeMessage();
// 加载快捷回复(如果有)
loadQuickReplies();
} catch (error) {
console.error('选择应用失败:', error);
showError('选择应用失败: ' + error.message);
}
}
/**
* 更新当前应用信息显示
*/
function updateCurrentAppInfo() {
if (!currentAppInfo) return;
const appAvatar = document.getElementById('current-app-avatar');
const appName = document.getElementById('current-app-name');
const appDesc = document.getElementById('current-app-desc');
if (currentAppInfo.appAvatar) {
appAvatar.src = currentAppInfo.appAvatar;
appAvatar.style.display = 'block';
} else {
appAvatar.style.display = 'none';
}
appName.textContent = currentAppInfo.appName || 'AI 助手';
appDesc.textContent = currentAppInfo.appDescription || currentAppInfo.scopeDescription || '';
}
/**
* 清空对话容器
*/
function clearChatContainer() {
const chatContainer = document.getElementById('chat-container');
chatContainer.innerHTML = '';
}
/**
* 显示欢迎消息
*/
function showWelcomeMessage() {
const chatContainer = document.getElementById('chat-container');
const welcomeMsg = currentAppInfo.conversationStarter || '你好!我是' + (currentAppInfo.appName || 'AI助手') + ',有什么可以帮助你的吗?';
chatContainer.innerHTML = `
<div class="flex justify-start slide-in">
<div class="max-w-xs lg:max-w-md ai-bubble text-white p-4 rounded-2xl rounded-bl-md">
<p>${welcomeMsg}</p>
</div>
</div>
`;
}
/**
* 加载快捷回复
*/
async function loadQuickReplies() {
const quickReplyContainer = document.getElementById('quick-reply-container');
const quickReplyButtons = document.getElementById('quick-reply-buttons');
// 如果应用启用了推荐问题
if (currentAppInfo.enableRecommendation) {
try {
const response = await fetch(`${API_BASE_URL}/chatapp/${currentAppId}/getRecommendQuestion?pageSize=3&current=1`, {
method: 'GET',
headers: getAuthHeaders()
});
const result = await response.json();
if (result.code === 200 && result.data && result.data.records && result.data.records.length > 0) {
quickReplyButtons.innerHTML = result.data.records.map(q => `
<button class="quick-reply-btn px-4 py-2 bg-slate-700 text-slate-300 rounded-lg text-sm hover:bg-slate-600 transition whitespace-nowrap"
onclick="sendQuickReply('${q.question.replace(/'/g, "\\'")}')">
<i class="fas fa-lightbulb mr-2"></i>${q.question}
</button>
`).join('');
quickReplyContainer.classList.remove('hidden');
} else {
quickReplyContainer.classList.add('hidden');
}
} catch (error) {
console.error('加载推荐问题失败:', error);
quickReplyContainer.classList.add('hidden');
}
} else {
quickReplyContainer.classList.add('hidden');
}
}
/**
* 返回应用选择
*/
function backToApps() {
// 取消正在进行的请求
if (abortController) {
abortController.abort();
abortController = null;
}
// 重置状态
currentAppId = null;
currentAppInfo = null;
currentChatId = null;
// 切换面板
document.getElementById('chat-panel').classList.add('hidden');
document.getElementById('app-selector-panel').classList.remove('hidden');
}
/**
* 添加消息到聊天容器
*/
function addMessage(message, isUser = true) {
const chatContainer = document.getElementById('chat-container');
// 移除欢迎消息
const welcomeDiv = chatContainer.querySelector('.flex.justify-center');
if (welcomeDiv) {
welcomeDiv.remove();
}
// 移除打字指示器
removeTypingIndicator();
@@ -119,62 +451,212 @@ function escapeHtml(text) {
}
/**
* 发送消息
* 发送消息(使用SSE流式响应)
*/
async function sendMessage() {
const input = document.getElementById('message-input');
const message = input.value.trim();
if (!message) return;
if (!message || !currentAppId) return;
// 添加用户消息
addMessage(message, true);
input.value = '';
// 显示打字指示器
showTypingIndicator();
try {
// 调用API
const response = await fetch(`${API_BASE_URL}/api/chat/send`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
appId: currentAppId,
message: message,
chatId: currentChatId,
stream: false
})
});
const data = await response.json();
// 移除打字指示器
removeTypingIndicator();
if (data.code === 200 && data.data) {
// 添加AI回复
addMessage(data.data.answer, false);
// 更新chatId
if (data.data.chatId) {
currentChatId = data.data.chatId;
// 构建请求体
const requestBody = {
chatId: currentChatId,
appId: currentAppId,
equipment: 'web',
messageTag: 'AI_TAG',
body: {
messages: [
{
role: 'user',
content: message
}
],
channel: 'web',
attachmentIds: [],
recommendQuestions: [],
variables: {},
reasoning: currentAppInfo.reasoningEnable ? 'true' : 'false'
}
} else {
addMessage(`错误: ${data.message || '未知错误'}`, false);
};
// 使用fetch进行SSE连接(支持自定义请求头)
const url = `${API_BASE_URL}/chat/completions/message`;
// 取消之前的请求
if (abortController) {
abortController.abort();
}
// 创建新的AbortController
abortController = new AbortController();
// 使用fetch发送POST请求,处理流式响应
const response = await fetch(url, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(requestBody),
signal: abortController.signal
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
let aiMessage = '';
let messageDiv = null;
// 读取流式响应
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
// 解码数据
buffer += decoder.decode(value, { stream: true });
// 按行分割
const lines = buffer.split('\n');
buffer = lines.pop() || ''; // 保留最后一个不完整的行
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.substring(6).trim();
if (data === '[DONE]') {
continue;
}
try {
const json = JSON.parse(data);
// 处理不同类型的事件
if (json.detailId) {
// 保存chatId
if (json.chatId && !currentChatId) {
currentChatId = json.chatId;
}
// 更新AI回复
if (json.answer && json.answer.content) {
aiMessage = json.answer.content;
// 移除打字指示器
removeTypingIndicator();
// 更新或创建消息气泡
if (!messageDiv) {
messageDiv = createAIMessageBubble(aiMessage);
} else {
updateAIMessageBubble(messageDiv, aiMessage);
}
}
// 检查是否完成
if (json.status === 'FINISH' || json.status === 'ERROR') {
if (json.status === 'ERROR') {
showError('AI回复出错');
}
break;
}
}
} catch (parseError) {
console.error('解析SSE数据失败:', parseError);
}
}
}
}
// 确保移除打字指示器
removeTypingIndicator();
} catch (error) {
console.error('发送消息失败:', error);
removeTypingIndicator();
addMessage('抱歉,发送消息失败,请稍后重试。', false);
showError('发送消息失败: ' + error.message);
}
}
/**
* 创建AI消息气泡
*/
function createAIMessageBubble(content) {
const chatContainer = document.getElementById('chat-container');
const messageDiv = document.createElement('div');
messageDiv.className = 'flex justify-start slide-in';
messageDiv.innerHTML = `
<div class="max-w-xs lg:max-w-md ai-bubble text-white p-4 rounded-2xl rounded-bl-md">
<p class="ai-message-content">${escapeHtml(content)}</p>
</div>
`;
chatContainer.appendChild(messageDiv);
scrollToBottom();
return messageDiv;
}
/**
* 更新AI消息气泡
*/
function updateAIMessageBubble(messageDiv, content) {
const contentElement = messageDiv.querySelector('.ai-message-content');
if (contentElement) {
contentElement.innerHTML = escapeHtml(content);
scrollToBottom();
}
}
/**
* 显示错误消息
*/
function showError(message) {
const chatContainer = document.getElementById('chat-container');
const errorDiv = document.createElement('div');
errorDiv.className = 'flex justify-center my-4';
errorDiv.innerHTML = `
<div class="bg-red-500/20 border border-red-500 text-red-300 px-4 py-2 rounded-lg">
<i class="fas fa-exclamation-circle mr-2"></i>${escapeHtml(message)}
</div>
`;
chatContainer.appendChild(errorDiv);
scrollToBottom();
// 3秒后自动移除
setTimeout(() => {
errorDiv.remove();
}, 3000);
}
/**
* 快捷回复按钮点击
*/
function sendQuickReply(text) {
const input = document.getElementById('message-input');
input.value = text;
sendMessage();
}
/**
* 快捷回复填充(不发送)
*/
function handleQuickReply(text) {
const input = document.getElementById('message-input');
input.value = text;
@@ -217,35 +699,8 @@ function changeFont(font) {
*/
function startNewChat() {
currentChatId = null;
const chatContainer = document.getElementById('chat-container');
// 清空聊天记录(保留欢迎消息)
const messages = chatContainer.querySelectorAll('.slide-in');
messages.forEach(msg => msg.remove());
// 添加欢迎消息
addMessage('您好!我是AI助手,有什么可以帮助您的吗?', false);
}
/**
* 待办事项管理
*/
function addTodoItem() {
const input = document.getElementById('todo-input');
const text = input.value.trim();
if (!text) return;
const todoList = document.getElementById('todo-list');
const todoItem = document.createElement('div');
todoItem.className = 'flex items-center space-x-2 p-2 bg-slate-800/30 rounded-lg';
todoItem.innerHTML = `
<input type="checkbox" class="rounded text-primary">
<span class="text-slate-300 text-sm">${escapeHtml(text)}</span>
`;
todoList.appendChild(todoItem);
input.value = '';
clearChatContainer();
showWelcomeMessage();
}
/**
@@ -257,6 +712,23 @@ document.addEventListener('DOMContentLoaded', function() {
// 创建粒子背景
createParticles();
// 加载应用列表
loadApplications();
// 绑定应用搜索
const searchInput = document.getElementById('app-search-input');
if (searchInput) {
searchInput.addEventListener('input', function(e) {
searchApplications(e.target.value);
});
}
// 绑定返回按钮
const backButton = document.getElementById('back-to-apps-btn');
if (backButton) {
backButton.addEventListener('click', backToApps);
}
// 绑定发送按钮事件
const sendButton = document.getElementById('send-button');
if (sendButton) {
@@ -274,27 +746,7 @@ document.addEventListener('DOMContentLoaded', function() {
});
}
// 绑定快捷回复按钮
const quickReplyButtons = document.querySelectorAll('.quick-reply-btn');
quickReplyButtons.forEach(btn => {
btn.addEventListener('click', function() {
const text = this.textContent.trim();
// 移除图标文本
const cleanText = text.replace(/[💡📅✅]/g, '').trim();
handleQuickReply(cleanText);
});
});
// 绑定主题切换按钮
const themeButtons = document.querySelectorAll('.theme-btn');
themeButtons.forEach(btn => {
btn.addEventListener('click', function() {
const theme = this.dataset.theme;
changeTheme(theme);
});
});
// 绑定字体选择
// 绑定字体切换
const fontSelect = document.getElementById('font-select');
if (fontSelect) {
fontSelect.addEventListener('change', function() {
@@ -302,23 +754,8 @@ document.addEventListener('DOMContentLoaded', function() {
});
}
// 绑定待办事项添加
const todoAddButton = document.getElementById('todo-add-btn');
const todoInput = document.getElementById('todo-input');
if (todoAddButton && todoInput) {
todoAddButton.addEventListener('click', addTodoItem);
todoInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
addTodoItem();
}
});
}
// 添加欢迎消息
setTimeout(() => {
addMessage('您好!我是AI助手,有什么可以帮助您的吗?', false);
}, 500);
});
// 初始化Token
initToken();
console.log('AI助手Web客户端初始化完成');
});
+108 -118
View File
@@ -45,152 +45,100 @@
<div class="particles" id="particles"></div>
<div class="flex h-screen">
<!-- 左侧用户信息面板 -->
<div class="w-80 bg-slate-900/80 glass-effect border-r border-slate-700 flex flex-col">
<div class="p-6 border-b border-slate-700">
<h2 class="text-xl font-bold text-white mb-4">用户画像</h2>
<div class="flex items-center space-x-3 mb-4">
<div class="w-16 h-16 rounded-full bg-gradient-to-r from-primary to-secondary flex items-center justify-center text-white text-xl font-bold">
U
</div>
<div>
<p class="text-white font-medium">张伟明</p>
<p class="text-slate-400 text-sm">AI 助手用户</p>
</div>
<!-- 应用选择面板(初始显示) -->
<div id="app-selector-panel" class="w-full bg-slate-900/80 glass-effect flex flex-col">
<div class="p-6 border-b border-slate-700 flex justify-between items-center">
<div>
<h1 class="text-3xl font-bold text-white mb-2 font-['Pacifico']">AI 助手</h1>
<p class="text-slate-400">选择一个智能体开始对话</p>
</div>
<!-- 个性化推荐 -->
<div class="mt-4 p-3 bg-slate-800/50 rounded-lg">
<h3 class="text-sm font-medium text-white mb-2">个性化推荐</h3>
<p class="text-slate-300 text-xs">根据您的使用习惯,推荐以下功能</p>
<div class="mt-2 flex flex-wrap gap-1">
<span class="px-2 py-1 bg-primary/20 text-primary text-xs rounded-full">智能问答</span>
<span class="px-2 py-1 bg-secondary/20 text-secondary text-xs rounded-full">语音识别</span>
<span class="px-2 py-1 bg-blue-500/20 text-blue-400 text-xs rounded-full">任务管理</span>
</div>
</div>
</div>
<!-- 待办事项 -->
<div class="p-6 border-b border-slate-700">
<h3 class="text-lg font-semibold text-white mb-4">待办事项</h3>
<div class="space-y-3" id="todo-list">
<div class="flex items-center space-x-2 p-2 bg-slate-800/30 rounded-lg">
<input type="checkbox" class="rounded text-primary">
<span class="text-slate-300 text-sm">完成项目报告</span>
</div>
<div class="flex items-center space-x-2 p-2 bg-slate-800/30 rounded-lg">
<input type="checkbox" class="rounded text-primary">
<span class="text-slate-300 text-sm">准备会议材料</span>
</div>
<div class="flex items-center space-x-2 p-2 bg-slate-800/30 rounded-lg">
<input type="checkbox" class="rounded text-primary">
<span class="text-slate-300 text-sm">回复客户邮件</span>
</div>
</div>
<div class="mt-3 flex">
<input type="text" id="todo-input" placeholder="添加新任务..." class="flex-1 bg-slate-800 text-white text-sm px-3 py-2 rounded-l-lg border border-slate-600 focus:outline-none focus:border-primary">
<button id="todo-add-btn" class="bg-primary text-white px-3 py-2 rounded-r-lg hover:bg-primary/80 transition">
<i class="fas fa-plus"></i>
</button>
</div>
</div>
<!-- 提醒设置 -->
<div class="p-6 flex-1">
<h3 class="text-lg font-semibold text-white mb-4">提醒设置</h3>
<div class="space-y-3">
<div class="p-3 bg-slate-800/30 rounded-lg">
<p class="text-slate-300 text-sm">每日 9:00 AM</p>
<p class="text-white text-sm">晨会准备</p>
</div>
<div class="p-3 bg-slate-800/30 rounded-lg">
<p class="text-slate-300 text-sm">每周三 2:00 PM</p>
<p class="text-white text-sm">团队同步会议</p>
</div>
<div class="p-3 bg-slate-800/30 rounded-lg">
<p class="text-slate-300 text-sm">每月 15 日</p>
<p class="text-white text-sm">月度总结报告</p>
</div>
</div>
<button class="w-full mt-4 bg-slate-700 text-white py-2 rounded-lg hover:bg-slate-600 transition flex items-center justify-center space-x-2">
<i class="fas fa-bell"></i>
<span>设置新提醒</span>
<!-- 设置按钮 -->
<button
onclick="openTokenModal()"
class="px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded-md transition-colors flex items-center gap-2"
title="设置认证Token"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
设置
</button>
</div>
<!-- 搜索框 -->
<div class="p-6 border-b border-slate-700 flex justify-center">
<div class="relative w-[30%]">
<input
type="text"
id="app-search-input"
placeholder="搜索智能体..."
class="w-full bg-slate-800 text-white px-4 py-3 pl-12 rounded-xl border border-slate-600 focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/20 transition-all duration-300"
>
<i class="fas fa-search absolute left-4 top-1/2 transform -translate-y-1/2 text-slate-400"></i>
</div>
</div>
<!-- 应用列表 -->
<div class="flex-1 overflow-y-auto p-6">
<div id="app-list" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
<!-- 应用卡片将通过JavaScript动态加载 -->
<div class="text-center text-slate-400 py-12 col-span-full">
<i class="fas fa-spinner fa-spin text-4xl mb-4"></i>
<p>正在加载智能体列表...</p>
</div>
</div>
</div>
</div>
<!-- 主对话区域 -->
<div class="flex-1 flex flex-col">
<!-- 主对话区域(初始隐藏) -->
<div id="chat-panel" class="flex-1 flex flex-col hidden">
<!-- 顶部导航栏 -->
<div class="h-16 bg-slate-900/80 glass-effect border-b border-slate-700 flex items-center justify-between px-6 relative">
<div class="flex items-center space-x-4">
<h1 class="text-xl font-bold text-white font-['Pacifico']">AI 助手</h1>
<div class="flex space-x-2">
<button class="theme-btn px-3 py-1 bg-slate-700 text-white rounded-lg text-sm hover:bg-slate-600 transition" data-theme="dark">深空黑</button>
<button class="theme-btn px-3 py-1 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-500 transition" data-theme="blue">极光蓝</button>
<button class="theme-btn px-3 py-1 bg-purple-600 text-white rounded-lg text-sm hover:bg-purple-500 transition" data-theme="purple">霓虹紫</button>
<button class="theme-btn px-3 py-1 bg-green-600 text-white rounded-lg text-sm hover:bg-green-500 transition" data-theme="green">科技绿</button>
<!-- 返回按钮 -->
<button id="back-to-apps-btn" class="text-white p-2 hover:bg-slate-700 rounded-lg transition">
<i class="fas fa-arrow-left"></i>
</button>
<!-- 当前应用信息 -->
<div class="flex items-center space-x-3">
<img id="current-app-avatar" src="" alt="" class="w-10 h-10 rounded-full bg-slate-700">
<div>
<h1 id="current-app-name" class="text-lg font-bold text-white">AI 助手</h1>
<p id="current-app-desc" class="text-xs text-slate-400"></p>
</div>
</div>
</div>
<!-- 字体选择 -->
<!-- 右侧工具栏 -->
<div class="flex items-center space-x-4">
<select id="font-select" class="bg-slate-800 text-white px-3 py-1 rounded-lg border border-slate-600 focus:outline-none focus:border-primary">
<select id="font-select" class="bg-slate-800 text-white px-3 py-1 rounded-lg border border-slate-600 focus:outline-none focus:border-primary text-sm">
<option value="'Inter', sans-serif">默认字体</option>
<option value="'Source Han Sans CN', sans-serif">思源黑体</option>
<option value="'PingFang SC', sans-serif">苹方</option>
<option value="'Roboto', sans-serif">Roboto</option>
</select>
<!-- 消息通知 -->
<div class="relative">
<button class="text-white p-2 hover:bg-slate-700 rounded-lg transition relative">
<i class="fas fa-bell text-lg"></i>
<div class="notification-badge">3</div>
</button>
</div>
<div class="w-8 h-8 bg-gradient-to-r from-primary to-secondary rounded-full flex items-center justify-center text-white">
<i class="fas fa-user text-sm"></i>
</div>
</div>
</div>
<!-- 智能回复提示 -->
<div class="p-4 bg-slate-800/50 border-b border-slate-700">
<div class="flex space-x-3">
<button class="quick-reply-btn px-4 py-2 bg-slate-700 text-slate-300 rounded-lg text-sm hover:bg-slate-600 transition whitespace-nowrap">
<i class="fas fa-lightbulb mr-2"></i>今日天气如何?
</button>
<button class="quick-reply-btn px-4 py-2 bg-slate-700 text-slate-300 rounded-lg text-sm hover:bg-slate-600 transition whitespace-nowrap">
<i class="fas fa-calendar mr-2"></i>明天的会议安排
</button>
<button class="quick-reply-btn px-4 py-2 bg-slate-700 text-slate-300 rounded-lg text-sm hover:bg-slate-600 transition whitespace-nowrap">
<i class="fas fa-tasks mr-2"></i>帮我制定工作计划
</button>
<!-- 智能回复提示(动态加载) -->
<div id="quick-reply-container" class="p-4 bg-slate-800/50 border-b border-slate-700 hidden">
<div id="quick-reply-buttons" class="flex space-x-3 overflow-x-auto">
<!-- 快捷回复按钮将通过JavaScript动态加载 -->
</div>
</div>
<!-- 对话内容区域 -->
<div class="flex-1 overflow-y-auto p-6 space-y-4" id="chat-container">
<!-- 用户消息示例 -->
<div class="flex justify-end slide-in">
<div class="max-w-xs lg:max-w-md user-bubble text-white p-4 rounded-2xl rounded-br-md">
<p>你好,我想了解一下今天的工作安排。</p>
</div>
</div>
<!-- AI 回复示例 -->
<div class="flex justify-start slide-in">
<div class="max-w-xs lg:max-w-md ai-bubble text-white p-4 rounded-2xl rounded-bl-md">
<p>您好!根据您的日程安排,今天有以下任务:</p>
<ul class="mt-2 space-y-1 text-sm">
<li>• 9:00 AM - 晨会准备</li>
<li>• 10:30 AM - 客户电话会议</li>
<li>• 2:00 PM - 项目进度汇报</li>
<li>• 4:00 PM - 团队协作讨论</li>
</ul>
<!-- 欢迎消息 -->
<div class="flex justify-center">
<div class="text-center text-slate-400 py-8">
<i class="fas fa-comments text-4xl mb-4"></i>
<p id="welcome-message">开始对话吧!</p>
</div>
</div>
</div>
@@ -248,6 +196,48 @@
</div>
</div>
<!-- Token设置模态框 -->
<div id="token-modal" class="hidden fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center">
<div class="bg-slate-800 rounded-lg p-6 w-full max-w-md mx-4 shadow-2xl">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold text-white">设置认证Token</h2>
<button onclick="closeTokenModal()" class="text-slate-400 hover:text-white transition">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-slate-300 mb-2">
Token(自动添加Bearer前缀)
</label>
<textarea
id="token-input"
rows="4"
placeholder="粘贴您的认证token..."
class="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-md text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent resize-none"
></textarea>
<p class="mt-2 text-xs text-slate-400">
当前状态: <span id="token-status-modal" class="font-medium">未设置</span>
</p>
</div>
<div class="flex gap-3">
<button
onclick="saveToken()"
class="flex-1 px-4 py-2 bg-primary hover:bg-primary/80 text-white rounded-md transition-colors"
>
保存
</button>
<button
onclick="closeTokenModal()"
class="px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded-md transition-colors"
>
取消
</button>
</div>
</div>
</div>
<!-- JavaScript -->
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
</body>
+151
View File
@@ -0,0 +1,151 @@
#!/usr/bin/env python3
"""
测试api后端连接
@author huazm
"""
import os
import sys
import requests
import json
# 配置
API_BASE_URL = os.environ.get('API_BASE_URL', 'http://localhost:8080')
API_PREFIX = '/api/ai-assistant'
def test_health():
"""测试健康检查"""
print("=" * 60)
print("测试1: 健康检查")
print("=" * 60)
try:
url = f'{API_BASE_URL}/actuator/health'
print(f"请求: GET {url}")
response = requests.get(url, timeout=5)
print(f"状态码: {response.status_code}")
print(f"响应: {response.text}")
return response.status_code == 200
except Exception as e:
print(f"❌ 错误: {e}")
return False
def test_get_applications():
"""测试获取应用列表"""
print("\n" + "=" * 60)
print("测试2: 获取应用列表")
print("=" * 60)
try:
url = f'{API_BASE_URL}{API_PREFIX}/chatapp'
print(f"请求: GET {url}")
response = requests.get(url, timeout=10)
print(f"状态码: {response.status_code}")
if response.status_code == 200:
data = response.json()
print(f"响应数据结构: {json.dumps(data, ensure_ascii=False, indent=2)[:500]}...")
if data.get('code') == 200 and data.get('data'):
apps = data['data']
print(f"\n✅ 成功获取 {len(apps)} 个应用")
if apps:
print(f"\n第一个应用示例:")
print(json.dumps(apps[0], ensure_ascii=False, indent=2))
return True
else:
print(f"❌ 响应格式不正确")
return False
else:
print(f"❌ 请求失败: {response.text}")
return False
except Exception as e:
print(f"❌ 错误: {e}")
return False
def test_get_recommend_questions(app_id):
"""测试获取推荐问题"""
print("\n" + "=" * 60)
print("测试3: 获取推荐问题")
print("=" * 60)
try:
url = f'{API_BASE_URL}{API_PREFIX}/chatapp/{app_id}/getRecommendQuestion'
params = {'pageSize': 3, 'current': 1}
print(f"请求: GET {url}")
print(f"参数: {params}")
response = requests.get(url, params=params, timeout=10)
print(f"状态码: {response.status_code}")
if response.status_code == 200:
data = response.json()
print(f"响应: {json.dumps(data, ensure_ascii=False, indent=2)[:500]}...")
if data.get('code') == 200:
records = data.get('data', {}).get('records', [])
print(f"\n✅ 成功获取 {len(records)} 个推荐问题")
for i, q in enumerate(records, 1):
print(f" {i}. {q.get('question', 'N/A')}")
return True
else:
print(f"⚠️ 响应code不为200")
return False
else:
print(f"❌ 请求失败: {response.text}")
return False
except Exception as e:
print(f"❌ 错误: {e}")
return False
def main():
"""主函数"""
print("\n" + "🔍 " * 20)
print("AI助手Web客户端 - API连接测试")
print("🔍 " * 20)
print(f"\n后端地址: {API_BASE_URL}")
print(f"API前缀: {API_PREFIX}\n")
results = []
# 测试1: 健康检查
results.append(("健康检查", test_health()))
# 测试2: 获取应用列表
apps_ok = test_get_applications()
results.append(("获取应用列表", apps_ok))
# 测试3: 获取推荐问题(使用第一个应用)
if apps_ok:
# 尝试获取第一个应用的ID
try:
url = f'{API_BASE_URL}{API_PREFIX}/chatapp'
response = requests.get(url, timeout=10)
data = response.json()
if data.get('code') == 200 and data.get('data'):
first_app_id = data['data'][0]['id']
results.append(("获取推荐问题", test_get_recommend_questions(first_app_id)))
else:
results.append(("获取推荐问题", False))
except:
results.append(("获取推荐问题", False))
else:
results.append(("获取推荐问题", False))
# 打印测试结果
print("\n" + "=" * 60)
print("测试结果汇总")
print("=" * 60)
for name, success in results:
status = "✅ 通过" if success else "❌ 失败"
print(f"{name:20s} {status}")
all_passed = all(result[1] for result in results)
print("\n" + "=" * 60)
if all_passed:
print("🎉 所有测试通过!Web客户端可以正常使用。")
else:
print("⚠️ 部分测试失败,请检查后端服务配置。")
print("=" * 60 + "\n")
return 0 if all_passed else 1
if __name__ == '__main__':
sys.exit(main())