diff --git a/.gitignore b/.gitignore index 13dd085..11ead7c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ node_modules/ dist/ target/ **/src-tauri/gen/ +backend/etc/agent.local.yaml .env data/ coverage/ diff --git a/README.md b/README.md index 1ce903c..3f21f87 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,101 @@ # SenlinAI Agent Workbench -Private project-centered workbench MVP. +SenlinAI Agent Workbench 是一个以项目为中心的私有化工作台 MVP。 -## Development +## 本地开发 + +启动本地 PostgreSQL: ```powershell docker compose -f infra/docker-compose.yml up -d +``` + +启动后端 API: + +```powershell Set-Location backend 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`. -- `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. +后端配置不再从多个环境变量直接读取,而是从 `backend/etc/agent..yaml` 读取。 -## 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..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`。 diff --git a/backend/etc/agent.dev.yaml b/backend/etc/agent.dev.yaml new file mode 100644 index 0000000..5659213 --- /dev/null +++ b/backend/etc/agent.dev.yaml @@ -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" diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index b78875d..9d1bd1b 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -1,32 +1,44 @@ package config -import "os" +import ( + "os" + "path/filepath" + "strings" + + "gopkg.in/yaml.v3" +) type Config struct { - Env string - Port string - DatabaseURL string - StorageDir string - AuthSecret string - SystemAIKey string - AIKeyEncryptionSecret string + Env string `yaml:"env"` + Port string `yaml:"port"` + DatabaseURL string `yaml:"database_url"` + StorageDir string `yaml:"storage_dir"` + AuthSecret string `yaml:"auth_secret"` + SystemAIKey string `yaml:"system_ai_key"` + AIKeyEncryptionSecret string `yaml:"ai_key_encryption_secret"` } func Load() Config { - return Config{ - Env: getenv("APP_ENV", "development"), - Port: getenv("PORT", "8080"), - 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"), + cfg, err := LoadFromDir("etc") + if err != nil { + panic(err) } + return cfg } -func getenv(key string, fallback string) string { - if value := os.Getenv(key); value != "" { - return value +func LoadFromDir(configDir string) (Config, error) { + mode := strings.TrimSpace(os.Getenv("SENLIN_APP_MODE")) + 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 } diff --git a/backend/internal/config/config_test.go b/backend/internal/config/config_test.go new file mode 100644 index 0000000..3c8fb98 --- /dev/null +++ b/backend/internal/config/config_test.go @@ -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)) +}