Compare commits

..
4 Commits
Author SHA1 Message Date
wonder 6daea9a5a1 🔄Update: T3/前端初步构建 2025-10-05 18:12:51 +08:00
wonder 8e5ba74080 🔧Chore: 前端框架初始化 2025-10-05 17:40:48 +08:00
wonder f9e2d6f3ad 🔧Chore: 后端框架初始化 2025-10-05 17:27:31 +08:00
wonder b71206f3b8 🔄Update: T2/整合接入平台的API文档 2025-10-05 17:18:51 +08:00
22 changed files with 4077 additions and 1 deletions
+7
View File
@@ -0,0 +1,7 @@
| 平台 | 端点(Endpoint) | 参考文档 |
|------------|-----------------------------------|-------------------------|
| OpenAI | `https://api.openai.com/v1/chat/completions` | https://openai.apifox.cn/ |
| 通义千问 | `https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions` |https://www.alibabacloud.com/help/zh/model-studio/use-qwen-by-calling-api#a25484c79d1ca|
| 智谱 | `https://open.bigmodel.cn/api/paas/v4/chat/completions` | https://docs.bigmodel.cn/api-reference/%E6%A8%A1%E5%9E%8B-api/%E5%AF%B9%E8%AF%9D%E8%A1%A5%E5%85%A8 |
| DeepSeek | `https://api.deepseek.com/chat/completions` | https://api-docs.deepseek.com/zh-cn/api/create-chat-completion|
| 硅基流动 | `https://api.siliconflow.cn/v1/chat/completions` | https://docs.siliconflow.cn/cn/api-reference/chat-completions/chat-completions |
+1 -1
View File
@@ -7,7 +7,7 @@
| 任务编号 | 任务名称 | 优先级 | 说明 | 负责人 | 交付成果 |
|----------|----------------------------------|--------|----------------------------------------------------------------------|----------|--------------------------------|
| T1 | 明确需求并撰写 PRD 文档 | 高 | 完成产品需求文档,明确功能及非功能需求 | 产品 | 🟢 PRD 文档(含厂商列表、功能说明等) |
| T2 | 确定支持厂商并获取 API 信息 | 高 | 确认 5 个主流 AI API 厂商的端点、密钥类型和请求格式 | 产品 | 🔵 完整厂商支持列表(含 API 示例) |
| T2 | 确定支持厂商并获取 API 信息 | 高 | 确认 5 个主流 AI API 厂商的端点、密钥类型和请求格式 | 产品 | 🟢 完整厂商支持列表(含 API 示例) |
| T3 | 搭建 Web 应用的基础架构(HTML/CSS/JS) | 中 | 创建 Web 首页和配置页、对话页,切换页面(可使用单页应用) | 开发 | 🔵 项目框架(HTML + CSS + 前端基础) |
| T4 | 实现厂商下拉选择功能 | 高 | 列出厂商选项,用户可选当前要配置的厂商 | 开发 | 🔵 下拉菜单 + 厂商动态展示 |
| T5 | 实现 API 配置输入界面 | 高 | 根据所选厂商显示对应输入项(如密钥 + 接入点) | 开发 | 🔵 动态表单(密钥/接入点输入) |
+33
View File
@@ -0,0 +1,33 @@
HELP.md
target/
.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
+54
View File
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.hezhaohui</groupId>
<artifactId>backend</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>backend</name>
<description>backend</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,13 @@
package cn.hezhaohui.backend;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class BackendApplication {
public static void main(String[] args) {
SpringApplication.run(BackendApplication.class, args);
}
}
@@ -0,0 +1 @@
spring.application.name=backend
@@ -0,0 +1,13 @@
package cn.hezhaohui.backend;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class BackendApplicationTests {
@Test
void contextLoads() {
}
}
+30
View File
@@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo
+3
View File
@@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
+8
View File
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}
+2854
View File
File diff suppressed because it is too large Load Diff
+23
View File
@@ -0,0 +1,23 @@
{
"name": "frontend",
"version": "0.0.0",
"private": true,
"type": "module",
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"crypto-js": "^4.2.0",
"vue": "^3.5.22"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.1",
"vite": "^7.1.7",
"vite-plugin-vue-devtools": "^8.0.2"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+80
View File
@@ -0,0 +1,80 @@
<template>
<div class="app-container">
<header class="app-header">
<h1>AI API 配置与对话工具</h1>
<p>快速配置主流厂商API并进行对话测试</p>
</header>
<main class="app-main">
<!-- 配置区域 -->
<section class="config-section">
<h2>厂商配置</h2>
<ConfigPanel />
</section>
<!-- 对话区域 -->
<section class="chat-section">
<h2>AI 对话</h2>
<ChatPanel />
</section>
</main>
</div>
</template>
<script setup>
import ConfigPanel from './components/ConfigPanel.vue'
import ChatPanel from './components/ChatPanel.vue'
</script>
<style scoped>
.app-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.app-header {
text-align: center;
margin-bottom: 30px;
padding: 20px;
background-color: #f5f5f5;
border-radius: 8px;
}
.app-header h1 {
margin: 0;
color: #333;
}
.app-header p {
margin: 10px 0 0 0;
color: #666;
}
.app-main {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
}
.config-section, .chat-section {
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.config-section h2, .chat-section h2 {
margin-top: 0;
color: #333;
border-bottom: 2px solid #007bff;
padding-bottom: 10px;
}
@media (max-width: 768px) {
.app-main {
grid-template-columns: 1fr;
}
}
</style>
+117
View File
@@ -0,0 +1,117 @@
/* 全局样式 */
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: #f5f7fa;
color: #333;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
/* 重置默认样式 */
h1, h2, h3, h4, h5, h6 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
/* 表单样式 */
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
}
.form-group input,
.form-group select,
.form-group textarea {
display: block;
width: 100%;
padding: 0.375rem 0.75rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #212529;
background-color: #fff;
background-clip: padding-box;
border: 1px solid #ced4da;
border-radius: 0.25rem;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
color: #212529;
background-color: #fff;
border-color: #86b7fe;
outline: 0;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
/* 按钮样式 */
.btn {
display: inline-block;
font-weight: 400;
text-align: center;
vertical-align: middle;
cursor: pointer;
user-select: none;
border: 1px solid transparent;
padding: 0.375rem 0.75rem;
font-size: 1rem;
line-height: 1.5;
border-radius: 0.25rem;
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.btn-primary {
color: #fff;
background-color: #007bff;
border-color: #007bff;
}
.btn-primary:hover {
color: #fff;
background-color: #0069d9;
border-color: #0062cc;
}
.btn-primary:disabled {
color: #fff;
background-color: #6c757d;
border-color: #6c757d;
}
/* 响应式设计 */
@media (max-width: 768px) {
.app-container {
padding: 10px;
}
.app-main {
grid-template-columns: 1fr;
}
}
+386
View File
@@ -0,0 +1,386 @@
<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>
+369
View File
@@ -0,0 +1,369 @@
<template>
<div class="config-panel">
<!-- 厂商选择 -->
<div class="form-group">
<label for="platform">选择厂商</label>
<select id="platform" v-model="selectedPlatform" @change="onPlatformChange">
<option value="">请选择厂商</option>
<option v-for="platform in platforms" :key="platform.id" :value="platform.id">
{{ platform.name }}
</option>
</select>
</div>
<!-- 密钥输入 -->
<div class="form-group">
<label for="api-key">API 密钥</label>
<input
id="api-key"
v-model="apiKey"
type="password"
:placeholder="currentPlatform?.keyPlaceholder || '请输入API密钥'"
:disabled="!selectedPlatform"
/>
</div>
<!-- 接入点输入 -->
<div class="form-group">
<label for="endpoint">接入点</label>
<input
id="endpoint"
v-model="endpoint"
type="text"
:placeholder="currentPlatform?.endpoint || '请输入接入点'"
:disabled="!selectedPlatform"
/>
</div>
<!-- 保存按钮 -->
<div class="form-group">
<button @click="saveConfig" :disabled="!canSave" class="save-btn">
保存配置
</button>
</div>
<!-- 验证状态 -->
<div v-if="validationMessage" class="validation-message" :class="validationClass">
{{ validationMessage }}
</div>
<!-- 已配置的厂商列表 -->
<div v-if="configuredPlatforms.length > 0" class="configured-list">
<h3>已配置的厂商</h3>
<div class="platform-item" v-for="platform in configuredPlatforms" :key="platform.id">
<span>{{ platform.name }}</span>
<button @click="removeConfig(platform.id)" class="remove-btn">删除</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { encrypt, decrypt } from '../utils/encryption'
// 厂商配置预设
const platforms = [
{
id: 'openai',
name: 'OpenAI',
endpoint: 'https://api.openai.com/v1/chat/completions',
keyPlaceholder: 'sk-...',
keyType: 'api_key'
},
{
id: 'anthropic',
name: 'Anthropic',
endpoint: 'https://api.anthropic.com/v1/messages',
keyPlaceholder: 'sk-...',
keyType: 'api_key'
},
{
id: 'google',
name: 'Google (Gemini)',
endpoint: 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent',
keyPlaceholder: 'AIza...',
keyType: 'api_key'
},
{
id: 'qwen',
name: '通义千问(阿里云)',
endpoint: 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation',
keyPlaceholder: 'sk-...',
keyType: 'api_key'
},
{
id: 'wenxin',
name: '文心一言(百度)',
endpoint: 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/ernie_bot',
keyPlaceholder: 'your_api_key',
keyType: 'api_key'
}
]
// 响应式数据
const selectedPlatform = ref('')
const apiKey = ref('')
const endpoint = ref('')
const validationMessage = ref('')
const validationClass = ref('')
const configuredPlatforms = ref([])
// 计算属性
const currentPlatform = computed(() => {
if (!selectedPlatform.value) return null
return platforms.find(p => p.id === selectedPlatform.value)
})
const canSave = computed(() => {
return selectedPlatform.value && apiKey.value && endpoint.value
})
// 从localStorage加载已配置的厂商
const loadConfiguredPlatforms = () => {
try {
const stored = localStorage.getItem('ai-api-configs')
if (stored) {
const configs = JSON.parse(stored)
configuredPlatforms.value = configs.map(config => {
const platform = platforms.find(p => p.id === config.id)
return {
...platform,
...config
}
})
}
} catch (error) {
console.error('加载配置失败:', error)
}
}
// 保存配置
const saveConfig = () => {
if (!canSave.value) {
showValidation('请填写所有必填字段', 'error')
return
}
// 验证密钥格式
if (!validateApiKey()) {
return
}
// 加密密钥
const encryptedKey = encrypt(apiKey.value)
// 构造配置对象
const config = {
id: selectedPlatform.value,
apiKey: encryptedKey,
endpoint: endpoint.value
}
// 保存到localStorage
try {
const stored = localStorage.getItem('ai-api-configs')
let configs = stored ? JSON.parse(stored) : []
// 检查是否已存在该厂商配置
const existingIndex = configs.findIndex(c => c.id === selectedPlatform.value)
if (existingIndex !== -1) {
configs[existingIndex] = config
} else {
configs.push(config)
}
localStorage.setItem('ai-api-configs', JSON.stringify(configs))
// 更新已配置列表
loadConfiguredPlatforms()
// 清空表单
resetForm()
showValidation('配置保存成功', 'success')
} catch (error) {
console.error('保存配置失败:', error)
showValidation('保存配置失败,请重试', 'error')
}
}
// 验证密钥格式
const validateApiKey = () => {
const key = apiKey.value.trim()
const platform = currentPlatform.value
if (!platform) return true
// 根据厂商类型验证密钥格式
if (platform.id === 'openai' || platform.id === 'qwen' || platform.id === 'anthropic') {
if (!key.startsWith('sk-')) {
showValidation('OpenAI/通义千问密钥应以 "sk-" 开头', 'error')
return false
}
} else if (platform.id === 'google') {
if (!key.startsWith('AIza')) {
showValidation('Google密钥应以 "AIza" 开头', 'error')
return false
}
}
return true
}
// 显示验证消息
const showValidation = (message, type) => {
validationMessage.value = message
validationClass.value = type
setTimeout(() => {
validationMessage.value = ''
}, 3000)
}
// 重置表单
const resetForm = () => {
selectedPlatform.value = ''
apiKey.value = ''
endpoint.value = ''
}
// 平台变更
const onPlatformChange = () => {
if (selectedPlatform.value) {
const platform = platforms.find(p => p.id === selectedPlatform.value)
if (platform) {
endpoint.value = platform.endpoint
}
} else {
endpoint.value = ''
}
}
// 删除配置
const removeConfig = (platformId) => {
try {
const stored = localStorage.getItem('ai-api-configs')
if (stored) {
const configs = JSON.parse(stored)
const filtered = configs.filter(c => c.id !== platformId)
localStorage.setItem('ai-api-configs', JSON.stringify(filtered))
loadConfiguredPlatforms()
showValidation('配置已删除', 'success')
}
} catch (error) {
console.error('删除配置失败:', error)
showValidation('删除配置失败', 'error')
}
}
// 初始化
onMounted(() => {
loadConfiguredPlatforms()
})
</script>
<style scoped>
.config-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 {
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.form-group input:disabled,
.form-group select:disabled {
background-color: #f5f5f5;
cursor: not-allowed;
}
.save-btn {
padding: 12px 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s;
}
.save-btn:hover:not(:disabled) {
background-color: #0056b3;
}
.save-btn:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.validation-message {
padding: 10px;
border-radius: 4px;
margin-top: 10px;
}
.validation-message.success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.validation-message.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.configured-list {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #eee;
}
.configured-list h3 {
margin-top: 0;
color: #333;
}
.platform-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
background-color: #f8f9fa;
border-radius: 4px;
margin-bottom: 10px;
}
.remove-btn {
padding: 5px 10px;
background-color: #dc3545;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.remove-btn:hover {
background-color: #c82333;
}
</style>
+10
View File
@@ -0,0 +1,10 @@
import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
import CryptoJS from 'crypto-js'
// 将CryptoJS挂载到全局,供组件使用
const app = createApp(App)
app.config.globalProperties.$crypto = CryptoJS
app.mount('#app')
+36
View File
@@ -0,0 +1,36 @@
// AES-256 加密/解密工具
// 注意:这是一个简化版本,实际生产环境应使用更安全的加密库
import CryptoJS from 'crypto-js'
// 密钥(在实际应用中应使用更安全的方式管理)
const SECRET_KEY = 'ai-api-tool-secret-key-2025'
/**
* 加密函数
* @param {string} text - 要加密的文本
* @returns {string} - 加密后的文本
*/
export function encrypt(text) {
try {
return CryptoJS.AES.encrypt(text, SECRET_KEY).toString()
} catch (error) {
console.error('加密失败:', error)
throw new Error('加密失败')
}
}
/**
* 解密函数
* @param {string} encryptedText - 要解密的文本
* @returns {string} - 解密后的文本
*/
export function decrypt(encryptedText) {
try {
const bytes = CryptoJS.AES.decrypt(encryptedText, SECRET_KEY)
return bytes.toString(CryptoJS.enc.Utf8)
} catch (error) {
console.error('解密失败:', error)
throw new Error('解密失败')
}
}
+18
View File
@@ -0,0 +1,18 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
})