Кабинет: страницы собирает ядро, на хостинге — ретранслятор (Go и PHP)

- modules/lk: конверт запроса/ответа (PageRequest/PageResponse), ServeEnvelope — тот же
  обработчик страниц внутри ядра; клиент к API внутри процесса (Config.Local/LocalKey);
  режим ретранслятора (Config.Relay, BJ_RENDER=core, выбор в установщике и lk.json)
- ядро: встроенный кабинет (lkcabinet.go), POST /api/lk/render для прямого режима,
  цикл режима ожидания выполняет конверты страниц; API через ретранслятор недостижимо
- PHP-ретранслятор для обычного хостинга (modules/lk/relay-php: index.php, .htaccess,
  README): установщик в браузере, прямой режим через curl, режим ожидания через очередь
  в SQLite (BEGIN IMMEDIATE, busy_timeout), конфиг в bj-lk-data рядом с сайтом или в
  data под .htaccess; архив со страницы настроек (/admin/setup/lk/relay-php.zip)
- настройки: карточка НПР/УДС перенесена из раздела «Лицензия» в «Экспорт QUIK»;
  кнопка «Обновить с сервера лицензий» видна при любом ключе
- инструкция /admin/help/lk: два варианта размещения, шаги для PHP-хостинга
- тесты: TestLKRelayRendersInCore (оба режима), TestLKPHPRelayAgentMode (настоящий PHP
  против цикла ядра, PHP_BIN), проверка безопасности через PHP-ретранслятор 34/34

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4YVo4UMopqT5DzicqpPjL
This commit is contained in:
zuevav
2026-09-18 11:15:29 +03:00
parent f51414269d
commit 054222d154
19 changed files with 1403 additions and 97 deletions
+17 -5
View File
@@ -29,10 +29,11 @@ const configFileName = "lk.json"
// fileConfig — что лежит в lk.json. Ключ — секрет, поэтому файл 0600.
type fileConfig struct {
Mode string `json:"mode"` // direct | agent
BJURL string `json:"bj_url,omitempty"`
BJKey string `json:"bj_key"`
Title string `json:"title,omitempty"`
Mode string `json:"mode"` // direct | agent — как связываемся с ядром
Render string `json:"render,omitempty"` // core | local — где собираются страницы
BJURL string `json:"bj_url,omitempty"`
BJKey string `json:"bj_key"`
Title string `json:"title,omitempty"`
}
// DefaultDataDir — каталог данных по умолчанию: data рядом с программой.
@@ -73,6 +74,9 @@ func LoadConfigFile(dataDir string, cfg Config) (Config, error) {
if !cfg.Agent && os.Getenv("BJ_MODE") == "" {
cfg.Agent = f.Mode == "agent"
}
if !cfg.Relay && os.Getenv("BJ_RENDER") == "" {
cfg.Relay = f.Render == "core"
}
return cfg, nil
}
@@ -81,10 +85,13 @@ func saveConfigFile(dataDir string, cfg Config) error {
if err := os.MkdirAll(dataDir, 0o700); err != nil {
return fmt.Errorf("каталог данных %s: %v", dataDir, err)
}
f := fileConfig{Mode: "direct", BJURL: cfg.BJURL, BJKey: cfg.BJKey, Title: cfg.Title}
f := fileConfig{Mode: "direct", Render: "local", BJURL: cfg.BJURL, BJKey: cfg.BJKey, Title: cfg.Title}
if cfg.Agent {
f.Mode, f.BJURL = "agent", ""
}
if cfg.Relay {
f.Render = "core"
}
raw, _ := json.MarshalIndent(f, "", " ")
tmp := filepath.Join(dataDir, configFileName+".tmp")
if err := os.WriteFile(tmp, raw, 0o600); err != nil {
@@ -155,6 +162,10 @@ type installData struct {
// registerInstall — страницы установщика.
func (s *Server) registerInstall(mux *http.ServeMux) {
mux.HandleFunc("/install", func(w http.ResponseWriter, r *http.Request) {
if s.conf().Local != nil {
http.NotFound(w, r) // внутри ядра установщика нет
return
}
// Уже настроено: повторный вход в установщик — только удалением файла настроек.
// Исключение — шаг «администратор», он идёт сразу после сохранения связи.
if s.Configured() && !(r.Method == http.MethodPost && r.FormValue("step") == "admin") {
@@ -199,6 +210,7 @@ func (s *Server) renderInstall(w http.ResponseWriter, r *http.Request, d install
func (s *Server) installConnect(w http.ResponseWriter, r *http.Request, d installData) {
cfg := d.Cfg
cfg.Agent = r.FormValue("mode") == "agent"
cfg.Relay = r.FormValue("render") == "core"
cfg.BJURL = strings.TrimRight(strings.TrimSpace(r.FormValue("bj_url")), "/")
cfg.BJKey = strings.TrimSpace(r.FormValue("bj_key"))
cfg.Title = strings.TrimSpace(r.FormValue("title"))