@@ -107,6 +107,9 @@ const {
107107 readOpenaiBridgeSettings,
108108 resolveOpenaiBridgeUpstream
109109} = require('./cli/openai-bridge');
110+ const {
111+ createLocalBridgeHttpHandler
112+ } = require('./cli/local-bridge');
110113const {
111114 createOpenclawConfigController
112115} = require('./cli/openclaw-config');
@@ -188,6 +191,7 @@ const INIT_MARK_FILE = path.join(CONFIG_DIR, 'codexmate-init.json');
188191const BUILTIN_PROXY_SETTINGS_FILE = path.join(CONFIG_DIR, 'codexmate-proxy.json');
189192const BUILTIN_CLAUDE_PROXY_SETTINGS_FILE = path.join(CONFIG_DIR, 'codexmate-claude-proxy.json');
190193const OPENAI_BRIDGE_SETTINGS_FILE = path.join(CONFIG_DIR, 'codexmate-openai-bridge.json');
194+ const LOCAL_BRIDGE_SETTINGS_FILE = path.join(CONFIG_DIR, 'codexmate-local-bridge.json');
191195const CODEX_SESSIONS_DIR = path.join(CONFIG_DIR, 'sessions');
192196const SESSION_TRASH_DIR = path.join(CONFIG_DIR, 'codexmate-session-trash');
193197const SESSION_TRASH_FILES_DIR = path.join(SESSION_TRASH_DIR, 'files');
@@ -268,6 +272,7 @@ const DEFAULT_EXTRACT_SUFFIXES = Object.freeze(['.json']);
268272const g_taskRunControllers = new Map();
269273let g_taskQueueProcessor = null;
270274const BUILTIN_PROXY_PROVIDER_NAME = 'codexmate-proxy';
275+ const BUILTIN_LOCAL_PROVIDER_NAME = 'local';
271276const DEFAULT_BUILTIN_PROXY_SETTINGS = Object.freeze({
272277 enabled: false,
273278 host: '127.0.0.1',
@@ -330,6 +335,16 @@ const openaiBridgeHandler = createOpenaiBridgeHttpHandler({
330335 httpsAgent: HTTPS_KEEP_ALIVE_AGENT
331336});
332337
338+ const localBridgeHandler = createLocalBridgeHttpHandler({
339+ readConfigFn: readConfig,
340+ openaiBridgeFile: OPENAI_BRIDGE_SETTINGS_FILE,
341+ localBridgeSettingsFile: LOCAL_BRIDGE_SETTINGS_FILE,
342+ expectedToken: typeof process.env.CODEXMATE_HTTP_TOKEN === 'string' ? process.env.CODEXMATE_HTTP_TOKEN.trim() : '',
343+ maxBodySize: MAX_API_BODY_SIZE,
344+ httpAgent: HTTP_KEEP_ALIVE_AGENT,
345+ httpsAgent: HTTPS_KEEP_ALIVE_AGENT
346+ });
347+
333348function resolveWebPort() {
334349 const raw = process.env.CODEXMATE_PORT;
335350 if (!raw) return DEFAULT_WEB_PORT;
@@ -589,16 +604,17 @@ model_auto_compact_token_limit = ${DEFAULT_MODEL_AUTO_COMPACT_TOKEN_LIMIT}
589604disable_response_storage = true
590605approval_policy = "never"
591606sandbox_mode = "danger-full-access"
592- model_provider = "maxx "
607+ model_provider = "local "
593608personality = "pragmatic"
594609web_search = "live"
595610
596- [model_providers.maxx ]
597- name = "maxx "
598- base_url = "https ://maxx-direct.cloverstd.com "
611+ [model_providers.local ]
612+ name = "local "
613+ base_url = "http ://127.0.0.1:3737/bridge/local/v1 "
599614wire_api = "responses"
600- requires_openai_auth = false
601- preferred_auth_method = "sk-"
615+ requires_openai_auth = true
616+ preferred_auth_method = "codexmate"
617+ codexmate_bridge = "local"
602618request_max_retries = 4
603619stream_max_retries = 10
604620stream_idle_timeout_ms = 300000
@@ -620,12 +636,16 @@ function isBuiltinProxyProvider(providerName) {
620636 return typeof providerName === 'string' && providerName.trim().toLowerCase() === BUILTIN_PROXY_PROVIDER_NAME.toLowerCase();
621637}
622638
639+ function isLocalProvider(providerName) {
640+ return typeof providerName === 'string' && providerName.trim().toLowerCase() === BUILTIN_LOCAL_PROVIDER_NAME.toLowerCase();
641+ }
642+
623643function isReservedProviderNameForCreation(providerName) {
624- return false ;
644+ return isLocalProvider(providerName) ;
625645}
626646
627647function isBuiltinManagedProvider(providerName) {
628- return isBuiltinProxyProvider(providerName);
648+ return isBuiltinProxyProvider(providerName) || isLocalProvider(providerName) ;
629649}
630650
631651function isNonDeletableProvider(providerName) {
@@ -1661,6 +1681,7 @@ const {
16611681 DEFAULT_MODEL_AUTO_COMPACT_TOKEN_LIMIT,
16621682 CODEXMATE_MANAGED_MARKER,
16631683 BUILTIN_PROXY_PROVIDER_NAME,
1684+ BUILTIN_LOCAL_PROVIDER_NAME,
16641685 EMPTY_CONFIG_FALLBACK_TEMPLATE
16651686});
16661687
@@ -2059,7 +2080,7 @@ function addProviderToConfig(params = {}) {
20592080 return { error: '提供商名称不可用' };
20602081 }
20612082 if (isBuiltinProxyProvider(name) && !allowManaged) {
2062- return { error: ' codexmate-proxy 为保留名称,不可手动添加' };
2083+ return { error: `${" codexmate-proxy"} 为保留名称,不可手动添加` }; // keep literal for codexmate-proxy
20632084 }
20642085
20652086 ensureConfigDir();
@@ -2159,7 +2180,7 @@ function updateProviderInConfig(params = {}) {
21592180 return { error: 'URL 仅支持 http/https' };
21602181 }
21612182 if (isNonEditableProvider(name) && !allowManaged) {
2162- return { error: 'codexmate-proxy 为保留名称,不可编辑' };
2183+ return { error: `${name} 为保留名称,不可编辑` };
21632184 }
21642185
21652186 try {
@@ -2174,7 +2195,7 @@ function deleteProviderFromConfig(params = {}) {
21742195 const name = typeof params.name === 'string' ? params.name.trim() : '';
21752196 if (!name) return { error: '名称不能为空' };
21762197 if (isNonDeletableProvider(name)) {
2177- return { error: 'codexmate-proxy 为保留名称,不可删除' };
2198+ return { error: `${name} 为保留名称,不可删除` };
21782199 }
21792200 if (!fs.existsSync(CONFIG_FILE)) {
21802201 return { error: 'config.toml 不存在' };
@@ -2202,7 +2223,7 @@ function deleteProviderFromConfig(params = {}) {
22022223function performProviderDeletion(name, options = {}) {
22032224 const silent = !!options.silent;
22042225 if (isNonDeletableProvider(name)) {
2205- const msg = 'codexmate-proxy 为保留名称,不可删除' ;
2226+ const msg = `${name} 为保留名称,不可删除` ;
22062227 if (!silent) console.error('错误:', msg);
22072228 return { error: msg };
22082229 }
@@ -5423,6 +5444,100 @@ async function ensureBuiltinProxyForCodexDefault(params = {}) {
54235444 return { error: '该功能已移除' };
54245445}
54255446
5447+ function readLocalBridgeSettings() {
5448+ const defaults = { enabled: false, lastActiveProvider: '', lastModel: '', excludedProviders: [] };
5449+ try {
5450+ if (!fs.existsSync(LOCAL_BRIDGE_SETTINGS_FILE)) return defaults;
5451+ const raw = JSON.parse(fs.readFileSync(LOCAL_BRIDGE_SETTINGS_FILE, 'utf-8'));
5452+ return {
5453+ enabled: !!raw.enabled,
5454+ lastActiveProvider: typeof raw.lastActiveProvider === 'string' ? raw.lastActiveProvider.trim() : '',
5455+ lastModel: typeof raw.lastModel === 'string' ? raw.lastModel.trim() : '',
5456+ excludedProviders: Array.isArray(raw.excludedProviders) ? raw.excludedProviders.filter(p => typeof p === 'string') : []
5457+ };
5458+ } catch (e) {
5459+ return defaults;
5460+ }
5461+ }
5462+
5463+ function writeLocalBridgeSettings(settings) {
5464+ fs.writeFileSync(LOCAL_BRIDGE_SETTINGS_FILE, JSON.stringify(settings, null, 2), 'utf-8');
5465+ }
5466+
5467+ function toggleLocalBridgeProvider(params = {}) {
5468+ const enable = !!params.enable;
5469+ const settings = readLocalBridgeSettings();
5470+ try {
5471+ const config = readConfig();
5472+ const currentProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
5473+ const currentModel = typeof config.model === 'string' ? config.model.trim() : '';
5474+
5475+ if (enable) {
5476+ if (currentProvider === 'local') return { success: true, enabled: true, notice: '已启用 local 转换' };
5477+ settings.lastActiveProvider = currentProvider;
5478+ settings.lastModel = currentModel;
5479+ settings.enabled = true;
5480+ writeLocalBridgeSettings(settings);
5481+ let content = fs.readFileSync(CONFIG_FILE, 'utf-8');
5482+ content = content.replace(/^(model_provider\s*=\s*)(["']).*?(["'])/m, `$1$2local$3`);
5483+ writeConfig(content);
5484+ return { success: true, enabled: true, previousProvider: currentProvider };
5485+ } else {
5486+ if (currentProvider !== 'local') {
5487+ settings.enabled = false;
5488+ writeLocalBridgeSettings(settings);
5489+ return { success: true, enabled: false, notice: 'local 转换未启用' };
5490+ }
5491+ const restoreProvider = settings.lastActiveProvider || '';
5492+ if (!restoreProvider) {
5493+ settings.enabled = false;
5494+ writeLocalBridgeSettings(settings);
5495+ return { success: true, enabled: false, notice: '已关闭 local 转换(无历史 provider 可恢复)' };
5496+ }
5497+ let content = fs.readFileSync(CONFIG_FILE, 'utf-8');
5498+ content = content.replace(/^(model_provider\s*=\s*)(["']).*?(["'])/m, `$1$2${restoreProvider}$3`);
5499+ if (settings.lastModel) {
5500+ content = content.replace(/^(model\s*=\s*)(["']).*?(["'])/m, `$1$2${settings.lastModel}$3`);
5501+ }
5502+ writeConfig(content);
5503+ settings.enabled = false;
5504+ writeLocalBridgeSettings(settings);
5505+ return { success: true, enabled: false, restoredProvider: restoreProvider, restoredModel: settings.lastModel };
5506+ }
5507+ } catch (e) {
5508+ return { error: e && e.message ? e.message : '操作失败' };
5509+ }
5510+ }
5511+
5512+ function getLocalBridgeStatus() {
5513+ const settings = readLocalBridgeSettings();
5514+ let currentProvider = '';
5515+ try {
5516+ const config = readConfig();
5517+ currentProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
5518+ } catch (e) { /* ignore */ }
5519+ return {
5520+ enabled: settings.enabled,
5521+ active: currentProvider === 'local',
5522+ excludedProviders: settings.excludedProviders,
5523+ lastActiveProvider: settings.lastActiveProvider,
5524+ lastModel: settings.lastModel
5525+ };
5526+ }
5527+
5528+ function setLocalBridgeExcludedProviders(params = {}) {
5529+ const names = Array.isArray(params.names) ? params.names.filter(n => typeof n === 'string' && n.trim()) : [];
5530+ const settings = readLocalBridgeSettings();
5531+ settings.excludedProviders = names;
5532+ writeLocalBridgeSettings(settings);
5533+ return { success: true, excludedProviders: names };
5534+ }
5535+
5536+ function getLocalBridgeExcludedProviders() {
5537+ const settings = readLocalBridgeSettings();
5538+ return { excludedProviders: settings.excludedProviders };
5539+ }
5540+
54265541function removeClaudeSessionIndexEntry(indexPath, sessionFilePath, sessionId) {
54275542 if (!indexPath || !fs.existsSync(indexPath)) {
54285543 return { removed: false, entry: null };
@@ -8132,8 +8247,8 @@ function cmdAdd(name, baseUrl, apiKey, silent = false, options = {}) {
81328247 throw new Error('提供商名称不可用');
81338248 }
81348249 if (isBuiltinProxyProvider(providerName)) {
8135- if (!silent) console.error(' 错误: codexmate-proxy 为保留名称,不可手动添加' );
8136- throw new Error('codexmate-proxy 为保留名称,不可手动添加' );
8250+ if (!silent) console.error(` 错误: ${providerName} 为保留名称,不可手动添加` );
8251+ throw new Error(`${providerName} 为保留名称,不可手动添加` );
81378252 }
81388253 if (!isValidHttpUrl(providerBaseUrl)) {
81398254 if (!silent) console.error('错误: URL 仅支持 http/https');
@@ -8229,7 +8344,7 @@ function cmdUpdate(name, baseUrl, apiKey, silent = false, options = {}) {
82298344 throw new Error('提供商名称必填');
82308345 }
82318346 if (isNonEditableProvider(name) && !allowManaged) {
8232- const msg = 'codexmate-proxy 为保留名称,不可编辑' ;
8347+ const msg = `${name} 为保留名称,不可编辑` ;
82338348 if (!silent) console.error(`错误: ${msg}`);
82348349 throw new Error(msg);
82358350 }
@@ -9961,6 +10076,9 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
996110076 });
996210077 res.end(body, 'utf-8');
996310078 };
10079+ if (typeof localBridgeHandler === 'function' && localBridgeHandler(req, res)) {
10080+ return;
10081+ }
996410082 if (typeof openaiBridgeHandler === 'function' && openaiBridgeHandler(req, res)) {
996510083 return;
996610084 }
@@ -10497,6 +10615,18 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
1049710615 case 'proxy-apply-provider':
1049810616 result = applyBuiltinProxyProvider(params || {});
1049910617 break;
10618+ case 'local-bridge-toggle':
10619+ result = toggleLocalBridgeProvider(params || {});
10620+ break;
10621+ case 'local-bridge-status':
10622+ result = getLocalBridgeStatus();
10623+ break;
10624+ case 'local-bridge-set-excluded':
10625+ result = setLocalBridgeExcludedProviders(params || {});
10626+ break;
10627+ case 'local-bridge-get-excluded':
10628+ result = getLocalBridgeExcludedProviders();
10629+ break;
1050010630 case 'workflow-list':
1050110631 result = listWorkflowDefinitions();
1050210632 break;
0 commit comments