mirror of
https://github.com/Cola-Echo/memory-manager-concurrent.git
synced 2026-06-06 03:05:51 +00:00
feat: add unified reverse proxy URL support for all API endpoints
Extract URL construction into src/utils/url-builder.js, replacing 8 scattered inline implementations. Now properly handles reverse proxy paths like /Gemini/v1 or /antigravity/v1 by detecting version segments at any path depth. Fixes inconsistent Anthropic URL in multi-ai-generator. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2
dist/index.js
vendored
2
dist/index.js
vendored
File diff suppressed because one or more lines are too long
@@ -7,6 +7,7 @@ import Logger from '@core/logger';
|
|||||||
import { StreamingHandler } from './streaming-handler';
|
import { StreamingHandler } from './streaming-handler';
|
||||||
import { getEnabledProviders } from '@config/config-manager';
|
import { getEnabledProviders } from '@config/config-manager';
|
||||||
import { buildMessagesFromPreset, getPromptPresetById } from '@ui/modals/prompt-preset';
|
import { buildMessagesFromPreset, getPromptPresetById } from '@ui/modals/prompt-preset';
|
||||||
|
import { buildOpenAIChatUrl, buildAnthropicUrl } from '@utils/url-builder';
|
||||||
|
|
||||||
const log = Logger.createModuleLogger('多AI生成');
|
const log = Logger.createModuleLogger('多AI生成');
|
||||||
|
|
||||||
@@ -225,18 +226,12 @@ export class MultiAIGenerator {
|
|||||||
async callProvider(provider, messages, signal, onChunk) {
|
async callProvider(provider, messages, signal, onChunk) {
|
||||||
const { apiFormat, apiUrl, apiKey, model, maxTokens, temperature, streaming } = provider;
|
const { apiFormat, apiUrl, apiKey, model, maxTokens, temperature, streaming } = provider;
|
||||||
|
|
||||||
// 构建请求URL
|
// 构建请求URL(统一的反代兼容 URL 构造)
|
||||||
let requestUrl = apiUrl;
|
let requestUrl = apiUrl;
|
||||||
if (apiFormat === 'openai') {
|
if (apiFormat === 'openai') {
|
||||||
if (apiUrl.endsWith('/v1') || apiUrl.endsWith('/v1/')) {
|
requestUrl = buildOpenAIChatUrl(apiUrl);
|
||||||
requestUrl = apiUrl.replace(/\/v1\/?$/, '/v1/chat/completions');
|
|
||||||
} else if (!apiUrl.includes('/chat/completions') && !apiUrl.includes('/completions')) {
|
|
||||||
requestUrl = apiUrl.replace(/\/?$/, '/chat/completions');
|
|
||||||
}
|
|
||||||
} else if (apiFormat === 'anthropic') {
|
} else if (apiFormat === 'anthropic') {
|
||||||
if (!apiUrl.includes('/messages')) {
|
requestUrl = buildAnthropicUrl(apiUrl);
|
||||||
requestUrl = apiUrl.replace(/\/?$/, '/messages');
|
|
||||||
}
|
|
||||||
} else if (apiFormat === 'google') {
|
} else if (apiFormat === 'google') {
|
||||||
// Google Gemini API
|
// Google Gemini API
|
||||||
if (!apiUrl.includes(':generateContent')) {
|
if (!apiUrl.includes(':generateContent')) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Logger from '@core/logger';
|
import Logger from '@core/logger';
|
||||||
|
import { buildAnthropicUrl } from '@utils/url-builder';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 模拟流式进度管理器
|
* 模拟流式进度管理器
|
||||||
@@ -90,12 +91,8 @@ export async function callAnthropic(config, systemPrompt, userMessage, signal =
|
|||||||
const { apiKey, model, maxTokens, temperature } = config;
|
const { apiKey, model, maxTokens, temperature } = config;
|
||||||
let { apiUrl } = config;
|
let { apiUrl } = config;
|
||||||
|
|
||||||
// 自动补全 /v1/messages
|
// 统一的反代兼容 URL 构造
|
||||||
if (apiUrl.endsWith("/v1") || apiUrl.endsWith("/v1/")) {
|
apiUrl = buildAnthropicUrl(apiUrl);
|
||||||
apiUrl = apiUrl.replace(/\/v1\/?$/, "/v1/messages");
|
|
||||||
} else if (!apiUrl.includes("/messages")) {
|
|
||||||
apiUrl = apiUrl.replace(/\/?$/, "/v1/messages");
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(apiUrl, {
|
const response = await fetch(apiUrl, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Logger from '@core/logger';
|
import Logger from '@core/logger';
|
||||||
|
import { buildGoogleUrl } from '@utils/url-builder';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 模拟流式进度管理器
|
* 模拟流式进度管理器
|
||||||
@@ -82,11 +83,8 @@ export async function callGoogle(config, systemPrompt, userMessage, signal = nul
|
|||||||
const { apiKey, model, maxTokens, temperature } = config;
|
const { apiKey, model, maxTokens, temperature } = config;
|
||||||
let { apiUrl } = config;
|
let { apiUrl } = config;
|
||||||
|
|
||||||
// Google API URL 格式: https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent
|
// 统一的反代兼容 URL 构造
|
||||||
if (!apiUrl.includes("/models")) {
|
const url = buildGoogleUrl(apiUrl, model, apiKey);
|
||||||
apiUrl = apiUrl.replace(/\/?$/, "/models");
|
|
||||||
}
|
|
||||||
const url = `${apiUrl}/${model}:generateContent?key=${apiKey}`;
|
|
||||||
|
|
||||||
// Google API 不支持流式,使用模拟进度
|
// Google API 不支持流式,使用模拟进度
|
||||||
let progressManager = null;
|
let progressManager = null;
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
* @module api/providers/openai
|
* @module api/providers/openai
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { buildOpenAIChatUrl } from '@utils/url-builder';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 模拟流式进度管理器
|
* 模拟流式进度管理器
|
||||||
* 使用检查点驱动的进度增长,模拟真实的流式传输体验
|
* 使用检查点驱动的进度增长,模拟真实的流式传输体验
|
||||||
@@ -237,15 +239,8 @@ export async function callOpenAI(
|
|||||||
const { apiKey, model, maxTokens, temperature } = config;
|
const { apiKey, model, maxTokens, temperature } = config;
|
||||||
let { apiUrl } = config;
|
let { apiUrl } = config;
|
||||||
|
|
||||||
// 自动补全 /chat/completions
|
// 统一的反代兼容 URL 构造
|
||||||
if (apiUrl.endsWith("/v1") || apiUrl.endsWith("/v1/")) {
|
apiUrl = buildOpenAIChatUrl(apiUrl);
|
||||||
apiUrl = apiUrl.replace(/\/v1\/?$/, "/v1/chat/completions");
|
|
||||||
} else if (
|
|
||||||
!apiUrl.includes("/chat/completions") &&
|
|
||||||
!apiUrl.includes("/completions")
|
|
||||||
) {
|
|
||||||
apiUrl = apiUrl.replace(/\/?$/, "/chat/completions");
|
|
||||||
}
|
|
||||||
|
|
||||||
const headers = { "Content-Type": "application/json" };
|
const headers = { "Content-Type": "application/json" };
|
||||||
if (apiKey) {
|
if (apiKey) {
|
||||||
@@ -357,15 +352,8 @@ export async function callOpenAIWithMessages(
|
|||||||
const { apiKey, model, maxTokens, temperature } = config;
|
const { apiKey, model, maxTokens, temperature } = config;
|
||||||
let { apiUrl } = config;
|
let { apiUrl } = config;
|
||||||
|
|
||||||
// 自动补全 /chat/completions
|
// 统一的反代兼容 URL 构造
|
||||||
if (apiUrl.endsWith("/v1") || apiUrl.endsWith("/v1/")) {
|
apiUrl = buildOpenAIChatUrl(apiUrl);
|
||||||
apiUrl = apiUrl.replace(/\/v1\/?$/, "/v1/chat/completions");
|
|
||||||
} else if (
|
|
||||||
!apiUrl.includes("/chat/completions") &&
|
|
||||||
!apiUrl.includes("/completions")
|
|
||||||
) {
|
|
||||||
apiUrl = apiUrl.replace(/\/?$/, "/chat/completions");
|
|
||||||
}
|
|
||||||
|
|
||||||
const headers = { "Content-Type": "application/json" };
|
const headers = { "Content-Type": "application/json" };
|
||||||
if (apiKey) {
|
if (apiKey) {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
import { disableTableFiller, reinitTableFiller } from "@table-filler/index";
|
import { disableTableFiller, reinitTableFiller } from "@table-filler/index";
|
||||||
import { APIAdapter } from "@api/adapter";
|
import { APIAdapter } from "@api/adapter";
|
||||||
import { bindIndependentTemplateEvents } from "@ui/modals/independent-template-modal";
|
import { bindIndependentTemplateEvents } from "@ui/modals/independent-template-modal";
|
||||||
|
import { buildOpenAIModelsUrl } from "@utils/url-builder";
|
||||||
|
|
||||||
const log = Logger.createModuleLogger('Amily表格并发');
|
const log = Logger.createModuleLogger('Amily表格并发');
|
||||||
|
|
||||||
@@ -783,12 +784,9 @@ async function handleFetchModels() {
|
|||||||
async function fetchModelsFromApi(apiUrl, apiKey, format) {
|
async function fetchModelsFromApi(apiUrl, apiKey, format) {
|
||||||
let modelsUrl = apiUrl;
|
let modelsUrl = apiUrl;
|
||||||
|
|
||||||
|
// 统一的反代兼容模型列表 URL 构造
|
||||||
if (format === "openai") {
|
if (format === "openai") {
|
||||||
if (apiUrl.endsWith("/v1") || apiUrl.endsWith("/v1/")) {
|
modelsUrl = buildOpenAIModelsUrl(apiUrl);
|
||||||
modelsUrl = apiUrl.replace(/\/v1\/?$/, "/v1/models");
|
|
||||||
} else if (!apiUrl.includes("/models")) {
|
|
||||||
modelsUrl = apiUrl.replace(/\/?$/, "/models");
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error("此API格式不支持获取模型列表");
|
throw new Error("此API格式不支持获取模型列表");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
import { defaultMultiAIProvider } from '@config/default-config';
|
import { defaultMultiAIProvider } from '@config/default-config';
|
||||||
import { APIAdapter } from '@api/adapter';
|
import { APIAdapter } from '@api/adapter';
|
||||||
import { getPromptPresets, showPromptPresetModal, getPromptPresetById } from './prompt-preset';
|
import { getPromptPresets, showPromptPresetModal, getPromptPresetById } from './prompt-preset';
|
||||||
|
import { buildOpenAIModelsUrl } from '@utils/url-builder';
|
||||||
|
|
||||||
const log = Logger.createModuleLogger('多AI配置');
|
const log = Logger.createModuleLogger('多AI配置');
|
||||||
|
|
||||||
@@ -457,13 +458,9 @@ export function showMultiAIConfigModal(providerId = null) {
|
|||||||
async function fetchModels(apiUrl, apiKey, format) {
|
async function fetchModels(apiUrl, apiKey, format) {
|
||||||
let modelsUrl = apiUrl;
|
let modelsUrl = apiUrl;
|
||||||
|
|
||||||
// 构建模型列表URL
|
// 统一的反代兼容模型列表 URL 构造
|
||||||
if (format === 'openai') {
|
if (format === 'openai') {
|
||||||
if (apiUrl.endsWith('/v1') || apiUrl.endsWith('/v1/')) {
|
modelsUrl = buildOpenAIModelsUrl(apiUrl);
|
||||||
modelsUrl = apiUrl.replace(/\/v1\/?$/, '/v1/models');
|
|
||||||
} else if (!apiUrl.includes('/models')) {
|
|
||||||
modelsUrl = apiUrl.replace(/\/?$/, '/models');
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// 其他格式暂不支持获取模型列表
|
// 其他格式暂不支持获取模型列表
|
||||||
throw new Error('此API格式不支持获取模型列表,请手动输入模型名称');
|
throw new Error('此API格式不支持获取模型列表,请手动输入模型名称');
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import { refreshWorldBookList, getSummaryParts } from "@worldbook/refresh";
|
import { refreshWorldBookList, getSummaryParts } from "@worldbook/refresh";
|
||||||
import { formatCharCount } from "@worldbook/summary-splitter";
|
import { formatCharCount } from "@worldbook/summary-splitter";
|
||||||
import APIAdapter from "@api/adapter";
|
import APIAdapter from "@api/adapter";
|
||||||
|
import { buildOpenAIModelsUrl } from "@utils/url-builder";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从API获取模型列表
|
* 从API获取模型列表
|
||||||
@@ -23,17 +24,9 @@ import APIAdapter from "@api/adapter";
|
|||||||
async function fetchModelsFromApi(apiUrl, apiKey, format) {
|
async function fetchModelsFromApi(apiUrl, apiKey, format) {
|
||||||
let modelsUrl = apiUrl;
|
let modelsUrl = apiUrl;
|
||||||
|
|
||||||
// 构建模型列表URL
|
// 统一的反代兼容模型列表 URL 构造
|
||||||
if (format === 'openai') {
|
if (format === 'openai') {
|
||||||
if (apiUrl.endsWith('/v1') || apiUrl.endsWith('/v1/')) {
|
modelsUrl = buildOpenAIModelsUrl(apiUrl);
|
||||||
modelsUrl = apiUrl.replace(/\/v1\/?$/, '/v1/models');
|
|
||||||
} else if (apiUrl.includes('/v1/chat/completions')) {
|
|
||||||
modelsUrl = apiUrl.replace('/v1/chat/completions', '/v1/models');
|
|
||||||
} else if (apiUrl.includes('/chat/completions')) {
|
|
||||||
modelsUrl = apiUrl.replace('/chat/completions', '/models');
|
|
||||||
} else if (!apiUrl.includes('/models')) {
|
|
||||||
modelsUrl = apiUrl.replace(/\/?$/, '/models');
|
|
||||||
}
|
|
||||||
} else if (format === 'anthropic') {
|
} else if (format === 'anthropic') {
|
||||||
// Anthropic 不支持获取模型列表,返回常用模型
|
// Anthropic 不支持获取模型列表,返回常用模型
|
||||||
return [
|
return [
|
||||||
@@ -190,7 +183,7 @@ export function showSummaryPartConfigModal(bookName, partId) {
|
|||||||
<div class="mm-form-group">
|
<div class="mm-form-group">
|
||||||
<label>API URL <span class="mm-required">*</span></label>
|
<label>API URL <span class="mm-required">*</span></label>
|
||||||
<input type="text" id="mm-part-api-url" placeholder="https://api.deepseek.com/v1" value="${escapeHtml(savedConfig.apiUrl || '')}">
|
<input type="text" id="mm-part-api-url" placeholder="https://api.deepseek.com/v1" value="${escapeHtml(savedConfig.apiUrl || '')}">
|
||||||
<small class="mm-hint">填写到 /v1 即可,会自动补全完整路径</small>
|
<small class="mm-hint">填写到 /v1 即可,支持反代路径如 /Gemini/v1</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- API Key -->
|
<!-- API Key -->
|
||||||
|
|||||||
@@ -27,3 +27,10 @@ export {
|
|||||||
reloadKeywordsPromptTemplate,
|
reloadKeywordsPromptTemplate,
|
||||||
reloadHistoricalPromptTemplate,
|
reloadHistoricalPromptTemplate,
|
||||||
} from './prompt-template';
|
} from './prompt-template';
|
||||||
|
|
||||||
|
export {
|
||||||
|
buildOpenAIChatUrl,
|
||||||
|
buildOpenAIModelsUrl,
|
||||||
|
buildAnthropicUrl,
|
||||||
|
buildGoogleUrl,
|
||||||
|
} from './url-builder';
|
||||||
|
|||||||
99
src/utils/url-builder.js
Normal file
99
src/utils/url-builder.js
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
/**
|
||||||
|
* 统一的 API URL 构造工具
|
||||||
|
* 支持标准接口和反代接口(如 域名/Gemini/v1、域名/antigravity/v1)
|
||||||
|
* @module utils/url-builder
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从用户输入的 apiUrl 中提取 base(到最后一个 /v... 路径段为止)
|
||||||
|
* 例:
|
||||||
|
* https://api.example.com/v1 → https://api.example.com/v1
|
||||||
|
* https://proxy.com/Gemini/v1 → https://proxy.com/Gemini/v1
|
||||||
|
* https://proxy.com/antigravity/v1beta → https://proxy.com/antigravity/v1beta
|
||||||
|
* https://proxy.com/v1/chat/completions → https://proxy.com/v1
|
||||||
|
* https://proxy.com → https://proxy.com (无 /v 段)
|
||||||
|
*
|
||||||
|
* @param {string} url
|
||||||
|
* @returns {{ base: string, hasVersionSegment: boolean }}
|
||||||
|
*/
|
||||||
|
function parseApiBase(url) {
|
||||||
|
// 去尾斜杠
|
||||||
|
url = url.replace(/\/+$/, '');
|
||||||
|
|
||||||
|
// 已包含完整终端路径,先剥离
|
||||||
|
const endpointPatterns = [
|
||||||
|
/\/chat\/completions$/,
|
||||||
|
/\/completions$/,
|
||||||
|
/\/messages$/,
|
||||||
|
/\/models(\/.*)?$/,
|
||||||
|
/:generateContent(\?.*)?$/,
|
||||||
|
];
|
||||||
|
for (const pattern of endpointPatterns) {
|
||||||
|
if (pattern.test(url)) {
|
||||||
|
url = url.replace(pattern, '');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 再次去尾斜杠
|
||||||
|
url = url.replace(/\/+$/, '');
|
||||||
|
|
||||||
|
// 检测是否包含版本号路径段 /v1、/v1beta、/v2 等
|
||||||
|
const hasVersionSegment = /\/v\d+[a-z]*$/i.test(url);
|
||||||
|
|
||||||
|
return { base: url, hasVersionSegment };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建 OpenAI 兼容的 chat/completions URL
|
||||||
|
* @param {string} apiUrl 用户输入的 API URL
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function buildOpenAIChatUrl(apiUrl) {
|
||||||
|
const { base, hasVersionSegment } = parseApiBase(apiUrl);
|
||||||
|
if (hasVersionSegment) {
|
||||||
|
return `${base}/chat/completions`;
|
||||||
|
}
|
||||||
|
// 无版本段,默认插入 /v1
|
||||||
|
return `${base}/v1/chat/completions`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建 OpenAI 兼容的 models 列表 URL
|
||||||
|
* @param {string} apiUrl 用户输入的 API URL
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function buildOpenAIModelsUrl(apiUrl) {
|
||||||
|
const { base, hasVersionSegment } = parseApiBase(apiUrl);
|
||||||
|
if (hasVersionSegment) {
|
||||||
|
return `${base}/models`;
|
||||||
|
}
|
||||||
|
return `${base}/v1/models`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建 Anthropic messages URL
|
||||||
|
* @param {string} apiUrl 用户输入的 API URL
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function buildAnthropicUrl(apiUrl) {
|
||||||
|
const { base, hasVersionSegment } = parseApiBase(apiUrl);
|
||||||
|
if (hasVersionSegment) {
|
||||||
|
return `${base}/messages`;
|
||||||
|
}
|
||||||
|
return `${base}/v1/messages`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建 Google Gemini generateContent URL
|
||||||
|
* @param {string} apiUrl 用户输入的 API URL
|
||||||
|
* @param {string} model 模型名称
|
||||||
|
* @param {string} apiKey API Key
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function buildGoogleUrl(apiUrl, model, apiKey) {
|
||||||
|
const { base } = parseApiBase(apiUrl);
|
||||||
|
// Google 格式:base/models/{model}:generateContent?key=xxx
|
||||||
|
const modelsBase = base.includes('/models') ? base : `${base}/models`;
|
||||||
|
return `${modelsBase}/${model}:generateContent?key=${apiKey}`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user