Revert "feat: 新增 Skills Usage 监控统计与图表 (#668)" (#670)

This reverts commit ce08d2b05a.
This commit is contained in:
ekko
2026-05-13 07:51:29 +08:00
committed by GitHub
parent ce08d2b05a
commit 91de3b12a1
19 changed files with 3 additions and 1662 deletions
@@ -6,7 +6,6 @@ import {
safeReadFile, extractDescription, listFilesRecursive, getHermesDir,
} from '../../services/config-helpers'
import { pinSkill } from '../../services/hermes/hermes-cli'
import { getSkillUsageStatsFromDb } from '../../db/hermes/sessions-db'
/** Read bundled manifest as a name→hash map from ~/.hermes/skills/.bundled_manifest */
function readBundledManifest(manifestContent: string | null): Map<string, string> {
@@ -240,18 +239,6 @@ export async function list(ctx: any) {
}
}
export async function usageStats(ctx: any) {
const rawDays = parseInt(String(ctx.query?.days ?? '7'), 10)
const days = Number.isFinite(rawDays) && rawDays > 0 ? Math.min(rawDays, 365) : 7
try {
ctx.body = await getSkillUsageStatsFromDb(days)
} catch (err: any) {
ctx.status = 500
ctx.body = { error: `Failed to read skill usage stats: ${err.message}` }
}
}
export async function toggle(ctx: any) {
const { name, enabled } = ctx.request.body as { name?: string; enabled?: boolean }
if (!name || typeof enabled !== 'boolean') {
@@ -783,42 +783,6 @@ export interface HermesUsageStats extends LocalUsageStats {
total_api_calls: number
}
export interface HermesSkillUsageRow {
skill: string
view_count: number
manage_count: number
total_count: number
percentage: number
last_used_at: number | null
}
export interface HermesSkillUsageDailySkillRow {
skill: string
view_count: number
manage_count: number
total_count: number
}
export interface HermesSkillUsageDailyRow {
date: string
view_count: number
manage_count: number
total_count: number
skills: HermesSkillUsageDailySkillRow[]
}
export interface HermesSkillUsageStats {
period_days: number
summary: {
total_skill_loads: number
total_skill_edits: number
total_skill_actions: number
distinct_skills_used: number
}
by_day: HermesSkillUsageDailyRow[]
top_skills: HermesSkillUsageRow[]
}
function tableHasColumn(
db: { prepare: (sql: string) => { all: (...params: any[]) => Record<string, unknown>[] } },
tableName: string,
@@ -828,197 +792,6 @@ function tableHasColumn(
return columns.some(column => String(column.name || '') === columnName)
}
function parseJsonObject(value: unknown): Record<string, unknown> | null {
if (value && typeof value === 'object' && !Array.isArray(value)) return value as Record<string, unknown>
if (typeof value !== 'string') return null
try {
const parsed = JSON.parse(value)
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as Record<string, unknown> : null
} catch {
return null
}
}
type SkillUsageAction = 'view' | 'manage'
interface RawSkillUsageEvent {
skill: string
action: SkillUsageAction
timestamp: number | null
}
function extractSkillNameFromViewContent(content: string): string {
const match = content.match(/^\[skill_view\]\s+name=(.+?)(?:\s+\(|\s*$)/)
return match?.[1]?.trim() || ''
}
function extractSkillNameFromManageContent(content: string): string {
const bracketMatch = content.match(/^\[skill_manage\]\s+name=(.+?)(?:\s+|\(|$)/)
if (bracketMatch?.[1]) return bracketMatch[1].trim()
const parsed = parseJsonObject(content)
const message = typeof parsed?.message === 'string' ? parsed.message : content
const quotedMatch = message.match(/skill ['"]([^'"]+)['"]/i)
if (quotedMatch?.[1]) return quotedMatch[1].trim()
const namedMatch = message.match(/\bname=([^\s)]+)/i)
return namedMatch?.[1]?.trim() || ''
}
function mapSkillUsageEvent(row: Record<string, unknown>): RawSkillUsageEvent | null {
const content = typeof row.content === 'string' ? row.content : ''
const toolName = typeof row.tool_name === 'string' ? row.tool_name : ''
const action: SkillUsageAction | null = toolName === 'skill_view' || content.startsWith('[skill_view]')
? 'view'
: toolName === 'skill_manage' || content.startsWith('[skill_manage]')
? 'manage'
: null
if (!action) return null
const skill = action === 'view'
? extractSkillNameFromViewContent(content)
: extractSkillNameFromManageContent(content)
if (!skill) return null
return {
skill,
action,
timestamp: normalizeNullableNumber(row.timestamp),
}
}
function formatUnixDate(timestamp: number | null): string {
if (timestamp == null) return ''
return new Date(timestamp * 1000).toISOString().slice(0, 10)
}
export async function getSkillUsageStatsFromDb(
days = 7,
nowSeconds = Math.floor(Date.now() / 1000),
): Promise<HermesSkillUsageStats> {
const normalizedDays = Number.isFinite(days) ? days : 7
const safeDays = Math.max(1, Math.floor(normalizedDays))
const since = nowSeconds - safeDays * 24 * 60 * 60
const db = await openSessionDb()
try {
const sourceFilter = tableHasColumn(db, 'sessions', 'source')
? " AND COALESCE(s.source, '') != 'api_server'"
: ''
const hasStartedIndex = db.prepare("PRAGMA index_list(sessions)").all()
.some(index => String(index.name || '') === 'idx_sessions_started')
const hasMessagesIndex = db.prepare("PRAGMA index_list(messages)").all()
.some(index => String(index.name || '') === 'idx_messages_session')
const sessionsTable = hasStartedIndex ? 'sessions s INDEXED BY idx_sessions_started' : 'sessions s'
const messagesTable = hasMessagesIndex ? 'messages m INDEXED BY idx_messages_session' : 'messages m'
const toolPredicate = `
m.role = 'tool'
AND (
m.tool_name IN ('skill_view', 'skill_manage')
OR m.content LIKE '[skill_view]%'
OR m.content LIKE '[skill_manage]%'
)
`
const recentRows = db.prepare(`
SELECT m.tool_name, SUBSTR(m.content, 1, 300) AS content, COALESCE(m.timestamp, s.started_at) AS timestamp
FROM ${sessionsTable}
JOIN ${messagesTable} ON m.session_id = s.id
WHERE s.started_at > ?${sourceFilter}
AND ${toolPredicate}
`).all(since) as Record<string, unknown>[]
const lateRows = db.prepare(`
SELECT m.tool_name, SUBSTR(m.content, 1, 300) AS content, COALESCE(m.timestamp, s.started_at) AS timestamp
FROM ${sessionsTable}
JOIN ${messagesTable} ON m.session_id = s.id
WHERE s.started_at <= ?
AND COALESCE(m.timestamp, s.started_at) > ?${sourceFilter}
AND ${toolPredicate}
`).all(since, since) as Record<string, unknown>[]
const skillMap = new Map<string, { skill: string; view_count: number; manage_count: number; last_used_at: number | null }>()
const dayMap = new Map<string, { date: string; view_count: number; manage_count: number }>()
const daySkillMap = new Map<string, Map<string, { skill: string; view_count: number; manage_count: number }>>()
for (const row of [...recentRows, ...lateRows]) {
const event = mapSkillUsageEvent(row)
if (!event) continue
const entry = skillMap.get(event.skill) || {
skill: event.skill,
view_count: 0,
manage_count: 0,
last_used_at: null,
}
if (event.action === 'view') entry.view_count += 1
else entry.manage_count += 1
if (event.timestamp != null && (entry.last_used_at == null || event.timestamp > entry.last_used_at)) {
entry.last_used_at = event.timestamp
}
skillMap.set(event.skill, entry)
const date = formatUnixDate(event.timestamp)
if (date) {
const day = dayMap.get(date) || { date, view_count: 0, manage_count: 0 }
if (event.action === 'view') day.view_count += 1
else day.manage_count += 1
dayMap.set(date, day)
const skillsForDay = daySkillMap.get(date) || new Map<string, { skill: string; view_count: number; manage_count: number }>()
const skillForDay = skillsForDay.get(event.skill) || { skill: event.skill, view_count: 0, manage_count: 0 }
if (event.action === 'view') skillForDay.view_count += 1
else skillForDay.manage_count += 1
skillsForDay.set(event.skill, skillForDay)
daySkillMap.set(date, skillsForDay)
}
}
const totalLoads = [...skillMap.values()].reduce((sum, skill) => sum + skill.view_count, 0)
const totalEdits = [...skillMap.values()].reduce((sum, skill) => sum + skill.manage_count, 0)
const totalActions = totalLoads + totalEdits
const byDay = [...dayMap.values()]
.map(day => ({
...day,
total_count: day.view_count + day.manage_count,
skills: [...(daySkillMap.get(day.date)?.values() || [])]
.map(skill => ({
...skill,
total_count: skill.view_count + skill.manage_count,
}))
.sort((a, b) => b.total_count - a.total_count || a.skill.localeCompare(b.skill)),
}))
.sort((a, b) => a.date.localeCompare(b.date))
const topSkills = [...skillMap.values()]
.map(skill => ({
...skill,
total_count: skill.view_count + skill.manage_count,
percentage: totalActions > 0 ? (skill.view_count + skill.manage_count) / totalActions * 100 : 0,
}))
.sort((a, b) =>
b.total_count - a.total_count ||
b.view_count - a.view_count ||
b.manage_count - a.manage_count ||
(b.last_used_at || 0) - (a.last_used_at || 0) ||
a.skill.localeCompare(b.skill),
)
return {
period_days: safeDays,
summary: {
total_skill_loads: totalLoads,
total_skill_edits: totalEdits,
total_skill_actions: totalActions,
distinct_skills_used: skillMap.size,
},
by_day: byDay,
top_skills: topSkills,
}
} finally {
db.close()
}
}
export async function getUsageStatsFromDb(
days = 30,
nowSeconds = Math.floor(Date.now() / 1000),
@@ -4,7 +4,6 @@ import * as ctrl from '../../controllers/hermes/skills'
export const skillRoutes = new Router()
skillRoutes.get('/api/hermes/skills', ctrl.list)
skillRoutes.get('/api/hermes/skills/usage/stats', ctrl.usageStats)
skillRoutes.put('/api/hermes/skills/toggle', ctrl.toggle)
skillRoutes.put('/api/hermes/skills/pin', ctrl.pin_)
skillRoutes.get('/api/hermes/skills/:category/:skill/files', ctrl.listFiles)