OPENCODE_CONFIG_DIR в opencode v1.18.18 перенаправляет Global.Path.config (global.ts: config = OPENCODE_CONFIG_DIR ?? ~/.config/opencode), из-за чего глобальный конфиг (модель/провайдеры, напр. tokentool) не загружался и opencode уходил в fallback-модель. Агенты открывались, т.к. OPENCODE_CONFIG_DIR дополнительно сканируется как каталог для agent/*.md. Теперь агенты распаковываются в <worktree>/.opencode/agent/*.md, где opencode находит их через project-каталог .opencode (paths.ts, cwd=worktree). Глобальный конфиг не трогаем вовсе. - internal/opencode: удалены Server.ConfigDir/Pool.ConfigDir и env OPENCODE_CONFIG_DIR - internal/config: удалено поле OpenCodeCfg.ConfigDir (config_dir) - internal/app: ensureAgentsDir пишет в <worktree>/.opencode/agent - internal/agents: WriteTo(dir) → dir/agent/*.md - README/config.yaml.example/memory обновлены
139 lines
6.2 KiB
Go
139 lines
6.2 KiB
Go
// Package config — загрузка конфигурации Ratatoskr-go.
|
||
//
|
||
// Источники (каждый следующий имеет приоритет над предыдущим):
|
||
// 1. Файл config.yaml (с подстановкой ${VAR:-default})
|
||
// 2. Переменные окружения (env-теги в структуре)
|
||
//
|
||
// Порядок: Load("config.yaml") → YAML → os.Expand → env override → Validate.
|
||
package config
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
// Duration — time.Duration с YAML-сериализацией (строка "5s", "10m").
|
||
type Duration time.Duration
|
||
|
||
func (d Duration) Duration() time.Duration { return time.Duration(d) }
|
||
func (d Duration) String() string { return time.Duration(d).String() }
|
||
|
||
func (d *Duration) UnmarshalYAML(unmarshal func(any) error) error {
|
||
var s string
|
||
if err := unmarshal(&s); err != nil {
|
||
return fmt.Errorf("%w: %v", ErrInvalidFormat, err)
|
||
}
|
||
td, err := time.ParseDuration(s)
|
||
if err != nil {
|
||
return fmt.Errorf("%w: parse duration %q: %v", ErrInvalidFormat, s, err)
|
||
}
|
||
*d = Duration(td)
|
||
return nil
|
||
}
|
||
|
||
// Config — полная конфигурация бинаря.
|
||
type Config struct {
|
||
Telegram TelegramCfg `yaml:"telegram"`
|
||
OpenCode OpenCodeCfg `yaml:"opencode"`
|
||
Git GitCfg `yaml:"git"`
|
||
Chat ChatCfg `yaml:"chat"`
|
||
Paths PathsCfg `yaml:"paths"`
|
||
Update UpdateCfg `yaml:"update"`
|
||
Log LogCfg `yaml:"log"`
|
||
}
|
||
|
||
// LogCfg — уровень логирования. Level: "info" (по умолчанию) или "debug".
|
||
// debug включает отладочные логи (напр. все API-вызовы к opencode serve).
|
||
type LogCfg struct {
|
||
Level string `yaml:"level" default:"info"`
|
||
}
|
||
|
||
// Debug возвращает true, если включён отладочный уровень логирования.
|
||
func (l LogCfg) Debug() bool { return strings.EqualFold(l.Level, "debug") }
|
||
|
||
// GitCfg — источник репозиториев (для git clone).
|
||
type GitCfg struct {
|
||
BaseURL string `yaml:"base_url" env:"GIT_BASE_URL"`
|
||
Token string `yaml:"token" env:"GIT_TOKEN"`
|
||
}
|
||
|
||
// UpdateCfg — автообновление.
|
||
// Enabled=false отключает и авто-проверку, и /update.
|
||
// BaseURL — хост Gitea для обновления (обязателен: без него update отключён,
|
||
// фоллбэк на git.base_url НЕ используется).
|
||
// Token — токен read:package. Приоритет: вшитый -X main.updateToken > этот.
|
||
// Package — имя пакета-хранилища (по умолчанию ratatoskr). Владелец пакета —
|
||
// жёсткая константа (packageOwner в app).
|
||
// CheckInterval — как часто авто-проверять наличие обновления
|
||
// (по умолчанию 24h). Авто-режим ТОЛЬКО уведомляет; замена — по /update.
|
||
type UpdateCfg struct {
|
||
Enabled bool `yaml:"enabled" default:"false"`
|
||
BaseURL string `yaml:"base_url" env:"UPDATE_BASE_URL"`
|
||
Token string `yaml:"token" env:"UPDATE_TOKEN"`
|
||
Package string `yaml:"package" default:"ratatoskr"`
|
||
CheckInterval Duration `yaml:"check_interval" default:"24h"`
|
||
}
|
||
|
||
// TelegramCfg — настройки Telegram-канала. Enabled=false отключает канал
|
||
// полностью: бот не опрашивается, сетевые вызовы к api.telegram.org не идут,
|
||
// token/chat_id не обязательны. По умолчанию включён (см. Load: presetTelegramEnabled).
|
||
type TelegramCfg struct {
|
||
Enabled bool `yaml:"enabled"`
|
||
Token string `yaml:"token" env:"TG_TOKEN"`
|
||
ChatID string `yaml:"chat_id" env:"TG_CHAT_ID"`
|
||
}
|
||
|
||
type OpenCodeCfg struct {
|
||
Bin string `yaml:"bin" default:"opencode"`
|
||
DBPath string `yaml:"db_path" default:""`
|
||
Config string `yaml:"config" env:"OPENCODE_CONFIG"`
|
||
HardTimeout Duration `yaml:"hard_timeout" default:"20m"`
|
||
IdleTimeout Duration `yaml:"idle_timeout" default:"5m"`
|
||
PollMs Duration `yaml:"poll_ms" default:"2s"`
|
||
Serve ServeCfg `yaml:"serve"`
|
||
}
|
||
|
||
// ServeCfg — настройки постоянных opencode serve-процессов (API-режим).
|
||
// Runner ходит к serve по HTTP API (v1.17+, /api). ratatoskr сам поднимает
|
||
// по одному serve на каталог через Pool (Hash: hostname/port; служебный
|
||
// root-сервер в worktree живёт всё время app, остальные — лениво).
|
||
//
|
||
// Поля enabled/url оставлены для обратной совместимости конфига и сейчас не
|
||
// меняют поведение: serve обязателен для API-режима, сервер всегда
|
||
// спавнится пулом (hostname/port задают базовые значения).
|
||
type ServeCfg struct {
|
||
Enabled bool `yaml:"enabled" default:"false"`
|
||
URL string `yaml:"url" env:"OPENCODE_SERVE_URL"`
|
||
Hostname string `yaml:"hostname" default:"127.0.0.1"`
|
||
Port int `yaml:"port" default:"4096"`
|
||
Password string `yaml:"password" env:"OPENCODE_SERVE_PASSWORD"`
|
||
}
|
||
|
||
type ChatCfg struct {
|
||
PollInterval Duration `yaml:"poll_interval" default:"30s"`
|
||
}
|
||
|
||
type PathsCfg struct {
|
||
Worktree string `yaml:"worktree" default:"./worktrees"`
|
||
DB string `yaml:"db" default:"./ratatoskr.db" env:"RATATOSKR_DB"`
|
||
}
|
||
|
||
// Validate проверяет обязательные поля. Возвращает C1 MissingField
|
||
// (склеенную, все ошибки сразу) или C2 InvalidFormat.
|
||
func (c *Config) Validate() error {
|
||
var errs []error
|
||
if c.Telegram.Enabled {
|
||
if c.Telegram.Token == "" {
|
||
errs = append(errs, fmt.Errorf("%w: telegram.token", ErrMissingField))
|
||
}
|
||
if c.Telegram.ChatID == "" {
|
||
errs = append(errs, fmt.Errorf("%w: telegram.chat_id", ErrMissingField))
|
||
}
|
||
}
|
||
if !c.Log.Debug() && !strings.EqualFold(c.Log.Level, "info") {
|
||
errs = append(errs, fmt.Errorf("%w: log.level (ожидается \"info\" или \"debug\")", ErrInvalidFormat))
|
||
}
|
||
return errors.Join(errs...)
|
||
} |