Repository navigation
feat(web-ui): claude local bridge, share command, session utilization & UI improvements #166
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 12 commits
564d27b
a9e567a
7e13a4e
2701016
ae446ef
12b3038
b839fc4
3d896b2
4ca5164
707462f
cde92c3
c746613
9c3e6c9
55322e5
db99a4f
2bac697
3ea1e0d
b43af44
82123ea
dec373a
58999a5
75ad600
e3854bf
3f54c4b
f9011e0
6a5b08f
655b04e
32489f5
5b4ee20
dd28a6b
52d577f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -192,6 +192,8 @@ const BUILTIN_PROXY_SETTINGS_FILE = path.join(CONFIG_DIR, 'codexmate-proxy.json' | |
| const BUILTIN_CLAUDE_PROXY_SETTINGS_FILE = path.join(CONFIG_DIR, 'codexmate-claude-proxy.json'); | ||
| const OPENAI_BRIDGE_SETTINGS_FILE = path.join(CONFIG_DIR, 'codexmate-openai-bridge.json'); | ||
| const LOCAL_BRIDGE_SETTINGS_FILE = path.join(CONFIG_DIR, 'codexmate-local-bridge.json'); | ||
| const CLAUDE_LOCAL_BRIDGE_SETTINGS_FILE = path.join(CONFIG_DIR, 'codexmate-claude-local-bridge.json'); | ||
| const CLAUDE_LOCAL_PROVIDERS_FILE = path.join(CONFIG_DIR, 'codexmate-claude-bridge.json'); | ||
| const CODEX_SESSIONS_DIR = path.join(CONFIG_DIR, 'sessions'); | ||
| const SESSION_TRASH_DIR = path.join(CONFIG_DIR, 'codexmate-session-trash'); | ||
| const SESSION_TRASH_FILES_DIR = path.join(SESSION_TRASH_DIR, 'files'); | ||
|
|
@@ -338,6 +340,7 @@ const openaiBridgeHandler = createOpenaiBridgeHttpHandler({ | |
| const localBridgeHandler = createLocalBridgeHttpHandler({ | ||
| readConfigFn: readConfig, | ||
| openaiBridgeFile: OPENAI_BRIDGE_SETTINGS_FILE, | ||
| claudeProvidersFile: CLAUDE_LOCAL_PROVIDERS_FILE, | ||
| localBridgeSettingsFile: LOCAL_BRIDGE_SETTINGS_FILE, | ||
| expectedToken: typeof process.env.CODEXMATE_HTTP_TOKEN === 'string' ? process.env.CODEXMATE_HTTP_TOKEN.trim() : '', | ||
| maxBodySize: MAX_API_BODY_SIZE, | ||
|
|
@@ -2202,6 +2205,30 @@ function updateProviderInConfig(params = {}) { | |
| } | ||
| } | ||
|
|
||
| function getProviderKey(params = {}) { | ||
| const name = typeof params.name === 'string' ? params.name.trim() : ''; | ||
| if (!name) return { error: '名称不能为空' }; | ||
| try { | ||
| const config = readConfig(); | ||
| const provider = config.model_providers && config.model_providers[name]; | ||
| if (!provider) return { error: '提供商不存在' }; | ||
|
|
||
| const bridge = typeof provider.codexmate_bridge === 'string' ? provider.codexmate_bridge.trim() : ''; | ||
| const isTransform = bridge === 'openai' || String(provider.base_url || '').includes('/bridge/openai/'); | ||
| if (isTransform) { | ||
| const settings = readOpenaiBridgeSettings(OPENAI_BRIDGE_SETTINGS_FILE); | ||
| const entry = settings.providers ? settings.providers[name] : null; | ||
| const key = entry && typeof entry.apiKey === 'string' ? entry.apiKey : ''; | ||
| return { key }; | ||
| } | ||
|
|
||
| const key = typeof provider.preferred_auth_method === 'string' ? provider.preferred_auth_method : ''; | ||
| return { key }; | ||
| } catch (e) { | ||
| return { error: e.message || '读取失败' }; | ||
| } | ||
| } | ||
|
Comment on lines
+2209
to
+2231
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Lock down plaintext key reads before exposing them over This new action returns raw provider secrets, but Also applies to: 10632-10634 🤖 Prompt for AI Agents |
||
|
|
||
| function deleteProviderFromConfig(params = {}) { | ||
| const name = typeof params.name === 'string' ? params.name.trim() : ''; | ||
| if (!name) return { error: '名称不能为空' }; | ||
|
|
@@ -5549,6 +5576,156 @@ function getLocalBridgeExcludedProviders() { | |
| return { excludedProviders: settings.excludedProviders }; | ||
| } | ||
|
|
||
| // ============================================================================ | ||
| // Claude Local Bridge | ||
| // ============================================================================ | ||
|
|
||
| function readClaudeLocalBridgeSettings() { | ||
| const defaults = { enabled: false, lastActiveBaseUrl: '', lastModel: '', excludedProviders: [] }; | ||
| try { | ||
| if (!fs.existsSync(CLAUDE_LOCAL_BRIDGE_SETTINGS_FILE)) return defaults; | ||
| const raw = JSON.parse(fs.readFileSync(CLAUDE_LOCAL_BRIDGE_SETTINGS_FILE, 'utf-8')); | ||
| return { | ||
| enabled: !!raw.enabled, | ||
| lastActiveBaseUrl: typeof raw.lastActiveBaseUrl === 'string' ? raw.lastActiveBaseUrl.trim() : '', | ||
| lastModel: typeof raw.lastModel === 'string' ? raw.lastModel.trim() : '', | ||
| excludedProviders: Array.isArray(raw.excludedProviders) ? raw.excludedProviders.filter(p => typeof p === 'string') : [] | ||
| }; | ||
| } catch (e) { | ||
| return defaults; | ||
| } | ||
| } | ||
|
|
||
| function writeClaudeLocalBridgeSettings(settings) { | ||
| fs.writeFileSync(CLAUDE_LOCAL_BRIDGE_SETTINGS_FILE, JSON.stringify(settings, null, 2), 'utf-8'); | ||
| } | ||
|
|
||
| function readClaudeLocalProvidersFile() { | ||
| try { | ||
| if (!fs.existsSync(CLAUDE_LOCAL_PROVIDERS_FILE)) return { providers: {} }; | ||
| const raw = JSON.parse(fs.readFileSync(CLAUDE_LOCAL_PROVIDERS_FILE, 'utf-8')); | ||
| return { providers: (raw && typeof raw.providers === 'object') ? raw.providers : {} }; | ||
| } catch (e) { | ||
|
Comment on lines
+5604
to
+5609
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Preserve
Suggested fix function readClaudeLocalProvidersFile() {
try {
- if (!fs.existsSync(CLAUDE_LOCAL_PROVIDERS_FILE)) return { providers: {} };
+ if (!fs.existsSync(CLAUDE_LOCAL_PROVIDERS_FILE)) {
+ return { providers: {}, excludedProviders: [] };
+ }
const raw = JSON.parse(fs.readFileSync(CLAUDE_LOCAL_PROVIDERS_FILE, 'utf-8'));
- return { providers: (raw && typeof raw.providers === 'object') ? raw.providers : {} };
+ return {
+ providers: raw && raw.providers && typeof raw.providers === 'object' && !Array.isArray(raw.providers)
+ ? raw.providers
+ : {},
+ excludedProviders: Array.isArray(raw && raw.excludedProviders)
+ ? raw.excludedProviders.filter((name) => typeof name === 'string' && name.trim())
+ : []
+ };
} catch (e) {
- return { providers: {} };
+ return { providers: {}, excludedProviders: [] };
}
}Also applies to: 5721-5725 🤖 Prompt for AI Agents |
||
| return { providers: {} }; | ||
| } | ||
| } | ||
|
|
||
| function writeClaudeLocalProvidersFile(data) { | ||
| ensureDir(CONFIG_DIR); | ||
| fs.writeFileSync(CLAUDE_LOCAL_PROVIDERS_FILE, JSON.stringify(data, null, 2), 'utf-8'); | ||
| } | ||
|
|
||
| function syncClaudeProvidersToBridgeFile() { | ||
| // Sync Claude configs from localStorage-equivalent (browser) to bridge file | ||
| // Called when providers are added/updated/deleted via web UI | ||
| const existing = readClaudeLocalProvidersFile(); | ||
| const providers = existing.providers || {}; | ||
| // Preserve existing entries, update from params | ||
| return { providers }; | ||
| } | ||
|
|
||
| function toggleClaudeLocalBridge(params = {}) { | ||
| const enable = !!params.enable; | ||
| const settings = readClaudeLocalBridgeSettings(); | ||
|
|
||
| try { | ||
| const readResult = readJsonObjectFromFile(CLAUDE_SETTINGS_FILE, {}); | ||
| if (!readResult.ok) { | ||
| return { success: false, error: readResult.error || '读取 Claude settings.json 失败' }; | ||
| } | ||
| const currentSettings = readResult.data || {}; | ||
| const currentEnv = (currentSettings.env && typeof currentSettings.env === 'object' && !Array.isArray(currentSettings.env)) | ||
| ? currentSettings.env : {}; | ||
| const currentBaseUrl = typeof currentEnv.ANTHROPIC_BASE_URL === 'string' ? currentEnv.ANTHROPIC_BASE_URL.trim() : ''; | ||
|
|
||
| if (enable) { | ||
| if (currentBaseUrl.includes('/bridge/claude-local/')) { | ||
| return { success: true, enabled: true, notice: '已启用 Claude 本地负载均衡' }; | ||
| } | ||
| settings.lastActiveBaseUrl = currentBaseUrl; | ||
| settings.lastModel = typeof currentEnv.ANTHROPIC_MODEL === 'string' ? currentEnv.ANTHROPIC_MODEL.trim() : ''; | ||
| settings.enabled = true; | ||
| writeClaudeLocalBridgeSettings(settings); | ||
|
|
||
| const localPort = resolveWebPort(); | ||
| const localBaseUrl = `http://127.0.0.1:${localPort}/bridge/claude-local/v1`; | ||
| const nextEnv = { ...currentEnv, ANTHROPIC_BASE_URL: localBaseUrl }; | ||
| const nextSettings = { ...currentSettings, env: nextEnv }; | ||
| ensureDir(CLAUDE_DIR); | ||
| backupFileIfNeededOnce(CLAUDE_SETTINGS_FILE); | ||
| writeJsonAtomic(CLAUDE_SETTINGS_FILE, nextSettings); | ||
| return { success: true, enabled: true, previousBaseUrl: currentBaseUrl }; | ||
| } else { | ||
| if (!currentBaseUrl.includes('/bridge/claude-local/')) { | ||
| settings.enabled = false; | ||
| writeClaudeLocalBridgeSettings(settings); | ||
| return { success: true, enabled: false, notice: 'Claude 本地负载均衡未启用' }; | ||
| } | ||
| const restoreBaseUrl = settings.lastActiveBaseUrl || ''; | ||
| if (!restoreBaseUrl) { | ||
| settings.enabled = false; | ||
| writeClaudeLocalBridgeSettings(settings); | ||
| return { success: true, enabled: false, notice: '已关闭 Claude 本地负载均衡(无历史配置可恢复)' }; | ||
| } | ||
| const nextEnv = { ...currentEnv, ANTHROPIC_BASE_URL: restoreBaseUrl }; | ||
| if (settings.lastModel) { | ||
| nextEnv.ANTHROPIC_MODEL = settings.lastModel; | ||
| } | ||
| const nextSettings = { ...currentSettings, env: nextEnv }; | ||
| ensureDir(CLAUDE_DIR); | ||
| backupFileIfNeededOnce(CLAUDE_SETTINGS_FILE); | ||
| writeJsonAtomic(CLAUDE_SETTINGS_FILE, nextSettings); | ||
| settings.enabled = false; | ||
| writeClaudeLocalBridgeSettings(settings); | ||
|
Comment on lines
+5665
to
+5680
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Clear bridge env vars when disabling without a previous base URL. If Claude had no Suggested fix const restoreBaseUrl = settings.lastActiveBaseUrl || '';
- if (!restoreBaseUrl) {
- settings.enabled = false;
- writeClaudeLocalBridgeSettings(settings);
- return { success: true, enabled: false, notice: '已关闭 Claude 本地负载均衡(无历史配置可恢复)' };
- }
- const nextEnv = { ...currentEnv, ANTHROPIC_BASE_URL: restoreBaseUrl };
- if (settings.lastModel) {
- nextEnv.ANTHROPIC_MODEL = settings.lastModel;
- }
+ const nextEnv = { ...currentEnv };
+ if (restoreBaseUrl) {
+ nextEnv.ANTHROPIC_BASE_URL = restoreBaseUrl;
+ } else {
+ delete nextEnv.ANTHROPIC_BASE_URL;
+ }
+ if (settings.lastModel) {
+ nextEnv.ANTHROPIC_MODEL = settings.lastModel;
+ } else {
+ delete nextEnv.ANTHROPIC_MODEL;
+ }
const nextSettings = { ...currentSettings, env: nextEnv };
ensureDir(CLAUDE_DIR);
backupFileIfNeededOnce(CLAUDE_SETTINGS_FILE);
writeJsonAtomic(CLAUDE_SETTINGS_FILE, nextSettings);
settings.enabled = false;
writeClaudeLocalBridgeSettings(settings);
- return { success: true, enabled: false, restoredBaseUrl: restoreBaseUrl, restoredModel: settings.lastModel };
+ return restoreBaseUrl
+ ? { success: true, enabled: false, restoredBaseUrl: restoreBaseUrl, restoredModel: settings.lastModel }
+ : { success: true, enabled: false, notice: '已关闭 Claude 本地负载均衡(无历史配置可恢复)' };🤖 Prompt for AI Agents |
||
| return { success: true, enabled: false, restoredBaseUrl: restoreBaseUrl, restoredModel: settings.lastModel }; | ||
| } | ||
| } catch (e) { | ||
| return { error: e && e.message ? e.message : '操作失败' }; | ||
| } | ||
| } | ||
|
|
||
| function getClaudeLocalBridgeStatus() { | ||
| const settings = readClaudeLocalBridgeSettings(); | ||
| let currentBaseUrl = ''; | ||
| try { | ||
| const readResult = readJsonObjectFromFile(CLAUDE_SETTINGS_FILE, {}); | ||
| if (readResult.ok && readResult.data && readResult.data.env) { | ||
| currentBaseUrl = typeof readResult.data.env.ANTHROPIC_BASE_URL === 'string' ? readResult.data.env.ANTHROPIC_BASE_URL.trim() : ''; | ||
| } | ||
| } catch (e) { /* ignore */ } | ||
| const providersData = readClaudeLocalProvidersFile(); | ||
| const providerNames = Object.keys(providersData.providers || {}); | ||
| return { | ||
| enabled: settings.enabled, | ||
| active: currentBaseUrl.includes('/bridge/claude-local/'), | ||
| excludedProviders: settings.excludedProviders, | ||
| lastActiveBaseUrl: settings.lastActiveBaseUrl, | ||
| lastModel: settings.lastModel, | ||
| providers: providerNames | ||
| }; | ||
| } | ||
|
|
||
| function setClaudeLocalBridgeExcludedProviders(params = {}) { | ||
| const names = Array.isArray(params.names) ? params.names.filter(n => typeof n === 'string' && n.trim()) : []; | ||
| const settings = readClaudeLocalBridgeSettings(); | ||
| settings.excludedProviders = names; | ||
| writeClaudeLocalBridgeSettings(settings); | ||
| return { success: true, excludedProviders: names }; | ||
| } | ||
|
|
||
| function getClaudeLocalBridgeExcludedProviders() { | ||
| const settings = readClaudeLocalBridgeSettings(); | ||
| return { excludedProviders: settings.excludedProviders }; | ||
| } | ||
|
|
||
| function syncClaudeBridgeProviders(params = {}) { | ||
| const providers = (params.providers && typeof params.providers === 'object') ? params.providers : {}; | ||
| const existing = readClaudeLocalProvidersFile(); | ||
| const excluded = existing.excludedProviders || []; | ||
| writeClaudeLocalProvidersFile({ providers, excludedProviders: excluded }); | ||
| return { success: true, count: Object.keys(providers).length }; | ||
| } | ||
|
|
||
| function removeClaudeSessionIndexEntry(indexPath, sessionFilePath, sessionId) { | ||
| if (!indexPath || !fs.existsSync(indexPath)) { | ||
| return { removed: false, entry: null }; | ||
|
|
@@ -10605,6 +10782,9 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser | |
| case 'update-provider': | ||
| result = updateProviderInConfig(params || {}); | ||
| break; | ||
| case 'get-provider-key': | ||
| result = getProviderKey(params || {}); | ||
| break; | ||
| case 'delete-provider': | ||
| result = deleteProviderFromConfig(params || {}); | ||
| break; | ||
|
|
@@ -10980,6 +11160,21 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser | |
| case 'local-bridge-get-excluded': | ||
| result = getLocalBridgeExcludedProviders(); | ||
| break; | ||
| case 'claude-local-bridge-toggle': | ||
| result = toggleClaudeLocalBridge(params || {}); | ||
| break; | ||
| case 'claude-local-bridge-status': | ||
| result = getClaudeLocalBridgeStatus(); | ||
| break; | ||
| case 'claude-local-bridge-set-excluded': | ||
| result = setClaudeLocalBridgeExcludedProviders(params || {}); | ||
| break; | ||
| case 'claude-local-bridge-get-excluded': | ||
| result = getClaudeLocalBridgeExcludedProviders(); | ||
| break; | ||
| case 'claude-local-bridge-sync-providers': | ||
| result = syncClaudeBridgeProviders(params || {}); | ||
| break; | ||
| case 'workflow-list': | ||
| result = listWorkflowDefinitions(); | ||
| break; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Narrow the transform-provider detection.
Line 2214 treats any
base_urlcontaining/bridge/openai/as a managed transform provider. That can misclassify a normal upstream URL and make this endpoint returnsettings.providers[name].apiKeyinstead of the provider’s realpreferred_auth_method, so the edit modal loads the wrong secret.Suggested fix
📝 Committable suggestion
🤖 Prompt for AI Agents