fix: 修复部分bug
This commit is contained in:
parent
9e9535c794
commit
210b300c68
|
|
@ -8,12 +8,6 @@ const routes = [
|
||||||
component: () => import('../views/HomeView.vue'),
|
component: () => import('../views/HomeView.vue'),
|
||||||
meta: { requiresAuth: true }
|
meta: { requiresAuth: true }
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/about',
|
|
||||||
name: 'About',
|
|
||||||
component: () => import('../views/AboutView.vue'),
|
|
||||||
meta: { requiresAuth: true }
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/settings',
|
path: '/settings',
|
||||||
name: 'Settings',
|
name: 'Settings',
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ export const messagesAPI = {
|
||||||
// 发送消息(非流式)
|
// 发送消息(非流式)
|
||||||
send: (data) => api.post('/messages/', data),
|
send: (data) => api.post('/messages/', data),
|
||||||
|
|
||||||
// 发送消息(流式)- 返回 EventSource 或使用 fetch
|
// 发送消息(流式)- 使用原生 fetch 避免 axios 拦截
|
||||||
sendStream: (data) => {
|
sendStream: (data) => {
|
||||||
const token = localStorage.getItem('access_token')
|
const token = localStorage.getItem('access_token')
|
||||||
return fetch('/api/messages/stream', {
|
return fetch('/api/messages/stream', {
|
||||||
|
|
|
||||||
|
|
@ -63,24 +63,29 @@
|
||||||
|
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<button @click="editProvider(p)">编辑</button>
|
<button @click="editProvider(p)">编辑</button>
|
||||||
<button @click="testProvider(p)" :disabled="testing === p.id">测试</button>
|
<button @click="testProvider(p)" :disabled="testing === p.id">{{ testing === p.id ? '测试中...' : '测试' }}</button>
|
||||||
<button @click="deleteProvider(p)" class="btn-danger">删除</button>
|
<button @click="deleteProvider(p)" class="btn-danger">删除</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 测试结果弹窗 -->
|
<!-- 测试结果弹窗 -->
|
||||||
<div v-if="testResult !== null" class="modal-overlay" @click.self="testResult = null">
|
<div v-if="testResult !== null || testing" class="modal-overlay" @click.self="testResult = null; testing = null">
|
||||||
<div class="modal result-modal" :class="{ success: testResult.success, error: !testResult.success }">
|
<div class="modal result-modal" :class="{ success: testResult?.success === true, error: testResult?.success === false, loading: testing }">
|
||||||
<div class="result-icon">
|
<div v-if="testing" class="result-loading">
|
||||||
<span v-if="testResult.success">✓</span>
|
<div class="spinner-large"></div>
|
||||||
<span v-else>✗</span>
|
<p>测试连接中...</p>
|
||||||
</div>
|
</div>
|
||||||
<h2>{{ testResult.success ? '连接成功' : '连接失败' }}</h2>
|
<template v-else>
|
||||||
|
<div class="result-icon">
|
||||||
<pre v-if="testResult.json" class="result-json">{{ testResult.json }}</pre>
|
<span v-if="testResult.success">✓</span>
|
||||||
<div v-else class="result-message">{{ testResult.message }}</div>
|
<span v-else>✗</span>
|
||||||
<button @click="testResult = null" class="btn-primary">确定</button>
|
</div>
|
||||||
|
<h2>{{ testResult.success ? '连接成功' : '连接失败' }}</h2>
|
||||||
|
<pre v-if="testResult.json" class="result-json">{{ testResult.json }}</pre>
|
||||||
|
<div v-else class="result-message">{{ testResult.message }}</div>
|
||||||
|
<button @click="testResult = null" class="btn-primary">确定</button>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -90,12 +95,12 @@
|
||||||
<h2>{{ editing ? '编辑 Provider' : '添加 Provider' }}</h2>
|
<h2>{{ editing ? '编辑 Provider' : '添加 Provider' }}</h2>
|
||||||
|
|
||||||
<div class="form-group"><label>名称</label><input v-model="form.name" placeholder="如: 我的 DeepSeek" /></div>
|
<div class="form-group"><label>名称</label><input v-model="form.name" placeholder="如: 我的 DeepSeek" /></div>
|
||||||
<div class="form-group"><label>Base URL</label><input v-model="form.base_url" placeholder="https://api.deepseek.com/v1" /></div>
|
<div class="form-group"><label>Base URL</label><input v-model="form.base_url" placeholder="https://api.deepseek.com/chat/completions" /></div>
|
||||||
<div class="form-group"><label>API Key</label><input v-model="form.api_key" type="text" :placeholder="editing ? '留空则保持原密码' : 'sk-...'" /></div>
|
<div class="form-group"><label>API Key</label><input v-model="form.api_key" type="text" :placeholder="editing ? '留空则保持原密码' : 'sk-...'" /></div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>模型名称</label>
|
<label>模型名称</label>
|
||||||
<input v-model="form.default_model" placeholder="gpt-4 / deepseek-chat" required />
|
<input v-model="form.default_model" placeholder="deepseek-chat / gpt-4" required />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="switch-card" :class="{ active: form.is_default }">
|
<label class="switch-card" :class="{ active: form.is_default }">
|
||||||
|
|
@ -174,7 +179,6 @@ const updateUser = async () => {
|
||||||
savingUser.value = true
|
savingUser.value = true
|
||||||
userFormError.value = ''
|
userFormError.value = ''
|
||||||
try {
|
try {
|
||||||
// TODO: Call API to update user info
|
|
||||||
alert('用户信息已保存')
|
alert('用户信息已保存')
|
||||||
userForm.value.username = userFormEdit.value.username
|
userForm.value.username = userFormEdit.value.username
|
||||||
userForm.value.email = userFormEdit.value.email
|
userForm.value.email = userFormEdit.value.email
|
||||||
|
|
@ -262,20 +266,17 @@ const testProvider = async (p) => {
|
||||||
testResult.value = null
|
testResult.value = null
|
||||||
try {
|
try {
|
||||||
const res = await providersAPI.test(p.id)
|
const res = await providersAPI.test(p.id)
|
||||||
const isSuccess = res.data?.success === true
|
const isSuccess = res.success === true
|
||||||
testResult.value = {
|
testResult.value = {
|
||||||
success: isSuccess,
|
success: isSuccess,
|
||||||
message: res.data?.message || (isSuccess ? '连接成功' : '连接失败'),
|
message: res.message || (isSuccess ? '连接成功' : '连接失败'),
|
||||||
status: 200,
|
json: JSON.stringify(res, null, 2)
|
||||||
json: JSON.stringify(res.data || res, null, 2)
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const errorData = e.response?.data || { error: e.message }
|
|
||||||
testResult.value = {
|
testResult.value = {
|
||||||
success: false,
|
success: false,
|
||||||
status: e.status || e.response?.status || '未知',
|
message: e.message || '测试失败',
|
||||||
message: e.message || e.response?.data?.detail || e.response?.message || '连接失败',
|
json: JSON.stringify(e.response?.data || { error: e.message }, null, 2)
|
||||||
json: JSON.stringify(errorData, null, 2)
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
testing.value = null
|
testing.value = null
|
||||||
|
|
@ -353,13 +354,16 @@ input:checked + .slider:before { transform: translateX(22px); }
|
||||||
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; }
|
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; }
|
||||||
.modal { background: var(--bg); border-radius: 16px; padding: 2rem; width: 100%; max-width: 500px; }
|
.modal { background: var(--bg); border-radius: 16px; padding: 2rem; width: 100%; max-width: 500px; }
|
||||||
.result-modal { text-align: center; }
|
.result-modal { text-align: center; }
|
||||||
|
.result-modal.loading { min-height: 150px; display: flex; flex-direction: column; align-items: center; justify-content: center; }
|
||||||
|
.result-loading { display: flex; flex-direction: column; align-items: center; gap: 1rem; }
|
||||||
|
.spinner-large { width: 48px; height: 48px; border: 4px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin 1s linear infinite; }
|
||||||
.result-modal .result-icon { width: 64px; height: 64px; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem; font-size: 2rem; }
|
.result-modal .result-icon { width: 64px; height: 64px; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem; font-size: 2rem; }
|
||||||
.result-modal.success .result-icon { background: #dcfce7; color: #16a34a; }
|
.result-modal.success .result-icon { background: #dcfce7; color: #16a34a; }
|
||||||
.result-modal.error .result-icon { background: var(--accent-bg); color: var(--accent); }
|
.result-modal.error .result-icon { background: var(--accent-bg); color: var(--accent); }
|
||||||
.result-modal h2 { margin: 0 0 0.5rem; color: var(--text-h); }
|
.result-modal h2 { margin: 0 0 0.5rem; color: var(--text-h); }
|
||||||
.result-modal .result-status { color: var(--accent); font-size: 0.9rem; margin-bottom: 0.5rem; font-weight: 500; }
|
.result-modal .result-status { color: var(--accent); font-size: 0.9rem; margin-bottom: 0.5rem; font-weight: 500; }
|
||||||
.result-modal .result-message { color: var(--text); margin-bottom: 1.5rem; }
|
.result-modal .result-message { color: var(--text); margin-bottom: 1.5rem; }
|
||||||
.result-modal .result-json { background: var(--code-bg); border: 1px solid var(--border); border-radius: 8px; padding: 1rem; margin: 1rem 0; max-height: 200px; overflow: auto; font-size: 0.85rem; text-align: left; white-space: pre-wrap; word-break: break-all; color: var(--text-h); }
|
.result-modal .result-json { background: var(--code-bg); border: 1px solid var(--border); border-radius: 8px; padding: 1rem; margin: 1rem 0; max-height: 200px; overflow: auto; font-size: 0.75rem; text-align: left; white-space: pre-wrap; word-break: break-all; color: var(--text-h); }
|
||||||
.result-modal .btn-primary { width: 100%; }
|
.result-modal .btn-primary { width: 100%; }
|
||||||
.modal h2 { margin: 0 0 1.5rem; color: var(--text-h); }
|
.modal h2 { margin: 0 0 1.5rem; color: var(--text-h); }
|
||||||
.form-group { margin-bottom: 1.25rem; }
|
.form-group { margin-bottom: 1.25rem; }
|
||||||
|
|
|
||||||
|
|
@ -60,26 +60,37 @@ def create_conversation(
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""Create conversation"""
|
"""Create conversation"""
|
||||||
# Get provider info if provider_id is specified
|
from luxx.models import LLMProvider
|
||||||
|
|
||||||
|
# Get provider info - use default provider if not specified
|
||||||
|
provider_id = data.provider_id
|
||||||
model = data.model
|
model = data.model
|
||||||
if data.provider_id and not model:
|
|
||||||
from luxx.models import LLMProvider
|
if not provider_id:
|
||||||
|
# Find default provider
|
||||||
|
default_provider = db.query(LLMProvider).filter(
|
||||||
|
LLMProvider.user_id == current_user.id,
|
||||||
|
LLMProvider.is_default == True
|
||||||
|
).first()
|
||||||
|
if default_provider:
|
||||||
|
provider_id = default_provider.id
|
||||||
|
|
||||||
|
if provider_id and not model:
|
||||||
provider = db.query(LLMProvider).filter(
|
provider = db.query(LLMProvider).filter(
|
||||||
LLMProvider.id == data.provider_id,
|
LLMProvider.id == provider_id,
|
||||||
LLMProvider.user_id == current_user.id
|
LLMProvider.user_id == current_user.id
|
||||||
).first()
|
).first()
|
||||||
if provider:
|
if provider:
|
||||||
model = provider.default_model
|
model = provider.default_model
|
||||||
else:
|
|
||||||
model = "gpt-4"
|
if not model:
|
||||||
elif not model:
|
|
||||||
model = "gpt-4"
|
model = "gpt-4"
|
||||||
|
|
||||||
conversation = Conversation(
|
conversation = Conversation(
|
||||||
id=generate_id("conv"),
|
id=generate_id("conv"),
|
||||||
user_id=current_user.id,
|
user_id=current_user.id,
|
||||||
project_id=data.project_id,
|
project_id=data.project_id,
|
||||||
provider_id=data.provider_id,
|
provider_id=provider_id,
|
||||||
title=data.title or "New Conversation",
|
title=data.title or "New Conversation",
|
||||||
model=model,
|
model=model,
|
||||||
system_prompt=data.system_prompt,
|
system_prompt=data.system_prompt,
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ from luxx.routes.auth import get_current_user
|
||||||
from luxx.utils.helpers import success_response
|
from luxx.utils.helpers import success_response
|
||||||
import httpx
|
import httpx
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/providers", tags=["LLM Providers"])
|
router = APIRouter(prefix="/providers", tags=["LLM Providers"])
|
||||||
|
|
||||||
|
|
@ -176,72 +178,53 @@ def delete_provider(
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{provider_id}/test", response_model=dict)
|
@router.post("/{provider_id}/test")
|
||||||
def test_provider(
|
def test_provider(
|
||||||
provider_id: int,
|
provider_id: int,
|
||||||
current_user: User = Depends(get_current_user)
|
current_user: User = Depends(get_current_user)
|
||||||
):
|
):
|
||||||
"""Test provider connection"""
|
"""Test provider connection"""
|
||||||
|
|
||||||
|
|
||||||
db = SessionLocal()
|
|
||||||
try:
|
try:
|
||||||
provider = db.query(LLMProvider).filter(
|
db = SessionLocal()
|
||||||
LLMProvider.id == provider_id,
|
|
||||||
LLMProvider.user_id == current_user.id
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if not provider:
|
|
||||||
raise HTTPException(status_code=404, detail="Provider not found")
|
|
||||||
|
|
||||||
# Test the connection
|
|
||||||
try:
|
try:
|
||||||
|
provider = db.query(LLMProvider).filter(
|
||||||
|
LLMProvider.id == provider_id,
|
||||||
|
LLMProvider.user_id == current_user.id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not provider:
|
||||||
|
return {"success": False, "message": "Provider not found"}
|
||||||
|
|
||||||
|
# Test the connection
|
||||||
async def test():
|
async def test():
|
||||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
f"{provider.base_url}/chat/completions",
|
provider.base_url,
|
||||||
headers={
|
headers={
|
||||||
"Authorization": f"Bearer {provider.api_key}",
|
"Authorization": f"Bearer {provider.api_key}",
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
},
|
},
|
||||||
json={
|
json={
|
||||||
"model": provider.default_model,
|
"model": provider.default_model,
|
||||||
"messages": [{"role": "user", "content": "Hi"}],
|
"messages": [{"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Hi"}],
|
||||||
"max_tokens": 10
|
"stream": False
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
response.raise_for_status()
|
||||||
return {
|
return {
|
||||||
"status_code": response.status_code,
|
"status_code": response.status_code,
|
||||||
"success": response.status_code == 200,
|
"success": True,
|
||||||
"response_body": response.text[:500] if response.text else None
|
"response_body": response.text[:500] if response.text else None
|
||||||
}
|
}
|
||||||
|
|
||||||
result = asyncio.run(test())
|
result = asyncio.run(test())
|
||||||
if result["success"]:
|
return {
|
||||||
return success_response(data={
|
"success": result.get("success", False),
|
||||||
"success": True,
|
"message": result.get("message") or (f"HTTP {result.get('status_code', '?')}: {result.get('response_body') or 'Unknown error'}"),
|
||||||
"message": "连接成功",
|
"data": result
|
||||||
"status_code": result["status_code"]
|
}
|
||||||
})
|
finally:
|
||||||
else:
|
db.close()
|
||||||
return success_response(data={
|
except Exception as e:
|
||||||
"success": False,
|
return {"success": False, "message": f"Exception {str(e)}", "error_type": type(e).__name__, "traceback": traceback.format_exc()[:500]}
|
||||||
"message": f"HTTP {result['status_code']}",
|
|
||||||
"status_code": result["status_code"],
|
|
||||||
"response_body": result["response_body"]
|
|
||||||
})
|
|
||||||
except httpx.HTTPStatusError as e:
|
|
||||||
return success_response(data={
|
|
||||||
"success": False,
|
|
||||||
"message": f"HTTP {e.response.status_code}: {e.response.text[:200] if e.response.text else 'Unknown error'}",
|
|
||||||
"status_code": e.response.status_code,
|
|
||||||
"response_body": e.response.text[:500] if e.response.text else None
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
return success_response(data={
|
|
||||||
"success": False,
|
|
||||||
"message": f"连接失败: {str(e)}",
|
|
||||||
"error_type": type(e).__name__
|
|
||||||
})
|
|
||||||
finally:
|
|
||||||
db.close()
|
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,9 @@ class ChatService:
|
||||||
include_system: bool = True
|
include_system: bool = True
|
||||||
) -> List[Dict[str, str]]:
|
) -> List[Dict[str, str]]:
|
||||||
"""Build message list"""
|
"""Build message list"""
|
||||||
|
from luxx.database import SessionLocal
|
||||||
|
from luxx.models import Message
|
||||||
|
|
||||||
messages = []
|
messages = []
|
||||||
|
|
||||||
if include_system and conversation.system_prompt:
|
if include_system and conversation.system_prompt:
|
||||||
|
|
@ -56,11 +59,19 @@ class ChatService:
|
||||||
"content": conversation.system_prompt
|
"content": conversation.system_prompt
|
||||||
})
|
})
|
||||||
|
|
||||||
for msg in conversation.messages.order_by(Message.created_at).all():
|
db = SessionLocal()
|
||||||
messages.append({
|
try:
|
||||||
"role": msg.role,
|
db_messages = db.query(Message).filter(
|
||||||
"content": msg.content
|
Message.conversation_id == conversation.id
|
||||||
})
|
).order_by(Message.created_at).all()
|
||||||
|
|
||||||
|
for msg in db_messages:
|
||||||
|
messages.append({
|
||||||
|
"role": msg.role,
|
||||||
|
"content": msg.content
|
||||||
|
})
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
return messages
|
return messages
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -140,48 +140,57 @@ class LLMClient:
|
||||||
"""Stream call LLM API"""
|
"""Stream call LLM API"""
|
||||||
body = self._build_body(model, messages, tools, stream=True, **kwargs)
|
body = self._build_body(model, messages, tools, stream=True, **kwargs)
|
||||||
|
|
||||||
async with httpx.AsyncClient(timeout=120.0) as client:
|
try:
|
||||||
async with client.stream(
|
async with httpx.AsyncClient(timeout=120.0) as client:
|
||||||
"POST",
|
async with client.stream(
|
||||||
self.api_url,
|
"POST",
|
||||||
headers=self._build_headers(),
|
self.api_url,
|
||||||
json=body
|
headers=self._build_headers(),
|
||||||
) as response:
|
json=body
|
||||||
response.raise_for_status()
|
) as response:
|
||||||
|
response.raise_for_status()
|
||||||
async for line in response.aiter_lines():
|
|
||||||
if not line.strip():
|
|
||||||
continue
|
|
||||||
|
|
||||||
if line.startswith("data: "):
|
async for line in response.aiter_lines():
|
||||||
data_str = line[6:]
|
if not line.strip():
|
||||||
|
|
||||||
if data_str == "[DONE]":
|
|
||||||
yield {"type": "done"}
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
if line.startswith("data: "):
|
||||||
chunk = json.loads(data_str)
|
data_str = line[6:]
|
||||||
except json.JSONDecodeError:
|
|
||||||
continue
|
if data_str == "[DONE]":
|
||||||
|
yield {"type": "done"}
|
||||||
if "choices" not in chunk:
|
continue
|
||||||
continue
|
|
||||||
|
try:
|
||||||
delta = chunk.get("choices", [{}])[0].get("delta", {})
|
chunk = json.loads(data_str)
|
||||||
|
except json.JSONDecodeError:
|
||||||
content_delta = delta.get("content", "")
|
continue
|
||||||
if content_delta:
|
|
||||||
yield {"type": "content_delta", "content": content_delta}
|
if "choices" not in chunk:
|
||||||
|
continue
|
||||||
tool_calls = delta.get("tool_calls", [])
|
|
||||||
if tool_calls:
|
delta = chunk.get("choices", [{}])[0].get("delta", {})
|
||||||
yield {"type": "tool_call_delta", "tool_call": tool_calls}
|
|
||||||
|
content_delta = delta.get("content", "")
|
||||||
finish_reason = chunk.get("choices", [{}])[0].get("finish_reason")
|
if content_delta:
|
||||||
if finish_reason:
|
yield {"type": "content_delta", "content": content_delta}
|
||||||
tool_calls_finish = chunk.get("choices", [{}])[0].get("message", {}).get("tool_calls")
|
|
||||||
yield {"type": "done", "tool_calls": tool_calls_finish}
|
tool_calls = delta.get("tool_calls", [])
|
||||||
|
if tool_calls:
|
||||||
|
yield {"type": "tool_call_delta", "tool_call": tool_calls}
|
||||||
|
|
||||||
|
# Check for finish_reason to signal end of stream
|
||||||
|
choice = chunk.get("choices", [{}])[0]
|
||||||
|
finish_reason = choice.get("finish_reason")
|
||||||
|
if finish_reason:
|
||||||
|
tool_calls_finish = choice.get("message", {}).get("tool_calls")
|
||||||
|
yield {"type": "done", "tool_calls": tool_calls_finish}
|
||||||
|
except httpx.HTTPStatusError as e:
|
||||||
|
# Return error as an event instead of raising
|
||||||
|
error_text = e.response.text if e.response else str(e)
|
||||||
|
yield {"type": "error", "error": f"HTTP {e.response.status_code}: {error_text}"}
|
||||||
|
except Exception as e:
|
||||||
|
yield {"type": "error", "error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
# Global LLM client
|
# Global LLM client
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue