Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec1cf20347 | ||
|
|
6a0c2d1fc4 | ||
|
|
5bc85effa7 | ||
|
|
13a670eadd |
@@ -2,6 +2,7 @@
|
|||||||
agent.md
|
agent.md
|
||||||
config.json
|
config.json
|
||||||
bot_user_config.json
|
bot_user_config.json
|
||||||
|
bot_node_config.json
|
||||||
*.exe
|
*.exe
|
||||||
nukumizu-linux-amd64
|
nukumizu-linux-amd64
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,51 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// LoadBotNodeConfig reads and parses bot_node_config.json and stores it as the
|
||||||
|
// global C_botNodeConfig singleton, mirroring LoadGlobalConfig. Unlike the
|
||||||
|
// other config files this one is auto-generated and optional: a missing or
|
||||||
|
// empty file yields an empty map, so every node falls back to its default
|
||||||
|
// enableStatusNotify value (true).
|
||||||
|
func LoadBotNodeConfig(configPath string) error {
|
||||||
|
cfg := BotNodeMembers{}
|
||||||
|
data, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
C_botNodeConfig = cfg
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed to read bot node config file: %w", err)
|
||||||
|
}
|
||||||
|
if len(bytes.TrimSpace(data)) > 0 {
|
||||||
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||||
|
return fmt.Errorf("failed to parse bot node config file: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
C_botNodeConfig = cfg
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NodeStatusNotifyEnabled reports whether the node identified by uuid should
|
||||||
|
// broadcast status-change notifications, per bot_node_config.json.
|
||||||
|
// enableStatusNotify defaults to true: a node notifies unless its entry
|
||||||
|
// explicitly sets the flag to false.
|
||||||
|
func NodeStatusNotifyEnabled(uuid string) bool {
|
||||||
|
if C_botNodeConfig == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
opts, ok := C_botNodeConfig[uuid]
|
||||||
|
if !ok || opts.EnableStatusNotify == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return *opts.EnableStatusNotify
|
||||||
|
}
|
||||||
|
|
||||||
// LoadGlobalConfig reads and parses the configuration file, applies defaults,
|
// LoadGlobalConfig reads and parses the configuration file, applies defaults,
|
||||||
// and stores it as a global singleton.
|
// and stores it as a global singleton.
|
||||||
func LoadGlobalConfig(configPath string) (*Config, error) {
|
func LoadGlobalConfig(configPath string) (*Config, error) {
|
||||||
@@ -115,6 +155,46 @@ func LoadBotUserConfig(configPath string) (*BotUserConfig, error) {
|
|||||||
return &cfg, nil
|
return &cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SaveBotNodeConfig persists the given node UUIDs to the bot node config file,
|
||||||
|
// which is a plain registry of the nodes Komari reports (read back at startup
|
||||||
|
// by LoadBotNodeConfig). Entries already present are always preserved: a UUID
|
||||||
|
// Komari no longer reports on a given fetch is kept rather than deleted, and
|
||||||
|
// any enableStatusNotify a user set by hand is left untouched. UUIDs seen for
|
||||||
|
// the first time are added as an empty object, i.e. no parameter is written, so
|
||||||
|
// they inherit the enableStatusNotify default (true). The resulting JSON has
|
||||||
|
// its object keys emitted in sorted order by encoding/json, keeping the file
|
||||||
|
// deterministic across writes. The path is supplied by the caller (typically
|
||||||
|
// global.ConfigPath.BotNodeConfig).
|
||||||
|
func SaveBotNodeConfig(configPath string, uuids []string) error {
|
||||||
|
// Start from whatever is already on disk so nothing is dropped. An empty or
|
||||||
|
// missing file is treated as an empty map.
|
||||||
|
members := make(BotNodeMembers)
|
||||||
|
if data, err := os.ReadFile(configPath); err == nil && len(bytes.TrimSpace(data)) > 0 {
|
||||||
|
if err := json.Unmarshal(data, &members); err != nil {
|
||||||
|
return fmt.Errorf("failed to parse existing bot node config %s: %w", configPath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add every currently-fetched UUID, but never overwrite an entry that is
|
||||||
|
// already configured.
|
||||||
|
for _, uuid := range uuids {
|
||||||
|
if _, ok := members[uuid]; !ok {
|
||||||
|
members[uuid] = BotNodeOptions{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.MarshalIndent(members, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal bot node config: %w", err)
|
||||||
|
}
|
||||||
|
data = append(data, '\n')
|
||||||
|
|
||||||
|
if err := os.WriteFile(configPath, data, 0o644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write bot node config %s: %w", configPath, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// IsDebugMode returns whether debug mode is enabled.
|
// IsDebugMode returns whether debug mode is enabled.
|
||||||
func IsDebugMode() bool {
|
func IsDebugMode() bool {
|
||||||
if C_globalConfig == nil {
|
if C_globalConfig == nil {
|
||||||
|
|||||||
@@ -170,3 +170,23 @@ type BotUserConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var C_botUserConfig *BotUserConfig
|
var C_botUserConfig *BotUserConfig
|
||||||
|
|
||||||
|
// BotNodeOptions holds per-node options stored in bot_node_config.json. The
|
||||||
|
// file is auto-populated by the node tracker for every node Komari reports;
|
||||||
|
// per-node options are edited by hand in the JSON file.
|
||||||
|
type BotNodeOptions struct {
|
||||||
|
// EnableStatusNotify controls whether this node broadcasts status-change
|
||||||
|
// notifications. It defaults to true: only an explicit false in the JSON
|
||||||
|
// file disables a node's notifications. The pointer (rather than a plain
|
||||||
|
// bool) lets an absent field be told apart from an explicit false, and
|
||||||
|
// omitempty keeps untouched nodes stored as {}.
|
||||||
|
EnableStatusNotify *bool `json:"enableStatusNotify,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BotNodeMembers maps a node UUID (as reported by Komari) to its per-node
|
||||||
|
// options.
|
||||||
|
type BotNodeMembers map[string]BotNodeOptions
|
||||||
|
|
||||||
|
// C_botNodeConfig is the global singleton mirroring bot_node_config.json,
|
||||||
|
// populated by LoadBotNodeConfig.
|
||||||
|
var C_botNodeConfig BotNodeMembers
|
||||||
@@ -22,3 +22,15 @@ var SoftwareInfo = SoftwareInfoStr{
|
|||||||
BuildType: "Debug",
|
BuildType: "Debug",
|
||||||
BuildTime: "unknown",
|
BuildTime: "unknown",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ConfigPathStr struct {
|
||||||
|
Global string
|
||||||
|
BotUserConfig string
|
||||||
|
BotNodeConfig string
|
||||||
|
}
|
||||||
|
|
||||||
|
var ConfigPath = ConfigPathStr{
|
||||||
|
Global: "config.json",
|
||||||
|
BotUserConfig: "bot_user_config.json",
|
||||||
|
BotNodeConfig: "bot_node_config.json",
|
||||||
|
}
|
||||||
@@ -165,26 +165,26 @@ func (m *Manager) ShowBotServerList() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NotifyStatusChange sends a status change notification to all enabled controllers.
|
// NotifyStatusChange sends a status change notification to all enabled
|
||||||
|
// controllers. It honors the per-node allow-list in bot_node_config.json: a
|
||||||
|
// node whose enableStatusNotify is not true is skipped entirely, so no
|
||||||
|
// controller (chat bots or notification pipes) broadcasts its change.
|
||||||
func (m *Manager) NotifyStatusChange(change node.StatusChange) {
|
func (m *Manager) NotifyStatusChange(change node.StatusChange) {
|
||||||
|
if !config.NodeStatusNotifyEnabled(change.UUID) {
|
||||||
|
postLog.Debug(fmt.Sprintf("Status change for node %s skipped: enableStatusNotify is not enabled", change.UUID))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
m.mu.RLock()
|
m.mu.RLock()
|
||||||
defer m.mu.RUnlock()
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
cfg := config.C_globalConfig
|
|
||||||
templateStr := cfg.ControllerMessage.ServerStatusChanged
|
|
||||||
params := template.BuildParamsFromStatusChange(change)
|
|
||||||
|
|
||||||
for _, ctrl := range m.controllers {
|
for _, ctrl := range m.controllers {
|
||||||
if !ctrl.IsEnabled() {
|
if !ctrl.IsEnabled() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the names of controllers that support commands for the message.
|
|
||||||
if err := ctrl.SendStatusChange(change); err != nil {
|
if err := ctrl.SendStatusChange(change); err != nil {
|
||||||
postLog.Warning(fmt.Sprintf("Controller %s failed to send status change: %v", ctrl.Name(), err))
|
postLog.Warning(fmt.Sprintf("Controller %s failed to send status change: %v", ctrl.Name(), err))
|
||||||
}
|
}
|
||||||
_ = templateStr
|
|
||||||
_ = params
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"nukumizu-backend/config"
|
"nukumizu-backend/config"
|
||||||
|
"nukumizu-backend/global"
|
||||||
"nukumizu-backend/internal/node"
|
"nukumizu-backend/internal/node"
|
||||||
"nukumizu-backend/postLog"
|
"nukumizu-backend/postLog"
|
||||||
)
|
)
|
||||||
@@ -225,6 +226,20 @@ func (c *Client) FetchNodes() ([]NodeInfo, error) {
|
|||||||
nodes = append(nodes, n)
|
nodes = append(nodes, n)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Persist the fetched node UUIDs to bot_node_config.json on every fetch
|
||||||
|
// (startup login, WebSocket reconnect and periodic refresh all funnel
|
||||||
|
// through FetchNodes) so the file always reflects the nodes Komari
|
||||||
|
// currently manages. A persistence failure is only logged: the node data
|
||||||
|
// itself was fetched successfully and must not be discarded over a disk
|
||||||
|
// write problem.
|
||||||
|
uuids := make([]string, len(nodes))
|
||||||
|
for i, n := range nodes {
|
||||||
|
uuids[i] = n.UUID
|
||||||
|
}
|
||||||
|
if err := config.SaveBotNodeConfig(global.ConfigPath.BotNodeConfig, uuids); err != nil {
|
||||||
|
postLog.Warning("Failed to save bot node config: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
postLog.Info(fmt.Sprintf("Fetched %d nodes from Komari", len(nodes)))
|
postLog.Info(fmt.Sprintf("Fetched %d nodes from Komari", len(nodes)))
|
||||||
return nodes, nil
|
return nodes, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -34,25 +33,27 @@ func main() {
|
|||||||
global.SoftwareInfo.CommitHash = CommitHash
|
global.SoftwareInfo.CommitHash = CommitHash
|
||||||
global.SoftwareInfo.BuildTime = BuildTime
|
global.SoftwareInfo.BuildTime = BuildTime
|
||||||
|
|
||||||
// Parse CLI flags.
|
|
||||||
configPath_global := flag.String("config", "config.json", "Path to configuration file")
|
|
||||||
configPath_bot_user := flag.String("bot-user-config", "bot_user_config.json", "Path to bot user configuration file")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
// Log startup banner.
|
// Log startup banner.
|
||||||
postLog.Info(fmt.Sprintf("%s Ver.%s.%d.%s.%s Developed by %s at %s", global.SoftwareInfo.Name, global.SoftwareInfo.Version, global.SoftwareInfo.BuildVer, global.SoftwareInfo.BuildType, global.SoftwareInfo.CommitHash, global.SoftwareInfo.Developer, global.SoftwareInfo.BuildTime))
|
postLog.Info(fmt.Sprintf("%s Ver.%s.%d.%s.%s Developed by %s at %s", global.SoftwareInfo.Name, global.SoftwareInfo.Version, global.SoftwareInfo.BuildVer, global.SoftwareInfo.BuildType, global.SoftwareInfo.CommitHash, global.SoftwareInfo.Developer, global.SoftwareInfo.BuildTime))
|
||||||
|
|
||||||
// Load configuration.
|
// Load configuration.
|
||||||
cfg, err := config.LoadGlobalConfig(*configPath_global)
|
cfg, err := config.LoadGlobalConfig(global.ConfigPath.Global)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to load global config: %v", err)
|
log.Fatalf("Failed to load global config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = config.LoadBotUserConfig(*configPath_bot_user)
|
_, err = config.LoadBotUserConfig(global.ConfigPath.BotUserConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to load bot user config: %v", err)
|
log.Fatalf("Failed to load bot user config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load the node registry config. Unlike the other files it is optional:
|
||||||
|
// missing or empty bot_node_config.json simply means every node keeps its
|
||||||
|
// default enableStatusNotify (true).
|
||||||
|
if err := config.LoadBotNodeConfig(global.ConfigPath.BotNodeConfig); err != nil {
|
||||||
|
log.Fatalf("Failed to load bot node config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize logging.
|
// Initialize logging.
|
||||||
postLog.SetDebugMode(cfg.System.DebugMode)
|
postLog.SetDebugMode(cfg.System.DebugMode)
|
||||||
postLog.InitLogBroadcaster()
|
postLog.InitLogBroadcaster()
|
||||||
|
|||||||
Reference in New Issue
Block a user