diff --git a/.env.example b/.env.example
index f93f491..60a7cdb 100644
--- a/.env.example
+++ b/.env.example
@@ -29,6 +29,7 @@ OPENCODE_PROXY_CLEANUP_MAX_AGE_MS=86400000
# Optional overrides
# OPENCODE_PROXY_PORT=10000
# OPENCODE_SERVER_PORT=10001
+# OPENCODE_SERVER_URL=http://127.0.0.1:10001
# BIND_HOST=0.0.0.0
# OPENCODE_PROXY_BIND_HOST=0.0.0.0
# Canonical tool switch (legacy DISABLE_TOOLS also read; canonical wins)
diff --git a/README.md b/README.md
index 9347e5d..9fedb6f 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
-
+
> **Language:** [English](./README.md) | [简体中文](./README.zh-CN.md)
diff --git a/README.zh-CN.md b/README.zh-CN.md
index db16ec2..347b278 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -3,7 +3,7 @@
-
+
> **語言:** [English](./README.md) | [简体中文](./README.zh-CN.md)
diff --git a/docs/AI_BOT.md b/docs/AI_BOT.md
index c02136b..7ca2c83 100644
--- a/docs/AI_BOT.md
+++ b/docs/AI_BOT.md
@@ -82,7 +82,7 @@ Point at a local gateway (start one with `API_KEY= OPENCODE_PROXY_MANAGE_BACKEND
or fill placeholders — never commit real values:
```bash
-export GATEWAY_BASE_URL='http://127.0.0.1:10000' GATEWAY_API_KEY='' CPA_BASE_URL='...' CPA_API_KEY='...'
+export GATEWAY_BASE_URL='http://127.0.0.1:10000' GATEWAY_API_KEY='' CPA_BASE_URL=your-cpa-url-here CPA_API_KEY=your-cpa-key-here
export PYTHONPATH=github_bot/src
python3 github_bot/src/github_runner.py --mode=review --dry-run
python3 github_bot/src/github_runner.py --mode=triage --dry-run
diff --git a/docs/configuration.md b/docs/configuration.md
index 14e894a..097bbc5 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -30,7 +30,7 @@
| 变量 | 默认值 | 说明 |
|:-----|:-------|:-----|
-| `DISABLE_TOOLS` | `true` | 禁用 OpenCode 工具调用(兼容别名;`OPENCODE_DISABLE_TOOLS` 优先,二者无效值都会让位给下一顺位:canonical env > legacy env > `config.json` > 默认) |
+| `OPENCODE_DISABLE_TOOLS` / `DISABLE_TOOLS` | `true` | 禁用 OpenCode 工具调用(兼容别名;`OPENCODE_DISABLE_TOOLS` 优先,二者无效值都会让位给下一顺位:canonical env > legacy env > `config.json` > 默认) |
| `OPENCODE_EXTERNAL_TOOLS_MODE` | `proxy-bridge` | 外部工具桥接模式;当前仅支持 `proxy-bridge` |
| `OPENCODE_EXTERNAL_TOOLS_CONFLICT_POLICY` | `namespace` | 外部工具冲突隔离策略;当前仅支持 `namespace` |
| `OPENCODE_INTERNAL_WEB_FETCH_ENABLED` | `false` | 兼容旧开关;未显式配置 allowlist 时,启用后默认放行 `web_fetch` |
@@ -72,6 +72,7 @@
| 变量 | 默认值 | 说明 |
|:-----|:-------|:-----|
| `OPENCODE_PROXY_DEBUG` | `false` | 开启调试日志(`config.json` 中用短键 `DEBUG`) |
+| `OPENCODE_PROXY_MANAGE_BACKEND` | `false` | 是否由代理拉起本地后端(`config.json` 中用短键 `MANAGE_BACKEND`;prod/`index.ts` 默认 `false`,library/`buildProxyConfig` 默认 `true`——已知双入口差异) |
| `OPENCODE_PATH` | `opencode` | OpenCode 可执行文件路径 |
| `OPENCODE_ZEN_API_KEY` | - | Zen API Key 透传 |
@@ -92,6 +93,10 @@
"INTERNAL_ALLOWED_TOOLS": ["web_fetch"],
"INTERNAL_TOOL_METRICS_ENABLED": true,
"INTERNAL_TOOL_DISCOVERY_FIXTURE": [],
+ "HEALTH_DETAILS_ENABLED": true,
+ "HEALTH_DETAILS_REQUIRE_AUTH": true,
+ "METRICS_ENABLED": false,
+ "METRICS_REQUIRE_AUTH": true,
"USE_ISOLATED_HOME": false,
"PROMPT_MODE": "standard",
"OMIT_SYSTEM_PROMPT": false,
@@ -100,9 +105,16 @@
"CLEANUP_MAX_AGE_MS": 86400000,
"DEBUG": false,
"OPENCODE_SERVER_URL": "http://127.0.0.1:10001",
+ "OPENCODE_SERVER_PASSWORD": "",
"OPENCODE_PATH": "opencode",
+ "ZEN_API_KEY": "",
+ "MANAGE_BACKEND": false,
"REQUEST_TIMEOUT_MS": 180000,
- "RETRY_MAX_RETRIES": 3
+ "RETRY_MAX_RETRIES": 3,
+ "UPSTREAM_PROXIES": [],
+ "UPSTREAM_PROXY_STRATEGY": "failover-rr",
+ "UPSTREAM_PROXY_COOLDOWN_MS": 300000,
+ "UPSTREAM_PROXY_NO_PROXY": ["localhost", "127.0.0.1", "::1"]
}
```
diff --git a/docs/development.md b/docs/development.md
index aae0254..c4e36f4 100644
--- a/docs/development.md
+++ b/docs/development.md
@@ -11,7 +11,7 @@
### Node.js 环境
```bash
-# Node.js 18+
+# Node.js >=22.19 (engines floor; CI uses 24)
node --version
# npm
diff --git a/github_bot/README.md b/github_bot/README.md
index 1bb047c..669c840 100644
--- a/github_bot/README.md
+++ b/github_bot/README.md
@@ -36,9 +36,9 @@ To use an external gateway or CPA, export names only (never commit values):
```bash
export GATEWAY_BASE_URL='http://127.0.0.1:10000' # external override; in-job default when unset
-export GATEWAY_API_KEY='...' # external gateway key; empty = no-auth localhost
-export CPA_BASE_URL='...' # never commit (fallback channel)
-export CPA_API_KEY='...' # never commit (fallback channel)
+export GATEWAY_API_KEY=your-gateway-key-here # external gateway key; empty = no-auth localhost
+export CPA_BASE_URL=your-cpa-url-here # never commit (fallback channel)
+export CPA_API_KEY=your-cpa-key-here # never commit (fallback channel)
export PYTHONPATH=github_bot/src
python3 github_bot/src/github_runner.py --mode=review --dry-run
python3 github_bot/src/github_runner.py --mode=triage --dry-run
diff --git a/src/proxy.ts b/src/proxy.ts
index 990d94f..e383fc1 100644
--- a/src/proxy.ts
+++ b/src/proxy.ts
@@ -100,7 +100,10 @@ export function createApp(config: ProxyConfig): CreateAppResult {
const client = rawClient as unknown as ProxyClient;
// Multi-key auth (A): legacy single + list merge; empty = no auth (unchanged).
- // Single shared verifier instance (also exposed via ctx for system routes).
+ // Two verifier instances read the same effective key content (middleware
+ // here, system routes via ctx.API_KEYS); each copies at construction
+ // (spread in system routes, filter in keys), so keep the inputs in sync
+ // instead of assuming a shared reference.
const effectiveApiKeys: string[] = buildEffectiveApiKeys(API_KEY, API_KEYS);
const apiKeyVerifier = createApiKeyVerifier(effectiveApiKeys);
diff --git a/src/search/grounding.ts b/src/search/grounding.ts
index 333fd8a..56940c9 100644
--- a/src/search/grounding.ts
+++ b/src/search/grounding.ts
@@ -3,6 +3,7 @@
// Maps OpenAI-style hosted search tools (`tools:[{type:web_search}]`) onto the
// opencode built-in `websearch` tool and rebuilds `web_search_call` output
// items + `url_citation` annotations from the backend tool parts.
+import { asRecord } from '../utils/guards.js';
export const HOSTED_SEARCH_TOOL_TYPES = ['web_search', 'web_search_preview', 'google_search'];
/** True for hosted search tool types, incl. versioned variants (e.g. Anthropic `web_search_20260222`). */
@@ -18,11 +19,6 @@ export const SEARCH_GROUNDING_INSTRUCTION =
'only from the conversation plus the search results. ' +
'Include the source URLs you relied on verbatim in your answer so they can be cited.';
-function asRecord(value: unknown): Record {
- if (value && typeof value === 'object' && !Array.isArray(value)) return value as Record;
- return {};
-}
-
function toolTypeOf(def: unknown): string {
return typeof asRecord(def)['type'] === 'string' ? String(asRecord(def)['type']) : '';
}