新增活跃度查询工具

功能:
- 查询指定时间范围内的活跃节点数和活跃人数
- 活跃节点:统计 nodeinfo 表 updated_at 字段
- 活跃人数:统计 text_message 表按 from_id 去重的用户数

使用场景:
- 用户问'现在有多少人活跃'时 AI 调用此工具
- 用户问'当前有多少节点在线'时 AI 调用此工具
- 支持附带时间条件,默认1小时,最大24小时

参数:
- hours: 查询最近N小时,默认1小时,最大24小时
- query_type: both/nodes/users,默认 both

实现:
- internal/agents/active/active.go - 工具主逻辑
- internal/store/active_store.go - 数据库查询方法
- 完整的单元测试,所有测试通过
- 在 ai/service.go 中注册工具

测试:
-  默认查询(1小时,both)
-  指定时间查询(6小时、24小时)
-  仅查询节点/人数
-  时间限制验证
-  项目编译成功

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 21:26:14 +08:00
co-authored by Claude Fable 5
parent 2f83308dce
commit cd5dcb29f5
5 changed files with 544 additions and 0 deletions
+187
View File
@@ -0,0 +1,187 @@
package active
import (
"context"
"encoding/json"
"testing"
"time"
"meshtastic_mqtt_server/internal/agenttool"
)
// mockActiveStore 是用于测试的 mock store
type mockActiveStore struct {
activeNodeCount int64
activeUserCount int64
}
func (m *mockActiveStore) CountActiveNodes(since time.Time) (int64, error) {
return m.activeNodeCount, nil
}
func (m *mockActiveStore) CountActiveUsers(since time.Time) (int64, error) {
return m.activeUserCount, nil
}
func TestActiveTool_Query(t *testing.T) {
now := time.Date(2024, 6, 23, 12, 0, 0, 0, time.UTC)
store := &mockActiveStore{
activeNodeCount: 25,
activeUserCount: 15,
}
tool := &Tool{
enabled: true,
store: store,
}
tests := []struct {
name string
hours float64
queryType string
expectNodes bool
expectUsers bool
expectError bool
}{
{
name: "默认查询1小时(both",
hours: 0, // 0 表示使用默认值
queryType: "",
expectNodes: true,
expectUsers: true,
expectError: false,
},
{
name: "查询6小时",
hours: 6,
queryType: "both",
expectNodes: true,
expectUsers: true,
expectError: false,
},
{
name: "仅查询节点",
hours: 1,
queryType: "nodes",
expectNodes: true,
expectUsers: false,
expectError: false,
},
{
name: "仅查询人数",
hours: 1,
queryType: "users",
expectNodes: false,
expectUsers: true,
expectError: false,
},
{
name: "查询24小时(最大值)",
hours: 24,
queryType: "both",
expectNodes: true,
expectUsers: true,
expectError: false,
},
{
name: "超过24小时应限制到24小时",
hours: 48,
queryType: "both",
expectNodes: true,
expectUsers: true,
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
params := activeParams{
Hours: tt.hours,
QueryType: tt.queryType,
}
argsJSON, _ := json.Marshal(params)
runtime := agenttool.Runtime{Now: now}
result, err := tool.Execute(context.Background(), string(argsJSON), runtime)
if tt.expectError && err == nil {
t.Errorf("Expected error but got none")
}
if !tt.expectError && err != nil {
t.Errorf("Unexpected error: %v", err)
}
if !tt.expectError {
t.Logf("Query result:\n%s", result)
// 验证结果包含预期的内容
if tt.expectNodes && result != "" {
// 应该包含节点统计
if !contains(result, "活跃节点") {
t.Errorf("Expected result to contain node count")
}
}
if tt.expectUsers && result != "" {
// 应该包含人数统计
if !contains(result, "活跃人数") {
t.Errorf("Expected result to contain user count")
}
}
}
})
}
}
func TestActiveTool_Enabled(t *testing.T) {
// 测试工具启用状态
tests := []struct {
name string
enabled bool
store ActiveStore
expect bool
}{
{
name: "启用且有store",
enabled: true,
store: &mockActiveStore{},
expect: true,
},
{
name: "启用但无store",
enabled: true,
store: nil,
expect: false,
},
{
name: "禁用且有store",
enabled: false,
store: &mockActiveStore{},
expect: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tool := &Tool{
enabled: tt.enabled,
store: tt.store,
}
if tool.Enabled() != tt.expect {
t.Errorf("Expected Enabled() = %v, got %v", tt.expect, tool.Enabled())
}
})
}
}
func contains(s, substr string) bool {
return len(s) > 0 && len(substr) > 0 && (s == substr || len(s) >= len(substr) && (s[:len(substr)] == substr || s[len(s)-len(substr):] == substr || containsMiddle(s, substr)))
}
func containsMiddle(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}