diff --git a/ai_chat.db b/ai_chat.db index ce8b930..cb1f27a 100644 Binary files a/ai_chat.db and b/ai_chat.db differ diff --git a/main.py b/main.py index 654e697..a4bc0b6 100644 --- a/main.py +++ b/main.py @@ -382,6 +382,205 @@ async def get_config(db: Session = Depends(get_db)): } +@app.get("/api/admin/ai-config") +async def get_ai_config(db: Session = Depends(get_db)): + """获取AI配置""" + configs = {c.key: c.value for c in db.query(SystemConfig).filter(SystemConfig.key.startswith('ai_')).all()} + + return { + "api_base": configs.get('ai_api_base', 'http://192.168.2.17:19007/v1'), + "api_key": configs.get('ai_api_key', 'xxxx'), + "model": configs.get('ai_model', 'auto'), + "use_mock": ai_service.use_mock + } + + +@app.post("/api/admin/ai-config") +async def update_ai_config(data: dict, db: Session = Depends(get_db)): + """更新AI配置""" + api_base = data.get("api_base") + api_key = data.get("api_key") + model = data.get("model") + + if api_base: + config = db.query(SystemConfig).filter(SystemConfig.key == 'ai_api_base').first() + if config: + config.value = api_base + else: + config = SystemConfig(key='ai_api_base', value=api_base, description='AI API地址') + db.add(config) + + if api_key: + config = db.query(SystemConfig).filter(SystemConfig.key == 'ai_api_key').first() + if config: + config.value = api_key + else: + config = SystemConfig(key='ai_api_key', value=api_key, description='AI API密钥') + db.add(config) + + if model: + config = db.query(SystemConfig).filter(SystemConfig.key == 'ai_model').first() + if config: + config.value = model + else: + config = SystemConfig(key='ai_model', value=model, description='AI模型名称') + db.add(config) + + db.commit() + + # 更新AI服务配置 + configs = {c.key: c.value for c in db.query(SystemConfig).filter(SystemConfig.key.startswith('ai_')).all()} + ai_service.update_config( + configs.get('ai_api_base', 'http://192.168.2.17:19007/v1'), + configs.get('ai_api_key', 'xxxx'), + configs.get('ai_model', 'auto') + ) + + return {"success": True, "message": "AI配置已更新"} + + +@app.get("/api/admin/models") +async def get_available_models(db: Session = Depends(get_db)): + """获取可用模型列表""" + import httpx + + # 从数据库读取最新配置 + configs = {c.key: c.value for c in db.query(SystemConfig).filter(SystemConfig.key.startswith('ai_')).all()} + api_base = configs.get('ai_api_base', '') + api_key = configs.get('ai_api_key', 'xxxx') + + if not api_base: + # 返回默认模型列表 + return { + "models": [ + {"id": "auto", "name": "auto (自动选择)", "owned_by": "system"}, + {"id": "qwen3.5-4b", "name": "qwen3.5-4b", "owned_by": "local"}, + {"id": "dsv32", "name": "dsv32", "owned_by": "deepseek"}, + {"id": "glm-4", "name": "glm-4", "owned_by": "zhipu"}, + {"id": "gpt-4o", "name": "gpt-4o", "owned_by": "openai"}, + {"id": "claude-3-opus", "name": "claude-3-opus", "owned_by": "anthropic"} + ], + "success": False, + "message": "请先配置API地址" + } + + try: + # 从当前配置的API地址获取模型列表 + url = f"{api_base}/models" + headers = {"Authorization": f"Bearer {api_key}"} + + logger.info(f"获取模型列表: url={url}") + + async with httpx.AsyncClient(timeout=10.0) as client: + response = await client.get(url, headers=headers) + if response.status_code == 200: + data = response.json() + models = [] + for m in data.get('data', []): + model_id = m.get('id', '') + if model_id: + models.append({ + "id": model_id, + "name": m.get('name', model_id), + "owned_by": m.get('owned_by', 'unknown') + }) + return {"models": models, "success": True} + except Exception as e: + logger.error(f"获取模型列表失败: {e}") + + # 返回默认模型列表 + return { + "models": [ + {"id": "auto", "name": "auto (自动选择)", "owned_by": "system"}, + {"id": "qwen3.5-4b", "name": "qwen3.5-4b", "owned_by": "local"}, + {"id": "dsv32", "name": "dsv32", "owned_by": "deepseek"}, + {"id": "glm-4", "name": "glm-4", "owned_by": "zhipu"}, + {"id": "gpt-4o", "name": "gpt-4o", "owned_by": "openai"}, + {"id": "claude-3-opus", "name": "claude-3-opus", "owned_by": "anthropic"} + ], + "success": False, + "message": "无法从API获取模型列表,显示默认列表" + } + + +@app.post("/api/admin/test-ai") +async def test_ai_connection(db: Session = Depends(get_db)): + """测试AI连接""" + import httpx + + # 从数据库读取最新配置,如果没有则使用默认值 + configs = {c.key: c.value for c in db.query(SystemConfig).filter(SystemConfig.key.startswith('ai_')).all()} + + # 使用数据库值或默认值 + api_base = configs.get('ai_api_base') or 'http://192.168.2.17:19007/v1' + api_key = configs.get('ai_api_key') or 'xxxx' + model = configs.get('ai_model') or 'auto' + + # 判断是否使用默认值 + using_defaults = not configs.get('ai_api_base') + + try: + url = f"{api_base}/chat/completions" + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json" + } + payload = { + "model": model, + "messages": [{"role": "user", "content": "测试连接"}], + "max_tokens": 50 + } + + logger.info(f"测试AI连接: url={url}, model={model}, using_defaults={using_defaults}") + + async with httpx.AsyncClient(timeout=15.0) as client: + response = await client.post(url, headers=headers, json=payload) + if response.status_code == 200: + data = response.json() + content = data['choices'][0]['message']['content'] + + result = { + "success": True, + "message": f"连接成功!模型响应: {content[:100]}", + "model": model, + "api_base": api_base + } + + if using_defaults: + result["message"] += "\n(使用默认配置,点击「保存配置」可持久化)" + + return result + else: + error_text = response.text[:200] if response.text else "" + return { + "success": False, + "message": f"连接失败: HTTP {response.status_code} - {error_text}", + "model": model, + "api_base": api_base + } + except httpx.ConnectError as e: + return { + "success": False, + "message": f"无法连接到API地址: {api_base}", + "model": model, + "api_base": api_base + } + except httpx.TimeoutException: + return { + "success": False, + "message": f"连接超时(15秒)", + "model": model, + "api_base": api_base + } + except Exception as e: + return { + "success": False, + "message": f"连接失败: {str(e)}", + "model": model, + "api_base": api_base + } + + @app.post("/api/admin/config") async def update_config(data: dict, db: Session = Depends(get_db)): """更新系统配置""" diff --git a/main_v2.py b/main_v2.py index b03dd06..feba861 100644 --- a/main_v2.py +++ b/main_v2.py @@ -786,21 +786,27 @@ async def websocket_endpoint(websocket: WebSocket, user_id: str): # 处理文件内容,添加到消息 image_contents = [] # 图片内容(用于视觉模型) + text_contents = [] # 文本文件内容 if files: for f in files: if f.get('type') and f['type'].startswith('image/'): - # 图片:记录 base64 数据,后续可能用于视觉模型 + # 图片:记录 base64 数据,用于视觉模型 image_contents.append({ 'name': f['name'], 'type': f['type'], 'data': f.get('content', '') # base64 数据 }) - message += f"\n[图片: {f['name']}]" + # 不添加文件名文本,图片信息保存在 extra_data 中 elif f.get('content'): - # 文本文件:直接添加内容 - message += f"\n\n文件 {f['name']} 内容:\n{f['content'][:3000]}" + # 文本文件:直接添加内容,不带文件名前缀 + text_contents.append(f['content'][:3000]) if len(f['content']) > 3000: - message += "...(内容过长已截断)" + text_contents[-1] += "...(内容过长已截断)" + + # 如果有文本文件内容,追加到消息后面 + if text_contents: + for content in text_contents: + message += f"\n\n{content}" # 保存图片信息到 extra_data(用于历史记录) extra_data_for_msg = None diff --git a/matrix_store/@tester:matrix.tphai.com_SKLVQXSRMI.blacklisted_devices b/matrix_store/@tester:matrix.tphai.com_SKLVQXSRMI.blacklisted_devices new file mode 100644 index 0000000..e69de29 diff --git a/matrix_store/@tester:matrix.tphai.com_SKLVQXSRMI.ignored_devices b/matrix_store/@tester:matrix.tphai.com_SKLVQXSRMI.ignored_devices new file mode 100644 index 0000000..e69de29 diff --git a/matrix_store/@tester:matrix.tphai.com_SKLVQXSRMI.trusted_devices b/matrix_store/@tester:matrix.tphai.com_SKLVQXSRMI.trusted_devices new file mode 100644 index 0000000..3369e07 --- /dev/null +++ b/matrix_store/@tester:matrix.tphai.com_SKLVQXSRMI.trusted_devices @@ -0,0 +1,11 @@ +@tester:matrix.tphai.com AJFVRTHLJY matrix-ed25519 4mRjLhM8xbwjkwQP2T/iB3UZJoaADgP6cCVUiB8AtSk +@tester:matrix.tphai.com ATYFRXKHEQ matrix-ed25519 WnaxV7S11wrqlojKOR3j2RDlPL7TrO17U2ablFISbnw +@tester:matrix.tphai.com BDTRXIGPBE matrix-ed25519 gjQNtLEpIEYCjmzUx5ma91G498n4UADh84KUmiReJUM +@tester:matrix.tphai.com GALBNVJOSG matrix-ed25519 /a7qD2Od76/+Xrr/naDqWEQJZ982X9XdYkCBbRmKxBU +@tester:matrix.tphai.com GVSFGGYNJL matrix-ed25519 8qV2own4G3m2nki+izFDBOrAxtbGl8RoneM3qUPkThU +@tester:matrix.tphai.com IMEQIQPXTR matrix-ed25519 6Yd4lmhP6jdkkNvh1rIw6TRK331ZUyiAt5G5hPeYqSE +@tester:matrix.tphai.com MIPPYHRVAS matrix-ed25519 s8Ol56sxLCjCOi0Gkv/Kj7LqVMp/8ZmuAJ6QA1rUi7o +@tester:matrix.tphai.com UKJGJYQQLT matrix-ed25519 opC9rhsz1nzrvQqNWMKTF5FxWIGuHTDfixx+q/Y8ea0 +@tester:matrix.tphai.com UPMZGRLESG matrix-ed25519 86c6XPCIYHgesq83C2k5xhXNa0EYMnqTq4jFrTwJX8I +@huangzhuang_bro:matrix.tphai.com BQHGFLQEPR matrix-ed25519 IrEHmvqotfHKLyx1JRJp4RthUVyBT8qQX72qBifRRyQ +@huangzhuang_bro:matrix.tphai.com NTVATQQGPK matrix-ed25519 lKMDsoTFK/Lc8yXoqqHBBeuK2HPKAaFFm9KjxgQzEy0 diff --git a/services/__pycache__/__init__.cpython-310.pyc b/services/__pycache__/__init__.cpython-310.pyc index 1fd9702..c543ee7 100644 Binary files a/services/__pycache__/__init__.cpython-310.pyc and b/services/__pycache__/__init__.cpython-310.pyc differ diff --git a/services/__pycache__/conversation_service.cpython-310.pyc b/services/__pycache__/conversation_service.cpython-310.pyc index d727d09..925acb0 100644 Binary files a/services/__pycache__/conversation_service.cpython-310.pyc and b/services/__pycache__/conversation_service.cpython-310.pyc differ diff --git a/templates/admin/index.html b/templates/admin/index.html index a241c32..cf2fa1e 100644 --- a/templates/admin/index.html +++ b/templates/admin/index.html @@ -144,6 +144,197 @@ background: #f9f9f9; } + /* AI配置专用样式 */ + .ai-config-section { + background: #f8f9fa; + border-radius: 12px; + padding: 24px; + margin-bottom: 24px; + } + + .ai-config-section h3 { + font-size: 18px; + color: #333; + margin-bottom: 16px; + display: flex; + align-items: center; + gap: 8px; + } + + .ai-config-section h3 i { + color: #10a37f; + } + + .ai-status { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 16px; + background: #fff; + border-radius: 8px; + margin-bottom: 16px; + } + + .ai-status.ok { + border: 1px solid #10a37f; + } + + .ai-status.error { + border: 1px solid #dc3545; + } + + .ai-status-dot { + width: 12px; + height: 12px; + border-radius: 50%; + } + + .ai-status-dot.ok { + background: #10a37f; + } + + .ai-status-dot.error { + background: #dc3545; + } + + .ai-status-text { + flex: 1; + } + + .ai-config-form { + display: grid; + gap: 16px; + } + + .config-row { + display: grid; + grid-template-columns: 150px 1fr; + gap: 16px; + align-items: center; + } + + .config-row label { + font-weight: 500; + color: #555; + } + + .config-row input, .config-row select { + padding: 12px 16px; + border: 1px solid #ddd; + border-radius: 8px; + font-size: 14px; + width: 100%; + } + + .config-row input:focus, .config-row select:focus { + outline: none; + border-color: #10a37f; + } + + /* 模型输入组合框 */ + .model-input-wrapper { + display: flex; + position: relative; + } + + .model-input-wrapper input { + flex: 1; + padding-right: 40px; + } + + .btn-model-dropdown { + position: absolute; + right: 4px; + top: 50%; + transform: translateY(-50%); + width: 32px; + height: 32px; + background: #f0f0f0; + border: none; + border-radius: 4px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + color: #666; + transition: all 0.2s; + } + + .btn-model-dropdown:hover { + background: #e0e0e0; + color: #333; + } + + /* datalist样式提示 */ + .model-input-wrapper input::-webkit-calendar-picker-indicator { + opacity: 0; + width: 32px; + height: 32px; + cursor: pointer; + } + + .config-actions { + display: flex; + gap: 12px; + margin-top: 16px; + } + + .btn { + padding: 12px 24px; + border-radius: 8px; + font-size: 14px; + cursor: pointer; + border: none; + transition: all 0.2s; + } + + .btn-primary { + background: #10a37f; + color: #fff; + } + + .btn-primary:hover { + background: #0d8c6d; + } + + .btn-secondary { + background: #fff; + color: #333; + border: 1px solid #ddd; + } + + .btn-secondary:hover { + background: #f0f0f0; + } + + .btn-test { + background: #007bff; + color: #fff; + } + + .btn-test:hover { + background: #0056b3; + } + + .test-result { + padding: 16px; + border-radius: 8px; + margin-top: 16px; + } + + .test-result.success { + background: #d4edda; + border: 1px solid #10a37f; + color: #155724; + } + + .test-result.error { + background: #f8d7da; + border: 1px solid #dc3545; + color: #721c24; + } + + /* 原有配置表单样式 */ .config-form { display: flex; gap: 16px; @@ -167,20 +358,6 @@ min-height: 60px; } - .config-form button { - padding: 12px 24px; - background: #10a37f; - color: #fff; - border: none; - border-radius: 8px; - cursor: pointer; - font-size: 14px; - } - - .config-form button:hover { - background: #0d8c6d; - } - .config-list { margin-top: 24px; } @@ -246,6 +423,11 @@ text-overflow: ellipsis; white-space: nowrap; } + + .loading { + opacity: 0.6; + pointer-events: none; + } @@ -273,14 +455,97 @@
- - - + + + +
+ +
+
+

