docs: document PostgreSQL-backed config/token store

This commit is contained in:
hkfires
2025-10-13 22:31:01 +08:00
parent 5123cfd47e
commit 50c1c50dbd
4 changed files with 47 additions and 18 deletions

View File

@@ -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_USERNAME` | No | | The username for Git authentication. |
| `GITSTORE_GIT_TOKEN` | No | | The personal access token (or password) for Git authentication. | | `GITSTORE_GIT_TOKEN` | No | | The personal access token (or password) for Git authentication. |
**How it Works** **How it Works**
1. **Cloning:** On startup, the application clones the remote Git repository to the `GITSTORE_LOCAL_PATH`. 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. 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. 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 `<value>/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_CACHE_DIR or CWD>/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 ### OpenAI Compatibility Providers
Configure upstream OpenAI-compatible providers (e.g., OpenRouter) via `openai-compatibility`. Configure upstream OpenAI-compatible providers (e.g., OpenRouter) via `openai-compatibility`.

View File

@@ -449,6 +449,26 @@ openai-compatibility:
3. **引导:** 如果仓库中不存在 `config/config.yaml`,应用程序会将本地的 `config.example.yaml` 复制到该位置,然后提交并推送到远程仓库作为初始配置。您必须确保 `config.example.yaml` 文件可用。 3. **引导:** 如果仓库中不存在 `config/config.yaml`,应用程序会将本地的 `config.example.yaml` 复制到该位置,然后提交并推送到远程仓库作为初始配置。您必须确保 `config.example.yaml` 文件可用。
4. **令牌同步:** `auth-dir` 也在此仓库中管理。对身份验证令牌的任何更改(例如,通过新的登录)都会自动提交并推送到远程 Git 仓库。 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_CACHE_DIR 或当前工作目录>/pgstore` 下建立可写缓存,复用 `config/config.yaml` 与 `auths/` 目录。
3. **引导:** 若数据库中无配置记录,会使用 `config.example.yaml` 初始化,并以固定标识 `config` 写入。
4. **令牌同步:** 配置与令牌的更改会写入 PostgreSQL同时数据库中的内容也会反向同步至本地镜像便于文件监听与管理接口继续工作。
### OpenAI 兼容上游提供商 ### OpenAI 兼容上游提供商
通过 `openai-compatibility` 配置上游 OpenAI 兼容提供商(例如 OpenRouter 通过 `openai-compatibility` 配置上游 OpenAI 兼容提供商(例如 OpenRouter

View File

@@ -105,7 +105,6 @@ func main() {
usePostgresStore bool usePostgresStore bool
pgStoreDSN string pgStoreDSN string
pgStoreSchema string pgStoreSchema string
pgStoreConfigKey string
pgStoreCacheDir string pgStoreCacheDir string
pgStoreInst *store.PostgresStore pgStoreInst *store.PostgresStore
gitStoreLocalPath string gitStoreLocalPath string
@@ -140,9 +139,6 @@ func main() {
if value, ok := lookupEnv("PGSTORE_SCHEMA", "pgstore_schema"); ok { if value, ok := lookupEnv("PGSTORE_SCHEMA", "pgstore_schema"); ok {
pgStoreSchema = value 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 { if value, ok := lookupEnv("PGSTORE_CACHE_DIR", "pgstore_cache_dir"); ok {
pgStoreCacheDir = value pgStoreCacheDir = value
} }
@@ -179,10 +175,9 @@ func main() {
pgStoreCacheDir = filepath.Join(pgStoreCacheDir, "pgstore") pgStoreCacheDir = filepath.Join(pgStoreCacheDir, "pgstore")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
pgStoreInst, err = store.NewPostgresStore(ctx, store.PostgresStoreConfig{ pgStoreInst, err = store.NewPostgresStore(ctx, store.PostgresStoreConfig{
DSN: pgStoreDSN, DSN: pgStoreDSN,
Schema: pgStoreSchema, Schema: pgStoreSchema,
ConfigKey: pgStoreConfigKey, SpoolDir: pgStoreCacheDir,
SpoolDir: pgStoreCacheDir,
}) })
cancel() cancel()
if err != nil { if err != nil {

View File

@@ -22,7 +22,7 @@ import (
const ( const (
defaultConfigTable = "config_store" defaultConfigTable = "config_store"
defaultAuthTable = "auth_store" defaultAuthTable = "auth_store"
defaultConfigKey = "default" defaultConfigKey = "config"
) )
// PostgresStoreConfig captures configuration required to initialize a Postgres-backed store. // PostgresStoreConfig captures configuration required to initialize a Postgres-backed store.
@@ -31,7 +31,6 @@ type PostgresStoreConfig struct {
Schema string Schema string
ConfigTable string ConfigTable string
AuthTable string AuthTable string
ConfigKey string
SpoolDir string SpoolDir string
} }
@@ -59,9 +58,6 @@ func NewPostgresStore(ctx context.Context, cfg PostgresStoreConfig) (*PostgresSt
if cfg.AuthTable == "" { if cfg.AuthTable == "" {
cfg.AuthTable = defaultAuthTable cfg.AuthTable = defaultAuthTable
} }
if cfg.ConfigKey == "" {
cfg.ConfigKey = defaultConfigKey
}
spoolRoot := strings.TrimSpace(cfg.SpoolDir) spoolRoot := strings.TrimSpace(cfg.SpoolDir)
if spoolRoot == "" { if spoolRoot == "" {
@@ -399,7 +395,7 @@ func (s *PostgresStore) PersistConfig(ctx context.Context) error {
func (s *PostgresStore) syncConfigFromDatabase(ctx context.Context, exampleConfigPath string) 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)) query := fmt.Sprintf("SELECT content FROM %s WHERE id = $1", s.fullTableName(s.cfg.ConfigTable))
var content string var content string
err := s.db.QueryRowContext(ctx, query, s.cfg.ConfigKey).Scan(&content) err := s.db.QueryRowContext(ctx, query, defaultConfigKey).Scan(&content)
switch { switch {
case errors.Is(err, sql.ErrNoRows): case errors.Is(err, sql.ErrNoRows):
if _, errStat := os.Stat(s.configPath); errors.Is(errStat, fs.ErrNotExist) { 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) ON CONFLICT (id)
DO UPDATE SET content = EXCLUDED.content, updated_at = NOW() DO UPDATE SET content = EXCLUDED.content, updated_at = NOW()
`, s.fullTableName(s.cfg.ConfigTable)) `, 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 fmt.Errorf("postgres store: upsert config: %w", err)
} }
return nil return nil
@@ -540,7 +536,7 @@ func (s *PostgresStore) persistConfig(ctx context.Context, data []byte) error {
func (s *PostgresStore) deleteConfigRecord(ctx context.Context) error { func (s *PostgresStore) deleteConfigRecord(ctx context.Context) error {
query := fmt.Sprintf("DELETE FROM %s WHERE id = $1", s.fullTableName(s.cfg.ConfigTable)) 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 fmt.Errorf("postgres store: delete config: %w", err)
} }
return nil return nil