feat: load backend config from yaml
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,6 +2,7 @@ node_modules/
|
|||||||
dist/
|
dist/
|
||||||
target/
|
target/
|
||||||
**/src-tauri/gen/
|
**/src-tauri/gen/
|
||||||
|
backend/etc/agent.local.yaml
|
||||||
.env
|
.env
|
||||||
data/
|
data/
|
||||||
coverage/
|
coverage/
|
||||||
|
|||||||
98
README.md
98
README.md
@@ -1,27 +1,101 @@
|
|||||||
# SenlinAI Agent Workbench
|
# SenlinAI Agent Workbench
|
||||||
|
|
||||||
Private project-centered workbench MVP.
|
SenlinAI Agent Workbench 是一个以项目为中心的私有化工作台 MVP。
|
||||||
|
|
||||||
## Development
|
## 本地开发
|
||||||
|
|
||||||
|
启动本地 PostgreSQL:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
docker compose -f infra/docker-compose.yml up -d
|
docker compose -f infra/docker-compose.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
启动后端 API:
|
||||||
|
|
||||||
|
```powershell
|
||||||
Set-Location backend
|
Set-Location backend
|
||||||
go run ./cmd/api
|
go run ./cmd/api
|
||||||
```
|
```
|
||||||
|
|
||||||
## Frontend
|
启动 Web 客户端:
|
||||||
|
|
||||||
The web client uses Svelte and TypeScript only. The login shell lets users enter a server IP address or domain name, which is saved as the API base URL.
|
```powershell
|
||||||
|
Set-Location apps/web
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
## Backend Configuration
|
## 后端配置
|
||||||
|
|
||||||
- `PORT`: API listen port, default `8080`.
|
后端配置不再从多个环境变量直接读取,而是从 `backend/etc/agent.<mode>.yaml` 读取。
|
||||||
- `DATABASE_URL`: PostgreSQL connection string. Do not commit real credentials.
|
|
||||||
- `AUTH_SECRET`: HMAC secret for invite and session tokens.
|
|
||||||
- `SYSTEM_AI_KEY`: optional fallback AI provider key.
|
|
||||||
- `AI_KEY_ENCRYPTION_SECRET`: secret used to encrypt user AI keys at rest.
|
|
||||||
|
|
||||||
## Verification
|
配置模式由环境变量 `SENLIN_APP_MODE` 决定:
|
||||||
|
|
||||||
See `docs/mvp-verification.md` for backend, web, desktop, PostgreSQL, and manual MVP verification steps.
|
- 未设置 `SENLIN_APP_MODE` 时,默认读取 `backend/etc/agent.dev.yaml`。
|
||||||
|
- 设置 `SENLIN_APP_MODE=prod` 时,读取 `backend/etc/agent.prod.yaml`。
|
||||||
|
- 设置其他值时,按同样规则读取 `backend/etc/agent.<value>.yaml`。
|
||||||
|
|
||||||
|
默认开发配置文件:
|
||||||
|
|
||||||
|
```text
|
||||||
|
backend/etc/agent.dev.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
配置字段:
|
||||||
|
|
||||||
|
- `env`: 运行环境,例如 `development`。
|
||||||
|
- `port`: API 监听端口,默认开发值为 `8080`。
|
||||||
|
- `database_url`: PostgreSQL 连接串。不要提交真实生产或测试凭据。
|
||||||
|
- `storage_dir`: 服务端本地文件存储目录。
|
||||||
|
- `auth_secret`: 邀请 token 和 session token 的 HMAC 密钥。
|
||||||
|
- `system_ai_key`: 可选的系统级 AI provider fallback key。
|
||||||
|
- `ai_key_encryption_secret`: 用户 AI key 静态加密密钥。
|
||||||
|
|
||||||
|
## 前端
|
||||||
|
|
||||||
|
Web 客户端只使用 Svelte 和 TypeScript。登录界面允许用户输入服务器 IP 地址或域名,并将其保存为 API base URL。
|
||||||
|
|
||||||
|
## 桌面端
|
||||||
|
|
||||||
|
桌面端使用 Tauri 包装 Web 客户端。
|
||||||
|
|
||||||
|
构建桌面可执行文件:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Set-Location apps/desktop
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
生成安装包:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Set-Location apps/desktop
|
||||||
|
npm run bundle
|
||||||
|
```
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
后端:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Set-Location backend
|
||||||
|
go test ./... -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Web:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Set-Location apps/web
|
||||||
|
npx tsc --noEmit -p tsconfig.app.json
|
||||||
|
npm test -- --run
|
||||||
|
npm run build
|
||||||
|
npx playwright test
|
||||||
|
```
|
||||||
|
|
||||||
|
桌面端:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Set-Location apps/desktop
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
更多验证说明见 `docs/mvp-verification.md`。
|
||||||
|
|||||||
7
backend/etc/agent.dev.yaml
Normal file
7
backend/etc/agent.dev.yaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
env: development
|
||||||
|
port: "8080"
|
||||||
|
database_url: "postgres://agent:agent@localhost:5432/agent?sslmode=disable"
|
||||||
|
storage_dir: "./data/files"
|
||||||
|
auth_secret: "development-auth-secret-change-me"
|
||||||
|
system_ai_key: ""
|
||||||
|
ai_key_encryption_secret: "development-ai-key-secret-change-me"
|
||||||
@@ -1,32 +1,44 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import "os"
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Env string
|
Env string `yaml:"env"`
|
||||||
Port string
|
Port string `yaml:"port"`
|
||||||
DatabaseURL string
|
DatabaseURL string `yaml:"database_url"`
|
||||||
StorageDir string
|
StorageDir string `yaml:"storage_dir"`
|
||||||
AuthSecret string
|
AuthSecret string `yaml:"auth_secret"`
|
||||||
SystemAIKey string
|
SystemAIKey string `yaml:"system_ai_key"`
|
||||||
AIKeyEncryptionSecret string
|
AIKeyEncryptionSecret string `yaml:"ai_key_encryption_secret"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func Load() Config {
|
func Load() Config {
|
||||||
return Config{
|
cfg, err := LoadFromDir("etc")
|
||||||
Env: getenv("APP_ENV", "development"),
|
if err != nil {
|
||||||
Port: getenv("PORT", "8080"),
|
panic(err)
|
||||||
DatabaseURL: getenv("DATABASE_URL", "postgres://agent:agent@localhost:5432/agent?sslmode=disable"),
|
|
||||||
StorageDir: getenv("STORAGE_DIR", "./data/files"),
|
|
||||||
AuthSecret: getenv("AUTH_SECRET", "development-auth-secret-change-me"),
|
|
||||||
SystemAIKey: os.Getenv("SYSTEM_AI_KEY"),
|
|
||||||
AIKeyEncryptionSecret: getenv("AI_KEY_ENCRYPTION_SECRET", "development-ai-key-secret-change-me"),
|
|
||||||
}
|
}
|
||||||
|
return cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
func getenv(key string, fallback string) string {
|
func LoadFromDir(configDir string) (Config, error) {
|
||||||
if value := os.Getenv(key); value != "" {
|
mode := strings.TrimSpace(os.Getenv("SENLIN_APP_MODE"))
|
||||||
return value
|
if mode == "" {
|
||||||
|
mode = "dev"
|
||||||
}
|
}
|
||||||
return fallback
|
path := filepath.Join(configDir, "agent."+strings.ToLower(mode)+".yaml")
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, err
|
||||||
|
}
|
||||||
|
var cfg Config
|
||||||
|
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||||
|
return Config{}, err
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|||||||
57
backend/internal/config/config_test.go
Normal file
57
backend/internal/config/config_test.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoadFromDirDefaultsToDevYAML(t *testing.T) {
|
||||||
|
configDir := t.TempDir()
|
||||||
|
writeConfig(t, configDir, "agent.dev.yaml", "development", "18080", "postgres://dev", "./dev-files", "dev-auth", "dev-system", "dev-ai")
|
||||||
|
t.Setenv("SENLIN_APP_MODE", "")
|
||||||
|
t.Setenv("PORT", "9999")
|
||||||
|
|
||||||
|
cfg, err := LoadFromDir(configDir)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "development", cfg.Env)
|
||||||
|
require.Equal(t, "18080", cfg.Port)
|
||||||
|
require.Equal(t, "postgres://dev", cfg.DatabaseURL)
|
||||||
|
require.Equal(t, "./dev-files", cfg.StorageDir)
|
||||||
|
require.Equal(t, "dev-auth", cfg.AuthSecret)
|
||||||
|
require.Equal(t, "dev-system", cfg.SystemAIKey)
|
||||||
|
require.Equal(t, "dev-ai", cfg.AIKeyEncryptionSecret)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadFromDirUsesSENLINAppMode(t *testing.T) {
|
||||||
|
configDir := t.TempDir()
|
||||||
|
writeConfig(t, configDir, "agent.dev.yaml", "development", "18080", "postgres://dev", "./dev-files", "dev-auth", "", "dev-ai")
|
||||||
|
writeConfig(t, configDir, "agent.prod.yaml", "production", "80", "postgres://prod", "/data/files", "prod-auth", "prod-system", "prod-ai")
|
||||||
|
t.Setenv("SENLIN_APP_MODE", "prod")
|
||||||
|
|
||||||
|
cfg, err := LoadFromDir(configDir)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "production", cfg.Env)
|
||||||
|
require.Equal(t, "80", cfg.Port)
|
||||||
|
require.Equal(t, "postgres://prod", cfg.DatabaseURL)
|
||||||
|
require.Equal(t, "/data/files", cfg.StorageDir)
|
||||||
|
require.Equal(t, "prod-auth", cfg.AuthSecret)
|
||||||
|
require.Equal(t, "prod-system", cfg.SystemAIKey)
|
||||||
|
require.Equal(t, "prod-ai", cfg.AIKeyEncryptionSecret)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeConfig(t *testing.T, dir string, name string, env string, port string, databaseURL string, storageDir string, authSecret string, systemAIKey string, aiKeySecret string) {
|
||||||
|
t.Helper()
|
||||||
|
content := []byte("env: " + env + "\n" +
|
||||||
|
"port: \"" + port + "\"\n" +
|
||||||
|
"database_url: \"" + databaseURL + "\"\n" +
|
||||||
|
"storage_dir: \"" + storageDir + "\"\n" +
|
||||||
|
"auth_secret: \"" + authSecret + "\"\n" +
|
||||||
|
"system_ai_key: \"" + systemAIKey + "\"\n" +
|
||||||
|
"ai_key_encryption_secret: \"" + aiKeySecret + "\"\n")
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(dir, name), content, 0o600))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user