统一API Base URL保存与请求时去除末尾斜杠
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
// AI评论生成核心逻辑
|
||||
|
||||
import { normalizeApiBaseUrl } from '@/lib/url';
|
||||
|
||||
export interface AIComment {
|
||||
id: string;
|
||||
userName: string;
|
||||
@@ -164,7 +166,8 @@ export async function generateAIComments(
|
||||
const prompt = buildCommentPrompt(movieName, movieInfo, searchResults, count);
|
||||
|
||||
// 3. 调用AI API
|
||||
const response = await fetch(`${aiConfig.CustomBaseURL}/chat/completions`, {
|
||||
const baseURL = normalizeApiBaseUrl(aiConfig.CustomBaseURL);
|
||||
const response = await fetch(`${baseURL}/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${aiConfig.CustomApiKey}`,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import { fetchDoubanData as fetchDoubanAPI } from '@/lib/douban';
|
||||
import { getNextApiKey } from '@/lib/tmdb.client';
|
||||
import { normalizeApiBaseUrl } from '@/lib/url';
|
||||
|
||||
export interface VideoContext {
|
||||
title?: string;
|
||||
@@ -468,7 +469,9 @@ ${availableSources.length === 0 ? '⚠️ 没有可用的数据源,请返回
|
||||
}
|
||||
} else {
|
||||
// OpenAI 或 自定义 (OpenAI兼容格式)
|
||||
const baseURL = config.baseURL || 'https://api.openai.com/v1';
|
||||
const baseURL = normalizeApiBaseUrl(
|
||||
config.baseURL || 'https://api.openai.com/v1'
|
||||
);
|
||||
response = await fetch(`${baseURL}/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any, no-console, @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
import { db } from '@/lib/db';
|
||||
import { normalizeApiBaseUrl } from '@/lib/url';
|
||||
|
||||
import { AdminConfig } from './admin.types';
|
||||
|
||||
@@ -1047,6 +1048,100 @@ export function configSelfCheck(adminConfig: AdminConfig): AdminConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// API Base URL 统一去尾斜杠,避免拼接路径时出现 //
|
||||
const site = adminConfig.SiteConfig;
|
||||
if (site) {
|
||||
site.DoubanProxy = normalizeApiBaseUrl(site.DoubanProxy);
|
||||
site.DoubanImageProxy = normalizeApiBaseUrl(site.DoubanImageProxy);
|
||||
site.DanmakuApiBase = normalizeApiBaseUrl(site.DanmakuApiBase);
|
||||
site.TMDBProxy = normalizeApiBaseUrl(site.TMDBProxy);
|
||||
site.TMDBReverseProxy = normalizeApiBaseUrl(site.TMDBReverseProxy);
|
||||
site.BangumiApiBaseUrl = normalizeApiBaseUrl(site.BangumiApiBaseUrl);
|
||||
site.BangumiImageBaseUrl = normalizeApiBaseUrl(site.BangumiImageBaseUrl);
|
||||
site.BangumiProxy = normalizeApiBaseUrl(site.BangumiProxy);
|
||||
site.PansouApiUrl = normalizeApiBaseUrl(site.PansouApiUrl);
|
||||
site.MagnetProxy = normalizeApiBaseUrl(site.MagnetProxy);
|
||||
site.MagnetMikanReverseProxy = normalizeApiBaseUrl(
|
||||
site.MagnetMikanReverseProxy
|
||||
);
|
||||
site.MagnetDmhyReverseProxy = normalizeApiBaseUrl(
|
||||
site.MagnetDmhyReverseProxy
|
||||
);
|
||||
site.MagnetAcgripReverseProxy = normalizeApiBaseUrl(
|
||||
site.MagnetAcgripReverseProxy
|
||||
);
|
||||
site.MagnetNyaaReverseProxy = normalizeApiBaseUrl(
|
||||
site.MagnetNyaaReverseProxy
|
||||
);
|
||||
site.OIDCIssuer = normalizeApiBaseUrl(site.OIDCIssuer);
|
||||
}
|
||||
|
||||
if (adminConfig.OpenListConfig) {
|
||||
adminConfig.OpenListConfig.URL = normalizeApiBaseUrl(
|
||||
adminConfig.OpenListConfig.URL
|
||||
);
|
||||
adminConfig.OpenListConfig.OfflineDownloadURL = normalizeApiBaseUrl(
|
||||
adminConfig.OpenListConfig.OfflineDownloadURL
|
||||
);
|
||||
}
|
||||
|
||||
if (adminConfig.MusicConfig) {
|
||||
adminConfig.MusicConfig.BaseUrl = normalizeApiBaseUrl(
|
||||
adminConfig.MusicConfig.BaseUrl
|
||||
);
|
||||
}
|
||||
|
||||
if (adminConfig.XiaoyaConfig) {
|
||||
adminConfig.XiaoyaConfig.ServerURL = normalizeApiBaseUrl(
|
||||
adminConfig.XiaoyaConfig.ServerURL
|
||||
);
|
||||
}
|
||||
|
||||
if (adminConfig.SuwayomiConfig) {
|
||||
adminConfig.SuwayomiConfig.ServerURL = normalizeApiBaseUrl(
|
||||
adminConfig.SuwayomiConfig.ServerURL
|
||||
);
|
||||
}
|
||||
|
||||
if (adminConfig.EmbyConfig) {
|
||||
if (adminConfig.EmbyConfig.ServerURL) {
|
||||
adminConfig.EmbyConfig.ServerURL = normalizeApiBaseUrl(
|
||||
adminConfig.EmbyConfig.ServerURL
|
||||
);
|
||||
}
|
||||
if (Array.isArray(adminConfig.EmbyConfig.Sources)) {
|
||||
adminConfig.EmbyConfig.Sources = adminConfig.EmbyConfig.Sources.map(
|
||||
(source) => ({
|
||||
...source,
|
||||
ServerURL: normalizeApiBaseUrl(source.ServerURL),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (adminConfig.AIConfig) {
|
||||
adminConfig.AIConfig.OpenAIBaseURL = normalizeApiBaseUrl(
|
||||
adminConfig.AIConfig.OpenAIBaseURL
|
||||
);
|
||||
adminConfig.AIConfig.CustomBaseURL = normalizeApiBaseUrl(
|
||||
adminConfig.AIConfig.CustomBaseURL
|
||||
);
|
||||
adminConfig.AIConfig.DecisionOpenAIBaseURL = normalizeApiBaseUrl(
|
||||
adminConfig.AIConfig.DecisionOpenAIBaseURL
|
||||
);
|
||||
adminConfig.AIConfig.DecisionCustomBaseURL = normalizeApiBaseUrl(
|
||||
adminConfig.AIConfig.DecisionCustomBaseURL
|
||||
);
|
||||
}
|
||||
|
||||
if (adminConfig.TelegramConfig) {
|
||||
adminConfig.TelegramConfig.apiBaseUrl = normalizeApiBaseUrl(
|
||||
adminConfig.TelegramConfig.apiBaseUrl
|
||||
);
|
||||
}
|
||||
|
||||
// 注意:OPDS source.url 是完整目录 URL,保留末尾 / 以便相对路径解析
|
||||
|
||||
return adminConfig;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { normalizeApiBaseUrl } from '@/lib/url';
|
||||
|
||||
interface EmbyConfig {
|
||||
ServerURL: string;
|
||||
ApiKey?: string;
|
||||
@@ -96,7 +98,7 @@ export class EmbyClient {
|
||||
private embyAuthorizationHeader: string;
|
||||
|
||||
constructor(config: EmbyConfig) {
|
||||
let serverUrl = config.ServerURL.replace(/\/$/, '');
|
||||
let serverUrl = normalizeApiBaseUrl(config.ServerURL);
|
||||
|
||||
// 存储高级选项
|
||||
this.removeEmbyPrefix = config.removeEmbyPrefix || false;
|
||||
|
||||
+4
-1
@@ -1,4 +1,5 @@
|
||||
import { getConfig } from '@/lib/config';
|
||||
import { normalizeApiBaseUrl } from '@/lib/url';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
@@ -209,7 +210,9 @@ export async function getMusicV2Config() {
|
||||
const musicConfig = config?.MusicConfig;
|
||||
|
||||
const enabled = musicConfig?.Enabled ?? false;
|
||||
const baseUrl = (musicConfig?.BaseUrl || process.env.MUSIC_V2_BASE_URL || '').replace(/\/$/, '');
|
||||
const baseUrl = normalizeApiBaseUrl(
|
||||
musicConfig?.BaseUrl || process.env.MUSIC_V2_BASE_URL || ''
|
||||
);
|
||||
const token = musicConfig?.Token || process.env.MUSIC_V2_TOKEN || '';
|
||||
|
||||
return { enabled, baseUrl, token };
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { normalizeApiBaseUrl } from '@/lib/url';
|
||||
|
||||
// Token 内存缓存
|
||||
const tokenCache = new Map<string, { token: string; expiresAt: number }>();
|
||||
|
||||
@@ -34,12 +36,15 @@ export interface OpenListGetResponse {
|
||||
|
||||
export class OpenListClient {
|
||||
private token = '';
|
||||
private baseURL: string;
|
||||
|
||||
constructor(
|
||||
private baseURL: string,
|
||||
baseURL: string,
|
||||
private username: string,
|
||||
private password: string
|
||||
) {}
|
||||
) {
|
||||
this.baseURL = normalizeApiBaseUrl(baseURL);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用账号密码登录获取Token
|
||||
@@ -49,7 +54,8 @@ export class OpenListClient {
|
||||
username: string,
|
||||
password: string
|
||||
): Promise<string> {
|
||||
const response = await fetch(`${baseURL}/api/auth/login`, {
|
||||
const normalizedBaseURL = normalizeApiBaseUrl(baseURL);
|
||||
const response = await fetch(`${normalizedBaseURL}/api/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
* 文档: https://github.com/fish2018/pansou
|
||||
*/
|
||||
|
||||
import { normalizeApiBaseUrl } from '@/lib/url';
|
||||
|
||||
// Token 缓存
|
||||
let cachedToken: string | null = null;
|
||||
let tokenExpiry: number | null = null;
|
||||
@@ -40,7 +42,8 @@ export async function loginPansou(
|
||||
password: string
|
||||
): Promise<string> {
|
||||
try {
|
||||
const response = await fetch(`${apiUrl}/api/auth/login`, {
|
||||
const baseUrl = normalizeApiBaseUrl(apiUrl);
|
||||
const response = await fetch(`${baseUrl}/api/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -111,9 +114,10 @@ export async function searchPansou(
|
||||
}
|
||||
): Promise<PansouSearchResult> {
|
||||
try {
|
||||
const baseUrl = normalizeApiBaseUrl(apiUrl);
|
||||
// 获取 Token(如果需要认证)
|
||||
const token = await getValidToken(
|
||||
apiUrl,
|
||||
baseUrl,
|
||||
options?.username,
|
||||
options?.password
|
||||
);
|
||||
@@ -141,7 +145,7 @@ export async function searchPansou(
|
||||
body.cloud_types = options.cloudTypes;
|
||||
}
|
||||
|
||||
const response = await fetch(`${apiUrl}/api/search`, {
|
||||
const response = await fetch(`${baseUrl}/api/search`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
@@ -201,7 +205,8 @@ export function clearPansouToken(): void {
|
||||
*/
|
||||
export async function checkPansouHealth(apiUrl: string): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${apiUrl}/api/health`, {
|
||||
const baseUrl = normalizeApiBaseUrl(apiUrl);
|
||||
const response = await fetch(`${baseUrl}/api/health`, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
MangaSearchResult,
|
||||
MangaSource,
|
||||
} from './manga.types';
|
||||
import { normalizeApiBaseUrl } from './url';
|
||||
|
||||
interface GraphQLResponse<T> {
|
||||
data?: T;
|
||||
@@ -167,7 +168,7 @@ async function resolveSuwayomiConfig(options: SuwayomiClientOptions = {}): Promi
|
||||
throw new Error('Suwayomi 未配置,请先在管理面板或环境变量中设置服务地址');
|
||||
}
|
||||
|
||||
const normalizedBaseUrl = serverUrl.replace(/\/$/, '');
|
||||
const normalizedBaseUrl = normalizeApiBaseUrl(serverUrl);
|
||||
|
||||
return {
|
||||
serverBaseUrl: normalizedBaseUrl,
|
||||
|
||||
+2
-1
@@ -13,6 +13,7 @@ import { getConfig } from './config';
|
||||
import { db, getStorage } from './db';
|
||||
import { lockManager } from './lock';
|
||||
import type { IStorage, Notification } from './types';
|
||||
import { normalizeApiBaseUrl } from './url';
|
||||
import { getNotificationClickUrl } from './web-push';
|
||||
|
||||
export interface TelegramConfig {
|
||||
@@ -426,7 +427,7 @@ function isCloudflareEnvironment(): boolean {
|
||||
}
|
||||
|
||||
function normalizeTelegramApiBaseUrl(input?: string | null): string {
|
||||
const base = (input || 'https://api.telegram.org').trim().replace(/\/+$/, '');
|
||||
const base = normalizeApiBaseUrl(input || 'https://api.telegram.org');
|
||||
return base || 'https://api.telegram.org';
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* 规范化 API Base URL:trim 并去除末尾斜杠,避免拼接路径时出现双斜杠。
|
||||
*/
|
||||
export function normalizeApiBaseUrl(
|
||||
url: string | undefined | null
|
||||
): string {
|
||||
return String(url || '')
|
||||
.trim()
|
||||
.replace(/\/+$/, '');
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { normalizeApiBaseUrl } from '@/lib/url';
|
||||
|
||||
// Token 内存缓存
|
||||
const tokenCache = new Map<string, { token: string; expiresAt: number }>();
|
||||
|
||||
@@ -17,13 +19,16 @@ export interface XiaoyaListResponse {
|
||||
|
||||
export class XiaoyaClient {
|
||||
private token = '';
|
||||
private baseURL: string;
|
||||
|
||||
constructor(
|
||||
private baseURL: string,
|
||||
baseURL: string,
|
||||
private username?: string,
|
||||
private password?: string,
|
||||
private configToken?: string
|
||||
) {}
|
||||
) {
|
||||
this.baseURL = normalizeApiBaseUrl(baseURL);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用账号密码登录获取Token
|
||||
@@ -33,7 +38,8 @@ export class XiaoyaClient {
|
||||
username: string,
|
||||
password: string
|
||||
): Promise<string> {
|
||||
const response = await fetch(`${baseURL}/api/auth/login`, {
|
||||
const normalizedBaseURL = normalizeApiBaseUrl(baseURL);
|
||||
const response = await fetch(`${normalizedBaseURL}/api/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
Reference in New Issue
Block a user