统一API Base URL保存与请求时去除末尾斜杠
This commit is contained in:
@@ -5,6 +5,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
|
import { normalizeApiBaseUrl } from '@/lib/url';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
@@ -156,22 +157,22 @@ export async function POST(request: NextRequest) {
|
|||||||
Enabled,
|
Enabled,
|
||||||
Provider,
|
Provider,
|
||||||
OpenAIApiKey,
|
OpenAIApiKey,
|
||||||
OpenAIBaseURL,
|
OpenAIBaseURL: normalizeApiBaseUrl(OpenAIBaseURL),
|
||||||
OpenAIModel,
|
OpenAIModel,
|
||||||
ClaudeApiKey,
|
ClaudeApiKey,
|
||||||
ClaudeModel,
|
ClaudeModel,
|
||||||
CustomApiKey,
|
CustomApiKey,
|
||||||
CustomBaseURL,
|
CustomBaseURL: normalizeApiBaseUrl(CustomBaseURL),
|
||||||
CustomModel,
|
CustomModel,
|
||||||
EnableDecisionModel,
|
EnableDecisionModel,
|
||||||
DecisionProvider,
|
DecisionProvider,
|
||||||
DecisionOpenAIApiKey,
|
DecisionOpenAIApiKey,
|
||||||
DecisionOpenAIBaseURL,
|
DecisionOpenAIBaseURL: normalizeApiBaseUrl(DecisionOpenAIBaseURL),
|
||||||
DecisionOpenAIModel,
|
DecisionOpenAIModel,
|
||||||
DecisionClaudeApiKey,
|
DecisionClaudeApiKey,
|
||||||
DecisionClaudeModel,
|
DecisionClaudeModel,
|
||||||
DecisionCustomApiKey,
|
DecisionCustomApiKey,
|
||||||
DecisionCustomBaseURL,
|
DecisionCustomBaseURL: normalizeApiBaseUrl(DecisionCustomBaseURL),
|
||||||
DecisionCustomModel,
|
DecisionCustomModel,
|
||||||
EnableWebSearch,
|
EnableWebSearch,
|
||||||
WebSearchProvider,
|
WebSearchProvider,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getConfig, setCachedConfig } from '@/lib/config';
|
import { getConfig, setCachedConfig } from '@/lib/config';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
|
import { normalizeApiBaseUrl } from '@/lib/url';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
@@ -63,7 +64,7 @@ export async function POST(request: NextRequest) {
|
|||||||
// 更新缓存中的音乐配置
|
// 更新缓存中的音乐配置
|
||||||
adminConfig.MusicConfig = {
|
adminConfig.MusicConfig = {
|
||||||
Enabled,
|
Enabled,
|
||||||
BaseUrl,
|
BaseUrl: normalizeApiBaseUrl(BaseUrl),
|
||||||
Token,
|
Token,
|
||||||
ProxyEnabled: ProxyEnabled ?? true,
|
ProxyEnabled: ProxyEnabled ?? true,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
normalizePathMetaMap,
|
normalizePathMetaMap,
|
||||||
type OpenListPathMetaMap,
|
type OpenListPathMetaMap,
|
||||||
} from '@/lib/openlist-path-meta';
|
} from '@/lib/openlist-path-meta';
|
||||||
|
import { normalizeApiBaseUrl } from '@/lib/url';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
@@ -82,18 +83,20 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
if (action === 'save') {
|
if (action === 'save') {
|
||||||
const cleanedPathMeta = parsePathMeta(PathMeta);
|
const cleanedPathMeta = parsePathMeta(PathMeta);
|
||||||
|
const normalizedURL = normalizeApiBaseUrl(URL);
|
||||||
|
const normalizedOfflineDownloadURL = normalizeApiBaseUrl(OfflineDownloadURL);
|
||||||
|
|
||||||
// 如果功能未启用,允许保存空配置
|
// 如果功能未启用,允许保存空配置
|
||||||
if (!Enabled) {
|
if (!Enabled) {
|
||||||
adminConfig.OpenListConfig = {
|
adminConfig.OpenListConfig = {
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
URL: URL || '',
|
URL: normalizedURL,
|
||||||
Username: Username || '',
|
Username: Username || '',
|
||||||
Password: Password || '',
|
Password: Password || '',
|
||||||
RootPaths: RootPaths || ['/'],
|
RootPaths: RootPaths || ['/'],
|
||||||
OfflineDownloadPath: OfflineDownloadPath || '/',
|
OfflineDownloadPath: OfflineDownloadPath || '/',
|
||||||
OfflineDownloadUseCustomSource: OfflineDownloadUseCustomSource || false,
|
OfflineDownloadUseCustomSource: OfflineDownloadUseCustomSource || false,
|
||||||
OfflineDownloadURL: OfflineDownloadURL || '',
|
OfflineDownloadURL: normalizedOfflineDownloadURL,
|
||||||
OfflineDownloadUsername: OfflineDownloadUsername || '',
|
OfflineDownloadUsername: OfflineDownloadUsername || '',
|
||||||
OfflineDownloadPassword: OfflineDownloadPassword || '',
|
OfflineDownloadPassword: OfflineDownloadPassword || '',
|
||||||
LastRefreshTime: adminConfig.OpenListConfig?.LastRefreshTime,
|
LastRefreshTime: adminConfig.OpenListConfig?.LastRefreshTime,
|
||||||
@@ -113,7 +116,7 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 功能启用时,验证必填字段
|
// 功能启用时,验证必填字段
|
||||||
if (!URL || !Username || !Password) {
|
if (!normalizedURL || !Username || !Password) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: '请提供 URL、账号和密码' },
|
{ error: '请提供 URL、账号和密码' },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
@@ -122,7 +125,9 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
OfflineDownloadUseCustomSource &&
|
OfflineDownloadUseCustomSource &&
|
||||||
(!OfflineDownloadURL || !OfflineDownloadUsername || !OfflineDownloadPassword)
|
(!normalizedOfflineDownloadURL ||
|
||||||
|
!OfflineDownloadUsername ||
|
||||||
|
!OfflineDownloadPassword)
|
||||||
) {
|
) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: '请提供离线下载 OpenList URL、账号和密码' },
|
{ error: '请提供离线下载 OpenList URL、账号和密码' },
|
||||||
@@ -153,7 +158,7 @@ export async function POST(request: NextRequest) {
|
|||||||
// 验证账号密码是否正确
|
// 验证账号密码是否正确
|
||||||
try {
|
try {
|
||||||
console.log('[OpenList Config] 验证账号密码');
|
console.log('[OpenList Config] 验证账号密码');
|
||||||
await OpenListClient.login(URL, Username, Password);
|
await OpenListClient.login(normalizedURL, Username, Password);
|
||||||
console.log('[OpenList Config] 账号密码验证成功');
|
console.log('[OpenList Config] 账号密码验证成功');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[OpenList Config] 账号密码验证失败:', error);
|
console.error('[OpenList Config] 账号密码验证失败:', error);
|
||||||
@@ -167,7 +172,7 @@ export async function POST(request: NextRequest) {
|
|||||||
try {
|
try {
|
||||||
console.log('[OpenList Config] 验证离线下载 OpenList 账号密码');
|
console.log('[OpenList Config] 验证离线下载 OpenList 账号密码');
|
||||||
await OpenListClient.login(
|
await OpenListClient.login(
|
||||||
OfflineDownloadURL,
|
normalizedOfflineDownloadURL,
|
||||||
OfflineDownloadUsername,
|
OfflineDownloadUsername,
|
||||||
OfflineDownloadPassword
|
OfflineDownloadPassword
|
||||||
);
|
);
|
||||||
@@ -183,13 +188,13 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
adminConfig.OpenListConfig = {
|
adminConfig.OpenListConfig = {
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
URL,
|
URL: normalizedURL,
|
||||||
Username,
|
Username,
|
||||||
Password,
|
Password,
|
||||||
RootPaths: cleanedRootPaths,
|
RootPaths: cleanedRootPaths,
|
||||||
OfflineDownloadPath: OfflineDownloadPath || '/',
|
OfflineDownloadPath: OfflineDownloadPath || '/',
|
||||||
OfflineDownloadUseCustomSource: OfflineDownloadUseCustomSource || false,
|
OfflineDownloadUseCustomSource: OfflineDownloadUseCustomSource || false,
|
||||||
OfflineDownloadURL: OfflineDownloadURL || '',
|
OfflineDownloadURL: normalizedOfflineDownloadURL,
|
||||||
OfflineDownloadUsername: OfflineDownloadUsername || '',
|
OfflineDownloadUsername: OfflineDownloadUsername || '',
|
||||||
OfflineDownloadPassword: OfflineDownloadPassword || '',
|
OfflineDownloadPassword: OfflineDownloadPassword || '',
|
||||||
LastRefreshTime: adminConfig.OpenListConfig?.LastRefreshTime,
|
LastRefreshTime: adminConfig.OpenListConfig?.LastRefreshTime,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
|
import { normalizeApiBaseUrl } from '@/lib/url';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
@@ -264,6 +265,7 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 更新缓存中的站点设置
|
// 更新缓存中的站点设置
|
||||||
|
// API Base URL 统一去尾斜杠,避免运行时拼接路径出现 //
|
||||||
adminConfig.SiteConfig = {
|
adminConfig.SiteConfig = {
|
||||||
SiteName,
|
SiteName,
|
||||||
Announcement,
|
Announcement,
|
||||||
@@ -271,33 +273,33 @@ export async function POST(request: NextRequest) {
|
|||||||
SearchDownstreamMaxPage,
|
SearchDownstreamMaxPage,
|
||||||
SiteInterfaceCacheTime,
|
SiteInterfaceCacheTime,
|
||||||
DoubanProxyType,
|
DoubanProxyType,
|
||||||
DoubanProxy,
|
DoubanProxy: normalizeApiBaseUrl(DoubanProxy),
|
||||||
DoubanImageProxyType,
|
DoubanImageProxyType,
|
||||||
DoubanImageProxy,
|
DoubanImageProxy: normalizeApiBaseUrl(DoubanImageProxy),
|
||||||
DisableYellowFilter,
|
DisableYellowFilter,
|
||||||
FluidSearch,
|
FluidSearch,
|
||||||
DanmakuSourceType,
|
DanmakuSourceType,
|
||||||
DanmakuApiBase,
|
DanmakuApiBase: normalizeApiBaseUrl(DanmakuApiBase),
|
||||||
DanmakuApiToken,
|
DanmakuApiToken,
|
||||||
DanmakuAutoLoadDefault,
|
DanmakuAutoLoadDefault,
|
||||||
TMDBApiKey,
|
TMDBApiKey,
|
||||||
TMDBProxy,
|
TMDBProxy: normalizeApiBaseUrl(TMDBProxy),
|
||||||
TMDBReverseProxy,
|
TMDBReverseProxy: normalizeApiBaseUrl(TMDBReverseProxy),
|
||||||
BangumiDataSource,
|
BangumiDataSource,
|
||||||
BangumiApiBaseUrl,
|
BangumiApiBaseUrl: normalizeApiBaseUrl(BangumiApiBaseUrl),
|
||||||
BangumiImageBaseUrl,
|
BangumiImageBaseUrl: normalizeApiBaseUrl(BangumiImageBaseUrl),
|
||||||
BangumiProxy,
|
BangumiProxy: normalizeApiBaseUrl(BangumiProxy),
|
||||||
BannerDataSource,
|
BannerDataSource,
|
||||||
RecommendationDataSource,
|
RecommendationDataSource,
|
||||||
PansouApiUrl,
|
PansouApiUrl: normalizeApiBaseUrl(PansouApiUrl),
|
||||||
PansouUsername,
|
PansouUsername,
|
||||||
PansouPassword,
|
PansouPassword,
|
||||||
PansouKeywordBlocklist,
|
PansouKeywordBlocklist,
|
||||||
MagnetProxy,
|
MagnetProxy: normalizeApiBaseUrl(MagnetProxy),
|
||||||
MagnetMikanReverseProxy,
|
MagnetMikanReverseProxy: normalizeApiBaseUrl(MagnetMikanReverseProxy),
|
||||||
MagnetDmhyReverseProxy,
|
MagnetDmhyReverseProxy: normalizeApiBaseUrl(MagnetDmhyReverseProxy),
|
||||||
MagnetAcgripReverseProxy,
|
MagnetAcgripReverseProxy: normalizeApiBaseUrl(MagnetAcgripReverseProxy),
|
||||||
MagnetNyaaReverseProxy,
|
MagnetNyaaReverseProxy: normalizeApiBaseUrl(MagnetNyaaReverseProxy),
|
||||||
EnableComments,
|
EnableComments,
|
||||||
CustomAdFilterCode,
|
CustomAdFilterCode,
|
||||||
CustomAdFilterVersion,
|
CustomAdFilterVersion,
|
||||||
@@ -311,7 +313,7 @@ export async function POST(request: NextRequest) {
|
|||||||
DefaultUserTags,
|
DefaultUserTags,
|
||||||
EnableOIDCLogin,
|
EnableOIDCLogin,
|
||||||
EnableOIDCRegistration,
|
EnableOIDCRegistration,
|
||||||
OIDCIssuer,
|
OIDCIssuer: normalizeApiBaseUrl(OIDCIssuer),
|
||||||
OIDCAuthorizationEndpoint,
|
OIDCAuthorizationEndpoint,
|
||||||
OIDCTokenEndpoint,
|
OIDCTokenEndpoint,
|
||||||
OIDCUserInfoEndpoint,
|
OIDCUserInfoEndpoint,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
|
import { normalizeApiBaseUrl } from '@/lib/url';
|
||||||
import { XiaoyaClient } from '@/lib/xiaoya.client';
|
import { XiaoyaClient } from '@/lib/xiaoya.client';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
@@ -27,7 +28,7 @@ export async function POST(request: NextRequest) {
|
|||||||
// 测试连接
|
// 测试连接
|
||||||
try {
|
try {
|
||||||
const client = new XiaoyaClient(
|
const client = new XiaoyaClient(
|
||||||
configData.ServerURL,
|
normalizeApiBaseUrl(configData.ServerURL),
|
||||||
configData.Username,
|
configData.Username,
|
||||||
configData.Password,
|
configData.Password,
|
||||||
configData.Token
|
configData.Token
|
||||||
@@ -51,7 +52,7 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
config.XiaoyaConfig = {
|
config.XiaoyaConfig = {
|
||||||
Enabled: configData.Enabled || false,
|
Enabled: configData.Enabled || false,
|
||||||
ServerURL: configData.ServerURL || '',
|
ServerURL: normalizeApiBaseUrl(configData.ServerURL),
|
||||||
Token: configData.Token,
|
Token: configData.Token,
|
||||||
Username: configData.Username,
|
Username: configData.Username,
|
||||||
Password: configData.Password,
|
Password: configData.Password,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
// AI评论生成核心逻辑
|
// AI评论生成核心逻辑
|
||||||
|
|
||||||
|
import { normalizeApiBaseUrl } from '@/lib/url';
|
||||||
|
|
||||||
export interface AIComment {
|
export interface AIComment {
|
||||||
id: string;
|
id: string;
|
||||||
userName: string;
|
userName: string;
|
||||||
@@ -164,7 +166,8 @@ export async function generateAIComments(
|
|||||||
const prompt = buildCommentPrompt(movieName, movieInfo, searchResults, count);
|
const prompt = buildCommentPrompt(movieName, movieInfo, searchResults, count);
|
||||||
|
|
||||||
// 3. 调用AI API
|
// 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',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${aiConfig.CustomApiKey}`,
|
Authorization: `Bearer ${aiConfig.CustomApiKey}`,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import { fetchDoubanData as fetchDoubanAPI } from '@/lib/douban';
|
import { fetchDoubanData as fetchDoubanAPI } from '@/lib/douban';
|
||||||
import { getNextApiKey } from '@/lib/tmdb.client';
|
import { getNextApiKey } from '@/lib/tmdb.client';
|
||||||
|
import { normalizeApiBaseUrl } from '@/lib/url';
|
||||||
|
|
||||||
export interface VideoContext {
|
export interface VideoContext {
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -468,7 +469,9 @@ ${availableSources.length === 0 ? '⚠️ 没有可用的数据源,请返回
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// OpenAI 或 自定义 (OpenAI兼容格式)
|
// 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`, {
|
response = await fetch(`${baseURL}/chat/completions`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any, no-console, @typescript-eslint/no-non-null-assertion */
|
/* eslint-disable @typescript-eslint/no-explicit-any, no-console, @typescript-eslint/no-non-null-assertion */
|
||||||
|
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
|
import { normalizeApiBaseUrl } from '@/lib/url';
|
||||||
|
|
||||||
import { AdminConfig } from './admin.types';
|
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;
|
return adminConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
|
import { normalizeApiBaseUrl } from '@/lib/url';
|
||||||
|
|
||||||
interface EmbyConfig {
|
interface EmbyConfig {
|
||||||
ServerURL: string;
|
ServerURL: string;
|
||||||
ApiKey?: string;
|
ApiKey?: string;
|
||||||
@@ -96,7 +98,7 @@ export class EmbyClient {
|
|||||||
private embyAuthorizationHeader: string;
|
private embyAuthorizationHeader: string;
|
||||||
|
|
||||||
constructor(config: EmbyConfig) {
|
constructor(config: EmbyConfig) {
|
||||||
let serverUrl = config.ServerURL.replace(/\/$/, '');
|
let serverUrl = normalizeApiBaseUrl(config.ServerURL);
|
||||||
|
|
||||||
// 存储高级选项
|
// 存储高级选项
|
||||||
this.removeEmbyPrefix = config.removeEmbyPrefix || false;
|
this.removeEmbyPrefix = config.removeEmbyPrefix || false;
|
||||||
|
|||||||
+4
-1
@@ -1,4 +1,5 @@
|
|||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
|
import { normalizeApiBaseUrl } from '@/lib/url';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
@@ -209,7 +210,9 @@ export async function getMusicV2Config() {
|
|||||||
const musicConfig = config?.MusicConfig;
|
const musicConfig = config?.MusicConfig;
|
||||||
|
|
||||||
const enabled = musicConfig?.Enabled ?? false;
|
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 || '';
|
const token = musicConfig?.Token || process.env.MUSIC_V2_TOKEN || '';
|
||||||
|
|
||||||
return { enabled, baseUrl, token };
|
return { enabled, baseUrl, token };
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
|
import { normalizeApiBaseUrl } from '@/lib/url';
|
||||||
|
|
||||||
// Token 内存缓存
|
// Token 内存缓存
|
||||||
const tokenCache = new Map<string, { token: string; expiresAt: number }>();
|
const tokenCache = new Map<string, { token: string; expiresAt: number }>();
|
||||||
|
|
||||||
@@ -34,12 +36,15 @@ export interface OpenListGetResponse {
|
|||||||
|
|
||||||
export class OpenListClient {
|
export class OpenListClient {
|
||||||
private token = '';
|
private token = '';
|
||||||
|
private baseURL: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private baseURL: string,
|
baseURL: string,
|
||||||
private username: string,
|
private username: string,
|
||||||
private password: string
|
private password: string
|
||||||
) {}
|
) {
|
||||||
|
this.baseURL = normalizeApiBaseUrl(baseURL);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 使用账号密码登录获取Token
|
* 使用账号密码登录获取Token
|
||||||
@@ -49,7 +54,8 @@ export class OpenListClient {
|
|||||||
username: string,
|
username: string,
|
||||||
password: string
|
password: string
|
||||||
): Promise<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',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
* 文档: https://github.com/fish2018/pansou
|
* 文档: https://github.com/fish2018/pansou
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { normalizeApiBaseUrl } from '@/lib/url';
|
||||||
|
|
||||||
// Token 缓存
|
// Token 缓存
|
||||||
let cachedToken: string | null = null;
|
let cachedToken: string | null = null;
|
||||||
let tokenExpiry: number | null = null;
|
let tokenExpiry: number | null = null;
|
||||||
@@ -40,7 +42,8 @@ export async function loginPansou(
|
|||||||
password: string
|
password: string
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${apiUrl}/api/auth/login`, {
|
const baseUrl = normalizeApiBaseUrl(apiUrl);
|
||||||
|
const response = await fetch(`${baseUrl}/api/auth/login`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -111,9 +114,10 @@ export async function searchPansou(
|
|||||||
}
|
}
|
||||||
): Promise<PansouSearchResult> {
|
): Promise<PansouSearchResult> {
|
||||||
try {
|
try {
|
||||||
|
const baseUrl = normalizeApiBaseUrl(apiUrl);
|
||||||
// 获取 Token(如果需要认证)
|
// 获取 Token(如果需要认证)
|
||||||
const token = await getValidToken(
|
const token = await getValidToken(
|
||||||
apiUrl,
|
baseUrl,
|
||||||
options?.username,
|
options?.username,
|
||||||
options?.password
|
options?.password
|
||||||
);
|
);
|
||||||
@@ -141,7 +145,7 @@ export async function searchPansou(
|
|||||||
body.cloud_types = options.cloudTypes;
|
body.cloud_types = options.cloudTypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`${apiUrl}/api/search`, {
|
const response = await fetch(`${baseUrl}/api/search`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers,
|
headers,
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
@@ -201,7 +205,8 @@ export function clearPansouToken(): void {
|
|||||||
*/
|
*/
|
||||||
export async function checkPansouHealth(apiUrl: string): Promise<boolean> {
|
export async function checkPansouHealth(apiUrl: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${apiUrl}/api/health`, {
|
const baseUrl = normalizeApiBaseUrl(apiUrl);
|
||||||
|
const response = await fetch(`${baseUrl}/api/health`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
MangaSearchResult,
|
MangaSearchResult,
|
||||||
MangaSource,
|
MangaSource,
|
||||||
} from './manga.types';
|
} from './manga.types';
|
||||||
|
import { normalizeApiBaseUrl } from './url';
|
||||||
|
|
||||||
interface GraphQLResponse<T> {
|
interface GraphQLResponse<T> {
|
||||||
data?: T;
|
data?: T;
|
||||||
@@ -167,7 +168,7 @@ async function resolveSuwayomiConfig(options: SuwayomiClientOptions = {}): Promi
|
|||||||
throw new Error('Suwayomi 未配置,请先在管理面板或环境变量中设置服务地址');
|
throw new Error('Suwayomi 未配置,请先在管理面板或环境变量中设置服务地址');
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizedBaseUrl = serverUrl.replace(/\/$/, '');
|
const normalizedBaseUrl = normalizeApiBaseUrl(serverUrl);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
serverBaseUrl: normalizedBaseUrl,
|
serverBaseUrl: normalizedBaseUrl,
|
||||||
|
|||||||
+2
-1
@@ -13,6 +13,7 @@ import { getConfig } from './config';
|
|||||||
import { db, getStorage } from './db';
|
import { db, getStorage } from './db';
|
||||||
import { lockManager } from './lock';
|
import { lockManager } from './lock';
|
||||||
import type { IStorage, Notification } from './types';
|
import type { IStorage, Notification } from './types';
|
||||||
|
import { normalizeApiBaseUrl } from './url';
|
||||||
import { getNotificationClickUrl } from './web-push';
|
import { getNotificationClickUrl } from './web-push';
|
||||||
|
|
||||||
export interface TelegramConfig {
|
export interface TelegramConfig {
|
||||||
@@ -426,7 +427,7 @@ function isCloudflareEnvironment(): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeTelegramApiBaseUrl(input?: string | null): string {
|
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';
|
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 */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
|
import { normalizeApiBaseUrl } from '@/lib/url';
|
||||||
|
|
||||||
// Token 内存缓存
|
// Token 内存缓存
|
||||||
const tokenCache = new Map<string, { token: string; expiresAt: number }>();
|
const tokenCache = new Map<string, { token: string; expiresAt: number }>();
|
||||||
|
|
||||||
@@ -17,13 +19,16 @@ export interface XiaoyaListResponse {
|
|||||||
|
|
||||||
export class XiaoyaClient {
|
export class XiaoyaClient {
|
||||||
private token = '';
|
private token = '';
|
||||||
|
private baseURL: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private baseURL: string,
|
baseURL: string,
|
||||||
private username?: string,
|
private username?: string,
|
||||||
private password?: string,
|
private password?: string,
|
||||||
private configToken?: string
|
private configToken?: string
|
||||||
) {}
|
) {
|
||||||
|
this.baseURL = normalizeApiBaseUrl(baseURL);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 使用账号密码登录获取Token
|
* 使用账号密码登录获取Token
|
||||||
@@ -33,7 +38,8 @@ export class XiaoyaClient {
|
|||||||
username: string,
|
username: string,
|
||||||
password: string
|
password: string
|
||||||
): Promise<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',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|||||||
Reference in New Issue
Block a user