From 61f6a612e3ca848ae5d561b70bcbe45d743666f3 Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Mon, 13 Oct 2025 22:31:01 +0800 Subject: [PATCH] docs: document PostgreSQL-backed config/token store --- README.md | 22 ++++++++++++++++++++-- README_CN.md | 20 ++++++++++++++++++++ cmd/server/main.go | 11 +++-------- internal/store/postgresstore.go | 12 ++++-------- 4 files changed, 47 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 61a72015..7d62b888 100644 --- a/README.md +++ b/README.md @@ -429,8 +429,6 @@ To enable this feature, set the `GITSTORE_GIT_URL` environment variable to the U | `GITSTORE_GIT_USERNAME` | No | | The username for Git authentication. | | `GITSTORE_GIT_TOKEN` | No | | The personal access token (or password) for Git authentication. | - - **How it Works** 1. **Cloning:** On startup, the application clones the remote Git repository to the `GITSTORE_LOCAL_PATH`. @@ -438,6 +436,26 @@ To enable this feature, set the `GITSTORE_GIT_URL` environment variable to the U 3. **Bootstrapping:** If `config/config.yaml` does not exist in the repository, the application will copy the local `config.example.yaml` to that location, commit, and push it to the remote repository as an initial configuration. You must have `config.example.yaml` available. 4. **Token Sync:** The `auth-dir` is also managed within this repository. Any changes to authentication tokens (e.g., through a new login) are automatically committed and pushed to the remote Git repository. +### PostgreSQL-backed Configuration and Token Store + +You can also persist configuration and authentication data in PostgreSQL when running CLIProxyAPI in hosted environments that favor managed databases over local files. + +**Environment Variables** + +| Variable | Required | Default | Description | +|-----------------------|----------|-----------------------|---------------------------------------------------------------------------------------------------------------| +| `MANAGEMENT_PASSWORD` | Yes | | Password for the management web UI (required when remote management is enabled). | +| `PGSTORE_DSN` | Yes | | PostgreSQL connection string (e.g. `postgresql://user:pass@host:5432/db`). | +| `PGSTORE_SCHEMA` | No | public | Schema where the tables will be created. Leave empty to use the default schema. | +| `PGSTORE_CACHE_DIR` | No | Current working directory | Root directory for the local mirror; the server writes to `/pgstore`. If unset and CWD is unavailable, `/tmp/pgstore` is used. | + +**How it Works** + +1. **Initialization:** On startup the server connects via `PGSTORE_DSN`, ensures the schema exists, and creates the `config_store` / `auth_store` tables when missing. +2. **Local Mirror:** A writable cache at `/pgstore` mirrors `config/config.yaml` and `auths/` so the rest of the application can reuse the existing file-based logic. +3. **Bootstrapping:** If no configuration row exists, `config.example.yaml` seeds the database using the fixed identifier `config`. +4. **Token Sync:** Changes flow both ways—file updates are written to PostgreSQL and database records are mirrored back to disk so watchers and management APIs continue to operate. + ### OpenAI Compatibility Providers Configure upstream OpenAI-compatible providers (e.g., OpenRouter) via `openai-compatibility`. diff --git a/README_CN.md b/README_CN.md index 0308168d..10572a76 100644 --- a/README_CN.md +++ b/README_CN.md @@ -449,6 +449,26 @@ openai-compatibility: 3. **引导:** 如果仓库中不存在 `config/config.yaml`,应用程序会将本地的 `config.example.yaml` 复制到该位置,然后提交并推送到远程仓库作为初始配置。您必须确保 `config.example.yaml` 文件可用。 4. **令牌同步:** `auth-dir` 也在此仓库中管理。对身份验证令牌的任何更改(例如,通过新的登录)都会自动提交并推送到远程 Git 仓库。 +### PostgreSQL 支持的配置与令牌存储 + +在托管环境中运行服务时,可以选择使用 PostgreSQL 来保存配置与令牌,借助托管数据库减轻本地文件管理压力。 + +**环境变量** + +| 变量 | 必需 | 默认值 | 描述 | +|-------------------------|----|---------------|----------------------------------------------------------------------| +| `MANAGEMENT_PASSWORD` | 是 | | 管理面板密码(启用远程管理时必需)。 | +| `PGSTORE_DSN` | 是 | | PostgreSQL 连接串,例如 `postgresql://user:pass@host:5432/db`。 | +| `PGSTORE_SCHEMA` | 否 | public | 创建表时使用的 schema;留空则使用默认 schema。 | +| `PGSTORE_CACHE_DIR` | 否 | 当前工作目录 | 本地镜像根目录,服务将在 `<值>/pgstore` 下写入缓存;若无法获取工作目录则退回 `/tmp/pgstore`。 | + +**工作原理** + +1. **初始化:** 启动时通过 `PGSTORE_DSN` 连接数据库,确保 schema 存在,并在缺失时创建 `config_store` 与 `auth_store`。 +2. **本地镜像:** 在 `/pgstore` 下建立可写缓存,复用 `config/config.yaml` 与 `auths/` 目录。 +3. **引导:** 若数据库中无配置记录,会使用 `config.example.yaml` 初始化,并以固定标识 `config` 写入。 +4. **令牌同步:** 配置与令牌的更改会写入 PostgreSQL,同时数据库中的内容也会反向同步至本地镜像,便于文件监听与管理接口继续工作。 + ### OpenAI 兼容上游提供商 通过 `openai-compatibility` 配置上游 OpenAI 兼容提供商(例如 OpenRouter)。 diff --git a/cmd/server/main.go b/cmd/server/main.go index 6690ac64..77f7b29b 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -105,7 +105,6 @@ func main() { usePostgresStore bool pgStoreDSN string pgStoreSchema string - pgStoreConfigKey string pgStoreCacheDir string pgStoreInst *store.PostgresStore gitStoreLocalPath string @@ -140,9 +139,6 @@ func main() { if value, ok := lookupEnv("PGSTORE_SCHEMA", "pgstore_schema"); ok { pgStoreSchema = value } - if value, ok := lookupEnv("PGSTORE_CONFIG_KEY", "pgstore_config_key"); ok { - pgStoreConfigKey = value - } if value, ok := lookupEnv("PGSTORE_CACHE_DIR", "pgstore_cache_dir"); ok { pgStoreCacheDir = value } @@ -179,10 +175,9 @@ func main() { pgStoreCacheDir = filepath.Join(pgStoreCacheDir, "pgstore") ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) pgStoreInst, err = store.NewPostgresStore(ctx, store.PostgresStoreConfig{ - DSN: pgStoreDSN, - Schema: pgStoreSchema, - ConfigKey: pgStoreConfigKey, - SpoolDir: pgStoreCacheDir, + DSN: pgStoreDSN, + Schema: pgStoreSchema, + SpoolDir: pgStoreCacheDir, }) cancel() if err != nil { diff --git a/internal/store/postgresstore.go b/internal/store/postgresstore.go index eb42e743..f5d968e8 100644 --- a/internal/store/postgresstore.go +++ b/internal/store/postgresstore.go @@ -22,7 +22,7 @@ import ( const ( defaultConfigTable = "config_store" defaultAuthTable = "auth_store" - defaultConfigKey = "default" + defaultConfigKey = "config" ) // PostgresStoreConfig captures configuration required to initialize a Postgres-backed store. @@ -31,7 +31,6 @@ type PostgresStoreConfig struct { Schema string ConfigTable string AuthTable string - ConfigKey string SpoolDir string } @@ -59,9 +58,6 @@ func NewPostgresStore(ctx context.Context, cfg PostgresStoreConfig) (*PostgresSt if cfg.AuthTable == "" { cfg.AuthTable = defaultAuthTable } - if cfg.ConfigKey == "" { - cfg.ConfigKey = defaultConfigKey - } spoolRoot := strings.TrimSpace(cfg.SpoolDir) if spoolRoot == "" { @@ -399,7 +395,7 @@ func (s *PostgresStore) PersistConfig(ctx context.Context) error { func (s *PostgresStore) syncConfigFromDatabase(ctx context.Context, exampleConfigPath string) error { query := fmt.Sprintf("SELECT content FROM %s WHERE id = $1", s.fullTableName(s.cfg.ConfigTable)) var content string - err := s.db.QueryRowContext(ctx, query, s.cfg.ConfigKey).Scan(&content) + err := s.db.QueryRowContext(ctx, query, defaultConfigKey).Scan(&content) switch { case errors.Is(err, sql.ErrNoRows): if _, errStat := os.Stat(s.configPath); errors.Is(errStat, fs.ErrNotExist) { @@ -532,7 +528,7 @@ func (s *PostgresStore) persistConfig(ctx context.Context, data []byte) error { ON CONFLICT (id) DO UPDATE SET content = EXCLUDED.content, updated_at = NOW() `, s.fullTableName(s.cfg.ConfigTable)) - if _, err := s.db.ExecContext(ctx, query, s.cfg.ConfigKey, string(data)); err != nil { + if _, err := s.db.ExecContext(ctx, query, defaultConfigKey, string(data)); err != nil { return fmt.Errorf("postgres store: upsert config: %w", err) } return nil @@ -540,7 +536,7 @@ func (s *PostgresStore) persistConfig(ctx context.Context, data []byte) error { func (s *PostgresStore) deleteConfigRecord(ctx context.Context) error { query := fmt.Sprintf("DELETE FROM %s WHERE id = $1", s.fullTableName(s.cfg.ConfigTable)) - if _, err := s.db.ExecContext(ctx, query, s.cfg.ConfigKey); err != nil { + if _, err := s.db.ExecContext(ctx, query, defaultConfigKey); err != nil { return fmt.Errorf("postgres store: delete config: %w", err) } return nil