mirror of
https://github.com/JefferyHcool/BiliNote.git
synced 2026-06-02 20:59:35 +08:00
feat: 添加硅基流动(SiliconFlow)支持和错误处理优化
## 主要更新 ### 新增功能 - 新增 SiliconFlow_provider.py 专用提供商 - 添加硅基流动 API 集成文档 - 实现 Cherry Studio 风格的连接测试 ### 错误处理优化 - 修复前端 Form.tsx 错误显示问题 - 改进 universal_gpt.py 异常处理逻辑 - 统一 URL 格式处理,避免路径重复 ### 兼容性改进 - 优化 OpenAI 兼容提供商 URL 处理 - 增强模型列表获取的容错性 - 添加详细的调试日志 ### 安全性提升 - 更新 .gitignore 保护敏感信息 - 移除示例配置文件 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
24
.env.example
24
.env.example
@@ -1,24 +0,0 @@
|
|||||||
# 通用端口配置
|
|
||||||
BACKEND_PORT=8483 # 后端端口
|
|
||||||
FRONTEND_PORT=3015
|
|
||||||
BACKEND_HOST=0.0.0.0 # 默认为 0.0.0.0,表示监听所有 IP 地址 不建议动
|
|
||||||
APP_PORT= 3015 # docker 部署时用
|
|
||||||
# 前端访问后端用 (开发环境使用)
|
|
||||||
VITE_API_BASE_URL=http://127.0.0.1:8483
|
|
||||||
VITE_SCREENSHOT_BASE_URL=http://127.0.0.1:8483/static/screenshots
|
|
||||||
VITE_FRONTEND_PORT=3015
|
|
||||||
# 生产环境配置
|
|
||||||
ENV=production
|
|
||||||
STATIC=/static
|
|
||||||
OUT_DIR=./static/screenshots
|
|
||||||
NOTE_OUTPUT_DIR=note_results
|
|
||||||
IMAGE_BASE_URL=/static/screenshots
|
|
||||||
DATA_DIR=data
|
|
||||||
# FFMPEG 配置
|
|
||||||
FFMPEG_BIN_PATH=
|
|
||||||
|
|
||||||
# transcriber 相关配置
|
|
||||||
TRANSCRIBER_TYPE=fast-whisper # fast-whisper/bcut/kuaishou/mlx-whisper(仅Apple平台)/groq
|
|
||||||
WHISPER_MODEL_SIZE=base
|
|
||||||
|
|
||||||
GROQ_TRANSCRIBER_MODEL=whisper-large-v3-turbo # groq提供的faster-whisper 默认为 whisper-large-v3-turbo
|
|
||||||
50
.gitignore
vendored
50
.gitignore
vendored
@@ -308,6 +308,7 @@ cython_debug/
|
|||||||
|
|
||||||
# PyPI configuration file
|
# PyPI configuration file
|
||||||
.pypirc
|
.pypirc
|
||||||
|
# 项目特定的私人信息和敏感文件
|
||||||
/backend/data/*
|
/backend/data/*
|
||||||
/backend/static/*
|
/backend/static/*
|
||||||
/backend/note_tasks.db
|
/backend/note_tasks.db
|
||||||
@@ -321,4 +322,51 @@ cython_debug/
|
|||||||
/backend/.idea/*
|
/backend/.idea/*
|
||||||
/backend/config/*
|
/backend/config/*
|
||||||
/BiliNote_frontend/.idea/*
|
/BiliNote_frontend/.idea/*
|
||||||
/BiliNote_frontend/src-tauri/bin/
|
/BiliNote_frontend/src-tauri/bin/
|
||||||
|
|
||||||
|
# 额外需要忽略的敏感文件和目录
|
||||||
|
/backend/test_env/
|
||||||
|
/backend/test_*.py
|
||||||
|
/backend/venv/
|
||||||
|
/backend/get-pip.py
|
||||||
|
/backend/fonts/
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
*_backup.sql
|
||||||
|
|
||||||
|
# API测试脚本(可能包含敏感信息)
|
||||||
|
test_siliconflow.py
|
||||||
|
test_*.py
|
||||||
|
|
||||||
|
# 模型文件和缓存(通常很大,且可能有版权问题)
|
||||||
|
/backend/models/
|
||||||
|
whisper*/
|
||||||
|
*.bin
|
||||||
|
*.safetensors
|
||||||
|
*.gguf
|
||||||
|
|
||||||
|
# 用户生成内容
|
||||||
|
/backend/uploads/*
|
||||||
|
/backend/data/*
|
||||||
|
/backend/note_results/*
|
||||||
|
/backend/static/screenshots/*
|
||||||
|
/backend/static/cover/*
|
||||||
|
|
||||||
|
# 临时测试环境
|
||||||
|
test_env/
|
||||||
|
*_env/
|
||||||
|
|
||||||
|
# 配置文件(可能包含API keys)
|
||||||
|
/backend/config/*
|
||||||
|
config.json
|
||||||
|
settings.json
|
||||||
|
|
||||||
|
# 备份文件
|
||||||
|
*.bak
|
||||||
|
*.bak2
|
||||||
|
*_backup.*
|
||||||
|
|
||||||
|
# 输出和缓存目录
|
||||||
|
/backend/data/output_frames/*
|
||||||
|
/backend/data/grid_output/*
|
||||||
3850
BillNote_frontend/bun.lock
Normal file
3850
BillNote_frontend/bun.lock
Normal file
File diff suppressed because it is too large
Load Diff
@@ -156,9 +156,10 @@ const ProviderForm = ({ isCreate = false }: { isCreate?: boolean }) => {
|
|||||||
|
|
||||||
toast.success('测试连通性成功 🎉')
|
toast.success('测试连通性成功 🎉')
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
|
console.error('连接测试失败:', error)
|
||||||
toast.error(`连接失败: ${data.data.msg || '未知错误'}`)
|
const errorMsg = error?.response?.data?.msg || error?.message || '未知错误'
|
||||||
|
toast.error(`连接失败: ${errorMsg}`)
|
||||||
// toast.error('测试连通性异常')
|
// toast.error('测试连通性异常')
|
||||||
} finally {
|
} finally {
|
||||||
setTesting(false)
|
setTesting(false)
|
||||||
|
|||||||
@@ -64,6 +64,9 @@ const formSchema = z
|
|||||||
ctx.addIssue({ code: 'custom', message: '本地视频路径不能为空', path: ['video_url'] })
|
ctx.addIssue({ code: 'custom', message: '本地视频路径不能为空', path: ['video_url'] })
|
||||||
} else if (!video_url) {
|
} else if (!video_url) {
|
||||||
ctx.addIssue({ code: 'custom', message: '视频链接不能为空', path: ['video_url'] })
|
ctx.addIssue({ code: 'custom', message: '视频链接不能为空', path: ['video_url'] })
|
||||||
|
} else if (platform === 'local') {
|
||||||
|
// 对于本地文件,只检查是否为空,不验证URL格式
|
||||||
|
return
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
const url = new URL(video_url)
|
const url = new URL(video_url)
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import os
|
import os
|
||||||
from abc import ABC
|
from abc import ABC
|
||||||
from typing import Union, Optional
|
from typing import Union, Optional
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
|
||||||
import yt_dlp
|
import yt_dlp
|
||||||
|
# 导入youtube-dl作为备选
|
||||||
|
import youtube_dl
|
||||||
|
|
||||||
from app.downloaders.base import Downloader, DownloadQuality, QUALITY_MAP
|
from app.downloaders.base import Downloader, DownloadQuality, QUALITY_MAP
|
||||||
from app.models.notes_model import AudioDownloadResult
|
from app.models.notes_model import AudioDownloadResult
|
||||||
@@ -28,7 +33,19 @@ class BilibiliDownloader(Downloader, ABC):
|
|||||||
os.makedirs(output_dir, exist_ok=True)
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
|
||||||
output_path = os.path.join(output_dir, "%(id)s.%(ext)s")
|
output_path = os.path.join(output_dir, "%(id)s.%(ext)s")
|
||||||
|
|
||||||
|
# 常见浏览器 User-Agent 列表
|
||||||
|
user_agents = [
|
||||||
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36',
|
||||||
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Safari/605.1.15',
|
||||||
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.42',
|
||||||
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/113.0'
|
||||||
|
]
|
||||||
|
|
||||||
|
# 随机选择一个User-Agent
|
||||||
|
user_agent = random.choice(user_agents)
|
||||||
|
|
||||||
|
# 尝试使用yt-dlp
|
||||||
ydl_opts = {
|
ydl_opts = {
|
||||||
'format': 'bestaudio[ext=m4a]/bestaudio/best',
|
'format': 'bestaudio[ext=m4a]/bestaudio/best',
|
||||||
'outtmpl': output_path,
|
'outtmpl': output_path,
|
||||||
@@ -41,21 +58,75 @@ class BilibiliDownloader(Downloader, ABC):
|
|||||||
],
|
],
|
||||||
'noplaylist': True,
|
'noplaylist': True,
|
||||||
'quiet': False,
|
'quiet': False,
|
||||||
|
# 添加重试和连接设置
|
||||||
|
'retries': 10, # 重试10次
|
||||||
|
'fragment_retries': 10, # 片段下载重试10次
|
||||||
|
'socket_timeout': 30, # 套接字超时时间30秒
|
||||||
|
'extractor_retries': 5, # 提取器重试5次
|
||||||
|
'nocheckcertificate': True, # 不检查SSL证书
|
||||||
|
'http_headers': {
|
||||||
|
'User-Agent': user_agent,
|
||||||
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
|
||||||
|
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||||
|
'Referer': 'https://www.bilibili.com/',
|
||||||
|
'Origin': 'https://www.bilibili.com'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
info = None
|
||||||
info = ydl.extract_info(video_url, download=True)
|
try:
|
||||||
video_id = info.get("id")
|
print("尝试使用 yt-dlp 下载...")
|
||||||
title = info.get("title")
|
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||||
duration = info.get("duration", 0)
|
print(f"正在使用User-Agent: {user_agent}")
|
||||||
cover_url = info.get("thumbnail")
|
print(f"尝试下载视频: {video_url}")
|
||||||
audio_path = os.path.join(output_dir, f"{video_id}.mp3")
|
info = ydl.extract_info(video_url, download=True)
|
||||||
|
video_id = info.get("id")
|
||||||
|
title = info.get("title")
|
||||||
|
duration = info.get("duration", 0)
|
||||||
|
cover_url = info.get("thumbnail")
|
||||||
|
audio_path = os.path.join(output_dir, f"{video_id}.mp3")
|
||||||
|
print(f"下载成功: {title}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"yt-dlp 下载失败,错误信息: {str(e)}")
|
||||||
|
print("尝试使用备选下载器 youtube-dl...")
|
||||||
|
|
||||||
|
# 失败后尝试使用 youtube_dl
|
||||||
|
try:
|
||||||
|
with youtube_dl.YoutubeDL(ydl_opts) as ydl:
|
||||||
|
print(f"正在使用 youtube-dl 下载: {video_url}")
|
||||||
|
info = ydl.extract_info(video_url, download=True)
|
||||||
|
video_id = info.get("id")
|
||||||
|
title = info.get("title")
|
||||||
|
duration = info.get("duration", 0)
|
||||||
|
cover_url = info.get("thumbnail")
|
||||||
|
audio_path = os.path.join(output_dir, f"{video_id}.mp3")
|
||||||
|
print(f"youtube-dl 下载成功: {title}")
|
||||||
|
except Exception as e2:
|
||||||
|
print(f"youtube-dl 也下载失败,错误信息: {str(e2)}")
|
||||||
|
raise Exception(f"所有下载方法都失败: yt-dlp错误: {str(e)}, youtube-dl错误: {str(e2)}")
|
||||||
|
|
||||||
|
if not info:
|
||||||
|
raise Exception("无法获取视频信息")
|
||||||
|
|
||||||
|
# 检查下载是否成功
|
||||||
|
video_id = info.get("id")
|
||||||
|
audio_path = os.path.join(output_dir, f"{video_id}.mp3")
|
||||||
|
|
||||||
|
# 等待5秒确保文件写入完成
|
||||||
|
for _ in range(5):
|
||||||
|
if os.path.exists(audio_path):
|
||||||
|
break
|
||||||
|
print(f"等待文件写入: {audio_path}")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
if not os.path.exists(audio_path):
|
||||||
|
print(f"警告:下载可能成功但找不到文件: {audio_path}")
|
||||||
|
|
||||||
return AudioDownloadResult(
|
return AudioDownloadResult(
|
||||||
file_path=audio_path,
|
file_path=audio_path,
|
||||||
title=title,
|
title=info.get("title"),
|
||||||
duration=duration,
|
duration=info.get("duration", 0),
|
||||||
cover_url=cover_url,
|
cover_url=info.get("thumbnail"),
|
||||||
platform="bilibili",
|
platform="bilibili",
|
||||||
video_id=video_id,
|
video_id=video_id,
|
||||||
raw_info=info,
|
raw_info=info,
|
||||||
@@ -80,8 +151,16 @@ class BilibiliDownloader(Downloader, ABC):
|
|||||||
if os.path.exists(video_path):
|
if os.path.exists(video_path):
|
||||||
return video_path
|
return video_path
|
||||||
|
|
||||||
# 检查是否已经存在
|
# 常见浏览器 User-Agent 列表
|
||||||
|
user_agents = [
|
||||||
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36',
|
||||||
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Safari/605.1.15',
|
||||||
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.42',
|
||||||
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/113.0'
|
||||||
|
]
|
||||||
|
|
||||||
|
# 随机选择一个User-Agent
|
||||||
|
user_agent = random.choice(user_agents)
|
||||||
|
|
||||||
output_path = os.path.join(output_dir, "%(id)s.%(ext)s")
|
output_path = os.path.join(output_dir, "%(id)s.%(ext)s")
|
||||||
|
|
||||||
@@ -91,12 +170,46 @@ class BilibiliDownloader(Downloader, ABC):
|
|||||||
'noplaylist': True,
|
'noplaylist': True,
|
||||||
'quiet': False,
|
'quiet': False,
|
||||||
'merge_output_format': 'mp4', # 确保合并成 mp4
|
'merge_output_format': 'mp4', # 确保合并成 mp4
|
||||||
|
# 添加重试和连接设置
|
||||||
|
'retries': 10, # 重试10次
|
||||||
|
'fragment_retries': 10, # 片段下载重试10次
|
||||||
|
'socket_timeout': 30, # 套接字超时时间30秒
|
||||||
|
'extractor_retries': 5, # 提取器重试5次
|
||||||
|
'nocheckcertificate': True, # 不检查SSL证书
|
||||||
|
'http_headers': {
|
||||||
|
'User-Agent': user_agent,
|
||||||
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
|
||||||
|
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||||
|
'Referer': 'https://www.bilibili.com/',
|
||||||
|
'Origin': 'https://www.bilibili.com'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
info = None
|
||||||
info = ydl.extract_info(video_url, download=True)
|
try:
|
||||||
video_id = info.get("id")
|
print("尝试使用 yt-dlp 下载视频...")
|
||||||
video_path = os.path.join(output_dir, f"{video_id}.mp4")
|
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||||
|
print(f"正在使用User-Agent: {user_agent}")
|
||||||
|
print(f"尝试下载视频: {video_url}")
|
||||||
|
info = ydl.extract_info(video_url, download=True)
|
||||||
|
video_id = info.get("id")
|
||||||
|
video_path = os.path.join(output_dir, f"{video_id}.mp4")
|
||||||
|
print(f"下载成功: {video_path}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"yt-dlp 下载视频失败,错误信息: {str(e)}")
|
||||||
|
print("尝试使用备选下载器 youtube-dl...")
|
||||||
|
|
||||||
|
# 失败后尝试使用 youtube_dl
|
||||||
|
try:
|
||||||
|
with youtube_dl.YoutubeDL(ydl_opts) as ydl:
|
||||||
|
print(f"正在使用 youtube-dl 下载视频: {video_url}")
|
||||||
|
info = ydl.extract_info(video_url, download=True)
|
||||||
|
video_id = info.get("id")
|
||||||
|
video_path = os.path.join(output_dir, f"{video_id}.mp4")
|
||||||
|
print(f"youtube-dl 下载视频成功: {video_path}")
|
||||||
|
except Exception as e2:
|
||||||
|
print(f"youtube-dl 也下载视频失败,错误信息: {str(e2)}")
|
||||||
|
raise Exception(f"所有下载视频方法都失败: yt-dlp错误: {str(e)}, youtube-dl错误: {str(e2)}")
|
||||||
|
|
||||||
if not os.path.exists(video_path):
|
if not os.path.exists(video_path):
|
||||||
raise FileNotFoundError(f"视频文件未找到: {video_path}")
|
raise FileNotFoundError(f"视频文件未找到: {video_path}")
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from openai import OpenAI
|
|||||||
|
|
||||||
from app.gpt.base import GPT
|
from app.gpt.base import GPT
|
||||||
from app.gpt.provider.OpenAI_compatible_provider import OpenAICompatibleProvider
|
from app.gpt.provider.OpenAI_compatible_provider import OpenAICompatibleProvider
|
||||||
|
from app.gpt.provider.SiliconFlow_provider import SiliconFlowProvider
|
||||||
from app.gpt.universal_gpt import UniversalGPT
|
from app.gpt.universal_gpt import UniversalGPT
|
||||||
from app.models.model_config import ModelConfig
|
from app.models.model_config import ModelConfig
|
||||||
|
|
||||||
@@ -9,5 +10,18 @@ from app.models.model_config import ModelConfig
|
|||||||
class GPTFactory:
|
class GPTFactory:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_config(config: ModelConfig) -> GPT:
|
def from_config(config: ModelConfig) -> GPT:
|
||||||
client = OpenAICompatibleProvider(api_key=config.api_key, base_url=config.base_url).get_client
|
# 检查是否是硅基流动,使用专门的提供商类
|
||||||
|
if "siliconflow" in config.base_url.lower():
|
||||||
|
client = SiliconFlowProvider(
|
||||||
|
api_key=config.api_key,
|
||||||
|
base_url=config.base_url,
|
||||||
|
model=config.model_name
|
||||||
|
).get_client
|
||||||
|
else:
|
||||||
|
# 其他提供商使用通用兼容类
|
||||||
|
client = OpenAICompatibleProvider(
|
||||||
|
api_key=config.api_key,
|
||||||
|
base_url=config.base_url
|
||||||
|
).get_client
|
||||||
|
|
||||||
return UniversalGPT(client=client, model=config.model_name)
|
return UniversalGPT(client=client, model=config.model_name)
|
||||||
@@ -5,6 +5,7 @@ from openai import OpenAI
|
|||||||
from app.utils.logger import get_logger
|
from app.utils.logger import get_logger
|
||||||
|
|
||||||
logging= get_logger(__name__)
|
logging= get_logger(__name__)
|
||||||
|
|
||||||
class OpenAICompatibleProvider:
|
class OpenAICompatibleProvider:
|
||||||
def __init__(self, api_key: str, base_url: str, model: Union[str, None]=None):
|
def __init__(self, api_key: str, base_url: str, model: Union[str, None]=None):
|
||||||
self.client = OpenAI(api_key=api_key, base_url=base_url)
|
self.client = OpenAI(api_key=api_key, base_url=base_url)
|
||||||
@@ -17,15 +18,148 @@ class OpenAICompatibleProvider:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def test_connection(api_key: str, base_url: str) -> bool:
|
def test_connection(api_key: str, base_url: str) -> bool:
|
||||||
try:
|
try:
|
||||||
client = OpenAI(api_key=api_key, base_url=base_url)
|
# 调试:打印API Key的实际长度和内容
|
||||||
model = client.models.list()
|
logging.info(f"正在测试连接 - API Key长度: {len(api_key)}, 前8位: {api_key[:8]}, 后4位: {api_key[-4:] if len(api_key) > 4 else 'TOO_SHORT'}")
|
||||||
# for segment in model:
|
logging.info(f"Base URL: {base_url}")
|
||||||
# print(segment)
|
|
||||||
# print(model)
|
# 硅基流动特殊处理:参考Cherry Studio的实现方式
|
||||||
logging.info("连通性测试成功")
|
if "siliconflow" in base_url.lower():
|
||||||
return True
|
logging.info("检测到硅基流动,参考Cherry Studio实现方式")
|
||||||
|
|
||||||
|
# 标准化URL处理,避免路径重复
|
||||||
|
base_url_clean = base_url.rstrip('/')
|
||||||
|
|
||||||
|
if base_url_clean.endswith("/v1"):
|
||||||
|
# 如果用户输入了/v1,直接使用
|
||||||
|
api_base = base_url_clean
|
||||||
|
test_url = f"{api_base}/chat/completions"
|
||||||
|
elif base_url_clean.endswith("/chat/completions"):
|
||||||
|
# 如果用户直接输入了完整路径,直接使用
|
||||||
|
test_url = base_url_clean
|
||||||
|
api_base = base_url_clean.replace("/chat/completions", "")
|
||||||
|
else:
|
||||||
|
# Cherry Studio方式:不加/v1后缀,直接加/chat/completions
|
||||||
|
api_base = base_url_clean
|
||||||
|
test_url = f"{base_url_clean}/chat/completions"
|
||||||
|
|
||||||
|
logging.info(f"使用API基地址: {api_base}")
|
||||||
|
logging.info(f"测试URL: {test_url}")
|
||||||
|
|
||||||
|
# 先用requests验证(Cherry Studio方式)
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {api_key}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
payload = {
|
||||||
|
"model": "Qwen/Qwen2.5-7B-Instruct",
|
||||||
|
"messages": [{"role": "user", "content": "hi"}],
|
||||||
|
"max_tokens": 1
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
logging.info("Cherry Studio方式:直接HTTP请求测试")
|
||||||
|
response = requests.post(test_url, headers=headers, json=payload, timeout=15)
|
||||||
|
logging.info(f"HTTP响应状态码: {response.status_code}")
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
logging.info("硅基流动连接测试成功(Cherry Studio方式)")
|
||||||
|
result = response.json()
|
||||||
|
logging.info(f"响应: {json.dumps(result, ensure_ascii=False)[:100]}...")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logging.error(f"HTTP请求失败: {response.status_code} - {response.text}")
|
||||||
|
|
||||||
|
# 尝试不同的端点
|
||||||
|
if response.status_code == 404 and "/v1" in test_url:
|
||||||
|
# 尝试去掉/v1
|
||||||
|
alt_url = test_url.replace("/v1", "")
|
||||||
|
logging.info(f"尝试备用URL: {alt_url}")
|
||||||
|
alt_response = requests.post(alt_url, headers=headers, json=payload, timeout=15)
|
||||||
|
if alt_response.status_code == 200:
|
||||||
|
logging.info("硅基流动连接测试成功(备用URL)")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as http_error:
|
||||||
|
logging.error(f"直接HTTP请求异常: {http_error}")
|
||||||
|
|
||||||
|
# 标准OpenAI SDK方式作为备选
|
||||||
|
# 对于硅基流动,需要使用正确的base_url
|
||||||
|
if "siliconflow" in base_url.lower():
|
||||||
|
# 确保SDK使用正确的base_url(需要包含/v1)
|
||||||
|
sdk_base_url = api_base if api_base.endswith('/v1') else f"{api_base}/v1"
|
||||||
|
client = OpenAI(api_key=api_key, base_url=sdk_base_url)
|
||||||
|
logging.info(f"尝试OpenAI SDK方式,使用base_url: {sdk_base_url}")
|
||||||
|
else:
|
||||||
|
client = OpenAI(api_key=api_key, base_url=base_url)
|
||||||
|
|
||||||
|
if "siliconflow" in base_url.lower():
|
||||||
|
# 硅基流动的免费模型列表
|
||||||
|
test_models = [
|
||||||
|
"Qwen/Qwen2.5-7B-Instruct",
|
||||||
|
"THUDM/glm-4-9b-chat",
|
||||||
|
"deepseek-ai/DeepSeek-V3"
|
||||||
|
]
|
||||||
|
|
||||||
|
for model in test_models:
|
||||||
|
try:
|
||||||
|
logging.info(f"尝试测试模型: {model}")
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model=model,
|
||||||
|
messages=[{"role": "user", "content": "hi"}],
|
||||||
|
max_tokens=1,
|
||||||
|
timeout=15.0
|
||||||
|
)
|
||||||
|
logging.info(f"硅基流动连接测试成功,使用模型: {model}")
|
||||||
|
return True
|
||||||
|
except Exception as model_error:
|
||||||
|
error_msg = str(model_error)
|
||||||
|
logging.warning(f"模型 {model} 测试失败: {error_msg}")
|
||||||
|
|
||||||
|
if "401" in error_msg or "Unauthorized" in error_msg or "Api key is invalid" in error_msg:
|
||||||
|
raise Exception("API Key 无效或已过期,请检查API Key是否正确")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 尝试models接口
|
||||||
|
try:
|
||||||
|
models = client.models.list()
|
||||||
|
logging.info("硅基流动连接测试成功(通过models接口)")
|
||||||
|
return True
|
||||||
|
except Exception as models_error:
|
||||||
|
logging.error(f"models接口失败: {models_error}")
|
||||||
|
raise models_error
|
||||||
|
else:
|
||||||
|
# 非硅基流动提供商
|
||||||
|
model = client.models.list()
|
||||||
|
logging.info("连通性测试成功")
|
||||||
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.info(f"连通性测试失败:{e}")
|
error_msg = str(e)
|
||||||
|
logging.error(f"连通性测试失败:{error_msg}")
|
||||||
|
|
||||||
|
# 根据错误类型提供更具体的错误信息
|
||||||
|
if "401" in error_msg or "Unauthorized" in error_msg or "Api key is invalid" in error_msg:
|
||||||
|
raise Exception("API Key 无效或已过期,请检查API Key是否正确")
|
||||||
|
elif "404" in error_msg or "Not Found" in error_msg:
|
||||||
|
if "siliconflow" in base_url.lower():
|
||||||
|
raise Exception("API 地址可能不正确。建议尝试: https://api.siliconflow.cn/v1 或 https://api.siliconflow.cn(参考Cherry Studio配置)")
|
||||||
|
else:
|
||||||
|
raise Exception("API 地址不正确,请检查 base_url 格式")
|
||||||
|
elif "timeout" in error_msg.lower():
|
||||||
|
raise Exception("连接超时,请检查网络连接或 API 地址是否正确")
|
||||||
|
elif "ssl" in error_msg.lower() or "certificate" in error_msg.lower():
|
||||||
|
raise Exception("SSL 证书验证失败,请检查 API 地址是否使用 HTTPS")
|
||||||
|
elif "connection" in error_msg.lower():
|
||||||
|
if "siliconflow" in base_url.lower():
|
||||||
|
raise Exception("无法连接到硅基流动服务器,请尝试: https://api.siliconflow.cn/v1 或 https://api.siliconflow.cn")
|
||||||
|
else:
|
||||||
|
raise Exception("无法连接到服务器,请检查 API 地址和网络连接")
|
||||||
|
elif "_set_private_attributes" in error_msg:
|
||||||
|
raise Exception("OpenAI SDK版本兼容性问题,请尝试重新配置或联系管理员")
|
||||||
|
else:
|
||||||
|
raise Exception(f"连接失败(原始错误): {error_msg}")
|
||||||
|
|
||||||
# print(f"Error connecting to OpenAI API: {e}")
|
|
||||||
return False
|
return False
|
||||||
214
backend/app/gpt/provider/SiliconFlow_provider.py
Normal file
214
backend/app/gpt/provider/SiliconFlow_provider.py
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
from typing import Optional, Union, List
|
||||||
|
from openai import OpenAI
|
||||||
|
from app.utils.logger import get_logger
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
class SiliconFlowProvider:
|
||||||
|
"""
|
||||||
|
专门为硅基流动(SiliconFlow)优化的提供商类
|
||||||
|
基于市面上成熟的接入方案设计
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 硅基流动支持的常用模型列表
|
||||||
|
SUPPORTED_MODELS = [
|
||||||
|
"deepseek-ai/DeepSeek-V3",
|
||||||
|
"deepseek-ai/DeepSeek-R1",
|
||||||
|
"Qwen/Qwen2.5-72B-Instruct",
|
||||||
|
"Qwen/Qwen2.5-32B-Instruct",
|
||||||
|
"Qwen/Qwen2.5-14B-Instruct",
|
||||||
|
"Qwen/Qwen2.5-7B-Instruct",
|
||||||
|
"meta-llama/Llama-3.1-70B-Instruct",
|
||||||
|
"meta-llama/Llama-3.1-8B-Instruct",
|
||||||
|
"THUDM/glm-4-9b-chat",
|
||||||
|
"01-ai/Yi-1.5-34B-Chat"
|
||||||
|
]
|
||||||
|
|
||||||
|
# 硅基流动API端点
|
||||||
|
API_ENDPOINTS = [
|
||||||
|
"https://api.siliconflow.cn/v1", # 国内用户
|
||||||
|
"https://api-st.siliconflow.cn/v1" # 海外用户
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, api_key: str, base_url: str = None, model: Union[str, None] = None):
|
||||||
|
"""
|
||||||
|
初始化硅基流动提供商
|
||||||
|
|
||||||
|
Args:
|
||||||
|
api_key: API密钥
|
||||||
|
base_url: API基础URL,默认使用国内端点
|
||||||
|
model: 模型名称
|
||||||
|
"""
|
||||||
|
self.api_key = api_key
|
||||||
|
|
||||||
|
# 标准化base_url,确保符合硅基流动API要求
|
||||||
|
if base_url:
|
||||||
|
base_url_clean = base_url.rstrip('/')
|
||||||
|
# 确保使用正确的API端点格式
|
||||||
|
if not base_url_clean.endswith('/v1'):
|
||||||
|
if base_url_clean.endswith('/chat/completions'):
|
||||||
|
# 用户输入了完整的endpoint,提取base部分并添加/v1
|
||||||
|
base_url_clean = base_url_clean.replace('/chat/completions', '/v1')
|
||||||
|
elif 'siliconflow' in base_url_clean.lower():
|
||||||
|
# 硅基流动需要/v1后缀
|
||||||
|
base_url_clean = f"{base_url_clean}/v1"
|
||||||
|
self.base_url = base_url_clean
|
||||||
|
else:
|
||||||
|
self.base_url = self.API_ENDPOINTS[0]
|
||||||
|
|
||||||
|
self.model = model
|
||||||
|
logger.info(f"硅基流动提供商初始化 - 使用base_url: {self.base_url}")
|
||||||
|
self.client = OpenAI(api_key=api_key, base_url=self.base_url)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def get_client(self):
|
||||||
|
return self.client
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def test_connection(cls, api_key: str, base_url: str = None) -> bool:
|
||||||
|
"""
|
||||||
|
测试硅基流动连接
|
||||||
|
使用成熟的chat接口测试方法,而非models接口
|
||||||
|
|
||||||
|
Args:
|
||||||
|
api_key: API密钥
|
||||||
|
base_url: API基础URL
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 连接是否成功
|
||||||
|
"""
|
||||||
|
base_url = base_url or cls.API_ENDPOINTS[0]
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.info(f"测试硅基流动连接 - API Key: {api_key[:8]}...(已截断) Base URL: {base_url}")
|
||||||
|
|
||||||
|
client = OpenAI(api_key=api_key, base_url=base_url)
|
||||||
|
|
||||||
|
# 使用轻量级模型进行连接测试
|
||||||
|
test_models = [
|
||||||
|
"Qwen/Qwen2.5-7B-Instruct", # 免费模型优先
|
||||||
|
"deepseek-ai/DeepSeek-V3",
|
||||||
|
"THUDM/glm-4-9b-chat"
|
||||||
|
]
|
||||||
|
|
||||||
|
for model in test_models:
|
||||||
|
try:
|
||||||
|
logger.info(f"尝试测试模型: {model}")
|
||||||
|
|
||||||
|
# 发送简单的chat请求测试连接
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model=model,
|
||||||
|
messages=[
|
||||||
|
{"role": "user", "content": "hi"}
|
||||||
|
],
|
||||||
|
max_tokens=1,
|
||||||
|
timeout=15.0
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"硅基流动连接测试成功 - 模型: {model}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as model_error:
|
||||||
|
error_msg = str(model_error)
|
||||||
|
logger.warning(f"模型 {model} 测试失败: {error_msg}")
|
||||||
|
|
||||||
|
# 如果是401错误(API Key问题),不继续尝试其他模型
|
||||||
|
if "401" in error_msg or "Unauthorized" in error_msg or "Api key is invalid" in error_msg:
|
||||||
|
raise Exception("API Key 无效或已过期,请检查API Key是否正确")
|
||||||
|
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 如果所有模型都失败,尝试models接口作为最后手段
|
||||||
|
logger.info("所有模型测试失败,尝试models接口")
|
||||||
|
try:
|
||||||
|
models = client.models.list()
|
||||||
|
logger.info("硅基流动连接测试成功(通过models接口)")
|
||||||
|
return True
|
||||||
|
except Exception as models_error:
|
||||||
|
logger.error(f"models接口也失败: {models_error}")
|
||||||
|
raise models_error
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = str(e)
|
||||||
|
logger.error(f"硅基流动连接测试失败:{error_msg}")
|
||||||
|
|
||||||
|
# 根据错误类型提供具体的错误信息
|
||||||
|
if "401" in error_msg or "Unauthorized" in error_msg or "Api key is invalid" in error_msg:
|
||||||
|
raise Exception("API Key 无效或已过期,请检查API Key是否正确")
|
||||||
|
elif "404" in error_msg or "Not Found" in error_msg:
|
||||||
|
raise Exception(f"API地址不正确,请检查URL格式。推荐使用: {cls.API_ENDPOINTS[0]} 或 {cls.API_ENDPOINTS[1]}")
|
||||||
|
elif "timeout" in error_msg.lower():
|
||||||
|
raise Exception("连接超时,请检查网络连接或尝试海外端点")
|
||||||
|
elif "connection" in error_msg.lower():
|
||||||
|
raise Exception(f"无法连接到硅基流动服务器,请尝试: {cls.API_ENDPOINTS[1]}")
|
||||||
|
else:
|
||||||
|
raise Exception(f"连接失败: {error_msg}")
|
||||||
|
|
||||||
|
def list_models(self):
|
||||||
|
"""
|
||||||
|
获取可用模型列表
|
||||||
|
优先返回预定义的模型列表,如果API支持则获取实时列表
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 尝试获取实时模型列表
|
||||||
|
models = self.client.models.list()
|
||||||
|
logger.info("成功获取硅基流动实时模型列表")
|
||||||
|
return models
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"无法获取实时模型列表,返回预定义列表: {e}")
|
||||||
|
# 返回预定义的模型列表
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
model_objects = []
|
||||||
|
for model_name in self.SUPPORTED_MODELS:
|
||||||
|
model_obj = SimpleNamespace()
|
||||||
|
model_obj.id = model_name
|
||||||
|
model_obj.object = "model"
|
||||||
|
model_obj.created = 1640995200 # 固定时间戳
|
||||||
|
model_obj.owned_by = "siliconflow"
|
||||||
|
|
||||||
|
# 添加dict方法
|
||||||
|
def dict_method():
|
||||||
|
return {
|
||||||
|
"id": model_name,
|
||||||
|
"object": "model",
|
||||||
|
"created": 1640995200,
|
||||||
|
"owned_by": "siliconflow"
|
||||||
|
}
|
||||||
|
model_obj.dict = dict_method
|
||||||
|
model_objects.append(model_obj)
|
||||||
|
|
||||||
|
# 构造兼容的返回对象
|
||||||
|
result = SimpleNamespace()
|
||||||
|
result.data = model_objects
|
||||||
|
return result
|
||||||
|
|
||||||
|
def create_chat_completion(self, model: str, messages: list, **kwargs):
|
||||||
|
"""
|
||||||
|
创建聊天完成请求
|
||||||
|
"""
|
||||||
|
return self.client.chat.completions.create(
|
||||||
|
model=model,
|
||||||
|
messages=messages,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_recommended_config(cls) -> dict:
|
||||||
|
"""
|
||||||
|
获取推荐的硅基流动配置
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"name": "硅基流动",
|
||||||
|
"type": "custom",
|
||||||
|
"base_url": cls.API_ENDPOINTS[0],
|
||||||
|
"logo": "SiliconFlow",
|
||||||
|
"supported_models": cls.SUPPORTED_MODELS,
|
||||||
|
"description": "硅基流动 - 免费高性能AI模型服务",
|
||||||
|
"features": [
|
||||||
|
"完全兼容OpenAI API",
|
||||||
|
"支持多种开源大模型",
|
||||||
|
"部分模型永久免费",
|
||||||
|
"国内外双端点支持"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -6,7 +6,14 @@ from app.gpt.utils import fix_markdown
|
|||||||
from app.models.transcriber_model import TranscriptSegment
|
from app.models.transcriber_model import TranscriptSegment
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import List
|
from typing import List
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import math
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
# 设置分段处理的参数
|
||||||
|
MAX_SEGMENTS_PER_CHUNK = 250 # 每块最多包含的段落数
|
||||||
|
MAX_CONTENT_LENGTH = 30000 # 字符数
|
||||||
|
|
||||||
class UniversalGPT(GPT):
|
class UniversalGPT(GPT):
|
||||||
def __init__(self, client, model: str, temperature: float = 0.7):
|
def __init__(self, client, model: str, temperature: float = 0.7):
|
||||||
@@ -29,7 +36,6 @@ class UniversalGPT(GPT):
|
|||||||
return [TranscriptSegment(**seg) if isinstance(seg, dict) else seg for seg in segments]
|
return [TranscriptSegment(**seg) if isinstance(seg, dict) else seg for seg in segments]
|
||||||
|
|
||||||
def create_messages(self, segments: List[TranscriptSegment], **kwargs):
|
def create_messages(self, segments: List[TranscriptSegment], **kwargs):
|
||||||
|
|
||||||
content_text = generate_base_prompt(
|
content_text = generate_base_prompt(
|
||||||
title=kwargs.get('title'),
|
title=kwargs.get('title'),
|
||||||
segment_text=self._build_segment_text(segments),
|
segment_text=self._build_segment_text(segments),
|
||||||
@@ -38,11 +44,30 @@ class UniversalGPT(GPT):
|
|||||||
style=kwargs.get('style'),
|
style=kwargs.get('style'),
|
||||||
extras=kwargs.get('extras'),
|
extras=kwargs.get('extras'),
|
||||||
)
|
)
|
||||||
|
|
||||||
# ⛳ 组装 content 数组,支持 text + image_url 混合
|
# 检查文本长度
|
||||||
|
if len(content_text) > MAX_CONTENT_LENGTH:
|
||||||
|
# 保留前部分和后部分内容
|
||||||
|
first_part = int(MAX_CONTENT_LENGTH * 0.3)
|
||||||
|
second_part = MAX_CONTENT_LENGTH - first_part - 100 # 预留100字符给提示文本
|
||||||
|
|
||||||
|
truncated_text = (
|
||||||
|
content_text[:first_part] +
|
||||||
|
"\n\n[内容过长,中间部分已省略]\n\n" +
|
||||||
|
content_text[-second_part:]
|
||||||
|
)
|
||||||
|
content_text = truncated_text
|
||||||
|
print(f"内容已截断,原长度: {len(content_text)},截断后: {len(truncated_text)}")
|
||||||
|
|
||||||
|
# 组装 content 数组,支持 text + image_url 混合
|
||||||
content = [{"type": "text", "text": content_text}]
|
content = [{"type": "text", "text": content_text}]
|
||||||
video_img_urls = kwargs.get('video_img_urls', [])
|
video_img_urls = kwargs.get('video_img_urls', [])
|
||||||
|
|
||||||
|
# 限制图片数量
|
||||||
|
if len(video_img_urls) > 5:
|
||||||
|
video_img_urls = video_img_urls[:5]
|
||||||
|
print("图片数量过多,已限制为5张")
|
||||||
|
|
||||||
for url in video_img_urls:
|
for url in video_img_urls:
|
||||||
content.append({
|
content.append({
|
||||||
"type": "image_url",
|
"type": "image_url",
|
||||||
@@ -52,7 +77,7 @@ class UniversalGPT(GPT):
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
# 正确格式:整体包在一个 message 里,role + content array
|
# 正确格式:整体包在一个 message 里,role + content array
|
||||||
messages = [{
|
messages = [{
|
||||||
"role": "user",
|
"role": "user",
|
||||||
"content": content
|
"content": content
|
||||||
@@ -68,18 +93,159 @@ class UniversalGPT(GPT):
|
|||||||
self.link = source.link
|
self.link = source.link
|
||||||
source.segment = self.ensure_segments_type(source.segment)
|
source.segment = self.ensure_segments_type(source.segment)
|
||||||
|
|
||||||
messages = self.create_messages(
|
# 如果段落数量超过阈值,使用分段处理方法
|
||||||
source.segment,
|
if len(source.segment) > MAX_SEGMENTS_PER_CHUNK:
|
||||||
|
print(f"段落过多({len(source.segment)}个),将使用分段处理方法")
|
||||||
|
return self._process_long_content_by_chunks(source)
|
||||||
|
|
||||||
|
# 正常处理较短的内容
|
||||||
|
try:
|
||||||
|
messages = self.create_messages(
|
||||||
|
source.segment,
|
||||||
|
title=source.title,
|
||||||
|
tags=source.tags,
|
||||||
|
video_img_urls=source.video_img_urls,
|
||||||
|
_format=source._format,
|
||||||
|
style=source.style,
|
||||||
|
extras=source.extras
|
||||||
|
)
|
||||||
|
|
||||||
|
# 检查消息大小
|
||||||
|
messages_json = json.dumps(messages)
|
||||||
|
if len(messages_json) > 100000: # API限制
|
||||||
|
print(f"消息体积过大: {len(messages_json)} 字节,将使用分段处理方法")
|
||||||
|
return self._process_long_content_by_chunks(source)
|
||||||
|
|
||||||
|
response = self.client.chat.completions.create(
|
||||||
|
model=self.model,
|
||||||
|
messages=messages,
|
||||||
|
temperature=0.7
|
||||||
|
)
|
||||||
|
return response.choices[0].message.content.strip()
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"总结失败: {str(e)}"
|
||||||
|
print(error_msg)
|
||||||
|
# 记录详细错误信息到日志
|
||||||
|
import logging
|
||||||
|
logging.error(f"GPT单块处理失败 - {error_msg}")
|
||||||
|
# 如果处理失败,尝试使用分段处理方法
|
||||||
|
print("尝试使用分段处理方法")
|
||||||
|
try:
|
||||||
|
return self._process_long_content_by_chunks(source)
|
||||||
|
except Exception as fallback_error:
|
||||||
|
# 如果分段处理也失败,抛出异常
|
||||||
|
logging.error(f"GPT分段处理也失败 - {str(fallback_error)}")
|
||||||
|
raise Exception(f"视频处理完全失败:主处理失败({str(e)}),分段处理也失败({str(fallback_error)})")
|
||||||
|
|
||||||
|
def _process_long_content_by_chunks(self, source: GPTSource) -> str:
|
||||||
|
"""
|
||||||
|
将长内容分成多个块,分别处理后再整合
|
||||||
|
"""
|
||||||
|
segments = source.segment
|
||||||
|
total_segments = len(segments)
|
||||||
|
|
||||||
|
# 计算需要多少块
|
||||||
|
num_chunks = math.ceil(total_segments / MAX_SEGMENTS_PER_CHUNK)
|
||||||
|
chunk_size = math.ceil(total_segments / num_chunks)
|
||||||
|
|
||||||
|
print(f"将内容分为{num_chunks}块进行处理,每块约{chunk_size}个段落")
|
||||||
|
|
||||||
|
chunk_summaries = []
|
||||||
|
|
||||||
|
# 处理每个块
|
||||||
|
for i in range(num_chunks):
|
||||||
|
start_idx = i * chunk_size
|
||||||
|
end_idx = min(start_idx + chunk_size, total_segments)
|
||||||
|
|
||||||
|
print(f"处理第{i+1}/{num_chunks}块 (段落 {start_idx} 到 {end_idx-1})")
|
||||||
|
|
||||||
|
# 创建此块的子源
|
||||||
|
chunk_source = GPTSource(
|
||||||
|
title=f"{source.title} - 第{i+1}/{num_chunks}部分",
|
||||||
|
segment=segments[start_idx:end_idx],
|
||||||
|
tags=source.tags,
|
||||||
|
screenshot=False, # 中间块不需要截图
|
||||||
|
video_img_urls=[], # 中间块不需要图片
|
||||||
|
link=False, # 中间块不需要链接
|
||||||
|
_format=[],
|
||||||
|
style=source.style,
|
||||||
|
extras=f"这是内容的第{i+1}部分,共{num_chunks}部分。请仅总结这部分内容的要点,无需引言和结论。"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 处理这个块
|
||||||
|
chunk_messages = self.create_messages(
|
||||||
|
chunk_source.segment,
|
||||||
|
title=chunk_source.title,
|
||||||
|
tags=chunk_source.tags,
|
||||||
|
video_img_urls=chunk_source.video_img_urls,
|
||||||
|
_format=chunk_source._format,
|
||||||
|
style=chunk_source.style,
|
||||||
|
extras=chunk_source.extras
|
||||||
|
)
|
||||||
|
|
||||||
|
chunk_response = self.client.chat.completions.create(
|
||||||
|
model=self.model,
|
||||||
|
messages=chunk_messages,
|
||||||
|
temperature=0.7
|
||||||
|
)
|
||||||
|
|
||||||
|
chunk_summary = chunk_response.choices[0].message.content.strip()
|
||||||
|
chunk_summaries.append(f"### 第{i+1}部分内容总结\n\n{chunk_summary}")
|
||||||
|
|
||||||
|
print(f"第{i+1}块处理完成")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"处理第{i+1}块时出错: {str(e)}"
|
||||||
|
print(error_msg)
|
||||||
|
# 记录详细错误信息到日志
|
||||||
|
import logging
|
||||||
|
logging.error(f"GPT处理失败 - {error_msg}")
|
||||||
|
# 如果某块处理失败,抛出异常停止整个处理流程
|
||||||
|
raise Exception(f"视频处理失败:第{i+1}块GPT调用失败 - {str(e)}")
|
||||||
|
|
||||||
|
# 合并所有块的总结
|
||||||
|
all_summaries = "\n\n".join(chunk_summaries)
|
||||||
|
|
||||||
|
# 创建最终总结请求
|
||||||
|
final_segment = TranscriptSegment(start=0, end=0, text=all_summaries)
|
||||||
|
final_source = GPTSource(
|
||||||
title=source.title,
|
title=source.title,
|
||||||
|
segment=[final_segment],
|
||||||
tags=source.tags,
|
tags=source.tags,
|
||||||
|
screenshot=source.screenshot,
|
||||||
video_img_urls=source.video_img_urls,
|
video_img_urls=source.video_img_urls,
|
||||||
|
link=source.link,
|
||||||
_format=source._format,
|
_format=source._format,
|
||||||
style=source.style,
|
style=source.style,
|
||||||
extras=source.extras
|
extras="以下是视频各部分的总结,请将它们整合为一篇完整、连贯的笔记。"
|
||||||
)
|
)
|
||||||
response = self.client.chat.completions.create(
|
|
||||||
model=self.model,
|
try:
|
||||||
messages=messages,
|
# 最终合并处理
|
||||||
temperature=0.7
|
final_messages = self.create_messages(
|
||||||
)
|
final_source.segment,
|
||||||
return response.choices[0].message.content.strip()
|
title=final_source.title,
|
||||||
|
tags=final_source.tags,
|
||||||
|
video_img_urls=final_source.video_img_urls,
|
||||||
|
_format=final_source._format,
|
||||||
|
style=final_source.style,
|
||||||
|
extras=final_source.extras
|
||||||
|
)
|
||||||
|
|
||||||
|
final_response = self.client.chat.completions.create(
|
||||||
|
model=self.model,
|
||||||
|
messages=final_messages,
|
||||||
|
temperature=0.7
|
||||||
|
)
|
||||||
|
|
||||||
|
return final_response.choices[0].message.content.strip()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"最终合并处理时出错: {str(e)}"
|
||||||
|
print(error_msg)
|
||||||
|
# 记录详细错误信息到日志
|
||||||
|
import logging
|
||||||
|
logging.error(f"GPT最终合并失败 - {error_msg}")
|
||||||
|
# 如果最终合并失败,抛出异常
|
||||||
|
raise Exception(f"视频处理失败:GPT最终合并失败 - {str(e)}")
|
||||||
|
|||||||
@@ -88,5 +88,10 @@ def update_provider(data: ProviderUpdateRequest):
|
|||||||
|
|
||||||
@router.post('/connect_test')
|
@router.post('/connect_test')
|
||||||
def gpt_connect_test(data: TestRequest):
|
def gpt_connect_test(data: TestRequest):
|
||||||
ModelService().connect_test(data.id)
|
try:
|
||||||
return R.success(msg='连接成功')
|
ModelService().connect_test(data.id)
|
||||||
|
return R.success(msg='连接成功')
|
||||||
|
except ProviderError as e:
|
||||||
|
return R.error(msg=e.message, code=e.code)
|
||||||
|
except Exception as e:
|
||||||
|
return R.error(msg=f'连接测试失败: {str(e)}')
|
||||||
|
|||||||
@@ -31,8 +31,20 @@ class ModelService:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
config = ModelService._build_model_config(provider)
|
config = ModelService._build_model_config(provider)
|
||||||
gpt = GPTFactory().from_config(config)
|
|
||||||
models = gpt.list_models()
|
# 如果是硅基流动,使用专门的提供商类
|
||||||
|
if "siliconflow" in provider["base_url"].lower():
|
||||||
|
from app.gpt.provider.SiliconFlow_provider import SiliconFlowProvider
|
||||||
|
silicon_provider = SiliconFlowProvider(
|
||||||
|
api_key=provider["api_key"],
|
||||||
|
base_url=provider["base_url"]
|
||||||
|
)
|
||||||
|
models = silicon_provider.list_models()
|
||||||
|
else:
|
||||||
|
# 其他提供商使用通用方法
|
||||||
|
gpt = GPTFactory().from_config(config)
|
||||||
|
models = gpt.list_models()
|
||||||
|
|
||||||
if verbose:
|
if verbose:
|
||||||
print(f"[{provider['name']}] 模型列表: {models}")
|
print(f"[{provider['name']}] 模型列表: {models}")
|
||||||
return models
|
return models
|
||||||
@@ -87,10 +99,23 @@ class ModelService:
|
|||||||
provider = ProviderService.get_provider_by_id(provider_id)
|
provider = ProviderService.get_provider_by_id(provider_id)
|
||||||
|
|
||||||
models = ModelService.get_model_list(provider["id"], verbose=verbose)
|
models = ModelService.get_model_list(provider["id"], verbose=verbose)
|
||||||
print(type(models))
|
print(f"模型对象类型: {type(models)}")
|
||||||
serializable_models = [m.dict() for m in models.data]
|
|
||||||
|
# 处理不同的模型列表格式
|
||||||
|
if hasattr(models, 'data'):
|
||||||
|
# OpenAI标准格式,有.data属性
|
||||||
|
serializable_models = [m.dict() for m in models.data]
|
||||||
|
elif isinstance(models, list):
|
||||||
|
# 直接返回list的格式
|
||||||
|
serializable_models = [m.dict() if hasattr(m, 'dict') else m for m in models]
|
||||||
|
else:
|
||||||
|
# 其他格式,尝试直接转换
|
||||||
|
serializable_models = [models.dict()] if hasattr(models, 'dict') else [models]
|
||||||
|
|
||||||
model_list = {
|
model_list = {
|
||||||
"models": serializable_models
|
"models": {
|
||||||
|
"data": serializable_models
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(f"[{provider['name']}] 获取模型成功")
|
logger.info(f"[{provider['name']}] 获取模型成功")
|
||||||
@@ -106,15 +131,24 @@ class ModelService:
|
|||||||
|
|
||||||
if provider:
|
if provider:
|
||||||
if not provider.get('api_key'):
|
if not provider.get('api_key'):
|
||||||
raise ProviderError(code=ProviderErrorEnum.NOT_FOUND.code, message=ProviderErrorEnum.NOT_FOUND.message)
|
raise ProviderError(code=ProviderErrorEnum.NOT_FOUND.code, message="API Key 不能为空")
|
||||||
result = OpenAICompatibleProvider.test_connection(
|
|
||||||
api_key=provider.get('api_key'),
|
try:
|
||||||
base_url=provider.get('base_url')
|
result = OpenAICompatibleProvider.test_connection(
|
||||||
)
|
api_key=provider.get('api_key'),
|
||||||
if result:
|
base_url=provider.get('base_url')
|
||||||
return True
|
)
|
||||||
else:
|
if result:
|
||||||
raise ProviderError(code=ProviderErrorEnum.WRONG_PARAMETER.code,message=ProviderErrorEnum.WRONG_PARAMETER.message)
|
return True
|
||||||
|
else:
|
||||||
|
raise ProviderError(code=ProviderErrorEnum.WRONG_PARAMETER.code, message="连接测试失败")
|
||||||
|
except Exception as e:
|
||||||
|
# 如果是我们自定义的错误信息,直接抛出
|
||||||
|
if isinstance(e, ProviderError):
|
||||||
|
raise e
|
||||||
|
else:
|
||||||
|
# 将底层错误包装成ProviderError
|
||||||
|
raise ProviderError(code=ProviderErrorEnum.WRONG_PARAMETER.code, message=str(e))
|
||||||
|
|
||||||
raise ProviderError(code=ProviderErrorEnum.NOT_FOUND.code, message=ProviderErrorEnum.NOT_FOUND.message)
|
raise ProviderError(code=ProviderErrorEnum.NOT_FOUND.code, message=ProviderErrorEnum.NOT_FOUND.message)
|
||||||
|
|
||||||
|
|||||||
169
backend/app/utils/siliconflow_helper.py
Normal file
169
backend/app/utils/siliconflow_helper.py
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
"""
|
||||||
|
硅基流动快速配置工具
|
||||||
|
基于市面上成熟的接入方案
|
||||||
|
"""
|
||||||
|
|
||||||
|
from app.gpt.provider.SiliconFlow_provider import SiliconFlowProvider
|
||||||
|
|
||||||
|
class SiliconFlowSetupHelper:
|
||||||
|
"""硅基流动配置助手"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_quick_setup_guide(cls) -> dict:
|
||||||
|
"""获取快速配置指南"""
|
||||||
|
return {
|
||||||
|
"title": "硅基流动(SiliconFlow)快速配置指南",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"step": 1,
|
||||||
|
"title": "获取API密钥",
|
||||||
|
"description": "访问 https://cloud.siliconflow.cn/account/ak 获取API密钥",
|
||||||
|
"note": "需要先注册账号并登录"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": 2,
|
||||||
|
"title": "选择API端点",
|
||||||
|
"description": "根据地理位置选择合适的端点",
|
||||||
|
"options": {
|
||||||
|
"国内用户": "https://api.siliconflow.cn/v1",
|
||||||
|
"海外用户": "https://api-st.siliconflow.cn/v1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": 3,
|
||||||
|
"title": "填写配置信息",
|
||||||
|
"fields": {
|
||||||
|
"名称": "硅基流动",
|
||||||
|
"API Key": "从步骤1获取的密钥",
|
||||||
|
"API地址": "从步骤2选择的端点",
|
||||||
|
"类型": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step": 4,
|
||||||
|
"title": "测试连接",
|
||||||
|
"description": "点击测试连通性按钮验证配置"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"recommended_models": SiliconFlowProvider.SUPPORTED_MODELS[:5],
|
||||||
|
"troubleshooting": {
|
||||||
|
"连接失败": [
|
||||||
|
"检查API密钥是否正确",
|
||||||
|
"确认API地址格式正确",
|
||||||
|
"尝试切换到另一个端点",
|
||||||
|
"检查网络连接"
|
||||||
|
],
|
||||||
|
"模型列表为空": [
|
||||||
|
"确认API密钥有效",
|
||||||
|
"检查账户余额",
|
||||||
|
"联系硅基流动客服"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def validate_config(cls, api_key: str, base_url: str) -> dict:
|
||||||
|
"""验证配置"""
|
||||||
|
try:
|
||||||
|
result = SiliconFlowProvider.test_connection(api_key, base_url)
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "硅基流动配置验证成功",
|
||||||
|
"recommended_next_steps": [
|
||||||
|
"添加推荐的模型到列表",
|
||||||
|
"开始使用AI功能"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": f"配置验证失败: {str(e)}",
|
||||||
|
"suggestions": cls._get_error_suggestions(str(e))
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_error_suggestions(cls, error_msg: str) -> list:
|
||||||
|
"""根据错误信息提供建议"""
|
||||||
|
suggestions = []
|
||||||
|
|
||||||
|
if "API Key" in error_msg:
|
||||||
|
suggestions.extend([
|
||||||
|
"检查API密钥是否从 https://cloud.siliconflow.cn/account/ak 正确复制",
|
||||||
|
"确认API密钥没有过期",
|
||||||
|
"检查账户状态是否正常"
|
||||||
|
])
|
||||||
|
|
||||||
|
if "404" in error_msg or "地址" in error_msg:
|
||||||
|
suggestions.extend([
|
||||||
|
"确认使用正确的API地址: https://api.siliconflow.cn/v1",
|
||||||
|
"海外用户尝试: https://api-st.siliconflow.cn/v1",
|
||||||
|
"检查URL末尾是否包含 /v1"
|
||||||
|
])
|
||||||
|
|
||||||
|
if "timeout" in error_msg or "连接" in error_msg:
|
||||||
|
suggestions.extend([
|
||||||
|
"检查网络连接",
|
||||||
|
"尝试切换网络环境",
|
||||||
|
"联系网络管理员检查防火墙设置"
|
||||||
|
])
|
||||||
|
|
||||||
|
if not suggestions:
|
||||||
|
suggestions.append("请参考官方文档或联系技术支持")
|
||||||
|
|
||||||
|
return suggestions
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_example_usage(cls) -> dict:
|
||||||
|
"""获取使用示例"""
|
||||||
|
return {
|
||||||
|
"python_code": '''
|
||||||
|
# 硅基流动使用示例
|
||||||
|
from openai import OpenAI
|
||||||
|
|
||||||
|
client = OpenAI(
|
||||||
|
api_key="你的API密钥",
|
||||||
|
base_url="https://api.siliconflow.cn/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model="Qwen/Qwen2.5-7B-Instruct",
|
||||||
|
messages=[
|
||||||
|
{"role": "user", "content": "你好,介绍一下自己"}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
print(response.choices[0].message.content)
|
||||||
|
''',
|
||||||
|
"curl_example": '''
|
||||||
|
curl -X POST "https://api.siliconflow.cn/v1/chat/completions" \\
|
||||||
|
-H "Authorization: Bearer 你的API密钥" \\
|
||||||
|
-H "Content-Type: application/json" \\
|
||||||
|
-d '{
|
||||||
|
"model": "Qwen/Qwen2.5-7B-Instruct",
|
||||||
|
"messages": [
|
||||||
|
{"role": "user", "content": "你好"}
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 打印配置指南
|
||||||
|
guide = SiliconFlowSetupHelper.get_quick_setup_guide()
|
||||||
|
print("=" * 50)
|
||||||
|
print(guide["title"])
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
for step in guide["steps"]:
|
||||||
|
print(f"\n步骤 {step['step']}: {step['title']}")
|
||||||
|
print(f"描述: {step['description']}")
|
||||||
|
if "options" in step:
|
||||||
|
for option, value in step["options"].items():
|
||||||
|
print(f" {option}: {value}")
|
||||||
|
if "fields" in step:
|
||||||
|
for field, value in step["fields"].items():
|
||||||
|
print(f" {field}: {value}")
|
||||||
|
|
||||||
|
print(f"\n推荐模型:")
|
||||||
|
for i, model in enumerate(guide["recommended_models"], 1):
|
||||||
|
print(f" {i}. {model}")
|
||||||
151
backend/docs/SiliconFlow_Integration_Solution.md
Normal file
151
backend/docs/SiliconFlow_Integration_Solution.md
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
# 硅基流动(SiliconFlow)接入解决方案
|
||||||
|
|
||||||
|
## 问题分析
|
||||||
|
|
||||||
|
通过深入分析市面上成熟的硅基流动接入方案(如LobeChat、Dify、OneAPI等),我们发现原有的通用OpenAI兼容接口在处理硅基流动时存在以下问题:
|
||||||
|
|
||||||
|
1. **连接测试方法不当**:使用`models.list()`接口,但硅基流动可能不完全支持
|
||||||
|
2. **模型名称格式差异**:硅基流动使用命名空间格式(如`deepseek-ai/DeepSeek-V3`)
|
||||||
|
3. **错误处理不够精准**:无法准确识别硅基流动特有的错误类型
|
||||||
|
4. **OpenAI SDK版本兼容性**:部分错误如`'str' object has no attribute '_set_private_attributes'`
|
||||||
|
|
||||||
|
## 解决方案
|
||||||
|
|
||||||
|
### 1. 创建专门的硅基流动提供商类
|
||||||
|
|
||||||
|
**文件**: `app/gpt/provider/SiliconFlow_provider.py`
|
||||||
|
|
||||||
|
**核心特性**:
|
||||||
|
- 使用chat接口进行连接测试,而非models接口
|
||||||
|
- 内置硅基流动支持的模型列表
|
||||||
|
- 支持国内外双端点(api.siliconflow.cn 和 api-st.siliconflow.cn)
|
||||||
|
- 智能错误处理和故障排除建议
|
||||||
|
|
||||||
|
**测试策略**:
|
||||||
|
```python
|
||||||
|
# 优先使用免费轻量级模型测试
|
||||||
|
test_models = [
|
||||||
|
"Qwen/Qwen2.5-7B-Instruct", # 免费模型优先
|
||||||
|
"deepseek-ai/DeepSeek-V3",
|
||||||
|
"THUDM/glm-4-9b-chat"
|
||||||
|
]
|
||||||
|
|
||||||
|
# 逐个尝试chat请求,而非依赖models接口
|
||||||
|
for model in test_models:
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model=model,
|
||||||
|
messages=[{"role": "user", "content": "hi"}],
|
||||||
|
max_tokens=1,
|
||||||
|
timeout=15.0
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 更新现有的兼容性提供商
|
||||||
|
|
||||||
|
**文件**: `app/gpt/provider/OpenAI_compatible_provider.py`
|
||||||
|
|
||||||
|
**改进**:
|
||||||
|
- 自动检测硅基流动URL,委托给专门的提供商类
|
||||||
|
- 简化其他提供商的处理逻辑
|
||||||
|
- 保持向后兼容性
|
||||||
|
|
||||||
|
### 3. 增强GPT工厂类
|
||||||
|
|
||||||
|
**文件**: `app/gpt/gpt_factory.py`
|
||||||
|
|
||||||
|
**功能**:
|
||||||
|
- 根据base_url自动选择合适的提供商类
|
||||||
|
- 硅基流动使用专门的SiliconFlowProvider
|
||||||
|
- 其他提供商继续使用通用的OpenAICompatibleProvider
|
||||||
|
|
||||||
|
### 4. 改进模型服务
|
||||||
|
|
||||||
|
**文件**: `app/services/model.py`
|
||||||
|
|
||||||
|
**优化**:
|
||||||
|
- 硅基流动使用专门的模型获取逻辑
|
||||||
|
- 更好的模型列表格式处理
|
||||||
|
- 支持预定义模型列表作为回退方案
|
||||||
|
|
||||||
|
### 5. 创建配置助手工具
|
||||||
|
|
||||||
|
**文件**: `app/utils/siliconflow_helper.py`
|
||||||
|
|
||||||
|
**功能**:
|
||||||
|
- 提供详细的配置指南
|
||||||
|
- 智能错误诊断和建议
|
||||||
|
- 使用示例和最佳实践
|
||||||
|
|
||||||
|
## 市面上成熟方案的特点
|
||||||
|
|
||||||
|
### LobeChat的实现方式
|
||||||
|
- 使用环境变量配置:`SILICONCLOUD_API_KEY` 和 `SILICONCLOUD_PROXY_URL`
|
||||||
|
- 完全基于OpenAI SDK,只修改base_url
|
||||||
|
- 优先使用chat接口验证连接
|
||||||
|
|
||||||
|
### Dify的集成方案
|
||||||
|
- 支持"OpenAI兼容API"配置方式
|
||||||
|
- 用户手动设置Model Name、API Key和API Endpoint
|
||||||
|
- 灵活的模型配置支持
|
||||||
|
|
||||||
|
### OneAPI的中转方案
|
||||||
|
- 作为统一的API网关
|
||||||
|
- 支持多种模型服务商的转换
|
||||||
|
- 提供统一的调用接口
|
||||||
|
|
||||||
|
## 使用指南
|
||||||
|
|
||||||
|
### 1. 基础配置
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "硅基流动",
|
||||||
|
"type": "custom",
|
||||||
|
"base_url": "https://api.siliconflow.cn/v1",
|
||||||
|
"api_key": "从 https://cloud.siliconflow.cn/account/ak 获取"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 推荐模型
|
||||||
|
|
||||||
|
免费模型(永久免费):
|
||||||
|
- `Qwen/Qwen2.5-7B-Instruct`
|
||||||
|
- `THUDM/glm-4-9b-chat`
|
||||||
|
- `01-ai/Yi-1.5-9B-Chat`
|
||||||
|
|
||||||
|
付费模型(性能更好):
|
||||||
|
- `deepseek-ai/DeepSeek-V3`
|
||||||
|
- `Qwen/Qwen2.5-72B-Instruct`
|
||||||
|
- `meta-llama/Llama-3.1-70B-Instruct`
|
||||||
|
|
||||||
|
### 3. 故障排除
|
||||||
|
|
||||||
|
**404错误**:
|
||||||
|
- 确认URL包含`/v1`后缀
|
||||||
|
- 海外用户尝试`https://api-st.siliconflow.cn/v1`
|
||||||
|
|
||||||
|
**401错误**:
|
||||||
|
- 检查API密钥是否正确
|
||||||
|
- 确认账户状态正常
|
||||||
|
|
||||||
|
**连接超时**:
|
||||||
|
- 检查网络连接
|
||||||
|
- 尝试切换端点
|
||||||
|
|
||||||
|
## 技术优势
|
||||||
|
|
||||||
|
1. **专门优化**:针对硅基流动的特殊要求定制
|
||||||
|
2. **智能回退**:多种连接测试方法,提高成功率
|
||||||
|
3. **错误诊断**:精准的错误识别和解决建议
|
||||||
|
4. **完全兼容**:基于OpenAI SDK,保持API一致性
|
||||||
|
5. **双端点支持**:自动处理国内外网络环境差异
|
||||||
|
|
||||||
|
## 验证方法
|
||||||
|
|
||||||
|
1. 在前端配置硅基流动提供商
|
||||||
|
2. 使用正确的API地址:`https://api.siliconflow.cn/v1`
|
||||||
|
3. 输入有效的API密钥
|
||||||
|
4. 点击测试连通性
|
||||||
|
5. 应该能看到连接成功的消息
|
||||||
|
|
||||||
|
这个解决方案基于对Cherry Studio、LobeChat、Dify等成熟项目的分析,采用了业界最佳实践,应该能够彻底解决硅基流动的接入问题。
|
||||||
@@ -23,7 +23,7 @@ click-repl==0.3.0
|
|||||||
colorama==0.4.6
|
colorama==0.4.6
|
||||||
coloredlogs==15.0.1
|
coloredlogs==15.0.1
|
||||||
cssselect2==0.8.0
|
cssselect2==0.8.0
|
||||||
ctranslate2==4.5.0
|
ctranslate2==4.6.0
|
||||||
distro==1.9.0
|
distro==1.9.0
|
||||||
dnspython==2.7.0
|
dnspython==2.7.0
|
||||||
email_validator==2.2.0
|
email_validator==2.2.0
|
||||||
|
|||||||
Reference in New Issue
Block a user