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
+3 -76
View File
@@ -39,57 +39,13 @@ function collectLiteralTranslationKeys(): string[] {
return [...keys].sort()
}
function getPath(messages: Record<string, unknown>, key: string): unknown {
function hasPath(messages: Record<string, unknown>, key: string): boolean {
let current: unknown = messages
for (const part of key.split('.')) {
if (!current || typeof current !== 'object' || !(part in current)) return undefined
if (!current || typeof current !== 'object' || !(part in current)) return false
current = (current as Record<string, unknown>)[part]
}
return current
}
function hasPath(messages: Record<string, unknown>, key: string): boolean {
return typeof getPath(messages, key) !== 'undefined'
}
const SKILLS_USAGE_LOCALIZED_KEYS = [
'sidebar.skillsUsage',
'skillsUsage.title',
'skillsUsage.subtitle',
'skillsUsage.refresh',
'skillsUsage.periodSelector',
'skillsUsage.periodLabel',
'skillsUsage.summary',
'skillsUsage.totalActions',
'skillsUsage.loads',
'skillsUsage.edits',
'skillsUsage.distinctSkills',
'skillsUsage.topSkills',
'skillsUsage.dailyTrend',
'skillsUsage.periodSummary',
'skillsUsage.skill',
'skillsUsage.share',
'skillsUsage.lastUsed',
'skillsUsage.noData',
'skillsUsage.loadFailed',
'skillsUsage.otherSkills',
]
const SKILLS_USAGE_COMPACT_LABEL_LIMITS: Record<string, number> = {
'skillsUsage.totalActions': 12,
'skillsUsage.loads': 10,
'skillsUsage.edits': 10,
'skillsUsage.distinctSkills': 12,
'skillsUsage.topSkills': 16,
'skillsUsage.dailyTrend': 16,
'skillsUsage.skill': 10,
'skillsUsage.share': 10,
'skillsUsage.lastUsed': 12,
'skillsUsage.otherSkills': 16,
}
function labelLength(value: unknown): number {
return typeof value === 'string' ? Array.from(value.replace(/\{[^}]+\}/g, '')).length : Infinity
return typeof current !== 'undefined'
}
describe('i18n locale coverage', () => {
@@ -119,35 +75,6 @@ describe('i18n locale coverage', () => {
expect(missing).toEqual([])
})
it('localizes Skills Usage page copy in every non-English locale instead of falling back to English', () => {
const englishMessages = rawMessages.en
const untranslated = Object.entries(rawMessages).flatMap(([locale, localeMessages]) => {
if (locale === 'en') return []
return SKILLS_USAGE_LOCALIZED_KEYS.flatMap((key) => {
const localeValue = getPath(localeMessages, key)
if (typeof localeValue === 'undefined') return [`${locale}: ${key} missing`]
return localeValue === getPath(englishMessages, key) ? [`${locale}: ${key}`] : []
})
})
expect(untranslated).toEqual([])
})
it('keeps Skills Usage summary and table labels compact across locales', () => {
const oversized = Object.entries(rawMessages).flatMap(([locale, localeMessages]) =>
Object.entries(SKILLS_USAGE_COMPACT_LABEL_LIMITS).flatMap(([key, maxLength]) => {
const localeValue = getPath(localeMessages, key)
return labelLength(localeValue) > maxLength
? [`${locale}: ${key} (${labelLength(localeValue)} > ${maxLength})`]
: []
}),
)
expect(oversized).toEqual([])
})
it('keeps the coverage scanner rooted in client source files', () => {
expect(relative(process.cwd(), SOURCE_ROOT)).toBe(join('packages', 'client', 'src'))
})
-219
View File
@@ -1,219 +0,0 @@
// @vitest-environment jsdom
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
const fetchSkillUsageStatsMock = vi.hoisted(() => vi.fn())
vi.mock('@/api/hermes/skills', () => ({
fetchSkillUsageStats: fetchSkillUsageStatsMock,
}))
vi.mock('vue-i18n', () => ({
useI18n: () => ({
t: (key: string, params?: Record<string, unknown>) => {
if (key === 'skillsUsage.periodLabel') return `${params?.days}d`
if (key === 'skillsUsage.periodSummary') return `Last ${params?.days} days`
return key
},
}),
}))
vi.mock('naive-ui', async () => {
const actual = await vi.importActual<any>('naive-ui')
return {
...actual,
NButton: {
props: ['loading', 'type', 'size', 'quaternary', 'secondary'],
inheritAttrs: false,
template: '<button :data-type="type" :aria-pressed="$attrs[\'aria-pressed\']" @click="$emit(\'click\')"><slot /></button>',
},
}
})
import SkillsUsageView from '@/views/hermes/SkillsUsageView.vue'
const sevenDayStats = {
period_days: 7,
summary: {
total_skill_loads: 3,
total_skill_edits: 1,
total_skill_actions: 4,
distinct_skills_used: 2,
},
by_day: [
{
date: '2026-05-10',
view_count: 1,
manage_count: 0,
total_count: 1,
skills: [
{ skill: 'github-pr-workflow', view_count: 1, manage_count: 0, total_count: 1 },
],
},
{
date: '2026-05-11',
view_count: 2,
manage_count: 1,
total_count: 3,
skills: [
{ skill: 'hermes-agent', view_count: 2, manage_count: 1, total_count: 3 },
],
},
],
top_skills: [
{ skill: 'hermes-agent', view_count: 2, manage_count: 1, total_count: 3, percentage: 75, last_used_at: 1_700_000_000 },
{ skill: 'github-pr-workflow', view_count: 1, manage_count: 0, total_count: 1, percentage: 25, last_used_at: null },
],
}
describe('SkillsUsageView', () => {
beforeEach(() => {
fetchSkillUsageStatsMock.mockReset()
fetchSkillUsageStatsMock.mockResolvedValue(sevenDayStats)
})
it('loads rolling 7 day skill usage and renders statistics beside a skill-colored visual trend', async () => {
const wrapper = mount(SkillsUsageView)
await flushPromises()
expect(fetchSkillUsageStatsMock).toHaveBeenCalledWith(7)
expect(wrapper.text()).toContain('skillsUsage.title')
expect(wrapper.find('[data-testid="skills-usage-chart"]').exists()).toBe(true)
expect(wrapper.findAll('.skill-bar-col')).toHaveLength(2)
expect(wrapper.findAll('.skill-bar-segment[data-skill="hermes-agent"]')).toHaveLength(1)
expect(wrapper.findAll('.skill-bar-segment[data-skill="github-pr-workflow"]')).toHaveLength(1)
expect(wrapper.find('[data-testid="skills-usage-legend"]').exists()).toBe(false)
expect(wrapper.find('[data-testid="skills-usage-stats"]').text()).toContain('4')
expect(wrapper.text()).toContain('hermes-agent')
expect(wrapper.text()).toContain('github-pr-workflow')
expect(wrapper.text()).toContain('75.0%')
})
it('reloads the selected period when the period button changes', async () => {
const wrapper = mount(SkillsUsageView)
await flushPromises()
fetchSkillUsageStatsMock.mockClear()
const thirtyDayButton = wrapper.findAll('button').find(button => button.text() === '30d')
expect(thirtyDayButton).toBeTruthy()
await thirtyDayButton!.trigger('click')
await flushPromises()
expect(fetchSkillUsageStatsMock).toHaveBeenCalledWith(30)
expect(thirtyDayButton!.attributes('aria-pressed')).toBe('true')
})
it('flips the chart tooltip away from the hovered side of the bars', async () => {
const wrapper = mount(SkillsUsageView)
await flushPromises()
const bars = wrapper.findAll('.skill-bar-col')
expect(bars).toHaveLength(2)
await bars[1].trigger('mouseenter')
expect(wrapper.find('.floating-tooltip.align-left').exists()).toBe(true)
expect(wrapper.find('.floating-tooltip').text()).toContain('2026-05-11')
await bars[0].trigger('mouseenter')
expect(wrapper.find('.floating-tooltip.align-right').exists()).toBe(true)
expect(wrapper.find('.floating-tooltip').text()).toContain('2026-05-10')
})
it('keeps stale data visible while refreshing an already loaded period', async () => {
const wrapper = mount(SkillsUsageView)
await flushPromises()
let resolveRefresh!: (value: unknown) => void
fetchSkillUsageStatsMock.mockReturnValueOnce(new Promise(resolve => {
resolveRefresh = resolve
}))
const refreshButton = wrapper.findAll('button').find(button => button.text() === 'skillsUsage.refresh')
expect(refreshButton).toBeTruthy()
await refreshButton!.trigger('click')
expect(fetchSkillUsageStatsMock).toHaveBeenCalledTimes(2)
expect(wrapper.find('[data-testid="skills-usage-chart"]').exists()).toBe(true)
expect(wrapper.find('.usage-panel.is-refreshing').exists()).toBe(true)
resolveRefresh({
period_days: 7,
summary: { total_skill_loads: 1, total_skill_edits: 0, total_skill_actions: 1, distinct_skills_used: 1 },
by_day: [
{
date: '2026-05-12',
view_count: 1,
manage_count: 0,
total_count: 1,
skills: [{ skill: 'test-driven-development', view_count: 1, manage_count: 0, total_count: 1 }],
},
],
top_skills: [
{ skill: 'test-driven-development', view_count: 1, manage_count: 0, total_count: 1, percentage: 100, last_used_at: null },
],
})
await flushPromises()
expect(wrapper.text()).toContain('test-driven-development')
})
it('does not let an older refresh overwrite newer stats for the same period', async () => {
const wrapper = mount(SkillsUsageView)
await flushPromises()
let resolveOlder!: (value: unknown) => void
let resolveNewer!: (value: unknown) => void
fetchSkillUsageStatsMock
.mockReturnValueOnce(new Promise(resolve => { resolveOlder = resolve }))
.mockReturnValueOnce(new Promise(resolve => { resolveNewer = resolve }))
const refreshButton = wrapper.findAll('button').find(button => button.text() === 'skillsUsage.refresh')
expect(refreshButton).toBeTruthy()
await refreshButton!.trigger('click')
await refreshButton!.trigger('click')
resolveNewer({
period_days: 7,
summary: { total_skill_loads: 2, total_skill_edits: 0, total_skill_actions: 2, distinct_skills_used: 1 },
by_day: [
{
date: '2026-05-13',
view_count: 2,
manage_count: 0,
total_count: 2,
skills: [{ skill: 'newer-skill', view_count: 2, manage_count: 0, total_count: 2 }],
},
],
top_skills: [
{ skill: 'newer-skill', view_count: 2, manage_count: 0, total_count: 2, percentage: 100, last_used_at: null },
],
})
await flushPromises()
expect(wrapper.text()).toContain('newer-skill')
resolveOlder({
period_days: 7,
summary: { total_skill_loads: 1, total_skill_edits: 0, total_skill_actions: 1, distinct_skills_used: 1 },
by_day: [
{
date: '2026-05-12',
view_count: 1,
manage_count: 0,
total_count: 1,
skills: [{ skill: 'older-skill', view_count: 1, manage_count: 0, total_count: 1 }],
},
],
top_skills: [
{ skill: 'older-skill', view_count: 1, manage_count: 0, total_count: 1, percentage: 100, last_used_at: null },
],
})
await flushPromises()
expect(wrapper.text()).toContain('newer-skill')
expect(wrapper.text()).not.toContain('older-skill')
})
})