Compare commits

...
5 Commits
Author SHA1 Message Date
wonder f35acba363 🔄Update: Final 2025-10-06 14:11:30 +08:00
wonder c7365e1338 🔄Update: 删除加密逻辑 2025-10-06 13:50:08 +08:00
wonder 34d5bc6038 📝Docs: Update TaskList 2025-10-06 01:15:35 +08:00
wonder 386cb79e1e 🐛Fix: 修复组件不及时更新的问题 2025-10-06 01:13:37 +08:00
wonder afadb0ced7 🐛Fix: T4 & T5 & T6 累积更新 2025-10-06 00:54:47 +08:00
14 changed files with 177 additions and 366 deletions
-7
View File
@@ -1,7 +0,0 @@
| 平台 | 端点(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 |
-122
View File
@@ -1,122 +0,0 @@
## 产品需求文档 (PRD):AI API 配置与对话简易工具
---
### **1. 背景**
随着大模型应用普及,开发者需频繁切换不同厂商的API(如 OpenAI、通义千问、硅基流动)。现有方案操作繁琐(需手动修改代码/环境变量),缺乏统一配置入口。本工具旨在提供**轻量级 Web 界面**,支持快速配置主流厂商 API 并进行简单对话,提升开发效率。
---
### **2. 目标**
- ✅ **核心目标**:用户通过 1 个页面完成所有厂商 API 配置与对话测试
- ✅ **关键指标**:
- 配置操作简易(选厂商 → 输入密钥/接入点 → 保存)
- 支持主流厂商
---
### **3. 项目范围**
| **范围** | **包含** | **不包含** |
|-------------------|---------------------------------------------|------------------------------------|
| **功能** | - 厂商配置管理(密钥/接入点)<br>- 多厂商对话界面<br>- 安全存储密钥 | - 用户认证/权限管理<br>- 聊天历史记录<br>- 复杂对话逻辑(如 multi-turn) |
| **支持厂商** | OpenAI, Anthropic, Google (Gemini), 阿里云(通义千问), 百度(文心一言) | 本地模型(如 Llama)、定制化厂商 API |
| **安全** | 密钥加密存储、禁止明文展示 | 无 Key 管理后台(纯客户端安全) |
| **平台** | 现代浏览器(Chrome/Firefox/Safari) | 移动端适配、桌面应用 |
> ✨ **附录:厂商支持列表**
> | 厂商 | 接入点示例 | 密钥类型 |
> |--------------|-------------------------------------|-------------|
> | OpenAI | `https://api.openai.com/v1/chat/completions` | `sk-...` |
> | Anthropic | `https://api.anthropic.com/v1/messages` | `sk-...` |
> | Google | `https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent` | `AIza...` |
> | 阿里云通义 | `https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation` | `sk-...` |
> | 百度文心 | `https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/ernie\_bot` | `your_api_key` |
---
### **4. 功能需求**
#### **4.1 厂商配置管理(核心功能)**
| 功能 | 说明 | 用户操作流 |
|---------------------|----------------------------------------------------------------------|-------------------------------------|
| **厂商下拉选择** | - 预置 5 个厂商(见附录表格),下拉菜单中**自动隐藏**未启用的厂商 | 1. 点击下拉框 → 2. 选中目标厂商 |
| **密钥/接入点输入** | - 动态显示对应字段(OpenAI 需输入 `密钥`+`接入点`,通义需输入 `API Key`) | 1. 填写密钥(如 `sk-abc123`)<br>2. 填写接入点(默认预填) |
| **保存与验证** | - 保存时**自动校验密钥格式**(如 OpenAI 密钥以 `sk-` 开头)<br>- **不存储明文**,使用 AES-256 加密后存入浏览器 `localStorage` | 1. 点击「保存配置」→ 2. 成功提示<br>→ 3. 即时生效(无需刷新) |
> ⚠️ **安全设计**:
> - 密钥在客户端加密存储,**服务端不接触密钥**
> - 配置页面关闭后,密钥不可恢复(用户需重新配置)
#### **4.2 AI 对话界面(核心功能)**
| 功能 | 说明 |
|---------------------|----------------------------------------------------------------------|
| **对话输入框** | 单行文本输入,支持中文/英文提问(示例:`“解释量子计算”`) |
| **厂商选择器** | **对话前必选**:从已配置的厂商列表中选(默认显示按配置顺序排序) |
| **发送与响应** | - 点击「发送」按钮 → 显示加载状态 → 厂商 API 返回结果显示在底部 |
| **错误处理** | - 密钥错误 → 提示 `“厂商 [X] 密钥无效”`<br>- 服务端错误 → 提示 `“[厂商] API 调用失败 (错误码: 500)”` |
> 🌰 **示例流程**:
> 1. 用户在配置页为 OpenAI 保存密钥 `sk-test123`
> 2. 进入对话页 → 选 OpenAI → 输入 `“写首关于AI的诗”` → 点击发送
> 3. 即时显示 OpenAI 返回的诗句
---
### **5. 非功能需求**
| 类别 | 要求 |
|---------------|---------------------------------------|
| **性能** | 配置保存响应时间 ≤ 500ms;对话响应 ≤ 3s(依赖厂商) |
| **可用性** | 0 学习成本:所有操作 ≤ 3 步,无复杂菜单 |
| **可扩展性** | 新增厂商仅需修改前端配置表(无需改代码) |
| **安全** | 100% 避免明文密钥存储,废弃旧配置自动清除 |
| **兼容性** | 支持 Chrome/Firefox/Safari 最新版,不兼容 IE |
---
### **6. 应用流程图**
```mermaid
graph TD
A[访问主页] --> B[配置厂商]
B --> C{是否选择新厂商}
C -- 是 --> D[填写厂商/密钥/接入点]
D --> E[保存加密]
E --> B
C -- 否 --> F[对话界面]
F --> G[选厂商 + 输入提问]
G --> H[调用厂商 API]
H --> I{成功}
I -- 是 --> J[显示结果]
I -- 否 --> K[显示错误提示]
```
---
### **7. 紧急问题处理**
| 问题场景 | 解决方案 |
|--------------------------|------------------------------------------|
| 用户误填错误密钥 | 配置页右侧显示“密钥验证失败”提醒 |
| 厂商 API 调用超时 | 支持重试(1 次),超时后提示“服务不可用” |
| 首次使用无任何厂商配置 | 顶部显示提示“请先配置厂商:OpenAI/通义千问” |
---
### **8. 附录:厂商配置预设**
```javascript
// 前端配置表(示例,可增加新厂商)
const PLATFORMS = [
{
id: 'openai',
name: 'OpenAI',
endpoint: 'https://api.openai.com/v1/chat/completions',
keyPlaceholder: 'sk-...',
keyType: 'api_key'
},
{
id: 'qwen',
name: '通义千问(阿里云)',
endpoint: 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation',
keyPlaceholder: 'sk-...',
keyType: 'api_key'
},
// 其他厂商...
];
```
+8 -25
View File
@@ -2,29 +2,12 @@
🔩 各大 LLM 厂商的 API 服务聚合
## 任务清单
| 平台 | 端点(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 |
| 任务编号 | 任务名称 | 优先级 | 说明 | 负责人 | 交付成果 |
|----------|----------------------------------|--------|----------------------------------------------------------------------|----------|--------------------------------|
| T1 | 明确需求并撰写 PRD 文档 | 高 | 完成产品需求文档,明确功能及非功能需求 | 产品 | 🟢 PRD 文档(含厂商列表、功能说明等) |
| T2 | 确定支持厂商并获取 API 信息 | 高 | 确认 5 个主流 AI API 厂商的端点、密钥类型和请求格式 | 产品 | 🟢 完整厂商支持列表(含 API 示例) |
| T3 | 搭建 Web 应用的基础架构(HTML/CSS/JS) | 中 | 创建 Web 首页和配置页、对话页,切换页面(可使用单页应用) | 开发 | 🔵 项目框架(HTML + CSS + 前端基础) |
| T4 | 实现厂商下拉选择功能 | 高 | 列出厂商选项,用户可选当前要配置的厂商 | 开发 | 🔵 下拉菜单 + 厂商动态展示 |
| T5 | 实现 API 配置输入界面 | 高 | 根据所选厂商显示对应输入项(如密钥 + 接入点) | 开发 | 🔵 动态表单(密钥/接入点输入) |
| T6 | 实现密钥输入验证 | 高 | 检查输入格式是否符合厂商要求(如 OpenAI 密钥以 `sk-` 开头) | 开发 | 🔵 格式校验 + 错误提示 |
| T7 | 实现配置保存与加密存储 | 高 | 加密存储用户输入的密钥和接入点,使用 localStorage 或 indexedDB | 开发 | 🔵 安全存储实现(AES 加密) |
| T8 | 创建厂商配置数据结构 | 中 | 在前端代码中定义厂商配置信息(如 ID、名称、端点、字段等) | 开发 | 🔵 `PLATFORMS` 配置数组(如 JS 对象结构) |
| T9 | 实现对话页界面 | 中 | 输入框 + 厂商选择器 + 发送按钮 + 响应展示区 | 开发 | 🔵 对话页面设计 + 基本交互 |
| T10 | 实现对话 API 调用逻辑 | 高 | 根据用户选择的厂商,使用对应密钥调用 AI API 并返回结果 | 开发 | 🔵 厂商 API 通信逻辑(POST 请求) |
| T11 | 实现响应展示与错误处理 | 高 | 显示 API 返回内容,处理 401、500 等错误信息 | 开发 | 🔵 响应展示模块 + 错误页/提示 |
| T12 | 添加「重试」机制(可选) | 中 | 对于调用失败的情况提供一键重试功能 | 开发 | 🔵 调用失败后支持重试按钮 |
| T13 | 构建配置页与对话页的切换逻辑 | 中 | 用户可切换配置页和对话页,保证数据在页面间同步(如密钥已保存则自动填充) | 开发 | 🔵 页面切换功能 + 自动填充已有配置 |
| T14 | 添加「首次未配置提醒」 | 中 | 如果没有配置任何厂商,引导用户进行配置 | 开发 | 🔵 首页友好提示信息 |
| T15 | 添加厂商配置删除功能 | 中 | 允许用户删除已有配置,避免冗余数据 | 开发 | 🔵 删除配置按钮 + localStorage 清理 |
| T16 | 添加厂商默认配置(可选) | 中 | 预设 OpenAI、通义等厂商的默认接入点和字段提示 | 开发 | 🔵 配置页面默认值设置 |
| T17 | 构建完整 UI 界面(风格统一) | 中 | 确保配置页和对话页操作一致、视觉统一 | UI / 前端 | 🔵 响应式设计 + 现代化界面 |
| T18 | 进行基本可用性测试 | 中 | 验证主要功能是否可用(配置保存、API 调用、错误提示等) | QA | 🔵 功能测试报告(手动/自动) |
| T19 | 添加部署方案(如 GitHub Pages) | 低 | 配置前端静态部署,便于快速访问 | 产品 | 🔵 部署文档 + 示例部署链接 |
| T20 | 撰写项目说明文档和使用教程 | 低 | 提供给用户快速上手的说明,包含配置方法和常见问题 | 产品 | 🔵 README 或使用指南文档 |
| T21 | 优化体验(如加载提示、输入缓存) | 低 | 提升用户体验,如加载动画、输入自动保存(可选) | 开发 | 🔵 更流畅、更友好的 UI 交互 |
| T22 | 收集用户反馈并迭代设计 | 低 | 收集可用性数据、建议,优化方案(可选) | 产品 | 🔵 用户反馈记录 + 迭代计划 |
![img.png](img.png)
+6
View File
@@ -40,6 +40,12 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
@@ -0,0 +1,13 @@
package cn.hezhaohui.backend.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RestTmplateConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
@@ -0,0 +1,21 @@
package cn.hezhaohui.backend.controller;
import cn.hezhaohui.backend.entity.ChatRequest;
import cn.hezhaohui.backend.entity.ChatResponse;
import cn.hezhaohui.backend.service.ChatService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@CrossOrigin(origins = "*")
@RestController
@RequestMapping
public class ChatController {
@Autowired
private ChatService chatService;
@PostMapping("/api/chat")
public ChatResponse chat(@RequestBody ChatRequest request) {
return chatService.chat(request);
}
}
@@ -0,0 +1,11 @@
package cn.hezhaohui.backend.entity;
import lombok.Data;
@Data
public class ChatRequest {
private String platformId;
private String apiKey;
private String endpoint;
private String message;
}
@@ -0,0 +1,10 @@
package cn.hezhaohui.backend.entity;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class ChatResponse {
private String response;
}
@@ -0,0 +1,33 @@
package cn.hezhaohui.backend.service;
import cn.hezhaohui.backend.entity.ChatRequest;
import cn.hezhaohui.backend.entity.ChatResponse;
import jakarta.annotation.Resource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Service
public class ChatService {
@Resource
private RestTemplate restTemplate;
public ChatResponse chat(ChatRequest req) {
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + req.getApiKey());
headers.set("Content-Type", "application/json");
String requestBody = String.format(
"{\"model\": \"Qwen/Qwen3-30B-A3B\", \"messages\": [{\"role\": \"user\", \"content\": \"%s\"}]}",
req.getMessage()
);
HttpEntity<String> stringHttpEntity = new HttpEntity<>(requestBody, headers);
ResponseEntity<String> response = restTemplate.postForEntity(req.getEndpoint(), stringHttpEntity, String.class);
System.out.println(response.getBody());
return ChatResponse.builder().response(response.getBody()).build();
}
}
-1
View File
@@ -12,7 +12,6 @@
"preview": "vite preview"
},
"dependencies": {
"crypto-js": "^4.2.0",
"vue": "^3.5.22"
},
"devDependencies": {
+39 -137
View File
@@ -50,7 +50,6 @@
<script setup>
import { ref, computed, onMounted } from 'vue'
import { decrypt } from '../utils/encryption'
// 响应式数据
const userInput = ref('')
@@ -89,10 +88,10 @@ const loadConfiguredPlatforms = () => {
const getPlatformName = (platformId) => {
const platforms = [
{ id: 'openai', name: 'OpenAI' },
{ id: 'anthropic', name: 'Anthropic' },
{ id: 'google', name: 'Google (Gemini)' },
{ id: 'qwen', name: '通义千问(阿里云)' },
{ id: 'wenxin', name: '文心一言(百度)' }
{ id: 'zhipu', name: '智谱' },
{ id: 'deepseek', name: 'DeepSeek' },
{ id: 'qwen', name: '通义千问' },
{ id: 'siliconflow', name: '硅基流动' }
]
const platform = platforms.find(p => p.id === platformId)
return platform ? platform.name : platformId
@@ -125,11 +124,8 @@ const sendMessage = async () => {
throw new Error('未找到厂商配置')
}
// 解密密钥
const apiKey = decrypt(config.apiKey)
// 调用API
const result = await callAIPlatform(selectedPlatform.value, apiKey, config.endpoint, userInput.value)
// 将配置信息和用户消息发送给后端处理
const result = await callAIPlatform(selectedPlatform.value, config.apiKey, config.endpoint, userInput.value)
// 显示响应
responseMessage.value = result
@@ -142,151 +138,57 @@ const sendMessage = async () => {
}
// 调用AI平台API
// TODO 这里需要调用实际后端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}`, {
try {
// 发送请求到后端API
// console.log(JSON.stringify({
// platformId,
// apiKey,
// endpoint,
// message
// }))
const response = await fetch('http://localhost:8080/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
contents: [{
parts: [
{ text: message }
]
}]
})
platformId,
apiKey,
endpoint,
message
})
}
)
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
throw new Error(`Google API 调用失败 (${response.status}): ${errorData.error?.message || '未知错误'}`)
throw new Error(`HTTP error! status: ${response.status}`)
}
const data = await response.json()
return data.candidates[0]?.content?.parts[0]?.text || '未获取到响应内容'
return data.response
} catch (error) {
console.error('调用AI平台API失败:', error)
throw new Error('调用AI平台API失败: ' + error.message)
}
// 调用通义千问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()
// 监听配置保存事件,以便刷新数据
const handleConfigSaved = () => {
loadConfiguredPlatforms()
}
window.addEventListener('config-saved', handleConfigSaved)
// 组件卸载时移除事件监听器
onUnmounted(() => {
window.removeEventListener('config-saved', handleConfigSaved)
})
})
</script>
+24 -26
View File
@@ -36,6 +36,7 @@
</div>
<!-- 保存按钮 -->
<!-- TODO 保存后应该使得 ChatPanel 组件刷新,使得能够选择厂商 -->
<div class="form-group">
<button @click="saveConfig" :disabled="!canSave" class="save-btn">
保存配置
@@ -60,7 +61,6 @@
<script setup>
import { ref, computed, onMounted } from 'vue'
import { encrypt, decrypt } from '../utils/encryption'
// 厂商配置预设
const platforms = [
@@ -72,31 +72,31 @@ const platforms = [
keyType: 'api_key'
},
{
id: 'anthropic',
name: 'Anthropic',
endpoint: 'https://api.anthropic.com/v1/messages',
id: 'zhipu',
name: '智谱',
endpoint: 'https://open.bigmodel.cn/api/paas/v4/chat/completions',
keyPlaceholder: 'sk-...',
keyType: 'api_key'
},
{
id: 'google',
name: 'Google (Gemini)',
endpoint: 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent',
keyPlaceholder: 'AIza...',
id: 'deepseek',
name: 'DeepSeek',
endpoint: 'https://api.deepseek.com/chat/completions',
keyPlaceholder: 'sk-...',
keyType: 'api_key'
},
{
id: 'qwen',
name: '通义千问(阿里云)',
endpoint: 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation',
name: '通义千问',
endpoint: 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions',
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',
id: 'siliconflow',
name: '硅基流动',
endpoint: 'https://api.siliconflow.cn/v1/chat/completions',
keyPlaceholder: 'sk-...',
keyType: 'api_key'
}
]
@@ -150,13 +150,10 @@ const saveConfig = () => {
return
}
// 加密密钥
const encryptedKey = encrypt(apiKey.value)
// 构造配置对象
const config = {
id: selectedPlatform.value,
apiKey: encryptedKey,
apiKey: apiKey.value,
endpoint: endpoint.value
}
@@ -182,6 +179,12 @@ const saveConfig = () => {
resetForm()
showValidation('配置保存成功', 'success')
// TODO: 保存后应该使得 ChatPanel 组件刷新,使得能够选择厂商
// 触发全局事件,通知 ChatPanel 刷新配置
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('config-saved', { detail: { platformId: selectedPlatform.value } }))
}
} catch (error) {
console.error('保存配置失败:', error)
showValidation('保存配置失败,请重试', 'error')
@@ -196,14 +199,9 @@ const validateApiKey = () => {
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')
if (platform.keyType === 'api_key') {
if (!key.startsWith('sk-') || key.length < 20) {
showValidation('API 密钥格式不正确,应以 "sk-" 开头且长度不少于20位', 'error')
return false
}
}
-36
View File
@@ -1,36 +0,0 @@
// 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('解密失败')
}
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 323 KiB