Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions internal/app/runtime_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,29 @@ func (r *Runtime) RuntimeDiagnostics() Result {
}
}

// RuntimeOverview 是 Nexus/NexusDock Cloud 的轻量控制面投影。
// 这里直接统计本地索引,不能调用 RuntimeSkills 等明细接口,否则一个概览请求
// 会重新触发 Skill 包校验、digest 和文件遍历,远程链路仍会被本地扫描拖慢。
func (r *Runtime) RuntimeOverview() (Result, error) {
tasks, err := r.taskTools.RuntimeOverview()
if err != nil {
return nil, err
}
skillCount, err := r.skills.RuntimeCount()
if err != nil {
return nil, err
}
pluginCount, pluginErr := r.plugins.RuntimeCount()
return Result{
"ok": true,
"source": runtimeAPISource,
"tasks": tasks,
"skills": map[string]any{"count": skillCount},
"plugins": map[string]any{"count": pluginCount, "available": pluginErr == nil},
"mcp": map[string]any{"count": r.dynamicMCP.RuntimeCount()},
}, nil
}

func (r *Runtime) RuntimeSkills() (Result, error) {
return r.skills.RuntimeSkills()
}
Expand Down
1 change: 1 addition & 0 deletions internal/httpx/runtime_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ func registerRuntimeAPI(mux *http.ServeMux, runtime runtimeapi.Runtime, cfg conf
h := runtimeAPIHandler(runtime, cfg, oauthStore)
mux.HandleFunc("/internal/runtime/status", h)
mux.HandleFunc("/internal/runtime/analytics", h)
mux.HandleFunc("/internal/runtime/overview", h)
mux.HandleFunc("/internal/runtime/diagnostics", h)
mux.HandleFunc("/internal/runtime/capabilities", h)
mux.HandleFunc("/internal/runtime/skills", h)
Expand Down
1 change: 1 addition & 0 deletions internal/httpx/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,7 @@ func TestRuntimeAPIMethodContract(t *testing.T) {
allow string
}{
{method: http.MethodGet, path: "/internal/runtime/status", status: http.StatusOK},
{method: http.MethodGet, path: "/internal/runtime/overview", status: http.StatusOK},
{method: http.MethodGet, path: "/internal/runtime/diagnostics", status: http.StatusOK},
{method: http.MethodPost, path: "/internal/runtime/capabilities", status: http.StatusOK},
{method: http.MethodPost, path: "/internal/runtime/status", status: http.StatusMethodNotAllowed, allow: "GET"},
Expand Down
3 changes: 3 additions & 0 deletions internal/runtimeapi/dispatch.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ func Dispatch(ctx context.Context, runtime Runtime, request Request) (map[string
return map[string]any(runtime.RuntimeStatus()), nil
case path == "/internal/runtime/analytics":
return map[string]any(runtime.RuntimeAnalytics()), nil
case path == "/internal/runtime/overview":
result, err := runtime.RuntimeOverview()
return map[string]any(result), err
case path == "/internal/runtime/diagnostics":
diagnostics, ok := runtime.(DiagnosticsRuntime)
if !ok {
Expand Down
30 changes: 28 additions & 2 deletions internal/runtimeapi/dispatch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,16 @@ type runtimeStub struct {
pluginName string
}

func (r *runtimeStub) RuntimeStatus() app.Result { return app.Result{"status": "ok"} }
func (r *runtimeStub) RuntimeAnalytics() app.Result { return app.Result{"total_calls": 7} }
func (r *runtimeStub) RuntimeStatus() app.Result { return app.Result{"status": "ok"} }
func (r *runtimeStub) RuntimeAnalytics() app.Result { return app.Result{"total_calls": 7} }
func (r *runtimeStub) RuntimeOverview() (app.Result, error) {
return app.Result{
"tasks": map[string]int{"active": 1, "active_recent_24h": 1},
"skills": map[string]any{"count": 2},
"plugins": map[string]any{"count": 3, "available": true},
"mcp": map[string]any{"count": 4},
}, nil
}
func (r *runtimeStub) RuntimeSkills() (app.Result, error) { return app.Result{}, nil }
func (r *runtimeStub) RuntimeSkill(skill string) (app.Result, error) {
r.skillTarget = skill
Expand Down Expand Up @@ -111,6 +119,24 @@ func TestDispatchAnalytics(t *testing.T) {
}
}

func TestDispatchOverviewUsesDedicatedProjection(t *testing.T) {
result, err := Dispatch(context.Background(), &runtimeStub{}, Request{
Method: "GET",
Path: "/internal/runtime/overview",
})
if err != nil {
t.Fatal(err)
}
tasks, ok := result["tasks"].(map[string]int)
if !ok || tasks["active_recent_24h"] != 1 {
t.Fatalf("overview tasks = %#v", result["tasks"])
}
skills, ok := result["skills"].(map[string]any)
if !ok || skills["count"] != 2 {
t.Fatalf("overview skills = %#v", result["skills"])
}
}

func TestDispatchDiagnostics(t *testing.T) {
result, err := Dispatch(context.Background(), &diagnosticsRuntimeStub{}, Request{
Method: "GET",
Expand Down
1 change: 1 addition & 0 deletions internal/runtimeapi/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
type Runtime interface {
RuntimeStatus() app.Result
RuntimeAnalytics() app.Result
RuntimeOverview() (app.Result, error)
RuntimeSkills() (app.Result, error)
RuntimeSkill(skill string) (app.Result, error)
RuntimeSkillFiles(skill string) (app.Result, error)
Expand Down
4 changes: 4 additions & 0 deletions internal/tool/mcp/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ func (s *Service) CapabilityItems() []CapabilityItem {
return items
}

func (s *Service) RuntimeCount() int {
return len(s.mcpClients.List())
}

func (s *Service) Close() error {
if s == nil || s.mcpClients == nil {
return nil
Expand Down
8 changes: 8 additions & 0 deletions internal/tool/plugin/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,14 @@ func (s *Service) RuntimeList() (Result, error) {
return Result{"plugins": plugins, "count": len(plugins)}, nil
}

func (s *Service) RuntimeCount() (int, error) {
items, err := s.manager.List()
if err != nil {
return 0, pluginToolError(err)
}
return len(items), nil
}

func (s *Service) sourcePath(raw string) (string, error) {
source := strings.TrimSpace(raw)
if source == "" {
Expand Down
23 changes: 23 additions & 0 deletions internal/tool/skill/runtime_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,29 @@ type CapabilityItem struct {
ContentDigest string
}

// RuntimeCount 只读取已安装索引,不校验 Skill 包、计算 digest 或遍历文件。
// 概览只需要“已安装多少个”,完整可用性与文件信息仍由 RuntimeSkills 负责。
func (s *Service) RuntimeCount() (int, error) {
names, err := s.state.ListSkills()
if err != nil {
return 0, err
}
count := len(names)
if s.plugins == nil {
return count, nil
}
installed, err := s.plugins.List()
if err != nil {
return 0, err
}
for _, plugin := range installed {
if plugin.Enabled {
count += len(plugin.Components.Skills)
}
}
return count, nil
}

func (s *Service) CapabilityItems() ([]CapabilityItem, error) {
names, err := s.state.ListSkills()
if err != nil {
Expand Down
26 changes: 26 additions & 0 deletions internal/tool/task/runtime_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,36 @@ package task

import (
"strings"
"time"

"github.com/uvwt/agentdock/internal/taskstate"
)

const runtimeOverviewTaskLimit = 200

// RuntimeOverview 返回控制面板需要的 Task 轻量统计,避免为了几个数字把完整
// Task 列表跨 Nexus Bridge 传输。统计上限与既有 Runtime 概览保持一致。
func (s *Service) RuntimeOverview() (map[string]int, error) {
tasks, err := s.tasks.List("", runtimeOverviewTaskLimit)
if err != nil {
return nil, taskToolError(err)
}
counts := map[string]int{
"active": 0,
"completed": 0,
"blocked": 0,
"active_recent_24h": 0,
}
recentCutoff := time.Now().UTC().Add(-24 * time.Hour)
for _, task := range tasks {
counts[string(task.Status)]++
if task.Status == taskstate.StatusActive && !task.UpdatedAt.Before(recentCutoff) {
counts["active_recent_24h"]++
}
}
return counts, nil
}

func (s *Service) RuntimeTasks(status string, limit int) (Result, error) {
statusFilter := taskstate.Status(strings.ToLower(strings.TrimSpace(status)))
if statusFilter != "" && statusFilter != taskstate.StatusActive && statusFilter != taskstate.StatusBlocked && statusFilter != taskstate.StatusCompleted {
Expand Down
48 changes: 48 additions & 0 deletions internal/tool/task/runtime_api_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package task

import (
"encoding/json"
"os"
"path/filepath"
"testing"
"time"
)

func TestRuntimeOverviewCountsTaskStatesAndRecentActiveTasks(t *testing.T) {
service, root := newTaskTestService(t)

_, err := service.tasks.Create("recent", "recent active", []string{"done"}, nil, nil)
if err != nil {
t.Fatal(err)
}
old, err := service.tasks.Create("old", "old active", []string{"done"}, nil, nil)
if err != nil {
t.Fatal(err)
}
blocked, err := service.tasks.Create("blocked", "blocked task", []string{"done"}, nil, nil)
if err != nil {
t.Fatal(err)
}
if _, err := service.tasks.Block(blocked.ID, "waiting"); err != nil {
t.Fatal(err)
}

// 用真实 Task JSON 只调整隔离测试目录里的更新时间,验证 24h 边界不会把旧任务算进 recent。
old.UpdatedAt = time.Now().UTC().Add(-25 * time.Hour)
data, err := json.MarshalIndent(old, "", " ")
if err != nil {
t.Fatal(err)
}
data = append(data, '\n')
if err := os.WriteFile(filepath.Join(root, ".agentdock", "tasks", old.ID+".json"), data, 0o600); err != nil {
t.Fatal(err)
}

counts, err := service.RuntimeOverview()
if err != nil {
t.Fatal(err)
}
if counts["active"] != 2 || counts["blocked"] != 1 || counts["completed"] != 0 || counts["active_recent_24h"] != 1 {
t.Fatalf("overview counts = %#v", counts)
}
}
Loading