mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-06 20:02:49 +08:00
feat(hermes): add .env editor page for unmanaged env vars (Step 4)
Users may need to configure custom environment variables for Hermes
(e.g. `TAVILY_API_KEY` for the tavily skill, `HTTP_PROXY`, SKILL_*
settings). Previously the only way to set these was to hand-edit
~/.hermes/.env, which risks clobbering the provider keys that
ClawPanel writes through configure_hermes.
This patch adds a dedicated editor UI backed by three new Tauri
commands that refuse to touch managed keys.
Backend (src-tauri/src/commands/hermes.rs):
- `hermes_env_read_unmanaged` — returns every KEY=VALUE pair whose key
is NOT in `hermes_providers::all_managed_env_keys()` (so provider
API keys, base URLs, `GATEWAY_ALLOW_ALL_USERS`, `API_SERVER_KEY`
stay hidden). Preserves file order, dedups.
- `hermes_env_set(key, value)` — validates key against `^[A-Z0-9_]+$`,
refuses managed keys, updates first occurrence or appends,
preserves comments/blanks.
- `hermes_env_delete(key)` — refuses managed keys, removes first
matching line, preserves other structure.
All three commands registered in `src-tauri/src/lib.rs`.
Frontend:
- New page `src/engines/hermes/pages/env-editor.js`:
- Header with "back to dashboard" link and warning banner listing
which keys are managed by ClawPanel.
- Table with Key / Value / Actions columns.
- Inline edit mode per row (save / cancel).
- "Add variable" button for new entries.
- Value column masks long secrets (`sk-a…xyz9`) so glances don't
leak credentials.
- Toast feedback on save / delete / validation errors.
- Inline Chinese copy (TODO: wire up i18n when the locales module
lands).
- Route registered at `/h/env` in `src/engines/hermes/index.js`.
- Dashboard "Model config" section now has a subtle link
".env 高级编辑 →" pointing to the new page.
API wiring:
- `src/lib/tauri-api.js`: added `hermesEnvReadUnmanaged`,
`hermesEnvSet`, `hermesEnvDelete`.
- `scripts/dev-api.js`: mirrors the three commands in Web mode with
a duplicated managed-key list (keep in sync with Rust's
`hermes_providers::all_managed_env_keys` as new providers land).
Verified: cargo fmt / cargo clippy -D warnings / cargo test / npm run
build all green. Dashboard chunk unchanged (24.30 kB); new env-editor
chunk is ~7 kB gzip.
This commit is contained in:
@@ -6845,6 +6845,135 @@ const handlers = {
|
||||
return []
|
||||
},
|
||||
|
||||
// =========================================================================
|
||||
// .env editor (Step 4) — Web-mode implementations mirroring Rust behavior.
|
||||
// The managed-key list is duplicated here since Rust's hermes_providers is
|
||||
// not accessible from Node. Keep in sync with
|
||||
// src-tauri/src/commands/hermes_providers.rs::all_managed_env_keys
|
||||
// whenever new providers are added to the registry.
|
||||
// =========================================================================
|
||||
_hermesManagedEnvKeys() {
|
||||
return [
|
||||
// Anthropic
|
||||
'ANTHROPIC_API_KEY', 'ANTHROPIC_TOKEN', 'CLAUDE_CODE_OAUTH_TOKEN',
|
||||
// Gemini
|
||||
'GOOGLE_API_KEY', 'GEMINI_API_KEY', 'GEMINI_BASE_URL',
|
||||
// DeepSeek
|
||||
'DEEPSEEK_API_KEY', 'DEEPSEEK_BASE_URL',
|
||||
// Z.AI / GLM
|
||||
'GLM_API_KEY', 'ZAI_API_KEY', 'Z_AI_API_KEY', 'GLM_BASE_URL',
|
||||
// Kimi
|
||||
'KIMI_API_KEY', 'KIMI_BASE_URL',
|
||||
// xAI
|
||||
'XAI_API_KEY', 'XAI_BASE_URL',
|
||||
// MiniMax intl + CN
|
||||
'MINIMAX_API_KEY', 'MINIMAX_BASE_URL',
|
||||
'MINIMAX_CN_API_KEY', 'MINIMAX_CN_BASE_URL',
|
||||
// Alibaba DashScope
|
||||
'DASHSCOPE_API_KEY', 'DASHSCOPE_BASE_URL',
|
||||
// Hugging Face
|
||||
'HF_TOKEN', 'HF_BASE_URL',
|
||||
// Xiaomi
|
||||
'XIAOMI_API_KEY', 'XIAOMI_BASE_URL',
|
||||
// AI Gateway
|
||||
'AI_GATEWAY_API_KEY', 'AI_GATEWAY_BASE_URL',
|
||||
// OpenCode Zen + Go
|
||||
'OPENCODE_ZEN_API_KEY', 'OPENCODE_ZEN_BASE_URL',
|
||||
'OPENCODE_GO_API_KEY', 'OPENCODE_GO_BASE_URL',
|
||||
// Kilocode
|
||||
'KILOCODE_API_KEY', 'KILOCODE_BASE_URL',
|
||||
// Copilot (PAT)
|
||||
'COPILOT_GITHUB_TOKEN', 'GH_TOKEN', 'GITHUB_TOKEN',
|
||||
// OpenRouter
|
||||
'OPENROUTER_API_KEY', 'OPENAI_BASE_URL',
|
||||
// Copilot ACP
|
||||
'COPILOT_ACP_BASE_URL',
|
||||
// Custom placeholder
|
||||
'CUSTOM_API_KEY', 'OPENAI_API_KEY',
|
||||
// ClawPanel-specific
|
||||
'GATEWAY_ALLOW_ALL_USERS', 'API_SERVER_KEY',
|
||||
]
|
||||
},
|
||||
|
||||
hermes_env_read_unmanaged() {
|
||||
const envPath = path.join(hermesHome(), '.env')
|
||||
if (!fs.existsSync(envPath)) return []
|
||||
const raw = fs.readFileSync(envPath, 'utf8')
|
||||
const managed = new Set(this._hermesManagedEnvKeys())
|
||||
const seen = new Set()
|
||||
const out = []
|
||||
for (const line of raw.split('\n')) {
|
||||
const t = line.trim()
|
||||
if (!t || t.startsWith('#')) continue
|
||||
const eq = t.indexOf('=')
|
||||
if (eq < 0) continue
|
||||
const key = t.slice(0, eq).trim()
|
||||
if (!key || managed.has(key) || seen.has(key)) continue
|
||||
seen.add(key)
|
||||
out.push([key, t.slice(eq + 1)])
|
||||
}
|
||||
return out
|
||||
},
|
||||
|
||||
hermes_env_set({ key, value } = {}) {
|
||||
key = (key || '').trim()
|
||||
if (!key) throw new Error('Key cannot be empty')
|
||||
if (!/^[A-Z0-9_]+$/i.test(key)) {
|
||||
throw new Error(`Invalid env var key '${key}': only [A-Z0-9_] are allowed`)
|
||||
}
|
||||
const managed = new Set(this._hermesManagedEnvKeys())
|
||||
if (managed.has(key)) {
|
||||
throw new Error(`'${key}' is managed by ClawPanel; please configure it via the provider setup page`)
|
||||
}
|
||||
const envPath = path.join(hermesHome(), '.env')
|
||||
fs.mkdirSync(path.dirname(envPath), { recursive: true })
|
||||
const raw = fs.existsSync(envPath) ? fs.readFileSync(envPath, 'utf8') : ''
|
||||
const lines = raw.split('\n')
|
||||
const out = []
|
||||
let replaced = false
|
||||
for (const line of lines) {
|
||||
const t = line.trim()
|
||||
if (!t || t.startsWith('#')) { out.push(line); continue }
|
||||
const eq = t.indexOf('=')
|
||||
if (eq > 0 && t.slice(0, eq).trim() === key && !replaced) {
|
||||
out.push(`${key}=${value == null ? '' : value}`)
|
||||
replaced = true
|
||||
continue
|
||||
}
|
||||
out.push(line)
|
||||
}
|
||||
if (!replaced) out.push(`${key}=${value == null ? '' : value}`)
|
||||
let content = out.join('\n')
|
||||
if (!content.endsWith('\n')) content += '\n'
|
||||
fs.writeFileSync(envPath, content)
|
||||
return null
|
||||
},
|
||||
|
||||
hermes_env_delete({ key } = {}) {
|
||||
key = (key || '').trim()
|
||||
if (!key) throw new Error('Key cannot be empty')
|
||||
const managed = new Set(this._hermesManagedEnvKeys())
|
||||
if (managed.has(key)) {
|
||||
throw new Error(`'${key}' is managed by ClawPanel; please configure it via the provider setup page`)
|
||||
}
|
||||
const envPath = path.join(hermesHome(), '.env')
|
||||
if (!fs.existsSync(envPath)) return null
|
||||
const raw = fs.readFileSync(envPath, 'utf8')
|
||||
const lines = raw.split('\n')
|
||||
const out = []
|
||||
for (const line of lines) {
|
||||
const t = line.trim()
|
||||
if (!t || t.startsWith('#')) { out.push(line); continue }
|
||||
const eq = t.indexOf('=')
|
||||
if (eq > 0 && t.slice(0, eq).trim() === key) continue
|
||||
out.push(line)
|
||||
}
|
||||
let content = out.join('\n')
|
||||
if (!content.endsWith('\n')) content += '\n'
|
||||
fs.writeFileSync(envPath, content)
|
||||
return null
|
||||
},
|
||||
|
||||
async hermes_fetch_models({ baseUrl, apiKey, apiType, provider: _provider } = {}) {
|
||||
const api = apiType || 'openai'
|
||||
let base = baseUrl.replace(/\/+$/, '')
|
||||
|
||||
Reference in New Issue
Block a user