feat: enhance provider functionality with base URL normalization and API protocol support; add tests for provider creation

This commit is contained in:
duanshuwen
2026-04-20 23:51:01 +08:00
parent 301f7d33ed
commit 6ac4bf1dd9
8 changed files with 126 additions and 150 deletions

View File

@@ -1,7 +1,2 @@
"use strict";
require("electron");
require("./main-CRe21cho.js");
require("electron-squirrel-startup");
require("electron-log");
require("bytenode");
require("axios");
require('bytenode')
require('./main.jsc')

View File

@@ -1,137 +1 @@
"use strict";
const electron = require("electron");
var IPC_EVENTS = /* @__PURE__ */ ((IPC_EVENTS2) => {
IPC_EVENTS2["EXTERNAL_OPEN"] = "external-open";
IPC_EVENTS2["APP_SET_FRAMELESS"] = "app:set-frameless";
IPC_EVENTS2["APP_LOAD_PAGE"] = "app:load-page";
IPC_EVENTS2["TAB_CREATE"] = "tab:create";
IPC_EVENTS2["TAB_LIST"] = "tab:list";
IPC_EVENTS2["TAB_NAVIGATE"] = "tab:navigate";
IPC_EVENTS2["TAB_RELOAD"] = "tab:reload";
IPC_EVENTS2["TAB_BACK"] = "tab:back";
IPC_EVENTS2["TAB_FORWARD"] = "tab:forward";
IPC_EVENTS2["TAB_SWITCH"] = "tab:switch";
IPC_EVENTS2["TAB_CLOSE"] = "tab:close";
IPC_EVENTS2["LOG_TO_MAIN"] = "log-to-main";
IPC_EVENTS2["READ_FILE"] = "read-file";
IPC_EVENTS2["INVOKE"] = "ipc:invoke";
IPC_EVENTS2["INVOKE_ASYNC"] = "ipc:invokeAsync";
IPC_EVENTS2["APP_MINIMIZE"] = "app:minimize";
IPC_EVENTS2["APP_MAXIMIZE"] = "app:maximize";
IPC_EVENTS2["APP_QUIT"] = "app:quit";
IPC_EVENTS2["FILE_READ"] = "file:read";
IPC_EVENTS2["FILE_WRITE"] = "file:write";
IPC_EVENTS2["GET_WINDOW_ID"] = "get-window-id";
IPC_EVENTS2["CUSTOM_EVENT"] = "custom:event";
IPC_EVENTS2["TIME_UPDATE"] = "time:update";
IPC_EVENTS2["RENDERER_IS_READY"] = "renderer-ready";
IPC_EVENTS2["SHOW_CONTEXT_MENU"] = "show-context-menu";
IPC_EVENTS2["START_A_DIALOGUE"] = "start-a-dialogue";
IPC_EVENTS2["OPEN_WINDOW"] = "open-window";
IPC_EVENTS2["LOG_DEBUG"] = "log-debug";
IPC_EVENTS2["LOG_INFO"] = "log-info";
IPC_EVENTS2["LOG_WARN"] = "log-warn";
IPC_EVENTS2["LOG_ERROR"] = "log-error";
IPC_EVENTS2["CONFIG_UPDATED"] = "config-updated";
IPC_EVENTS2["SET_CONFIG"] = "set-config";
IPC_EVENTS2["GET_CONFIG"] = "get-config";
IPC_EVENTS2["UPDATE_CONFIG"] = "update-config";
IPC_EVENTS2["SET_THEME_MODE"] = "set-theme-mode";
IPC_EVENTS2["GET_THEME_MODE"] = "get-theme-mode";
IPC_EVENTS2["IS_DARK_THEME"] = "is-dark-theme";
IPC_EVENTS2["THEME_MODE_UPDATED"] = "theme-mode-updated";
IPC_EVENTS2["EXECUTE_SCRIPT"] = "execute-script";
IPC_EVENTS2["TASK_PROGRESS"] = "task:progress";
IPC_EVENTS2["TASK_STARTED"] = "task:started";
IPC_EVENTS2["TASK_COMPLETED"] = "task:completed";
IPC_EVENTS2["OPEN_CHANNEL"] = "open-channel";
IPC_EVENTS2["SCRIPT_LIST"] = "script:list";
IPC_EVENTS2["SCRIPT_GET"] = "script:get";
IPC_EVENTS2["SCRIPT_SAVE"] = "script:save";
IPC_EVENTS2["SCRIPT_DELETE"] = "script:delete";
IPC_EVENTS2["SCRIPT_TOGGLE"] = "script:toggle";
IPC_EVENTS2["SCRIPT_RUN"] = "script:run";
IPC_EVENTS2["SCRIPT_RECORD_START"] = "script:record-start";
IPC_EVENTS2["SCRIPT_RECORD_STOP"] = "script:record-stop";
IPC_EVENTS2["SCRIPT_CODEGEN"] = "script:codegen";
IPC_EVENTS2["GATEWAY_RPC"] = "gateway:rpc";
IPC_EVENTS2["GATEWAY_EVENT"] = "gateway:event";
IPC_EVENTS2["UPDATE_CHECK"] = "update:check";
IPC_EVENTS2["UPDATE_DOWNLOAD"] = "update:download";
IPC_EVENTS2["UPDATE_INSTALL"] = "update:install";
IPC_EVENTS2["UPDATE_VERSION"] = "update:version";
IPC_EVENTS2["UPDATE_STATUS_CHANGED"] = "update:status-changed";
return IPC_EVENTS2;
})(IPC_EVENTS || {});
const api = {
versions: process.versions,
external: {
open: (url) => electron.ipcRenderer.invoke("external-open", url)
},
platform: process.platform,
windowMinimize: () => electron.ipcRenderer.invoke("window:minimize"),
windowMaximize: () => electron.ipcRenderer.invoke("window:maximize"),
windowClose: () => electron.ipcRenderer.invoke("window:close"),
windowIsMaximized: () => electron.ipcRenderer.invoke("window:isMaximized"),
viewIsReady: () => electron.ipcRenderer.send(IPC_EVENTS.RENDERER_IS_READY),
app: {
setFrameless: (route) => electron.ipcRenderer.invoke(IPC_EVENTS.APP_SET_FRAMELESS, route),
loadPage: (page) => electron.ipcRenderer.invoke(IPC_EVENTS.APP_LOAD_PAGE, page)
},
// 通过 IPC 调用主进程
readFile: (filePath) => electron.ipcRenderer.invoke(IPC_EVENTS.READ_FILE, filePath),
// 异步调用(映射为 electron 的 invoke)
invoke: (channel, ...args) => electron.ipcRenderer.invoke(channel, ...args),
// 异步调用(为了兼容老代码)
invokeAsync: (channel, ...args) => electron.ipcRenderer.invoke(channel, ...args),
// 监听主进程消息
on: (event, callback) => {
const subscription = (_event, ...args) => callback(...args);
electron.ipcRenderer.on(event, subscription);
return () => electron.ipcRenderer.removeListener(event, subscription);
},
// 发送消息到主进程
send: (channel, ...args) => electron.ipcRenderer.send(channel, ...args),
// 获取窗口ID
getCurrentWindowId: () => electron.ipcRenderer.sendSync(IPC_EVENTS.GET_WINDOW_ID),
// 发送日志
logger: {
debug: (message, ...meta) => electron.ipcRenderer.send(IPC_EVENTS.LOG_DEBUG, message, ...meta),
info: (message, ...meta) => electron.ipcRenderer.send(IPC_EVENTS.LOG_INFO, message, ...meta),
warn: (message, ...meta) => electron.ipcRenderer.send(IPC_EVENTS.LOG_WARN, message, ...meta),
error: (message, ...meta) => electron.ipcRenderer.send(IPC_EVENTS.LOG_ERROR, message, ...meta)
},
// 执行脚本
executeScript: (params) => electron.ipcRenderer.invoke(IPC_EVENTS.EXECUTE_SCRIPT, params),
// 任务事件
onTaskProgress: (cb) => {
const subscription = (_event, payload) => cb(payload);
electron.ipcRenderer.on(IPC_EVENTS.TASK_PROGRESS, subscription);
return () => electron.ipcRenderer.removeListener(IPC_EVENTS.TASK_PROGRESS, subscription);
},
onTaskStarted: (cb) => {
const subscription = (_event, payload) => cb(payload);
electron.ipcRenderer.on(IPC_EVENTS.TASK_STARTED, subscription);
return () => electron.ipcRenderer.removeListener(IPC_EVENTS.TASK_STARTED, subscription);
},
onTaskCompleted: (cb) => {
const subscription = (_event, payload) => cb(payload);
electron.ipcRenderer.on(IPC_EVENTS.TASK_COMPLETED, subscription);
return () => electron.ipcRenderer.removeListener(IPC_EVENTS.TASK_COMPLETED, subscription);
},
// 打开渠道
openChannel: (channels) => electron.ipcRenderer.invoke(IPC_EVENTS.OPEN_CHANNEL, channels),
// 脚本管理
scriptApi: {
list: () => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_LIST),
get: (id) => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_GET, id),
save: (input) => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_SAVE, input),
delete: (id) => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_DELETE, id),
toggle: (id, enabled) => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_TOGGLE, id, enabled),
run: (id) => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_RUN, id),
startRecording: (url) => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_RECORD_START, url),
stopRecording: () => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_RECORD_STOP),
codegen: (id, url) => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_CODEGEN, id, url)
}
};
electron.contextBridge.exposeInMainWorld("api", api);
"use strict";const r=require("electron");var i=(e=>(e.EXTERNAL_OPEN="external-open",e.APP_SET_FRAMELESS="app:set-frameless",e.APP_LOAD_PAGE="app:load-page",e.TAB_CREATE="tab:create",e.TAB_LIST="tab:list",e.TAB_NAVIGATE="tab:navigate",e.TAB_RELOAD="tab:reload",e.TAB_BACK="tab:back",e.TAB_FORWARD="tab:forward",e.TAB_SWITCH="tab:switch",e.TAB_CLOSE="tab:close",e.LOG_TO_MAIN="log-to-main",e.READ_FILE="read-file",e.INVOKE="ipc:invoke",e.INVOKE_ASYNC="ipc:invokeAsync",e.APP_MINIMIZE="app:minimize",e.APP_MAXIMIZE="app:maximize",e.APP_QUIT="app:quit",e.FILE_READ="file:read",e.FILE_WRITE="file:write",e.GET_WINDOW_ID="get-window-id",e.CUSTOM_EVENT="custom:event",e.TIME_UPDATE="time:update",e.RENDERER_IS_READY="renderer-ready",e.SHOW_CONTEXT_MENU="show-context-menu",e.START_A_DIALOGUE="start-a-dialogue",e.OPEN_WINDOW="open-window",e.LOG_DEBUG="log-debug",e.LOG_INFO="log-info",e.LOG_WARN="log-warn",e.LOG_ERROR="log-error",e.CONFIG_UPDATED="config-updated",e.SET_CONFIG="set-config",e.GET_CONFIG="get-config",e.UPDATE_CONFIG="update-config",e.SET_THEME_MODE="set-theme-mode",e.GET_THEME_MODE="get-theme-mode",e.IS_DARK_THEME="is-dark-theme",e.THEME_MODE_UPDATED="theme-mode-updated",e.EXECUTE_SCRIPT="execute-script",e.TASK_PROGRESS="task:progress",e.TASK_STARTED="task:started",e.TASK_COMPLETED="task:completed",e.OPEN_CHANNEL="open-channel",e.SCRIPT_LIST="script:list",e.SCRIPT_GET="script:get",e.SCRIPT_SAVE="script:save",e.SCRIPT_DELETE="script:delete",e.SCRIPT_TOGGLE="script:toggle",e.SCRIPT_RUN="script:run",e.SCRIPT_RECORD_START="script:record-start",e.SCRIPT_RECORD_STOP="script:record-stop",e.SCRIPT_CODEGEN="script:codegen",e.GATEWAY_RPC="gateway:rpc",e.GATEWAY_EVENT="gateway:event",e.UPDATE_CHECK="update:check",e.UPDATE_DOWNLOAD="update:download",e.UPDATE_INSTALL="update:install",e.UPDATE_VERSION="update:version",e.UPDATE_STATUS_CHANGED="update:status-changed",e))(i||{});const d={versions:process.versions,external:{open:e=>r.ipcRenderer.invoke("external-open",e)},platform:process.platform,windowMinimize:()=>r.ipcRenderer.invoke("window:minimize"),windowMaximize:()=>r.ipcRenderer.invoke("window:maximize"),windowClose:()=>r.ipcRenderer.invoke("window:close"),windowIsMaximized:()=>r.ipcRenderer.invoke("window:isMaximized"),viewIsReady:()=>r.ipcRenderer.send(i.RENDERER_IS_READY),app:{setFrameless:e=>r.ipcRenderer.invoke(i.APP_SET_FRAMELESS,e),loadPage:e=>r.ipcRenderer.invoke(i.APP_LOAD_PAGE,e)},readFile:e=>r.ipcRenderer.invoke(i.READ_FILE,e),invoke:(e,...n)=>r.ipcRenderer.invoke(e,...n),invokeAsync:(e,...n)=>r.ipcRenderer.invoke(e,...n),on:(e,n)=>{const t=(o,...R)=>n(...R);return r.ipcRenderer.on(e,t),()=>r.ipcRenderer.removeListener(e,t)},send:(e,...n)=>r.ipcRenderer.send(e,...n),getCurrentWindowId:()=>r.ipcRenderer.sendSync(i.GET_WINDOW_ID),logger:{debug:(e,...n)=>r.ipcRenderer.send(i.LOG_DEBUG,e,...n),info:(e,...n)=>r.ipcRenderer.send(i.LOG_INFO,e,...n),warn:(e,...n)=>r.ipcRenderer.send(i.LOG_WARN,e,...n),error:(e,...n)=>r.ipcRenderer.send(i.LOG_ERROR,e,...n)},executeScript:e=>r.ipcRenderer.invoke(i.EXECUTE_SCRIPT,e),onTaskProgress:e=>{const n=(t,o)=>e(o);return r.ipcRenderer.on(i.TASK_PROGRESS,n),()=>r.ipcRenderer.removeListener(i.TASK_PROGRESS,n)},onTaskStarted:e=>{const n=(t,o)=>e(o);return r.ipcRenderer.on(i.TASK_STARTED,n),()=>r.ipcRenderer.removeListener(i.TASK_STARTED,n)},onTaskCompleted:e=>{const n=(t,o)=>e(o);return r.ipcRenderer.on(i.TASK_COMPLETED,n),()=>r.ipcRenderer.removeListener(i.TASK_COMPLETED,n)},openChannel:e=>r.ipcRenderer.invoke(i.OPEN_CHANNEL,e),scriptApi:{list:()=>r.ipcRenderer.invoke(i.SCRIPT_LIST),get:e=>r.ipcRenderer.invoke(i.SCRIPT_GET,e),save:e=>r.ipcRenderer.invoke(i.SCRIPT_SAVE,e),delete:e=>r.ipcRenderer.invoke(i.SCRIPT_DELETE,e),toggle:(e,n)=>r.ipcRenderer.invoke(i.SCRIPT_TOGGLE,e,n),run:e=>r.ipcRenderer.invoke(i.SCRIPT_RUN,e),startRecording:e=>r.ipcRenderer.invoke(i.SCRIPT_RECORD_START,e),stopRecording:()=>r.ipcRenderer.invoke(i.SCRIPT_RECORD_STOP),codegen:(e,n)=>r.ipcRenderer.invoke(i.SCRIPT_CODEGEN,e,n)}};r.contextBridge.exposeInMainWorld("api",d);

