diff --git a/API.en.md b/API.en.md index 2db8d03..967c258 100644 --- a/API.en.md +++ b/API.en.md @@ -587,6 +587,9 @@ Returns sanitized config. { "keys": ["k1", "k2"], "env_backed": false, + "env_source_present": true, + "env_writeback_enabled": true, + "config_path": "/data/config.json", "accounts": [ { "identifier": "user@example.com", diff --git a/API.md b/API.md index af4a1d9..aac0e3b 100644 --- a/API.md +++ b/API.md @@ -596,6 +596,9 @@ data: {"type":"message_stop"} { "keys": ["k1", "k2"], "env_backed": false, + "env_source_present": true, + "env_writeback_enabled": true, + "config_path": "/data/config.json", "accounts": [ { "identifier": "user@example.com", diff --git a/README.MD b/README.MD index fc15630..d22025a 100644 --- a/README.MD +++ b/README.MD @@ -320,6 +320,7 @@ cp opencode.json.example opencode.json | `DS2API_CONFIG_PATH` | 配置文件路径 | `config.json` | | `DS2API_CONFIG_JSON` | 直接注入配置(JSON 或 Base64) | — | | `CONFIG_JSON` | 旧版兼容配置注入 | — | +| `DS2API_ENV_WRITEBACK` | 环境变量模式下自动写回配置文件并切换文件模式(`1/true/yes/on`) | 关闭 | | `DS2API_WASM_PATH` | PoW WASM 文件路径 | 自动查找 | | `DS2API_STATIC_ADMIN_DIR` | 管理台静态文件目录 | `static/admin` | | `DS2API_AUTO_BUILD_WEBUI` | 启动时自动构建 WebUI | 本地开启,Vercel 关闭 | @@ -342,6 +343,8 @@ cp opencode.json.example opencode.json | `VERCEL_TEAM_ID` | Vercel 团队 ID | — | | `DS2API_VERCEL_PROTECTION_BYPASS` | Vercel 部署保护绕过密钥(内部 Node→Go 调用) | — | +> 提示:当检测到 `DS2API_CONFIG_JSON/CONFIG_JSON` 时,管理台会显示当前模式风险与自动持久化状态(含 `DS2API_CONFIG_PATH` 路径与模式切换说明)。 + ## 鉴权模式 调用业务接口(`/v1/*`、`/anthropic/*`、Gemini 路由)时支持两种模式: diff --git a/README.en.md b/README.en.md index 2127aa0..cf2e376 100644 --- a/README.en.md +++ b/README.en.md @@ -320,6 +320,7 @@ cp opencode.json.example opencode.json | `DS2API_CONFIG_PATH` | Config file path | `config.json` | | `DS2API_CONFIG_JSON` | Inline config (JSON or Base64) | — | | `CONFIG_JSON` | Legacy compatibility config input | — | +| `DS2API_ENV_WRITEBACK` | Auto-write env-backed config to file and transition to file mode (`1/true/yes/on`) | Disabled | | `DS2API_WASM_PATH` | PoW WASM file path | Auto-detect | | `DS2API_STATIC_ADMIN_DIR` | Admin static assets dir | `static/admin` | | `DS2API_AUTO_BUILD_WEBUI` | Auto-build WebUI on startup | Enabled locally, disabled on Vercel | @@ -339,6 +340,8 @@ cp opencode.json.example opencode.json | `VERCEL_TEAM_ID` | Vercel team ID | — | | `DS2API_VERCEL_PROTECTION_BYPASS` | Vercel deployment protection bypass for internal Node→Go calls | — | +> Note: when `DS2API_CONFIG_JSON/CONFIG_JSON` is detected, the Admin UI shows mode risk and auto-persistence status (including `DS2API_CONFIG_PATH` and mode-transition hints). + ## Authentication Modes For business endpoints (`/v1/*`, `/anthropic/*`, Gemini routes), DS2API supports two modes: diff --git a/docs/DEPLOY.en.md b/docs/DEPLOY.en.md index 33bf0c7..917bd71 100644 --- a/docs/DEPLOY.en.md +++ b/docs/DEPLOY.en.md @@ -248,6 +248,7 @@ VERCEL_TEAM_ID=team_xxxxxxxxxxxx # optional for personal accounts | `DS2API_ACCOUNT_QUEUE_SIZE` | Alias (legacy compat) | — | | `DS2API_GLOBAL_MAX_INFLIGHT` | Global inflight limit | `recommended_concurrency` | | `DS2API_MAX_INFLIGHT` | Alias (legacy compat) | — | +| `DS2API_ENV_WRITEBACK` | When `DS2API_CONFIG_JSON` is present, auto-write to `DS2API_CONFIG_PATH` and switch to file-backed mode after success (`1/true/yes/on`) | Disabled | | `DS2API_VERCEL_INTERNAL_SECRET` | Hybrid streaming internal auth | Falls back to `DS2API_ADMIN_KEY` | | `DS2API_VERCEL_STREAM_LEASE_TTL_SECONDS` | Stream lease TTL | `900` | | `VERCEL_TOKEN` | Vercel sync token | — | diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index 01b64ac..61b50f0 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -248,6 +248,7 @@ VERCEL_TEAM_ID=team_xxxxxxxxxxxx # 个人账号可留空 | `DS2API_ACCOUNT_QUEUE_SIZE` | 同上(兼容别名) | — | | `DS2API_GLOBAL_MAX_INFLIGHT` | 全局并发上限 | `recommended_concurrency` | | `DS2API_MAX_INFLIGHT` | 同上(兼容别名) | — | +| `DS2API_ENV_WRITEBACK` | 检测到 `DS2API_CONFIG_JSON` 时自动写入 `DS2API_CONFIG_PATH`,并在成功后转为文件模式(`1/true/yes/on`) | 关闭 | | `DS2API_VERCEL_INTERNAL_SECRET` | 混合流式内部鉴权 | 回退用 `DS2API_ADMIN_KEY` | | `DS2API_VERCEL_STREAM_LEASE_TTL_SECONDS` | 流式 lease TTL | `900` | | `VERCEL_TOKEN` | Vercel 同步 token | — | diff --git a/internal/admin/deps.go b/internal/admin/deps.go index c7a8472..054deb0 100644 --- a/internal/admin/deps.go +++ b/internal/admin/deps.go @@ -21,6 +21,9 @@ type ConfigStore interface { Update(mutator func(*config.Config) error) error ExportJSONAndBase64() (string, string, error) IsEnvBacked() bool + IsEnvWritebackEnabled() bool + HasEnvConfigSource() bool + ConfigPath() string SetVercelSync(hash string, ts int64) error AdminPasswordHash() string AdminJWTExpireHours() int diff --git a/internal/admin/handler_config_read.go b/internal/admin/handler_config_read.go index 9839887..73eda25 100644 --- a/internal/admin/handler_config_read.go +++ b/internal/admin/handler_config_read.go @@ -8,9 +8,12 @@ import ( func (h *Handler) getConfig(w http.ResponseWriter, _ *http.Request) { snap := h.Store.Snapshot() safe := map[string]any{ - "keys": snap.Keys, - "accounts": []map[string]any{}, - "env_backed": h.Store.IsEnvBacked(), + "keys": snap.Keys, + "accounts": []map[string]any{}, + "env_backed": h.Store.IsEnvBacked(), + "env_source_present": h.Store.HasEnvConfigSource(), + "env_writeback_enabled": h.Store.IsEnvWritebackEnabled(), + "config_path": h.Store.ConfigPath(), "claude_mapping": func() map[string]string { if len(snap.ClaudeMapping) > 0 { return snap.ClaudeMapping diff --git a/internal/config/store.go b/internal/config/store.go index feebb99..cf02967 100644 --- a/internal/config/store.go +++ b/internal/config/store.go @@ -267,6 +267,22 @@ func (s *Store) IsEnvBacked() bool { return s.fromEnv } +func (s *Store) IsEnvWritebackEnabled() bool { + return envWritebackEnabled() +} + +func (s *Store) HasEnvConfigSource() bool { + rawCfg := strings.TrimSpace(os.Getenv("DS2API_CONFIG_JSON")) + if rawCfg == "" { + rawCfg = strings.TrimSpace(os.Getenv("CONFIG_JSON")) + } + return rawCfg != "" +} + +func (s *Store) ConfigPath() string { + return s.path +} + func (s *Store) SetVercelSync(hash string, ts int64) error { return s.Update(func(c *Config) error { c.VercelSyncHash = hash diff --git a/webui/src/features/account/AccountManagerContainer.jsx b/webui/src/features/account/AccountManagerContainer.jsx index 6f70b66..bfe77d7 100644 --- a/webui/src/features/account/AccountManagerContainer.jsx +++ b/webui/src/features/account/AccountManagerContainer.jsx @@ -64,6 +64,27 @@ export default function AccountManagerContainer({ config, onRefresh, onMessage, return (
+ {Boolean(config?.env_source_present) && ( +
+

+ {config?.env_writeback_enabled + ? (config?.env_backed + ? t('accountManager.envModeWritebackPendingTitle') + : t('accountManager.envModeWritebackActiveTitle')) + : t('accountManager.envModeRiskTitle')} +

+

+ {config?.env_writeback_enabled + ? t('accountManager.envModeWritebackDesc', { path: config?.config_path || 'config.json' }) + : t('accountManager.envModeRiskDesc')} +

+
+ )} +