🐛Fix: T4 & T5 & T6 累积更新
This commit is contained in:
@@ -4,27 +4,27 @@
|
|||||||
|
|
||||||
## 任务清单
|
## 任务清单
|
||||||
|
|
||||||
| 任务编号 | 任务名称 | 优先级 | 说明 | 负责人 | 交付成果 |
|
| 任务编号 | 任务名称 | 优先级 | 说明 | 负责人 | 交付成果 |
|
||||||
|----------|----------------------------------|--------|----------------------------------------------------------------------|----------|--------------------------------|
|
|----------|---------------------------------------|--------|----------------------------------------------------------------------|----------|--------------------------------|
|
||||||
| T1 | 明确需求并撰写 PRD 文档 | 高 | 完成产品需求文档,明确功能及非功能需求 | 产品 | 🟢 PRD 文档(含厂商列表、功能说明等) |
|
| T1 | 明确需求并撰写 PRD 文档 | 高 | 完成产品需求文档,明确功能及非功能需求 | 产品 | 🟢 PRD 文档(含厂商列表、功能说明等) |
|
||||||
| T2 | 确定支持厂商并获取 API 信息 | 高 | 确认 5 个主流 AI API 厂商的端点、密钥类型和请求格式 | 产品 | 🟢 完整厂商支持列表(含 API 示例) |
|
| T2 | 确定支持厂商并获取 API 信息 | 高 | 确认 5 个主流 AI API 厂商的端点、密钥类型和请求格式 | 产品 | 🟢 完整厂商支持列表(含 API 示例) |
|
||||||
| T3 | 搭建 Web 应用的基础架构(HTML/CSS/JS) | 中 | 创建 Web 首页和配置页、对话页,切换页面(可使用单页应用) | 开发 | 🔵 项目框架(HTML + CSS + 前端基础) |
|
| T3 | 搭建 Web 应用的基础架构(前端 VUE + 后端 SpringWEB) | 中 | 创建 Web 首页和配置页、对话页,切换页面(可使用单页应用) | 开发 | 🟢 项目框架 |
|
||||||
| T4 | 实现厂商下拉选择功能 | 高 | 列出厂商选项,用户可选当前要配置的厂商 | 开发 | 🔵 下拉菜单 + 厂商动态展示 |
|
| T4 | 实现厂商下拉选择功能 | 高 | 列出厂商选项,用户可选当前要配置的厂商 | 开发 | 🟢 下拉菜单 |
|
||||||
| T5 | 实现 API 配置输入界面 | 高 | 根据所选厂商显示对应输入项(如密钥 + 接入点) | 开发 | 🔵 动态表单(密钥/接入点输入) |
|
| T5 | 实现 API 配置输入界面 | 高 | 根据所选厂商显示对应输入项(如密钥 + 接入点) | 开发 | 🟢 动态表单(密钥/接入点输入) |
|
||||||
| T6 | 实现密钥输入验证 | 高 | 检查输入格式是否符合厂商要求(如 OpenAI 密钥以 `sk-` 开头) | 开发 | 🔵 格式校验 + 错误提示 |
|
| T6 | 实现密钥输入验证 | 高 | 检查输入格式是否符合厂商要求(如 OpenAI 密钥以 `sk-` 开头) | 开发 | 🟢 格式校验 + 错误提示 |
|
||||||
| T7 | 实现配置保存与加密存储 | 高 | 加密存储用户输入的密钥和接入点,使用 localStorage 或 indexedDB | 开发 | 🔵 安全存储实现(AES 加密) |
|
| T7 | 实现配置保存与加密存储 | 高 | 加密存储用户输入的密钥和接入点,使用 localStorage 或 indexedDB | 开发 | 🔵 安全存储实现(AES 加密) |
|
||||||
| T8 | 创建厂商配置数据结构 | 中 | 在前端代码中定义厂商配置信息(如 ID、名称、端点、字段等) | 开发 | 🔵 `PLATFORMS` 配置数组(如 JS 对象结构) |
|
| T8 | 创建厂商配置数据结构 | 中 | 在前端代码中定义厂商配置信息(如 ID、名称、端点、字段等) | 开发 | 🔵 `PLATFORMS` 配置数组(如 JS 对象结构) |
|
||||||
| T9 | 实现对话页界面 | 中 | 输入框 + 厂商选择器 + 发送按钮 + 响应展示区 | 开发 | 🔵 对话页面设计 + 基本交互 |
|
| T9 | 实现对话页界面 | 中 | 输入框 + 厂商选择器 + 发送按钮 + 响应展示区 | 开发 | 🔵 对话页面设计 + 基本交互 |
|
||||||
| T10 | 实现对话 API 调用逻辑 | 高 | 根据用户选择的厂商,使用对应密钥调用 AI API 并返回结果 | 开发 | 🔵 厂商 API 通信逻辑(POST 请求) |
|
| T10 | 实现对话 API 调用逻辑 | 高 | 根据用户选择的厂商,使用对应密钥调用 AI API 并返回结果 | 开发 | 🔵 厂商 API 通信逻辑(POST 请求) |
|
||||||
| T11 | 实现响应展示与错误处理 | 高 | 显示 API 返回内容,处理 401、500 等错误信息 | 开发 | 🔵 响应展示模块 + 错误页/提示 |
|
| T11 | 实现响应展示与错误处理 | 高 | 显示 API 返回内容,处理 401、500 等错误信息 | 开发 | 🔵 响应展示模块 + 错误页/提示 |
|
||||||
| T12 | 添加「重试」机制(可选) | 中 | 对于调用失败的情况提供一键重试功能 | 开发 | 🔵 调用失败后支持重试按钮 |
|
| T12 | 添加「重试」机制(可选) | 中 | 对于调用失败的情况提供一键重试功能 | 开发 | 🔵 调用失败后支持重试按钮 |
|
||||||
| T13 | 构建配置页与对话页的切换逻辑 | 中 | 用户可切换配置页和对话页,保证数据在页面间同步(如密钥已保存则自动填充) | 开发 | 🔵 页面切换功能 + 自动填充已有配置 |
|
| T13 | 构建配置页与对话页的切换逻辑 | 中 | 用户可切换配置页和对话页,保证数据在页面间同步(如密钥已保存则自动填充) | 开发 | 🔵 页面切换功能 + 自动填充已有配置 |
|
||||||
| T14 | 添加「首次未配置提醒」 | 中 | 如果没有配置任何厂商,引导用户进行配置 | 开发 | 🔵 首页友好提示信息 |
|
| T14 | 添加「首次未配置提醒」 | 中 | 如果没有配置任何厂商,引导用户进行配置 | 开发 | 🔵 首页友好提示信息 |
|
||||||
| T15 | 添加厂商配置删除功能 | 中 | 允许用户删除已有配置,避免冗余数据 | 开发 | 🔵 删除配置按钮 + localStorage 清理 |
|
| T15 | 添加厂商配置删除功能 | 中 | 允许用户删除已有配置,避免冗余数据 | 开发 | 🔵 删除配置按钮 + localStorage 清理 |
|
||||||
| T16 | 添加厂商默认配置(可选) | 中 | 预设 OpenAI、通义等厂商的默认接入点和字段提示 | 开发 | 🔵 配置页面默认值设置 |
|
| T16 | 添加厂商默认配置(可选) | 中 | 预设 OpenAI、通义等厂商的默认接入点和字段提示 | 开发 | 🔵 配置页面默认值设置 |
|
||||||
| T17 | 构建完整 UI 界面(风格统一) | 中 | 确保配置页和对话页操作一致、视觉统一 | UI / 前端 | 🔵 响应式设计 + 现代化界面 |
|
| T17 | 构建完整 UI 界面(风格统一) | 中 | 确保配置页和对话页操作一致、视觉统一 | UI / 前端 | 🔵 响应式设计 + 现代化界面 |
|
||||||
| T18 | 进行基本可用性测试 | 中 | 验证主要功能是否可用(配置保存、API 调用、错误提示等) | QA | 🔵 功能测试报告(手动/自动) |
|
| T18 | 进行基本可用性测试 | 中 | 验证主要功能是否可用(配置保存、API 调用、错误提示等) | QA | 🔵 功能测试报告(手动/自动) |
|
||||||
| T19 | 添加部署方案(如 GitHub Pages) | 低 | 配置前端静态部署,便于快速访问 | 产品 | 🔵 部署文档 + 示例部署链接 |
|
| T19 | 添加部署方案(如 GitHub Pages) | 低 | 配置前端静态部署,便于快速访问 | 产品 | 🔵 部署文档 + 示例部署链接 |
|
||||||
| T20 | 撰写项目说明文档和使用教程 | 低 | 提供给用户快速上手的说明,包含配置方法和常见问题 | 产品 | 🔵 README 或使用指南文档 |
|
| T20 | 撰写项目说明文档和使用教程 | 低 | 提供给用户快速上手的说明,包含配置方法和常见问题 | 产品 | 🔵 README 或使用指南文档 |
|
||||||
| T21 | 优化体验(如加载提示、输入缓存) | 低 | 提升用户体验,如加载动画、输入自动保存(可选) | 开发 | 🔵 更流畅、更友好的 UI 交互 |
|
| T21 | 优化体验(如加载提示、输入缓存) | 低 | 提升用户体验,如加载动画、输入自动保存(可选) | 开发 | 🔵 更流畅、更友好的 UI 交互 |
|
||||||
| T22 | 收集用户反馈并迭代设计 | 低 | 收集可用性数据、建议,优化方案(可选) | 产品 | 🔵 用户反馈记录 + 迭代计划 |
|
| T22 | 收集用户反馈并迭代设计 | 低 | 收集可用性数据、建议,优化方案(可选) | 产品 | 🔵 用户反馈记录 + 迭代计划 |
|
||||||
@@ -89,16 +89,17 @@ const loadConfiguredPlatforms = () => {
|
|||||||
const getPlatformName = (platformId) => {
|
const getPlatformName = (platformId) => {
|
||||||
const platforms = [
|
const platforms = [
|
||||||
{ id: 'openai', name: 'OpenAI' },
|
{ id: 'openai', name: 'OpenAI' },
|
||||||
{ id: 'anthropic', name: 'Anthropic' },
|
{ id: 'zhipu', name: '智谱' },
|
||||||
{ id: 'google', name: 'Google (Gemini)' },
|
{ id: 'deepseek', name: 'DeepSeek' },
|
||||||
{ id: 'qwen', name: '通义千问(阿里云)' },
|
{ id: 'qwen', name: '通义千问' },
|
||||||
{ id: 'wenxin', name: '文心一言(百度)' }
|
{ id: 'siliconflow', name: '硅基流动' }
|
||||||
]
|
]
|
||||||
const platform = platforms.find(p => p.id === platformId)
|
const platform = platforms.find(p => p.id === platformId)
|
||||||
return platform ? platform.name : platformId
|
return platform ? platform.name : platformId
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送消息
|
// 发送消息
|
||||||
|
// TODO 需要将用户配置(端点、密钥)和用户消息发送给后端统一处理
|
||||||
const sendMessage = async () => {
|
const sendMessage = async () => {
|
||||||
if (!canSend.value) return
|
if (!canSend.value) return
|
||||||
|
|
||||||
@@ -125,6 +126,7 @@ const sendMessage = async () => {
|
|||||||
throw new Error('未找到厂商配置')
|
throw new Error('未找到厂商配置')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO 这里无需解密,应该交由后端解密
|
||||||
// 解密密钥
|
// 解密密钥
|
||||||
const apiKey = decrypt(config.apiKey)
|
const apiKey = decrypt(config.apiKey)
|
||||||
|
|
||||||
@@ -142,146 +144,9 @@ const sendMessage = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 调用AI平台API
|
// 调用AI平台API
|
||||||
|
// TODO 此处由后端统一处理
|
||||||
const callAIPlatform = async (platformId, apiKey, endpoint, message) => {
|
const callAIPlatform = async (platformId, apiKey, endpoint, message) => {
|
||||||
// 根据不同平台调用不同的API
|
// TODO 待完善
|
||||||
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 || '未获取到响应内容'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 保存按钮 -->
|
<!-- 保存按钮 -->
|
||||||
|
<!-- TODO 保存后应该使得 ChatPanel 组件刷新,使得能够选择厂商 -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<button @click="saveConfig" :disabled="!canSave" class="save-btn">
|
<button @click="saveConfig" :disabled="!canSave" class="save-btn">
|
||||||
保存配置
|
保存配置
|
||||||
@@ -60,7 +61,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { encrypt, decrypt } from '../utils/encryption'
|
import { encrypt } from '../utils/encryption'
|
||||||
|
|
||||||
// 厂商配置预设
|
// 厂商配置预设
|
||||||
const platforms = [
|
const platforms = [
|
||||||
@@ -72,31 +73,31 @@ const platforms = [
|
|||||||
keyType: 'api_key'
|
keyType: 'api_key'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'anthropic',
|
id: 'zhipu',
|
||||||
name: 'Anthropic',
|
name: '智谱',
|
||||||
endpoint: 'https://api.anthropic.com/v1/messages',
|
endpoint: 'https://open.bigmodel.cn/api/paas/v4/chat/completions',
|
||||||
keyPlaceholder: 'sk-...',
|
keyPlaceholder: 'sk-...',
|
||||||
keyType: 'api_key'
|
keyType: 'api_key'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'google',
|
id: 'deepseek',
|
||||||
name: 'Google (Gemini)',
|
name: 'DeepSeek',
|
||||||
endpoint: 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent',
|
endpoint: 'https://api.deepseek.com/chat/completions',
|
||||||
keyPlaceholder: 'AIza...',
|
keyPlaceholder: 'sk-...',
|
||||||
keyType: 'api_key'
|
keyType: 'api_key'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'qwen',
|
id: 'qwen',
|
||||||
name: '通义千问(阿里云)',
|
name: '通义千问',
|
||||||
endpoint: 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation',
|
endpoint: 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions',
|
||||||
keyPlaceholder: 'sk-...',
|
keyPlaceholder: 'sk-...',
|
||||||
keyType: 'api_key'
|
keyType: 'api_key'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'wenxin',
|
id: 'siliconflow',
|
||||||
name: '文心一言(百度)',
|
name: '硅基流动',
|
||||||
endpoint: 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/ernie_bot',
|
endpoint: 'https://api.siliconflow.cn/v1/chat/completions',
|
||||||
keyPlaceholder: 'your_api_key',
|
keyPlaceholder: 'sk-...',
|
||||||
keyType: 'api_key'
|
keyType: 'api_key'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -196,14 +197,9 @@ const validateApiKey = () => {
|
|||||||
if (!platform) return true
|
if (!platform) return true
|
||||||
|
|
||||||
// 根据厂商类型验证密钥格式
|
// 根据厂商类型验证密钥格式
|
||||||
if (platform.id === 'openai' || platform.id === 'qwen' || platform.id === 'anthropic') {
|
if (platform.keyType === 'api_key') {
|
||||||
if (!key.startsWith('sk-')) {
|
if (!key.startsWith('sk-') || key.length < 20) {
|
||||||
showValidation('OpenAI/通义千问密钥应以 "sk-" 开头', 'error')
|
showValidation('API 密钥格式不正确,应以 "sk-" 开头且长度不少于20位', 'error')
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else if (platform.id === 'google') {
|
|
||||||
if (!key.startsWith('AIza')) {
|
|
||||||
showValidation('Google密钥应以 "AIza" 开头', 'error')
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user