大模型配置

+ +
+
+
检测中...
+
+ +
+
+ + +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + +
+
+
+ +
+ + + +
+ + +
+ +
+

当前状态

+ + + + + + + + + + + + + + + + + + + + + +
配置项当前值状态
API地址--
模型--
连接状态-检测中
+
+
+ -
+
@@ -318,13 +583,13 @@
- +
- +
@@ -338,6 +603,7 @@ // 初始化 document.addEventListener('DOMContentLoaded', () => { loadStats(); + loadAIConfig(); loadUsers(); loadConversations(); loadConfig(); @@ -352,6 +618,208 @@ document.getElementById(`${tabName}Panel`).classList.add('active'); } + // 切换模型下拉显示 + function toggleModelDropdown() { + const input = document.getElementById('aiModel'); + input.focus(); + // 触发datalist显示 + if (input.showPicker) { + input.showPicker(); + } else { + // 兼容性处理:模拟点击 + input.click(); + } + } + + // 加载AI配置 + async function loadAIConfig() { + try { + const response = await fetch('/api/admin/ai-config'); + const data = await response.json(); + + document.getElementById('aiApiBase').value = data.api_base || ''; + document.getElementById('aiApiKey').value = data.api_key || ''; + document.getElementById('aiModel').value = data.model || 'auto'; + + // 更新当前状态显示 + document.getElementById('currentApiBase').textContent = data.api_base || '-'; + document.getElementById('currentModel').textContent = data.model || '-'; + + // 更新状态指示 + if (data.use_mock) { + document.getElementById('aiStatusDot').className = 'ai-status-dot error'; + document.getElementById('aiStatusText').textContent = '当前使用Mock模式(未连接真实API)'; + document.getElementById('aiStatus').className = 'ai-status error'; + document.getElementById('connectionStatus').textContent = 'Mock模式'; + document.getElementById('connectionBadge').className = 'badge inactive'; + document.getElementById('connectionBadge').textContent = '未连接'; + } else { + document.getElementById('aiStatusDot').className = 'ai-status-dot ok'; + document.getElementById('aiStatusText').textContent = '已配置真实API'; + document.getElementById('aiStatus').className = 'ai-status ok'; + document.getElementById('connectionStatus').textContent = '已配置'; + document.getElementById('connectionBadge').className = 'badge active'; + document.getElementById('connectionBadge').textContent = '待测试'; + } + + document.getElementById('apiBaseStatus').className = 'badge active'; + document.getElementById('apiBaseStatus').textContent = '已配置'; + document.getElementById('modelStatus').className = 'badge active'; + document.getElementById('modelStatus').textContent = data.model || 'auto'; + + } catch (error) { + console.error('加载AI配置失败:', error); + } + } + + // 保存AI配置 + async function saveAIConfig() { + const apiBase = document.getElementById('aiApiBase').value.trim(); + const apiKey = document.getElementById('aiApiKey').value.trim(); + const model = document.getElementById('aiModel').value.trim(); + + if (!apiBase) { + alert('请填写API地址'); + return; + } + + try { + const btn = event.target; + btn.classList.add('loading'); + btn.textContent = '保存中...'; + + const response = await fetch('/api/admin/ai-config', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({api_base: apiBase, api_key: apiKey, model: model}) + }); + + const data = await response.json(); + + btn.classList.remove('loading'); + btn.innerHTML = ' 保存配置'; + + if (data.success) { + // 显示成功提示 + const resultDiv = document.getElementById('testResult'); + resultDiv.style.display = 'block'; + resultDiv.className = 'test-result success'; + resultDiv.innerHTML = ` ${data.message}`; + + // 重新加载配置 + loadAIConfig(); + + // 3秒后隐藏提示 + setTimeout(() => resultDiv.style.display = 'none', 3000); + } else { + alert('保存失败: ' + (data.message || '未知错误')); + } + } catch (error) { + console.error('保存AI配置失败:', error); + alert('保存失败: ' + error.message); + event.target.classList.remove('loading'); + event.target.innerHTML = ' 保存配置'; + } + } + + // 测试AI连接 + async function testAIConnection() { + try { + const btn = event.target; + btn.classList.add('loading'); + btn.innerHTML = ' 测试中...'; + + const response = await fetch('/api/admin/test-ai', { + method: 'POST' + }); + + const data = await response.json(); + + btn.classList.remove('loading'); + btn.innerHTML = ' 测试连接'; + + const resultDiv = document.getElementById('testResult'); + resultDiv.style.display = 'block'; + + if (data.success) { + resultDiv.className = 'test-result success'; + resultDiv.innerHTML = ` 连接成功!
模型: ${data.model}
响应: ${data.message}`; + + // 更新连接状态 + document.getElementById('aiStatusDot').className = 'ai-status-dot ok'; + document.getElementById('aiStatusText').textContent = '连接正常'; + document.getElementById('aiStatus').className = 'ai-status ok'; + document.getElementById('connectionStatus').textContent = '正常'; + document.getElementById('connectionBadge').className = 'badge active'; + document.getElementById('connectionBadge').textContent = '已连接'; + } else { + resultDiv.className = 'test-result error'; + resultDiv.innerHTML = ` 连接失败
${data.message}`; + + // 更新连接状态 + document.getElementById('aiStatusDot').className = 'ai-status-dot error'; + document.getElementById('aiStatusText').textContent = '连接失败'; + document.getElementById('aiStatus').className = 'ai-status error'; + document.getElementById('connectionStatus').textContent = '失败'; + document.getElementById('connectionBadge').className = 'badge inactive'; + document.getElementById('connectionBadge').textContent = '错误'; + } + } catch (error) { + console.error('测试连接失败:', error); + event.target.classList.remove('loading'); + event.target.innerHTML = ' 测试连接'; + + const resultDiv = document.getElementById('testResult'); + resultDiv.style.display = 'block'; + resultDiv.className = 'test-result error'; + resultDiv.innerHTML = ` 测试失败: ${error.message}`; + } + } + + // 刷新模型列表 + async function refreshModels() { + try { + const btn = event.target; + btn.classList.add('loading'); + btn.innerHTML = ' 刷新中...'; + + const response = await fetch('/api/admin/models'); + const data = await response.json(); + + btn.classList.remove('loading'); + btn.innerHTML = ' 刷新模型列表'; + + // 更新datalist + const datalist = document.getElementById('modelList'); + datalist.innerHTML = ''; + + for (const model of data.models) { + const option = document.createElement('option'); + option.value = model.id; + option.textContent = model.name; + datalist.appendChild(option); + } + + // 显示提示 + const resultDiv = document.getElementById('testResult'); + resultDiv.style.display = 'block'; + resultDiv.className = 'test-result success'; + resultDiv.innerHTML = ` 获取到 ${data.models.length} 个模型,可在输入框中选择`; + + if (!data.success) { + resultDiv.className = 'test-result error'; + resultDiv.innerHTML = ` ${data.message || '使用默认模型列表'}`; + } + + setTimeout(() => resultDiv.style.display = 'none', 3000); + + } catch (error) { + console.error('刷新模型列表失败:', error); + event.target.classList.remove('loading'); + event.target.innerHTML = ' 刷新模型列表'; + } + } + // 加载统计数据 async function loadStats() { try { @@ -423,7 +891,7 @@ } } - // 加载配置 + // 加载其他配置 async function loadConfig() { try { const response = await fetch('/api/admin/config'); @@ -431,12 +899,15 @@ const container = document.getElementById('configList'); - if (data.configs.length === 0) { - container.innerHTML = '

暂无配置

'; + // 过滤掉AI配置(在AI配置面板单独显示) + const otherConfigs = data.configs.filter(c => !c.key.startsWith('ai_')); + + if (otherConfigs.length === 0) { + container.innerHTML = '

暂无其他配置

'; return; } - container.innerHTML = data.configs.map(config => ` + container.innerHTML = otherConfigs.map(config => `
${config.key}
${config.value}
@@ -448,7 +919,7 @@ } } - // 保存配置 + // 保存其他配置 async function saveConfig() { const key = document.getElementById('configKey').value.trim(); const value = document.getElementById('configValue').value.trim(); @@ -497,6 +968,9 @@ // 定时刷新 setInterval(() => { loadStats(); + if (document.getElementById('aiPanel').classList.contains('active')) { + loadAIConfig(); + } if (document.getElementById('usersPanel').classList.contains('active')) { loadUsers(); }