diff --git a/README.en.md b/README.en.md index 537b0d57..1ece6424 100644 --- a/README.en.md +++ b/README.en.md @@ -329,6 +329,25 @@ Gemini / OpenCode / Antigravity / GitHub Copilot), with no MCP protocol support --- +### Per-Bot Environment Variables + +Each `bots.json` entry can define its own `env` object. These variables are injected into that bot's daemon/CLI process, which is useful for per-bot proxies, API base URLs, or CLI-specific feature flags: + +```json +{ + "cliId": "codex", + "workingDir": "~/projects", + "env": { + "HTTPS_PROXY": "http://127.0.0.1:7890", + "OPENAI_BASE_URL": "https://api.example.com/v1" + } +} +``` + +`env` only accepts valid environment variable names, with string, number, or boolean values. Do not treat it as a secret vault: process environments may be visible to local diagnostic tools. + +--- + ## 📖 Documentation The full reference — commands, config, best practices, troubleshooting — lives in the docs site; not duplicated here — diff --git a/README.md b/README.md index abd28a83..6c2fef5f 100644 --- a/README.md +++ b/README.md @@ -322,6 +322,25 @@ PersonalAgent 默认配好事件订阅 + bot 能力,正常情况下不用动 --- +### Bot 级环境变量 + +每个 `bots.json` 条目都可以配置独立的 `env` 对象,用于给对应 bot 的 daemon/CLI 进程注入环境变量,例如代理、API base URL 或 CLI 专属开关: + +```json +{ + "cliId": "codex", + "workingDir": "~/projects", + "env": { + "HTTPS_PROXY": "http://127.0.0.1:7890", + "OPENAI_BASE_URL": "https://api.example.com/v1" + } +} +``` + +`env` 只接受合法环境变量名,值支持字符串、数字、布尔值;不要把它当成安全密钥库,进程环境可能被本机诊断工具看到。 + +--- + ## 📖 完整文档 命令、配置、最佳实践、排错的完整内容都在文档站,这里不再重复 —— diff --git a/bots.json.example b/bots.json.example index 6b362b75..16661898 100644 --- a/bots.json.example +++ b/bots.json.example @@ -8,7 +8,11 @@ "allowedUsers": ["alice@company.com"], "allowedChatGroups": ["oc_xxx_team"], "workingDir": "~/projects", - "customPassthroughCommands": ["/goal", "/export"] + "customPassthroughCommands": ["/goal", "/export"], + "env": { + "HTTPS_PROXY": "http://127.0.0.1:7890", + "OPENAI_BASE_URL": "https://api.example.com/v1" + } }, { "larkAppId": "cli_xxx_bot2", diff --git a/src/cli.ts b/src/cli.ts index 2bca01ce..103cac44 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -35,6 +35,7 @@ import { writeBotsJsonAtomic as writeBotsAtomic } from './setup/bots-store.js'; import { applyBotConfigEdits, assertUniqueBotProcessNames, + botProcessEnv, botProcessName, normalizeBotConfig, parseBotConfigsJson, @@ -254,6 +255,7 @@ function ecosystemConfig(): string { error_file: join(LOG_DIR, `daemon-${i}-error.log`), out_file: join(LOG_DIR, `daemon-${i}-out.log`), env: { + ...botProcessEnv(_bot), SESSION_DATA_DIR: DATA_DIR, BOTMUX_BOT_INDEX: String(i), // Native-memory diagnostics. Default off; operator can flip it on diff --git a/src/setup/bot-config-editor.ts b/src/setup/bot-config-editor.ts index 3b3f3eb3..c8259751 100644 --- a/src/setup/bot-config-editor.ts +++ b/src/setup/bot-config-editor.ts @@ -166,6 +166,19 @@ export function botProcessName( return `${prefix}-${name ?? index}`; } +export function botProcessEnv(bot: { env?: unknown }): Record { + const raw = bot && typeof bot === 'object' && bot.env && typeof bot.env === 'object' && !Array.isArray(bot.env) + ? bot.env as Record + : {}; + const out: Record = {}; + for (const [key, value] of Object.entries(raw)) { + if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue; + if (typeof value === 'string') out[key] = value; + else if (typeof value === 'number' || typeof value === 'boolean') out[key] = String(value); + } + return out; +} + export function normalizeBotConfig>(bot: T): T { const out: Record = { ...bot }; if (typeof out.name !== 'string') return out as T; diff --git a/test/bot-config-editor.test.ts b/test/bot-config-editor.test.ts index 561e94a3..871db99d 100644 --- a/test/bot-config-editor.test.ts +++ b/test/bot-config-editor.test.ts @@ -3,6 +3,7 @@ import { applyBotConfigEdits, assertUniqueBotProcessNames, assertOwnerWhenChatGroups, + botProcessEnv, botProcessName, findInvalidAllowedUserEntries, hasOwnerEntry, @@ -222,6 +223,43 @@ describe('applyBotConfigEdits', () => { }); }); +describe('botProcessEnv', () => { + it('keeps valid process env keys and stringifies primitive values', () => { + expect(botProcessEnv({ + env: { + HTTPS_PROXY: 'http://127.0.0.1:7890', + OPENAI_TIMEOUT_MS: 30000, + FEATURE_FLAG: true, + EMPTY_VALUE: '', + }, + })).toEqual({ + HTTPS_PROXY: 'http://127.0.0.1:7890', + OPENAI_TIMEOUT_MS: '30000', + FEATURE_FLAG: 'true', + EMPTY_VALUE: '', + }); + }); + + it('drops invalid keys and non-primitive values', () => { + expect(botProcessEnv({ + env: { + '1BAD': 'x', + 'BAD-NAME': 'x', + OK_NAME: ['x'], + ALSO_OK: { nested: true }, + NULLISH: null, + VALID_NAME: false, + }, + })).toEqual({ VALID_NAME: 'false' }); + }); + + it('returns an empty object when env is missing or not an object', () => { + expect(botProcessEnv({})).toEqual({}); + expect(botProcessEnv({ env: [] })).toEqual({}); + expect(botProcessEnv({ env: 'HTTPS_PROXY=x' })).toEqual({}); + }); +}); + describe('resolveCliId', () => { it('returns undefined for empty input so callers can preserve current cliId', () => { expect(resolveCliId('')).toBeUndefined();