4
dist/index.html vendored
View File

@@ -8,8 +8,8 @@
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: http://8.138.234.141 https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com; connect-src 'self' http://8.138.234.141 https://api.iconify.design wss://onefeel.brother7.cn"
/>
<script type="module" crossorigin src="./assets/index-9QEXaMkT.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-BkNDj7QF.css">
<script type="module" crossorigin src="./assets/index-BiKJhvBJ.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-BpT6fRea.css">
</head>
<body>
<div id="app"></div>

View File

@@ -1,8 +1,30 @@
import { BaseProvider } from "./BaseProvider";
import { OpenAIProvider } from "./OpenAIProvider";
import { providerApiService } from '@electron/service/provider-api-service';
import type { ProviderAccount } from '@runtime/lib/providers';
import { getProviderTypeInfo } from '@runtime/lib/providers';
function normalizeBaseUrl(baseUrl?: string): string | undefined {
const trimmed = baseUrl?.trim();
if (!trimmed) {
return undefined;
}
if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
return trimmed;
}
return `https://${trimmed}`;
}
function resolveProviderBaseUrl(account: ProviderAccount): string | undefined {
return normalizeBaseUrl(account.baseUrl)
|| normalizeBaseUrl(account.metadata?.resourceUrl)
|| getProviderTypeInfo(account.vendorId)?.defaultBaseUrl;
}
function resolveProviderApiProtocol(account: ProviderAccount): ProviderAccount['apiProtocol'] | undefined {
return account.apiProtocol || getProviderTypeInfo(account.vendorId)?.apiProtocol;
}
export function createProvider(accountId: string): BaseProvider {
const account = providerApiService.getAccounts().find((a) => a.id === accountId);
if (!account) {
@@ -15,12 +37,12 @@ export function createProvider(accountId: string): BaseProvider {
throw new Error(`API key for account ${accountId} not found`);
}
const baseURL = account.baseUrl || getProviderTypeInfo(account.vendorId)?.defaultBaseUrl;
const baseURL = resolveProviderBaseUrl(account);
if (!baseURL) {
throw new Error(`Base URL for account ${accountId} not found`);
}
switch (account.apiProtocol) {
switch (resolveProviderApiProtocol(account)) {
case 'anthropic-messages':
throw new Error('Anthropic provider not yet implemented');
case 'openai-completions':

View File

@@ -181,6 +181,8 @@ export const PROVIDER_TYPE_INFO: ProviderTypeInfo[] = [
requiresApiKey: false,
isOAuth: true,
supportsApiKey: true,
defaultBaseUrl: 'https://api.minimaxi.com/v1',
apiProtocol: 'openai-completions',
defaultModelId: 'MiniMax-M2.7',
showModelId: true,
showModelIdInDevModeOnly: true,
@@ -235,6 +237,8 @@ export const PROVIDER_TYPE_INFO: ProviderTypeInfo[] = [
requiresApiKey: false,
isOAuth: true,
supportsApiKey: true,
defaultBaseUrl: 'https://api.minimax.io/v1',
apiProtocol: 'openai-completions',
defaultModelId: 'MiniMax-M2.7',
showModelId: true,
showModelIdInDevModeOnly: true,

View File

@@ -164,11 +164,11 @@ export const PROVIDER_TYPE_INFO: ProviderTypeInfo[] = [
apiKeyUrl: 'https://aistudio.google.com/app/apikey',
},
{ id: 'openrouter', name: 'OpenRouter', icon: '🌐', placeholder: 'sk-or-v1-...', model: 'Multi-Model', requiresApiKey: true, showModelId: true, modelIdPlaceholder: 'openai/gpt-5.4', defaultModelId: 'openai/gpt-5.4', docsUrl: 'https://openrouter.ai/models' },
{ id: 'minimax-portal-cn', name: 'MiniMax (CN)', icon: '☁️', placeholder: 'sk-...', model: 'MiniMax', requiresApiKey: false, isOAuth: true, supportsApiKey: true, defaultModelId: 'MiniMax-M2.7', showModelId: true, showModelIdInDevModeOnly: true, modelIdPlaceholder: 'MiniMax-M2.7', apiKeyUrl: 'https://platform.minimaxi.com/' },
{ id: 'minimax-portal-cn', name: 'MiniMax (CN)', icon: '☁️', placeholder: 'sk-...', model: 'MiniMax', requiresApiKey: false, isOAuth: true, supportsApiKey: true, defaultBaseUrl: 'https://api.minimaxi.com/v1', apiProtocol: 'openai-completions', defaultModelId: 'MiniMax-M2.7', showModelId: true, showModelIdInDevModeOnly: true, modelIdPlaceholder: 'MiniMax-M2.7', apiKeyUrl: 'https://platform.minimaxi.com/' },
{ id: 'moonshot', name: 'Moonshot (CN)', icon: '🌙', placeholder: 'sk-...', model: 'Kimi', requiresApiKey: true, defaultBaseUrl: 'https://api.moonshot.cn/v1', defaultModelId: 'kimi-k2.5', docsUrl: 'https://platform.moonshot.cn/' },
{ id: 'siliconflow', name: 'SiliconFlow (CN)', icon: '🌊', placeholder: 'sk-...', model: 'Multi-Model', requiresApiKey: true, defaultBaseUrl: 'https://api.siliconflow.cn/v1', showModelId: true, showModelIdInDevModeOnly: true, modelIdPlaceholder: 'deepseek-ai/DeepSeek-V3', defaultModelId: 'deepseek-ai/DeepSeek-V3', docsUrl: 'https://docs.siliconflow.cn/cn/userguide/introduction' },
{ id: 'deepseek', name: 'DeepSeek', icon: '🐋', placeholder: 'sk-...', model: 'DeepSeek', requiresApiKey: true, defaultBaseUrl: 'https://api.deepseek.com/v1', showModelId: true, modelIdPlaceholder: 'deepseek-chat', defaultModelId: 'deepseek-chat', apiKeyUrl: 'https://platform.deepseek.com/api_keys', docsUrl: 'https://api-docs.deepseek.com/' },
{ id: 'minimax-portal', name: 'MiniMax (Global)', icon: '☁️', placeholder: 'sk-...', model: 'MiniMax', requiresApiKey: false, isOAuth: true, supportsApiKey: true, defaultModelId: 'MiniMax-M2.7', showModelId: true, showModelIdInDevModeOnly: true, modelIdPlaceholder: 'MiniMax-M2.7', apiKeyUrl: 'https://platform.minimax.io' },
{ id: 'minimax-portal', name: 'MiniMax (Global)', icon: '☁️', placeholder: 'sk-...', model: 'MiniMax', requiresApiKey: false, isOAuth: true, supportsApiKey: true, defaultBaseUrl: 'https://api.minimax.io/v1', apiProtocol: 'openai-completions', defaultModelId: 'MiniMax-M2.7', showModelId: true, showModelIdInDevModeOnly: true, modelIdPlaceholder: 'MiniMax-M2.7', apiKeyUrl: 'https://platform.minimax.io' },
{ id: 'modelstudio', name: 'Model Studio', icon: '☁️', placeholder: 'sk-...', model: 'Qwen', requiresApiKey: true, defaultBaseUrl: 'https://coding.dashscope.aliyuncs.com/v1', showBaseUrl: true, defaultModelId: 'qwen3.5-plus', showModelId: true, showModelIdInDevModeOnly: true, modelIdPlaceholder: 'qwen3.5-plus', apiKeyUrl: 'https://bailian.console.aliyun.com/', hidden: true },
{ id: 'ark', name: 'ByteDance Ark', icon: 'A', placeholder: 'your-ark-api-key', model: 'Doubao', requiresApiKey: true, defaultBaseUrl: 'https://ark.cn-beijing.volces.com/api/v3', showBaseUrl: true, showModelId: true, modelIdPlaceholder: 'ep-20260228000000-xxxxx', docsUrl: 'https://www.volcengine.com/', codePlanPresetBaseUrl: 'https://ark.cn-beijing.volces.com/api/coding/v3', codePlanPresetModelId: 'ark-code-latest', codePlanDocsUrl: 'https://www.volcengine.com/docs/82379/1928261?lang=zh' },
{ id: 'ollama', name: 'Ollama', icon: '🦙', placeholder: 'Not required', placeholderZh: '无需填写', placeholderJa: '不要', requiresApiKey: false, defaultBaseUrl: 'http://localhost:11434/v1', showBaseUrl: true, showModelId: true, modelIdPlaceholder: 'qwen3:latest' },

View File

@@ -218,6 +218,7 @@ export default function ProvidersSection() {
label: provider.name,
authMode: provider.requiresApiKey ? 'api_key' : 'local',
baseUrl: provider.defaultBaseUrl,
apiProtocol: provider.apiProtocol,
model: provider.defaultModelId,
enabled: true,
isDefault: false,

View File

@@ -0,0 +1,90 @@
// @vitest-environment node
import { beforeEach, describe, expect, it, vi } from 'vitest';
const mocks = vi.hoisted(() => ({
getAccounts: vi.fn(),
getApiKey: vi.fn(),
openAI: vi.fn(),
}));
vi.mock('@electron/service/provider-api-service', () => ({
providerApiService: {
getAccounts: mocks.getAccounts,
getApiKey: mocks.getApiKey,
},
}));
vi.mock('@electron/service/logger', () => ({
default: {
logApiRequest: vi.fn(),
logApiResponse: vi.fn(),
},
}));
vi.mock('openai', () => ({
default: mocks.openAI,
}));
describe('createProvider', () => {
beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();
});
it('falls back to the MiniMax CN OpenAI-compatible base URL for legacy accounts', async () => {
mocks.getAccounts.mockReturnValue([
{
id: 'minimax-portal-cn-account',
vendorId: 'minimax-portal-cn',
label: 'MiniMax (CN)',
authMode: 'local',
model: 'MiniMax-M2.7',
enabled: true,
isDefault: true,
createdAt: '2026-04-20T00:00:00.000Z',
updatedAt: '2026-04-20T00:00:00.000Z',
},
]);
mocks.getApiKey.mockReturnValue({ apiKey: 'secret-key' });
const { createProvider, OpenAIProvider } = await import('../electron/providers');
const provider = createProvider('minimax-portal-cn-account');
expect(provider).toBeInstanceOf(OpenAIProvider);
expect(mocks.openAI).toHaveBeenCalledWith({
apiKey: 'secret-key',
baseURL: 'https://api.minimaxi.com/v1',
defaultHeaders: undefined,
});
});
it('prefers metadata resource URLs when a legacy OAuth account has no stored base URL', async () => {
mocks.getAccounts.mockReturnValue([
{
id: 'minimax-portal-account',
vendorId: 'minimax-portal',
label: 'MiniMax (Global)',
authMode: 'oauth_browser',
model: 'MiniMax-M2.7',
enabled: true,
isDefault: true,
metadata: {
resourceUrl: 'tenant.runtime.minimax.io/v1',
},
createdAt: '2026-04-20T00:00:00.000Z',
updatedAt: '2026-04-20T00:00:00.000Z',
},
]);
mocks.getApiKey.mockReturnValue({ apiKey: 'oauth-token' });
const { createProvider } = await import('../electron/providers');
createProvider('minimax-portal-account');
expect(mocks.openAI).toHaveBeenCalledWith({
apiKey: 'oauth-token',
baseURL: 'https://tenant.runtime.minimax.io/v1',
defaultHeaders: undefined,
});
});
});