mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-02 20:40:52 +08:00
docs: document PostgreSQL-backed config/token store
This commit is contained in:
22
README.md
22
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_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`.
|
||||||
|
|||||||
20
README_CN.md
20
README_CN.md
@@ -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)。
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user