feat(skills): 新增 Skill 调用统计(账本 + 页面总览/详情)

后端:
- 新表 skill_usage(无外键,任务/会话删除后统计保留),一次 Skill()
  调用记一行,只记维度不记 args 正文;含 found 标记未命中调用
- 用 RunInfo 经 ctx 把 task/exploration/intent/session 传到工具装配层,
  在 Skill meta-tool 外包一层做埋点(可记 args 长度与不存在的 skill)
- fsListSkills 返回附带调用次数/覆盖任务/最近调用;新增
  GET /api/skills/missing 与 GET /api/skills/{name}/usage

前端(/system/skills):
- 树节点显示调用次数 badge;目录优先排序 + 子项缩进错开一级
- 空态改为技能库总览(指标卡 + 调用排行 + 最近/未使用)
- 选中 skill 的详情面板改为响应式左右分栏(配置在左、统计在右,
  窄屏回落上下),含最近调用明细与未命中缺口清单

Claude-Session: https://claude.ai/code/session_01BNN7npEJs4X6LfTtgniMd8
This commit is contained in:
Autumn-27 committed 2026-08-20 12:30:01 -04:00
1 parent 43f40dd74f
commit 834e94535a
16 files changed
+831 -52

No files matched your search

+1
View File
@@ -92,6 +92,7 @@ func (c *ChatAgent) Chat(ctx context.Context, agentKey, sessionID, message strin
// visible skills/MCP and lets the DB tools table filter/override. DefaultTools
// have no tools-table rows, so they always pass through.
base := actool.DefaultTools()
ctx = WithRunInfo(ctx, RunInfo{SessionID: sessionID})
tools, def, cleanup := AugmentTools(ctx, agentKey, base)
defer cleanup()
+1
View File
@@ -85,6 +85,7 @@ func (m *MainAgent) Chat(ctx context.Context, taskID int64, as *db.AssetStore, t
tsx.SetNotifyGoal(notifyGoal) // set_goals 新增目标 → 给 planner 记一条「人新增了目标:…」触发
// 领域工具 + 基础默认工具集(Read/Write/Edit/MultiEdit/LS/Glob/Grep/Bash)
base := append(tsx.MainAgentTools(), actool.DefaultTools()...)
ctx = WithRunInfo(ctx, RunInfo{TaskID: taskID, ExplorationID: explorationID(ts)})
tools, def, cleanup := AugmentTools(ctx, "mainagent", base)
defer cleanup()
// 本任务的工作目录 <workDir>/tasks/<taskID>,先建好。
+1
View File
@@ -273,6 +273,7 @@ func (p *Planner) Plan(ctx context.Context, taskID int64, as *db.AssetStore, ts
}
// 领域工具 + 基础默认工具集(Read/Write/Edit/MultiEdit/LS/Glob/Grep/Bash)
base := append(tsx.PlannerTools(), actool.DefaultTools()...)
ctx = WithRunInfo(ctx, RunInfo{TaskID: taskID, ExplorationID: explorationID(ts)})
tools, def, cleanup := AugmentTools(ctx, "planner", base)
defer cleanup()
// 关键态势(刚完成的意图 + 预取的完整图)改放【本轮 user 输入】(见下方 input),system
+47
View File
@@ -0,0 +1,47 @@
package agent
import (
"context"
"github.com/Autumn-27/artex/db"
)
// RunInfo identifies WHICH run a tool call belongs to. Tool assembly only receives
// (ctx, agentKey) — the task/exploration ids live in the caller's arguments, not the
// ctx — so anything wired at assembly time (currently the Skill ledger in
// server/assembly.go) has no way to attribute a call to a task. Each run attaches
// its own RunInfo before calling AugmentTools; the wiring closure reads it once and
// captures it, so per-run attribution stays correct without threading parameters
// through the tool layer. Same pattern as TaskClock (see taskclock.go).
//
// Zero value = attribution unknown; every consumer must treat it as optional.
type RunInfo struct {
TaskID int64 // task registry id; 0 for non-task runs (chat sessions)
ExplorationID int64 // exploration id; 0 when unknown
IntentID int64 // worker's intent node; 0 for planner/mainagent/chat
SessionID string // chat conversation id; empty for task runs
}
// explorationID reads a store's exploration id, tolerating a nil store (planner and
// worker runs can be driven without one in tests).
func explorationID(ts *db.ExplorationStore) int64 {
if ts == nil {
return 0
}
return ts.ID()
}
type runInfoKey struct{}
// WithRunInfo attaches run attribution to ctx.
func WithRunInfo(ctx context.Context, ri RunInfo) context.Context {
return context.WithValue(ctx, runInfoKey{}, ri)
}
// RunInfoFrom reads the RunInfo (zero value if none attached).
func RunInfoFrom(ctx context.Context) RunInfo {
if v, ok := ctx.Value(runInfoKey{}).(RunInfo); ok {
return v
}
return RunInfo{}
}
+1
View File
@@ -265,6 +265,7 @@ func (w *Worker) Execute(ctx context.Context, name string, taskID int64, as *db.
// phase, Bash is hidden via Settlement.DisabledTools (no local gating needed).
base := append(tsx.WorkerTools(), w.extraTools...)
base = append(base, actool.DefaultTools()...)
ctx = WithRunInfo(ctx, RunInfo{TaskID: taskID, ExplorationID: explorationID(ts), IntentID: intent.ID})
tools, def, cleanup := AugmentTools(ctx, "worker", base)
defer cleanup()