Compare commits
9
Commits
1abf12fbde
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f35acba363 | ||
|
|
c7365e1338 | ||
|
|
34d5bc6038 | ||
|
|
386cb79e1e | ||
|
|
afadb0ced7 | ||
|
|
6daea9a5a1 | ||
|
|
8e5ba74080 | ||
|
|
f9e2d6f3ad | ||
|
|
b71206f3b8 |
@@ -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'
|
||||
},
|
||||
// 其他厂商...
|
||||
];
|
||||
```
|
||||
@@ -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 | 收集用户反馈并迭代设计 | 低 | 收集可用性数据、建议,优化方案(可选) | 产品 | 🔵 用户反馈记录 + 迭代计划 |
|
||||

|
||||
@@ -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/
|
||||
@@ -0,0 +1,60 @@
|
||||
<?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>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</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,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();
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Generated
+2854
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"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": {
|
||||
"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 |
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
<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'
|
||||
|
||||
// 响应式数据
|
||||
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: '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
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
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 result = await callAIPlatform(selectedPlatform.value, config.apiKey, config.endpoint, userInput.value)
|
||||
|
||||
// 显示响应
|
||||
responseMessage.value = result
|
||||
} catch (error) {
|
||||
console.error('发送消息失败:', error)
|
||||
errorMessage.value = error.message || '发送消息失败,请重试'
|
||||
} finally {
|
||||
isSending.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 调用AI平台API
|
||||
// TODO 这里需要调用实际后端API接口
|
||||
const callAIPlatform = async (platformId, apiKey, endpoint, message) => {
|
||||
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({
|
||||
platformId,
|
||||
apiKey,
|
||||
endpoint,
|
||||
message
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return data.response
|
||||
} catch (error) {
|
||||
console.error('调用AI平台API失败:', error)
|
||||
throw new Error('调用AI平台API失败: ' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
loadConfiguredPlatforms()
|
||||
|
||||
// 监听配置保存事件,以便刷新数据
|
||||
const handleConfigSaved = () => {
|
||||
loadConfiguredPlatforms()
|
||||
}
|
||||
|
||||
window.addEventListener('config-saved', handleConfigSaved)
|
||||
|
||||
// 组件卸载时移除事件监听器
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('config-saved', handleConfigSaved)
|
||||
})
|
||||
})
|
||||
</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>
|
||||
@@ -0,0 +1,367 @@
|
||||
<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>
|
||||
|
||||
<!-- 保存按钮 -->
|
||||
<!-- TODO 保存后应该使得 ChatPanel 组件刷新,使得能够选择厂商 -->
|
||||
<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'
|
||||
|
||||
// 厂商配置预设
|
||||
const platforms = [
|
||||
{
|
||||
id: 'openai',
|
||||
name: 'OpenAI',
|
||||
endpoint: 'https://api.openai.com/v1/chat/completions',
|
||||
keyPlaceholder: 'sk-...',
|
||||
keyType: 'api_key'
|
||||
},
|
||||
{
|
||||
id: 'zhipu',
|
||||
name: '智谱',
|
||||
endpoint: 'https://open.bigmodel.cn/api/paas/v4/chat/completions',
|
||||
keyPlaceholder: 'sk-...',
|
||||
keyType: 'api_key'
|
||||
},
|
||||
{
|
||||
id: 'deepseek',
|
||||
name: 'DeepSeek',
|
||||
endpoint: 'https://api.deepseek.com/chat/completions',
|
||||
keyPlaceholder: 'sk-...',
|
||||
keyType: 'api_key'
|
||||
},
|
||||
{
|
||||
id: 'qwen',
|
||||
name: '通义千问',
|
||||
endpoint: 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions',
|
||||
keyPlaceholder: 'sk-...',
|
||||
keyType: 'api_key'
|
||||
},
|
||||
{
|
||||
id: 'siliconflow',
|
||||
name: '硅基流动',
|
||||
endpoint: 'https://api.siliconflow.cn/v1/chat/completions',
|
||||
keyPlaceholder: 'sk-...',
|
||||
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 config = {
|
||||
id: selectedPlatform.value,
|
||||
apiKey: apiKey.value,
|
||||
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')
|
||||
|
||||
// TODO: 保存后应该使得 ChatPanel 组件刷新,使得能够选择厂商
|
||||
// 触发全局事件,通知 ChatPanel 刷新配置
|
||||
if (typeof window !== 'undefined') {
|
||||
window.dispatchEvent(new CustomEvent('config-saved', { detail: { platformId: selectedPlatform.value } }))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存配置失败:', error)
|
||||
showValidation('保存配置失败,请重试', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
// 验证密钥格式
|
||||
const validateApiKey = () => {
|
||||
const key = apiKey.value.trim()
|
||||
const platform = currentPlatform.value
|
||||
|
||||
if (!platform) return true
|
||||
|
||||
// 根据厂商类型验证密钥格式
|
||||
if (platform.keyType === 'api_key') {
|
||||
if (!key.startsWith('sk-') || key.length < 20) {
|
||||
showValidation('API 密钥格式不正确,应以 "sk-" 开头且长度不少于20位', '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>
|
||||
@@ -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')
|
||||
@@ -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))
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user