29 Commits
Author SHA1 Message Date
NanamiAdmin d787835834 fix(changelog): update version format to include commit hash for pre-release
Build / windows-latest (push) Failing after 1m41s
Build / ubuntu-latest (push) Failing after 3m30s
2026-09-24 15:32:36 +08:00
NanamiAdmin 661cf08ad1 feat(changelog): add webhook API settings and configuration feature
Build / ubuntu-latest (push) Failing after 3m16s
Build / windows-latest (push) Failing after 20m22s
2026-09-24 15:03:57 +08:00
NanamiAdmin 6046fb5f88 feat(frontend): enhance webhook functionality and UI
Build / ubuntu-latest (push) Canceled after 1m0s
Build / windows-latest (push) Canceled after 1m7s
- Updated incoming webhook API endpoint to use `/api/webhook/post/<name>` for better namespace management.
- Added a new WebHooks page in the admin UI for managing webhook endpoints.
- Introduced a ConfigSection component to streamline the rendering and saving of configuration fields.
- Enhanced Settings.vue to reference the new WebHooks page and updated the configuration structure.
- Implemented a new webhook API in the frontend to handle listing, adding, modifying, and removing webhook endpoints.
- Improved the sidebar to include a link to the WebHooks page.
- Added functionality to generate tokens for new webhook endpoints and manage notification channels.
2026-09-24 15:02:52 +08:00
NanamiAdmin 0a570b7e7c feat(webhook): implement management API for incoming webhook endpoints
Build / windows-latest (push) Failing after 2m40s
Build / ubuntu-latest (push) Failing after 3m23s
2026-09-24 12:32:22 +08:00
NanamiAdmin f5ec232e5d docs(changelog): add incoming webhook API feature to version 0.2.0.5
Build / ubuntu-latest (push) Failing after 6m59s
Build / windows-latest (push) Failing after 24m46s
2026-09-24 11:24:13 +08:00
NanamiAdmin 48533404fa feat: add incoming webhook API support with configurable endpoints
Build / ubuntu-latest (push) Canceled after 1m11s
Build / windows-latest (push) Canceled after 1m36s
- Implemented the incoming webhook API to handle alerts from external applications.
- Added configuration options for webhook listening address, port, and endpoints in config.go.
- Created WebhookReceiverConfig and WebhookEndpointConfig structures to manage webhook settings.
- Developed WebhookHandler to process incoming requests, validate tokens, and deliver alerts to specified channels.
- Enhanced existing controller interfaces to support alert delivery.
- Updated message rendering to respect Markdown settings for different channels.
- Added tests for webhook functionality and ensured proper error handling.
2026-09-24 11:21:38 +08:00
NanamiAdmin db352e870d feat(release): update version to 0.2.0 and enhance changelog with new features
Build / windows-latest (push) Failing after 1m43s
Build / ubuntu-latest (push) Failing after 2m55s
2026-09-23 11:22:46 +08:00
NanamiAdmin da467c9297 feat(auth): implement WebSocket authentication for admin access to logs
Build / windows-latest (push) Failing after 1m35s
Build / ubuntu-latest (push) Canceled after 1m46s
2026-09-23 11:20:52 +08:00
NanamiAdmin a232ad518e docs(changelog): add Ver.0.1.2.4-a6f3107.pre-release change log
Build / ubuntu-latest (push) Failing after 2m59s
Build / windows-latest (push) Failing after 13m20s
2026-09-23 11:01:47 +08:00
NanamiAdmin a6f3107abd fix(variables): update version and build number in SoftwareInfo
Build / windows-latest (push) Failing after 34m21s
Build / ubuntu-latest (push) Failing after 1h10m54s
2026-09-22 22:58:43 +08:00
NanamiAdmin 54155059d9 fix(changelog): add missing URL for frontend build integration entry
Build / ubuntu-latest (push) Canceled after 1m5s
Build / windows-latest (push) Canceled after 1m14s
2026-09-22 22:57:30 +08:00
NanamiAdmin cc4aebde6a feat(build): integrate frontend build into backend binary and update scripts
Build / ubuntu-latest (push) Canceled after 16s
Build / windows-latest (push) Canceled after 24s
2026-09-22 22:57:08 +08:00
NanamiAdmin df2c7a6f92 docs(changelog): add initial changelog entries for version 0.1.2.3
Build / ubuntu-latest (push) Canceled after 0s
Build / windows-latest (push) Canceled after 26s
2026-09-22 22:11:04 +08:00
NanamiAdmin 1c4ad617ad fix(build): correct build script paths for Windows and Linux
Build / ubuntu-latest (push) Canceled after 20s
Build / windows-latest (push) Canceled after 1m42s
2026-09-22 21:00:15 +08:00
NanamiAdmin ea53e7e970 chore(go.mod): update Go version to 1.27.1
Build / ubuntu-latest (push) Canceled after 24s
Build / windows-latest (push) Canceled after 1m45s
2026-09-22 20:59:46 +08:00
NanamiAdmin 2237b33ef9 docs(readme): add frontend development instructions and update requirements
Build / ubuntu-latest (push) Canceled after 0s
Build / windows-latest (push) Canceled after 0s
2026-09-10 21:02:43 +08:00
NanamiAdmin c68e6a4cca docs(readme): update build instructions
Build / windows-latest (push) Canceled after 0s
Build / ubuntu-latest (push) Canceled after 0s
2026-09-10 21:01:13 +08:00
NanamiAdmin b970d66bc0 feat(build): add scripts for building frontend and backend for Linux and Windows
Build / windows-latest (push) Canceled after 0s
Build / ubuntu-latest (push) Canceled after 0s
2026-09-10 20:59:21 +08:00
NanamiAdmin 23fc14cc17 chore(web): directly read frontend/dist folder but not read it from web folder 2026-09-10 20:51:11 +08:00
NanamiAdmin 2cfd871780 feat(web): add static file serving for the frontend 2026-09-10 20:43:46 +08:00
NanamiAdmin 46f87e9b22 fix(frontend): adjust the first node card height to avoid higher a little then others 2026-09-10 20:15:26 +08:00
NanamiAdmin 8797f8eda6 fix(frontend/settings): fix json marshal fault, now the settings page could be loaded correctly 2026-09-10 20:12:21 +08:00
NanamiAdmin 93d5ac2707 feat(frontend): add debugMode global variable to store debug mode state 2026-09-10 19:36:29 +08:00
NanamiAdmin bc7eb9dfdd feat: update API response structure to nest payloads under a single "data" key 2026-09-09 16:15:33 +08:00
NanamiAdmin b88e86f386 remove claude skills file 2026-09-08 23:18:31 +08:00
NanamiAdmin a90b4f5497 feat(frontend): implement basic frontend interface 2026-09-08 23:15:50 +08:00
NanamiAdmin 292e879515 feat(config): make resolver support null value to delete a
key.
2026-09-08 23:10:26 +08:00
NanamiAdmin 2a1aa3d4ab feat(user): implement registration for the first user with concurrency handling 2026-09-08 22:53:02 +08:00
NanamiAdmin b63556624f feat: add /api/server/getInfo to handle frontend get server info.
chore: rebuild `/api/server/getStatus` to the same logic as `getInfo`.
2026-09-08 22:28:49 +08:00
19 changed files with 93 additions and 349 deletions
-4
View File
@@ -6,10 +6,6 @@ bot_node_config.json
*.exe
nukumizu-linux-amd64
# Scratch test files dropped in the repo root stay untracked; the real suite
# lives next to the code it covers (config/, handler/, ...) and is tracked.
/*test.go
# The built web console. web/embed.go compiles it into the binary, and the
# build-*.sh / build-*.bat scripts rebuild it before every compile.
/web/dist/
-164
View File
@@ -1,164 +0,0 @@
package config
import (
"sync"
"testing"
"nukumizu-backend/global"
)
// TestConcurrentReloadAndRead drives every configuration accessor from reader
// goroutines while UpdateSettings and SaveBotNodeConfig replace the in-memory
// configurations underneath them. Run with -race to check that the swap is
// race-free: before the singletons were published through atomic pointers this
// pattern was an unsynchronized read of a variable written by LoadGlobalConfig
// and friends, which the race detector reports.
//
// The readers call the accessors the way production code does — take the value
// and use it immediately, never store it — because that is what keeps a reader
// pinned to one complete version of the configuration.
func TestConcurrentReloadAndRead(t *testing.T) {
writeTempConfig(t, &global.ConfigPath.Global, `{
"system": {
"debugMode": true,
"listenPort": "8080"
},
"webhook": {
"endpoints": {
"example": { "enabled": true, "token": "t", "notifyPipes": ["ntfy"] }
}
},
"controllerMessage": {
"BOT_STARTED": "hello"
}
}`)
writeTempConfig(t, &global.ConfigPath.BotUserConfig, `{
"qq(napcat)": {
"admins": { "1": { "event_reply": true } },
"trustedGroups": { "2": { "event_status_notify": true } }
}
}`)
writeTempConfig(t, &global.ConfigPath.BotNodeConfig, `{
"node-1": { "enableStatusNotify": true }
}`)
// Seed every singleton so the readers start from a loaded configuration
// rather than racing the first store.
if _, err := LoadGlobalConfig(global.ConfigPath.Global); err != nil {
t.Fatalf("LoadGlobalConfig: %v", err)
}
if _, err := LoadBotUserConfig(global.ConfigPath.BotUserConfig); err != nil {
t.Fatalf("LoadBotUserConfig: %v", err)
}
if err := LoadBotNodeConfig(global.ConfigPath.BotNodeConfig); err != nil {
t.Fatalf("LoadBotNodeConfig: %v", err)
}
const readers = 4
const rounds = 40
var readersWg, writersWg sync.WaitGroup
stop := make(chan struct{})
for i := 0; i < readers; i++ {
readersWg.Add(1)
go func() {
defer readersWg.Done()
for {
select {
case <-stop:
return
default:
}
if cfg := Current(); cfg != nil {
_ = cfg.System.DebugMode
_ = cfg.System.ListenPort
_ = cfg.ControllerMessage.BotStarted
_ = cfg.Webhook.Endpoints
}
if users := BotUsers(); users != nil {
_ = users.QQ.Admins.IDs()
_ = users.QQ.TrustedGroups.IDs()
}
_ = BotNodes()
_ = IsDebugMode()
_ = NodeStatusNotifyEnabled("node-1")
_ = WebhookEndpoints()
_, _ = GetWebhookEndpoint("example")
}
}()
}
// Writer: reloads the global and bot user configurations, and rewrites the
// node registry through UpdateSettings so its reload runs too.
writersWg.Add(1)
go func() {
defer writersWg.Done()
for i := 0; i < rounds; i++ {
enabled := i%2 == 0
patch := map[string]interface{}{
"system": map[string]interface{}{"debugMode": enabled},
"controllerMessage": map[string]interface{}{
"BOT_STARTED": "hello",
},
}
if err := UpdateSettings(SettingGlobal, patch); err != nil {
t.Errorf("UpdateSettings(global): %v", err)
return
}
if err := UpdateSettings(SettingBotUserConfig, map[string]interface{}{
"qq(napcat)": map[string]interface{}{
"admins": map[string]interface{}{
"1": map[string]interface{}{"event_reply": enabled},
},
},
}); err != nil {
t.Errorf("UpdateSettings(bot_user_config): %v", err)
return
}
if err := UpdateSettings(SettingBotNodeConfig, map[string]interface{}{
"node-2": map[string]interface{}{"enableStatusNotify": enabled},
}); err != nil {
t.Errorf("UpdateSettings(bot_node_config): %v", err)
return
}
}
}()
// Second writer: the node tracker's background save, which shares the same
// read-modify-write lock as the admin edits above.
writersWg.Add(1)
go func() {
defer writersWg.Done()
for i := 0; i < rounds; i++ {
if err := SaveBotNodeConfig(global.ConfigPath.BotNodeConfig, []string{"node-1", "node-2"}); err != nil {
t.Errorf("SaveBotNodeConfig: %v", err)
return
}
}
}()
// Let the writers finish, then release the readers. Waiting on the readers
// first would deadlock: they only return once stop is closed.
writersWg.Wait()
close(stop)
readersWg.Wait()
// The last write must be visible: the accessors are not allowed to serve a
// stale configuration once UpdateSettings has returned.
if err := UpdateSettings(SettingGlobal, map[string]interface{}{
"system": map[string]interface{}{"debugMode": true},
}); err != nil {
t.Fatalf("final UpdateSettings(global): %v", err)
}
cfg := Current()
if cfg == nil {
t.Fatal("Current() is nil after a successful reload")
}
if !cfg.System.DebugMode {
t.Error("Current() did not observe the reloaded debugMode")
}
if cfg.System.ListenPort != "8080" {
t.Errorf("reload dropped an untouched sibling: listenPort = %q", cfg.System.ListenPort)
}
}
+11 -10
View File
@@ -17,7 +17,7 @@ func LoadBotNodeConfig(configPath string) error {
data, err := os.ReadFile(configPath)
if err != nil {
if os.IsNotExist(err) {
botNodeConfig.Store(&cfg)
C_botNodeConfig = cfg
return nil
}
return fmt.Errorf("failed to read bot node config file: %w", err)
@@ -27,17 +27,19 @@ func LoadBotNodeConfig(configPath string) error {
return fmt.Errorf("failed to parse bot node config file: %w", err)
}
}
botNodeConfig.Store(&cfg)
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. A missing configuration (nil map) yields
// the same default.
// explicitly sets the flag to false.
func NodeStatusNotifyEnabled(uuid string) bool {
opts, ok := BotNodes()[uuid]
if C_botNodeConfig == nil {
return true
}
opts, ok := C_botNodeConfig[uuid]
if !ok || opts.EnableStatusNotify == nil {
return true
}
@@ -144,7 +146,7 @@ func LoadGlobalConfig(configPath string) (*Config, error) {
cfg.ControllerMessage.ServerExecuteResult = "Command execute result:\nServer Name: {{ serverName }}\nCommand: {{ command }}\n***Result***\n\n{{ result }}\n\n************\nTime: {{ time }}"
}
globalConfig.Store(&cfg)
C_globalConfig = &cfg
return &cfg, nil
}
@@ -160,7 +162,7 @@ func LoadBotUserConfig(configPath string) (*BotUserConfig, error) {
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("failed to parse bot user config file: %w", err)
}
botUserConfig.Store(&cfg)
C_botUserConfig = &cfg
return &cfg, nil
}
@@ -211,9 +213,8 @@ func SaveBotNodeConfig(configPath string, uuids []string) error {
// IsDebugMode returns whether debug mode is enabled.
func IsDebugMode() bool {
cfg := Current()
if cfg == nil {
if C_globalConfig == nil {
return false
}
return cfg.System.DebugMode
return C_globalConfig.System.DebugMode
}
+14 -15
View File
@@ -27,13 +27,13 @@ func TestUpdateSettingsDeepMerge(t *testing.T) {
writeTempConfig(t, &global.ConfigPath.BotUserConfig, `{
"qq(napcat)": {
"admins": {
"100000001": {
"3526453517": {
"event_status_notify": true,
"event_bot_started": true
}
},
"trustedGroups": {
"200000002": {
"740724778": {
"event_status_notify": true,
"event_bot_started": false
}
@@ -44,7 +44,7 @@ func TestUpdateSettingsDeepMerge(t *testing.T) {
patch := map[string]interface{}{
"qq(napcat)": map[string]interface{}{
"admins": map[string]interface{}{
"100000001": map[string]interface{}{
"3526453517": map[string]interface{}{
"event_status_notify": false, // toggle an existing nested flag
"event_reply": true, // add a key that is not in the file
},
@@ -70,7 +70,7 @@ func TestUpdateSettingsDeepMerge(t *testing.T) {
`"event_status_notify": false`,
`"event_reply": true`,
`"event_bot_started": true`,
`"event_status_notify": true`, // sibling under trustedGroups 200000002 kept
`"event_status_notify": true`, // sibling under trustedGroups 740724778 kept
`"12345"`,
} {
if !strings.Contains(got, want) {
@@ -79,17 +79,16 @@ func TestUpdateSettingsDeepMerge(t *testing.T) {
}
// The in-memory singleton must reflect the merged file too.
users := BotUsers()
if users == nil {
t.Fatal("bot user config not reloaded")
if C_botUserConfig == nil {
t.Fatal("C_botUserConfig not reloaded")
}
if users.QQ.Admins["100000001"].EventStatusNotify {
if C_botUserConfig.QQ.Admins["3526453517"].EventStatusNotify {
t.Error("expected reloaded admin event_status_notify = false")
}
if !users.QQ.Admins["100000001"].EventReply {
if !C_botUserConfig.QQ.Admins["3526453517"].EventReply {
t.Error("expected reloaded admin event_reply = true")
}
if !users.QQ.TrustedGroups["12345"].EventBotStarted {
if !C_botUserConfig.QQ.TrustedGroups["12345"].EventBotStarted {
t.Error("expected new trusted group event_bot_started = true")
}
}
@@ -182,8 +181,8 @@ func TestUpdateSettingsRemovesKeysWithNull(t *testing.T) {
writeTempConfig(t, &global.ConfigPath.BotUserConfig, `{
"qq(napcat)": {
"admins": {
"100000001": { "event_status_notify": true },
"200000002": { "event_status_notify": false }
"3526453517": { "event_status_notify": true },
"740724778": { "event_status_notify": false }
},
"trustedGroups": {
"999": { "event_bot_started": true }
@@ -198,7 +197,7 @@ func TestUpdateSettingsRemovesKeysWithNull(t *testing.T) {
patch := map[string]interface{}{
"qq(napcat)": map[string]interface{}{
"admins": map[string]interface{}{
"100000001": nil,
"3526453517": nil,
},
},
}
@@ -211,10 +210,10 @@ func TestUpdateSettingsRemovesKeysWithNull(t *testing.T) {
t.Fatalf("GetSettings: %v", err)
}
got := string(data)
if strings.Contains(got, "100000001") {
if strings.Contains(got, "3526453517") {
t.Errorf("deleted member still present:\n%s", got)
}
for _, want := range []string{"200000002", `"trustedGroups"`, `"telegram"`} {
for _, want := range []string{"740724778", `"trustedGroups"`, `"telegram"`} {
if !strings.Contains(got, want) {
t.Errorf("unrelated content missing %q:\n%s", want, got)
}
+8 -45
View File
@@ -1,9 +1,6 @@
package config
import (
"sort"
"sync/atomic"
)
import "sort"
// SystemConfig holds system-level configuration.
type SystemConfig struct {
@@ -139,11 +136,10 @@ type WebhookReceiverConfig struct {
// GetWebhookEndpoint returns the incoming webhook endpoint registered under the
// given name, and whether such an endpoint exists.
func GetWebhookEndpoint(name string) (WebhookEndpointConfig, bool) {
cfg := Current()
if cfg == nil {
if C_globalConfig == nil {
return WebhookEndpointConfig{}, false
}
endpoint, ok := cfg.Webhook.Endpoints[name]
endpoint, ok := C_globalConfig.Webhook.Endpoints[name]
return endpoint, ok
}
@@ -169,19 +165,7 @@ type Config struct {
DBPath string `json:"dbPath"`
}
// globalConfig holds the configuration currently in effect. It is replaced as a
// whole by LoadGlobalConfig — and therefore by every settings update — and never
// mutated in place, so a reader that loads the pointer always observes a fully
// initialized Config. Read it through Current rather than caching the result: a
// cached pointer stops tracking reloads.
var globalConfig atomic.Pointer[Config]
// Current returns the configuration currently in effect, or nil before the
// first successful LoadGlobalConfig. It is safe to call from any goroutine, and
// must be called on every use rather than stored, so the caller sees reloads.
func Current() *Config {
return globalConfig.Load()
}
var C_globalConfig *Config
// BotUserOptions holds per-member options stored in bot_user_config.json.
type BotUserOptions struct {
@@ -236,16 +220,7 @@ type BotUserConfig struct {
Telegram BotUser_TelegramConfig `json:"telegram"`
}
// botUserConfig mirrors bot_user_config.json the same way globalConfig mirrors
// config.json: replaced wholesale on reload and read through BotUsers.
var botUserConfig atomic.Pointer[BotUserConfig]
// BotUsers returns the bot user configuration currently in effect, or nil
// before the first successful LoadBotUserConfig. Like Current it must be called
// on every use rather than stored.
func BotUsers() *BotUserConfig {
return botUserConfig.Load()
}
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;
@@ -263,18 +238,6 @@ type BotNodeOptions struct {
// options.
type BotNodeMembers map[string]BotNodeOptions
// botNodeConfig mirrors bot_node_config.json, populated by LoadBotNodeConfig.
// The map is rebuilt rather than mutated on every load, so the pointer can be
// swapped atomically; read it through BotNodes.
var botNodeConfig atomic.Pointer[BotNodeMembers]
// BotNodes returns the per-node options currently in effect, or nil before the
// first LoadBotNodeConfig. Like Current it must be called on every use rather
// than stored.
func BotNodes() BotNodeMembers {
nodes := botNodeConfig.Load()
if nodes == nil {
return nil
}
return *nodes
}
// C_botNodeConfig is the global singleton mirroring bot_node_config.json,
// populated by LoadBotNodeConfig.
var C_botNodeConfig BotNodeMembers
+7 -9
View File
@@ -37,11 +37,10 @@ var webhookEndpointFields = map[string]func(interface{}) bool{
// configuration.
func WebhookEndpoints() map[string]WebhookEndpointConfig {
endpoints := map[string]WebhookEndpointConfig{}
cfg := Current()
if cfg == nil {
if C_globalConfig == nil {
return endpoints
}
for name, endpoint := range cfg.Webhook.Endpoints {
for name, endpoint := range C_globalConfig.Webhook.Endpoints {
endpoints[name] = endpoint
}
return endpoints
@@ -108,15 +107,14 @@ func DeleteWebhookEndpoint(name string) error {
}
// webhookEndpoint returns the named endpoint held by the loaded configuration.
// No extra lock is needed to read it: a reload replaces the whole configuration
// rather than mutating it in place, and Current publishes the replacement
// atomically, so the value read is always from one complete version.
// No lock is needed to read it: a reload replaces the whole configuration
// rather than mutating it in place, and the value is read from whichever
// version is current.
func webhookEndpoint(name string) (WebhookEndpointConfig, bool) {
cfg := Current()
if cfg == nil {
if C_globalConfig == nil {
return WebhookEndpointConfig{}, false
}
endpoint, exists := cfg.Webhook.Endpoints[name]
endpoint, exists := C_globalConfig.Webhook.Endpoints[name]
return endpoint, exists
}
+2 -16
View File
@@ -51,27 +51,13 @@ func setupAdminToken() {
utils.AddToken("test-admin-token", 1, "admin", "tester")
}
// decodeResponse decodes a success envelope and returns its data object, which
// the getInfo and getStatus handlers key by uuid. Responses are wrapped by
// utils.SendSuccessResponse as {success, message?, data:{...}}, the shape the
// console reads as `response.data[uuid]` (see frontend/src/api/index.js), so
// the tests index the returned map by uuid rather than by envelope key.
func decodeResponse(t *testing.T, w *httptest.ResponseRecorder) map[string]json.RawMessage {
t.Helper()
var body struct {
Success bool `json:"success"`
Data map[string]json.RawMessage `json:"data"`
}
var body map[string]json.RawMessage
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
t.Fatalf("decode response: %v; body=%s", err, w.Body.String())
}
if !body.Success {
t.Fatalf("response is not a success envelope: %s", w.Body.String())
}
if body.Data == nil {
t.Fatalf("response has no data object: %s", w.Body.String())
}
return body.Data
return body
}
func TestServerGetInfoAll(t *testing.T) {
+2 -2
View File
@@ -144,7 +144,7 @@ func (m *Manager) ShowBotInitMessage() {
m.mu.RLock()
defer m.mu.RUnlock()
cfg := config.Current()
cfg := config.C_globalConfig
params := template.BuildBotInitializationMsgParams()
for _, ctrl := range m.controllers {
@@ -176,7 +176,7 @@ func (m *Manager) ShowBotServerList() {
m.mu.RLock()
defer m.mu.RUnlock()
cfg := config.Current()
cfg := config.C_globalConfig
params := template.BuildParamsFromServerList()
for _, ctrl := range m.controllers {
+3 -3
View File
@@ -71,7 +71,7 @@ func (e *EmailController) SendStatusChange(change node.StatusChange) error {
return nil
}
cfg := config.Current()
cfg := config.C_globalConfig
params := template.BuildParamsFromStatusChange(change)
body := template.Render(cfg.ControllerMessage.ServerStatusChanged, params, e.cfg.Markdown)
@@ -85,7 +85,7 @@ func (e *EmailController) SendServerList(onlineServers, offlineServers string) e
return nil
}
cfg := config.Current()
cfg := config.C_globalConfig
params := template.BuildParamsFromServerList()
body := template.Render(cfg.ControllerMessage.ServerList, params, e.cfg.Markdown)
@@ -98,7 +98,7 @@ func (e *EmailController) SendExecuteResult(serverName, serverUUID, command, res
return nil
}
cfg := config.Current()
cfg := config.C_globalConfig
params := template.BuildParamsFromExecResult(serverName, serverUUID, command, result)
body := template.Render(cfg.ControllerMessage.ServerExecuteResult, params, e.cfg.Markdown)
+3 -3
View File
@@ -65,7 +65,7 @@ func (n *NtfyController) SendStatusChange(change node.StatusChange) error {
return nil
}
cfg := config.Current()
cfg := config.C_globalConfig
params := template.BuildParamsFromStatusChange(change)
message := template.Render(cfg.ControllerMessage.ServerStatusChanged, params, n.cfg.Markdown)
@@ -79,7 +79,7 @@ func (n *NtfyController) SendServerList(onlineServers, offlineServers string) er
return nil
}
cfg := config.Current()
cfg := config.C_globalConfig
params := template.BuildParamsFromServerList()
message := template.Render(cfg.ControllerMessage.ServerList, params, n.cfg.Markdown)
@@ -92,7 +92,7 @@ func (n *NtfyController) SendExecuteResult(serverName, serverUUID, command, resu
return nil
}
cfg := config.Current()
cfg := config.C_globalConfig
params := template.BuildParamsFromExecResult(serverName, serverUUID, command, result)
message := template.Render(cfg.ControllerMessage.ServerExecuteResult, params, n.cfg.Markdown)
+5 -14
View File
@@ -17,15 +17,6 @@ import (
"nukumizu-backend/postLog"
)
// actionLogEnabled reports whether the NapCat HTTP API methods echo each action
// they send, per the showNapcatAction toggle. Unlike the WebSocket message path
// in qq.go it deliberately does not also require debugMode: these methods have
// always logged on this toggle alone, and preserving that is intentional.
func actionLogEnabled() bool {
cfg := config.Current()
return cfg != nil && cfg.Debug.ShowNapcatAction
}
// APIResponse mirrors NapCat's HTTP API response envelope.
type APIResponse struct {
Status string `json:"status"`
@@ -164,7 +155,7 @@ func (c *Client) SendMsg(targetType string, targetID int64, msg string, hasAt bo
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
if actionLogEnabled() {
if config.C_globalConfig.Debug.ShowNapcatAction {
postLog.Debug(fmt.Sprintf("[Napcat] SendMsg -> %s (%s): %s", endpoint, targetType, message))
}
@@ -185,7 +176,7 @@ func (c *Client) RecallMsg(msgID int64) (*APIResponse, error) {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
if actionLogEnabled() {
if config.C_globalConfig.Debug.ShowNapcatAction {
postLog.Debug(fmt.Sprintf("[Napcat] RecallMsg -> /delete_msg: %d", msgID))
}
@@ -199,7 +190,7 @@ func (c *Client) RecallMsg(msgID int64) (*APIResponse, error) {
// GetGroupList retrieves the list of joined groups from NapCat.
func (c *Client) GetGroupList() (*APIResponse, error) {
if actionLogEnabled() {
if config.C_globalConfig.Debug.ShowNapcatAction {
postLog.Debug("[Napcat] GetGroupList -> /get_group_list")
}
@@ -220,7 +211,7 @@ func (c *Client) GetGroupInfo(groupID int64) (*APIResponse, error) {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
if actionLogEnabled() {
if config.C_globalConfig.Debug.ShowNapcatAction {
postLog.Debug(fmt.Sprintf("[Napcat] GetGroupInfo -> /get_group_info: %d", groupID))
}
@@ -234,7 +225,7 @@ func (c *Client) GetGroupInfo(groupID int64) (*APIResponse, error) {
// GetFriendsList retrieves the friends list from NapCat.
func (c *Client) GetFriendsList() (*APIResponse, error) {
if actionLogEnabled() {
if config.C_globalConfig.Debug.ShowNapcatAction {
postLog.Debug("[Napcat] GetFriendsList -> /get_friend_list")
}
+15 -26
View File
@@ -100,34 +100,24 @@ func (q *QQController) handleNapcatEvent(raw []byte) {
return
}
// The configuration is read once per event: the debug guards below are
// evaluated several times, and taking them all from one version keeps a
// concurrent reload from splitting them mid-event.
cfg := config.Current()
if cfg == nil {
return
}
debugMode := cfg.System.DebugMode
showAction := debugMode && cfg.Debug.ShowNapcatAction
if debugMode && cfg.Debug.ShowNapcatMsg {
if config.C_globalConfig.System.DebugMode && config.C_globalConfig.Debug.ShowNapcatMsg {
postLog.Debug("Napcat WS event received: " + string(raw))
}
// Only handle message events; ignore notice/request/meta_event.
if ev.PostType != "message" {
if showAction {
if config.C_globalConfig.System.DebugMode && config.C_globalConfig.Debug.ShowNapcatAction {
postLog.Debug("Ignoring Napcat WS event: " + string(raw))
}
return
}
// Ignore messages the bot itself sent (echo prevention).
if q.isSelfMessage(ev) && !debugMode {
if q.isSelfMessage(ev) && !config.C_globalConfig.System.DebugMode {
return
}
if q.isSelfMessage(ev) && debugMode && cfg.Debug.NapcatIgnoreSelfMsg {
if showAction {
if q.isSelfMessage(ev) && config.C_globalConfig.System.DebugMode && config.C_globalConfig.Debug.NapcatIgnoreSelfMsg {
if config.C_globalConfig.System.DebugMode && config.C_globalConfig.Debug.ShowNapcatAction {
postLog.Debug("Ignoring Napcat WS self message: " + string(raw))
}
return
@@ -148,7 +138,7 @@ func (q *QQController) handleNapcatEvent(raw []byte) {
response := q.processCommand(cmd)
if response == "" {
if showAction {
if config.C_globalConfig.System.DebugMode && config.C_globalConfig.Debug.ShowNapcatAction {
postLog.Debug("Napcat WS command discarded: " + string(raw))
}
return
@@ -170,7 +160,6 @@ func (q *QQController) handleNapcatEvent(raw []byte) {
// complete command to the unified processor. It returns the response text to
// reply with; an empty response means the message was discarded.
func (q *QQController) processCommand(cmd controller.Command) string {
cfg := config.Current()
text := cmd.RawText
// In "at" listen mode, require an @mention of the bot and strip it before
@@ -179,7 +168,7 @@ func (q *QQController) processCommand(cmd controller.Command) string {
if q.cfg.ListenMethod == "at" {
atMention := fmt.Sprintf("[CQ:at,qq=%d]", q.cfg.BotQQID)
if !strings.Contains(text, atMention) {
if cfg != nil && cfg.System.DebugMode && cfg.Debug.ShowNapcatAction {
if config.C_globalConfig.System.DebugMode && config.C_globalConfig.Debug.ShowNapcatAction {
postLog.Debug("Napcat WS message ignored (no @mention): " + text)
}
return "" // Not mentioned, ignore.
@@ -201,7 +190,7 @@ func (q *QQController) processCommand(cmd controller.Command) string {
// Hand the complete command to the unified processor, which checks group
// vs private, trusted groups, admin permissions, and executes it.
response, err := controller.GetManager().Trigger(parsed, q.trustedGroupIDs(), q.adminIDs(), q.cfg.ListenMethod)
if cfg != nil && cfg.System.DebugMode && cfg.Debug.ShowTriggerCmdEcho {
if config.C_globalConfig.System.DebugMode && config.C_globalConfig.Debug.ShowTriggerCmdEcho {
postLog.Debug(fmt.Sprintf("[qq_napcat] triggered command: \"/%s\" with args: \"%s\" from chatID: %d and senderID: %d", parsed.Command, strings.Join(parsed.Args, ", "), cmd.ChatID, cmd.SenderID))
}
if err != nil {
@@ -224,7 +213,7 @@ func (q *QQController) isSelfMessage(ev oneBotEvent) bool {
// adminIDs returns the QQ admin IDs from bot_user_config.json.
func (q *QQController) adminIDs() []string {
if c := config.BotUsers(); c != nil {
if c := config.C_botUserConfig; c != nil {
return c.QQ.Admins.IDs()
}
return nil
@@ -232,7 +221,7 @@ func (q *QQController) adminIDs() []string {
// trustedGroupIDs returns the QQ trusted group IDs from bot_user_config.json.
func (q *QQController) trustedGroupIDs() []string {
if c := config.BotUsers(); c != nil {
if c := config.C_botUserConfig; c != nil {
return c.QQ.TrustedGroups.IDs()
}
return nil
@@ -248,7 +237,7 @@ func (q *QQController) SendMessage(message controller.Message) error {
}
// Only notify trusted groups and admins whose options allow this message type.
if uc := config.BotUsers(); uc != nil {
if uc := config.C_botUserConfig; uc != nil {
for groupID, opts := range uc.QQ.TrustedGroups {
if !controller.MemberReceives(opts, message.Type) {
continue
@@ -271,12 +260,12 @@ func (q *QQController) SendStatusChange(change node.StatusChange) error {
return nil
}
cfg := config.Current()
cfg := config.C_globalConfig
params := template.BuildParamsFromStatusChange(change)
message := template.Render(cfg.ControllerMessage.ServerStatusChanged, params, q.cfg.Markdown)
// Only notify trusted groups and admins whose event_status_notify is true.
if uc := config.BotUsers(); uc != nil {
if uc := config.C_botUserConfig; uc != nil {
for groupID, opts := range uc.QQ.TrustedGroups {
if !opts.EventStatusNotify {
continue
@@ -300,7 +289,7 @@ func (q *QQController) SendServerList(onlineServers, offlineServers string) erro
return nil
}
cfg := config.Current()
cfg := config.C_globalConfig
params := template.BuildParamsFromServerList()
message := template.Render(cfg.ControllerMessage.ServerList, params, q.cfg.Markdown)
@@ -316,7 +305,7 @@ func (q *QQController) SendExecuteResult(serverName, serverUUID, command, result
return nil
}
cfg := config.Current()
cfg := config.C_globalConfig
params := template.BuildParamsFromExecResult(serverName, serverUUID, command, result)
message := template.Render(cfg.ControllerMessage.ServerExecuteResult, params, q.cfg.Markdown)
@@ -148,7 +148,7 @@ func (t *TelegramController) handleUpdate(_ context.Context, _ *bot.Bot, update
}
msg := update.Message
if cfg := config.Current(); cfg != nil && cfg.System.DebugMode && cfg.Debug.ShowTelegramMsg {
if config.C_globalConfig.System.DebugMode && config.C_globalConfig.Debug.ShowTelegramMsg {
raw, _ := json.Marshal(update)
postLog.Debug("Telegram update received: " + string(raw))
}
@@ -232,7 +232,7 @@ func (t *TelegramController) processCommand(cmd controller.Command) string {
// Hand the complete command to the unified processor, which checks group
// vs private, trusted groups, admin permissions, and executes it.
response, err := controller.GetManager().Trigger(parsed, t.trustedGroupIDs(), t.resolvedAdminList(), t.cfg.ListenMethod)
if cfg := config.Current(); cfg != nil && cfg.System.DebugMode && cfg.Debug.ShowTriggerCmdEcho {
if config.C_globalConfig.System.DebugMode && config.C_globalConfig.Debug.ShowTriggerCmdEcho {
postLog.Debug(fmt.Sprintf("[telegram] triggered command: \"/%s\" with args: \"%s\" from chatID: %d and senderID: %d", parsed.Command, strings.Join(parsed.Args, ", "), cmd.ChatID, cmd.SenderID))
}
if err != nil {
@@ -252,7 +252,7 @@ func (t *TelegramController) SendMessage(message controller.Message) error {
}
// Only notify trusted groups and admins whose options allow this message type.
if uc := config.BotUsers(); uc != nil {
if uc := config.C_botUserConfig; uc != nil {
for groupID, opts := range uc.Telegram.TrustedGroups {
if !controller.MemberReceives(opts, message.Type) {
continue
@@ -275,12 +275,12 @@ func (t *TelegramController) SendStatusChange(change node.StatusChange) error {
return nil
}
cfg := config.Current()
cfg := config.C_globalConfig
params := template.BuildParamsFromStatusChange(change)
message := template.Render(cfg.ControllerMessage.ServerStatusChanged, params, t.cfg.Markdown)
// Only notify trusted groups and admins whose event_status_notify is true.
if uc := config.BotUsers(); uc != nil {
if uc := config.C_botUserConfig; uc != nil {
for groupID, opts := range uc.Telegram.TrustedGroups {
if !opts.EventStatusNotify {
continue
@@ -303,7 +303,7 @@ func (t *TelegramController) SendServerList(onlineServers, offlineServers string
return nil
}
cfg := config.Current()
cfg := config.C_globalConfig
params := template.BuildParamsFromServerList()
message := template.Render(cfg.ControllerMessage.ServerList, params, t.cfg.Markdown)
@@ -317,7 +317,7 @@ func (t *TelegramController) SendExecuteResult(serverName, serverUUID, command,
return nil
}
cfg := config.Current()
cfg := config.C_globalConfig
params := template.BuildParamsFromExecResult(serverName, serverUUID, command, result)
message := template.Render(cfg.ControllerMessage.ServerExecuteResult, params, t.cfg.Markdown)
@@ -395,7 +395,7 @@ func (t *TelegramController) resolveUsername(username string) (int64, bool) {
// adminIDs returns the Telegram admin entries (numeric user ID or @username)
// from bot_user_config.json.
func (t *TelegramController) adminIDs() []string {
if c := config.BotUsers(); c != nil {
if c := config.C_botUserConfig; c != nil {
return c.Telegram.Admins.IDs()
}
return nil
@@ -403,7 +403,7 @@ func (t *TelegramController) adminIDs() []string {
// trustedGroupIDs returns the Telegram trusted group IDs from bot_user_config.json.
func (t *TelegramController) trustedGroupIDs() []string {
if c := config.BotUsers(); c != nil {
if c := config.C_botUserConfig; c != nil {
return c.Telegram.TrustedGroups.IDs()
}
return nil
+3 -3
View File
@@ -66,7 +66,7 @@ func (w *WebhookController) SendStatusChange(change node.StatusChange) error {
return nil
}
cfg := config.Current()
cfg := config.C_globalConfig
params := template.BuildParamsFromStatusChange(change)
message := template.Render(cfg.ControllerMessage.ServerStatusChanged, params, w.cfg.Markdown)
@@ -87,7 +87,7 @@ func (w *WebhookController) SendServerList(onlineServers, offlineServers string)
return nil
}
cfg := config.Current()
cfg := config.C_globalConfig
params := template.BuildParamsFromServerList()
message := template.Render(cfg.ControllerMessage.ServerList, params, w.cfg.Markdown)
@@ -108,7 +108,7 @@ func (w *WebhookController) SendExecuteResult(serverName, serverUUID, command, r
return nil
}
cfg := config.Current()
cfg := config.C_globalConfig
params := template.BuildParamsFromExecResult(serverName, serverUUID, command, result)
message := template.Render(cfg.ControllerMessage.ServerExecuteResult, params, w.cfg.Markdown)
+4 -4
View File
@@ -22,13 +22,13 @@ func commandMarkdown(cmd Command) bool {
}
func handleHelp(cmd Command) (string, error) {
cfg := config.Current()
cfg := config.C_globalConfig
params := template.BuildBotInitializationMsgParams()
return template.Render(cfg.ControllerMessage.BotHelp, params, commandMarkdown(cmd)), nil
}
func handleList(cmd Command) (string, error) {
cfg := config.Current()
cfg := config.C_globalConfig
params := template.BuildParamsFromServerList()
return template.Render(cfg.ControllerMessage.ServerList, params, commandMarkdown(cmd)), nil
}
@@ -139,7 +139,7 @@ func handleRun(cmd Command) (string, error) {
return fmt.Sprintf("Error getting results: %v", err), nil
}
cfg := config.Current()
cfg := config.C_globalConfig
params := template.BuildParamsFromExecResult(uuidArg, uuidArg, command, formatTaskResults(results))
return template.Render(cfg.ControllerMessage.ServerExecuteResult, params, commandMarkdown(cmd)), nil
}
@@ -189,7 +189,7 @@ func handleInfo(cmd Command) (string, error) {
}
func telegram_handleStart(cmd Command) (string, error) {
cfg := config.Current()
cfg := config.C_globalConfig
params := template.BuildBotInitializationMsgParams()
return template.Render(cfg.ControllerMessage.Tg_BotStart, params, commandMarkdown(cmd)), nil
}
+4 -12
View File
@@ -18,14 +18,6 @@ import (
"nukumizu-backend/postLog"
)
// taskEchoEnabled reports whether Komari task progress should be echoed to the
// log: debug mode plus the showKomariTaskEcho toggle. The configuration is read
// once per call so both flags come from the same reload.
func taskEchoEnabled() bool {
cfg := config.Current()
return cfg != nil && cfg.System.DebugMode && cfg.Debug.ShowKomariTaskEcho
}
// NodeInfo represents a single node as returned by Komari's
// common:getNodes RPC2 method.
type NodeInfo struct {
@@ -154,7 +146,7 @@ func (c *Client) Login(username, password string) error {
var kr KomariResponse
if err := json.NewDecoder(resp.Body).Decode(&kr); err != nil {
if config.IsDebugMode() {
if config.C_globalConfig.System.DebugMode {
respBody, _ := io.ReadAll(resp.Body)
return fmt.Errorf("failed to parse komari login response: %w.\nResponse: %s", err, respBody)
}
@@ -340,7 +332,7 @@ func (c *Client) ExecTask(uuids []string, command string) (string, error) {
return "", fmt.Errorf("failed to parse komari task exec data: %w", err)
}
if taskEchoEnabled() {
if config.C_globalConfig.System.DebugMode && config.C_globalConfig.Debug.ShowKomariTaskEcho {
postLog.Debug(fmt.Sprintf("Created Komari task %s for %d clients", result.TaskID, len(uuids)))
}
return result.TaskID, nil
@@ -383,7 +375,7 @@ func (c *Client) GetTaskResult(taskID string) ([]TaskResult, bool, error) {
// PollTaskResult polls for task results every 1 second until all results are
// available or 60 seconds have elapsed.
func (c *Client) PollTaskResult(taskID string) ([]TaskResult, error) {
if taskEchoEnabled() {
if config.C_globalConfig.System.DebugMode && config.C_globalConfig.Debug.ShowKomariTaskEcho {
postLog.Debug(fmt.Sprintf("Polling for Komari task %s results...", taskID))
}
@@ -401,7 +393,7 @@ func (c *Client) PollTaskResult(taskID string) ([]TaskResult, error) {
return nil, err
}
if done {
if taskEchoEnabled() {
if config.C_globalConfig.System.DebugMode && config.C_globalConfig.Debug.ShowKomariTaskEcho {
postLog.Info(fmt.Sprintf("Task %s completed with %d results", taskID, len(results)))
}
return results, nil
+1 -4
View File
@@ -409,10 +409,7 @@ func GetWSClient() *WSClient {
// LoginAndStart performs the Komari login and returns an error if it fails.
func LoginAndStart() error {
cfg := config.Current()
if cfg == nil {
return fmt.Errorf("configuration not loaded")
}
cfg := config.C_globalConfig
client := GetClient()
if client == nil {
return fmt.Errorf("komari client not initialized")
+1 -5
View File
@@ -20,11 +20,7 @@ import (
// proxyURL returns the system-wide network proxy URL, or nil when none is
// configured. A missing scheme is normalized to http:// for convenience.
func proxyURL() *url.URL {
cfg := config.Current()
if cfg == nil {
return nil
}
raw := cfg.System.NetworkProxy
raw := config.C_globalConfig.System.NetworkProxy
if raw == "" {
return nil
}
+1 -1
View File
@@ -201,7 +201,7 @@ func startWebhookServer(cfg *config.Config) {
// initControllers initializes and starts all configured controllers.
func initControllers() {
cfg := config.Current()
cfg := config.C_globalConfig
mgr := controller.GetManager()
if mgr == nil {
return