386 lines
9.7 KiB
Vue
386 lines
9.7 KiB
Vue
|
|
<template>
|
||
|
|
<div class="chat-panel">
|
||
|
|
<!-- 厂商选择器 -->
|
||
|
|
<div class="form-group">
|
||
|
|
<label for="chat-platform">选择厂商</label>
|
||
|
|
<select id="chat-platform" v-model="selectedPlatform" :disabled="!configuredPlatforms.length">
|
||
|
|
<option value="">请选择厂商</option>
|
||
|
|
<option v-for="platform in configuredPlatforms" :key="platform.id" :value="platform.id">
|
||
|
|
{{ platform.name }}
|
||
|
|
</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 对话输入框 -->
|
||
|
|
<div class="form-group">
|
||
|
|
<label for="user-input">输入问题</label>
|
||
|
|
<textarea
|
||
|
|
id="user-input"
|
||
|
|
v-model="userInput"
|
||
|
|
placeholder="请输入您的问题..."
|
||
|
|
:disabled="!selectedPlatform || isSending"
|
||
|
|
rows="4"
|
||
|
|
></textarea>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 发送按钮 -->
|
||
|
|
<div class="form-group">
|
||
|
|
<button @click="sendMessage" :disabled="!canSend || isSending" class="send-btn">
|
||
|
|
{{ isSending ? '发送中...' : '发送' }}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 响应显示区域 -->
|
||
|
|
<div v-if="responseMessage" class="response-area">
|
||
|
|
<h3>AI 响应</h3>
|
||
|
|
<div class="response-content" v-html="responseMessage"></div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 错误信息显示 -->
|
||
|
|
<div v-if="errorMessage" class="error-message">
|
||
|
|
{{ errorMessage }}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 提示信息 -->
|
||
|
|
<div v-if="!configuredPlatforms.length" class="info-message">
|
||
|
|
<p>请先在配置页面添加厂商配置</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script setup>
|
||
|
|
import { ref, computed, onMounted } from 'vue'
|
||
|
|
import { decrypt } from '../utils/encryption'
|
||
|
|
|
||
|
|
// 响应式数据
|
||
|
|
const userInput = ref('')
|
||
|
|
const selectedPlatform = ref('')
|
||
|
|
const responseMessage = ref('')
|
||
|
|
const errorMessage = ref('')
|
||
|
|
const isSending = ref(false)
|
||
|
|
const configuredPlatforms = ref([])
|
||
|
|
|
||
|
|
// 计算属性
|
||
|
|
const canSend = computed(() => {
|
||
|
|
return selectedPlatform.value && userInput.value.trim() !== ''
|
||
|
|
})
|
||
|
|
|
||
|
|
// 从localStorage加载已配置的厂商
|
||
|
|
const loadConfiguredPlatforms = () => {
|
||
|
|
try {
|
||
|
|
const stored = localStorage.getItem('ai-api-configs')
|
||
|
|
if (stored) {
|
||
|
|
const configs = JSON.parse(stored)
|
||
|
|
configuredPlatforms.value = configs.map(config => {
|
||
|
|
// 这里需要根据实际的平台配置来构建完整信息
|
||
|
|
// 为简化实现,我们只保留必要的信息
|
||
|
|
return {
|
||
|
|
id: config.id,
|
||
|
|
name: getPlatformName(config.id)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('加载配置失败:', error)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 获取平台名称
|
||
|
|
const getPlatformName = (platformId) => {
|
||
|
|
const platforms = [
|
||
|
|
{ id: 'openai', name: 'OpenAI' },
|
||
|
|
{ id: 'anthropic', name: 'Anthropic' },
|
||
|
|
{ id: 'google', name: 'Google (Gemini)' },
|
||
|
|
{ id: 'qwen', name: '通义千问(阿里云)' },
|
||
|
|
{ id: 'wenxin', name: '文心一言(百度)' }
|
||
|
|
]
|
||
|
|
const platform = platforms.find(p => p.id === platformId)
|
||
|
|
return platform ? platform.name : platformId
|
||
|
|
}
|
||
|
|
|
||
|
|
// 发送消息
|
||
|
|
const sendMessage = async () => {
|
||
|
|
if (!canSend.value) return
|
||
|
|
|
||
|
|
isSending.value = true
|
||
|
|
responseMessage.value = ''
|
||
|
|
errorMessage.value = ''
|
||
|
|
|
||
|
|
try {
|
||
|
|
// 获取选中的平台配置
|
||
|
|
const platformConfig = configuredPlatforms.value.find(p => p.id === selectedPlatform.value)
|
||
|
|
if (!platformConfig) {
|
||
|
|
throw new Error('未找到厂商配置')
|
||
|
|
}
|
||
|
|
|
||
|
|
// 从localStorage获取密钥
|
||
|
|
const stored = localStorage.getItem('ai-api-configs')
|
||
|
|
if (!stored) {
|
||
|
|
throw new Error('未找到厂商配置')
|
||
|
|
}
|
||
|
|
|
||
|
|
const configs = JSON.parse(stored)
|
||
|
|
const config = configs.find(c => c.id === selectedPlatform.value)
|
||
|
|
if (!config) {
|
||
|
|
throw new Error('未找到厂商配置')
|
||
|
|
}
|
||
|
|
|
||
|
|
// 解密密钥
|
||
|
|
const apiKey = decrypt(config.apiKey)
|
||
|
|
|
||
|
|
// 调用API
|
||
|
|
const result = await callAIPlatform(selectedPlatform.value, apiKey, config.endpoint, userInput.value)
|
||
|
|
|
||
|
|
// 显示响应
|
||
|
|
responseMessage.value = result
|
||
|
|
} catch (error) {
|
||
|
|
console.error('发送消息失败:', error)
|
||
|
|
errorMessage.value = error.message || '发送消息失败,请重试'
|
||
|
|
} finally {
|
||
|
|
isSending.value = false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 调用AI平台API
|
||
|
|
const callAIPlatform = async (platformId, apiKey, endpoint, message) => {
|
||
|
|
// 根据不同平台调用不同的API
|
||
|
|
switch (platformId) {
|
||
|
|
case 'openai':
|
||
|
|
return await callOpenAI(apiKey, endpoint, message)
|
||
|
|
case 'anthropic':
|
||
|
|
return await callAnthropic(apiKey, endpoint, message)
|
||
|
|
case 'google':
|
||
|
|
return await callGoogle(apiKey, endpoint, message)
|
||
|
|
case 'qwen':
|
||
|
|
return await callQwen(apiKey, endpoint, message)
|
||
|
|
case 'wenxin':
|
||
|
|
return await callWenxin(apiKey, endpoint, message)
|
||
|
|
default:
|
||
|
|
throw new Error(`不支持的厂商: ${platformId}`)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 调用OpenAI API
|
||
|
|
const callOpenAI = async (apiKey, endpoint, message) => {
|
||
|
|
const response = await fetch(endpoint, {
|
||
|
|
method: 'POST',
|
||
|
|
headers: {
|
||
|
|
'Content-Type': 'application/json',
|
||
|
|
'Authorization': `Bearer ${apiKey}`
|
||
|
|
},
|
||
|
|
body: JSON.stringify({
|
||
|
|
model: 'gpt-3.5-turbo',
|
||
|
|
messages: [{ role: 'user', content: message }],
|
||
|
|
temperature: 0.7
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
const errorData = await response.json().catch(() => ({}))
|
||
|
|
throw new Error(`OpenAI API 调用失败 (${response.status}): ${errorData.error?.message || '未知错误'}`)
|
||
|
|
}
|
||
|
|
|
||
|
|
const data = await response.json()
|
||
|
|
return data.choices[0]?.message?.content || '未获取到响应内容'
|
||
|
|
}
|
||
|
|
|
||
|
|
// 调用Anthropic API
|
||
|
|
const callAnthropic = async (apiKey, endpoint, message) => {
|
||
|
|
const response = await fetch(endpoint, {
|
||
|
|
method: 'POST',
|
||
|
|
headers: {
|
||
|
|
'Content-Type': 'application/json',
|
||
|
|
'x-api-key': apiKey,
|
||
|
|
'anthropic-version': '2023-06-01'
|
||
|
|
},
|
||
|
|
body: JSON.stringify({
|
||
|
|
model: 'claude-3-haiku-20240307',
|
||
|
|
messages: [{ role: 'user', content: message }],
|
||
|
|
max_tokens: 1024
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
const errorData = await response.json().catch(() => ({}))
|
||
|
|
throw new Error(`Anthropic API 调用失败 (${response.status}): ${errorData.error?.message || '未知错误'}`)
|
||
|
|
}
|
||
|
|
|
||
|
|
const data = await response.json()
|
||
|
|
return data.content[0]?.text || '未获取到响应内容'
|
||
|
|
}
|
||
|
|
|
||
|
|
// 调用Google Gemini API
|
||
|
|
const callGoogle = async (apiKey, endpoint, message) => {
|
||
|
|
const response = await fetch(`${endpoint}?key=${apiKey}`, {
|
||
|
|
method: 'POST',
|
||
|
|
headers: {
|
||
|
|
'Content-Type': 'application/json'
|
||
|
|
},
|
||
|
|
body: JSON.stringify({
|
||
|
|
contents: [{
|
||
|
|
parts: [
|
||
|
|
{ text: message }
|
||
|
|
]
|
||
|
|
}]
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
const errorData = await response.json().catch(() => ({}))
|
||
|
|
throw new Error(`Google API 调用失败 (${response.status}): ${errorData.error?.message || '未知错误'}`)
|
||
|
|
}
|
||
|
|
|
||
|
|
const data = await response.json()
|
||
|
|
return data.candidates[0]?.content?.parts[0]?.text || '未获取到响应内容'
|
||
|
|
}
|
||
|
|
|
||
|
|
// 调用通义千问API
|
||
|
|
const callQwen = async (apiKey, endpoint, message) => {
|
||
|
|
const response = await fetch(endpoint, {
|
||
|
|
method: 'POST',
|
||
|
|
headers: {
|
||
|
|
'Content-Type': 'application/json',
|
||
|
|
'Authorization': `Bearer ${apiKey}`,
|
||
|
|
'X-DashScope-Scene': 'chat'
|
||
|
|
},
|
||
|
|
body: JSON.stringify({
|
||
|
|
model: 'qwen-plus',
|
||
|
|
input: {
|
||
|
|
messages: [{ role: 'user', content: message }]
|
||
|
|
},
|
||
|
|
parameters: {
|
||
|
|
result_format: 'message'
|
||
|
|
}
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
const errorData = await response.json().catch(() => ({}))
|
||
|
|
throw new Error(`通义千问 API 调用失败 (${response.status}): ${errorData.error?.message || '未知错误'}`)
|
||
|
|
}
|
||
|
|
|
||
|
|
const data = await response.json()
|
||
|
|
return data.output?.choices[0]?.message?.content || '未获取到响应内容'
|
||
|
|
}
|
||
|
|
|
||
|
|
// 调用文心一言API
|
||
|
|
const callWenxin = async (apiKey, endpoint, message) => {
|
||
|
|
const response = await fetch(`${endpoint}?access_token=${apiKey}`, {
|
||
|
|
method: 'POST',
|
||
|
|
headers: {
|
||
|
|
'Content-Type': 'application/json'
|
||
|
|
},
|
||
|
|
body: JSON.stringify({
|
||
|
|
messages: [{ role: 'user', content: message }]
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
const errorData = await response.json().catch(() => ({}))
|
||
|
|
throw new Error(`文心一言 API 调用失败 (${response.status}): ${errorData.error?.message || '未知错误'}`)
|
||
|
|
}
|
||
|
|
|
||
|
|
const data = await response.json()
|
||
|
|
return data.result || '未获取到响应内容'
|
||
|
|
}
|
||
|
|
|
||
|
|
// 初始化
|
||
|
|
onMounted(() => {
|
||
|
|
loadConfiguredPlatforms()
|
||
|
|
})
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<style scoped>
|
||
|
|
.chat-panel {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
gap: 20px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.form-group {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
}
|
||
|
|
|
||
|
|
.form-group label {
|
||
|
|
margin-bottom: 5px;
|
||
|
|
font-weight: bold;
|
||
|
|
color: #333;
|
||
|
|
}
|
||
|
|
|
||
|
|
.form-group input,
|
||
|
|
.form-group select,
|
||
|
|
.form-group textarea {
|
||
|
|
padding: 10px;
|
||
|
|
border: 1px solid #ddd;
|
||
|
|
border-radius: 4px;
|
||
|
|
font-size: 14px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.form-group input:disabled,
|
||
|
|
.form-group select:disabled,
|
||
|
|
.form-group textarea:disabled {
|
||
|
|
background-color: #f5f5f5;
|
||
|
|
cursor: not-allowed;
|
||
|
|
}
|
||
|
|
|
||
|
|
.send-btn {
|
||
|
|
padding: 12px 20px;
|
||
|
|
background-color: #28a745;
|
||
|
|
color: white;
|
||
|
|
border: none;
|
||
|
|
border-radius: 4px;
|
||
|
|
cursor: pointer;
|
||
|
|
font-size: 16px;
|
||
|
|
transition: background-color 0.3s;
|
||
|
|
}
|
||
|
|
|
||
|
|
.send-btn:hover:not(:disabled) {
|
||
|
|
background-color: #218838;
|
||
|
|
}
|
||
|
|
|
||
|
|
.send-btn:disabled {
|
||
|
|
background-color: #ccc;
|
||
|
|
cursor: not-allowed;
|
||
|
|
}
|
||
|
|
|
||
|
|
.response-area {
|
||
|
|
margin-top: 20px;
|
||
|
|
padding: 15px;
|
||
|
|
background-color: #f8f9fa;
|
||
|
|
border-radius: 4px;
|
||
|
|
border-left: 4px solid #007bff;
|
||
|
|
}
|
||
|
|
|
||
|
|
.response-area h3 {
|
||
|
|
margin-top: 0;
|
||
|
|
color: #333;
|
||
|
|
}
|
||
|
|
|
||
|
|
.response-content {
|
||
|
|
white-space: pre-wrap;
|
||
|
|
line-height: 1.5;
|
||
|
|
}
|
||
|
|
|
||
|
|
.error-message {
|
||
|
|
padding: 10px;
|
||
|
|
background-color: #f8d7da;
|
||
|
|
color: #721c24;
|
||
|
|
border: 1px solid #f5c6cb;
|
||
|
|
border-radius: 4px;
|
||
|
|
margin-top: 10px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.info-message {
|
||
|
|
padding: 15px;
|
||
|
|
background-color: #e7f3ff;
|
||
|
|
color: #004085;
|
||
|
|
border: 1px solid #b8daff;
|
||
|
|
border-radius: 4px;
|
||
|
|
text-align: center;
|
||
|
|
}
|
||
|
|
|
||
|
|
.info-message p {
|
||
|
|
margin: 0;
|
||
|
|
}
|
||
|
|
</style>
|