dsh-skill-hub 0.2.2 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +10 -14
  2. package/README.zh.md +9 -12
  3. package/lib/client.js +493 -177
  4. package/lib/client.js.map +1 -1
  5. package/lib/index.js +213 -36
  6. package/lib/types/client/SkillHubSettingsCard.d.ts +4 -6
  7. package/lib/types/client/icons.d.ts +25 -0
  8. package/lib/types/client/locales.d.ts +7 -3
  9. package/lib/types/client/panel/format.d.ts +10 -0
  10. package/lib/types/client/settings-card.d.ts +18 -0
  11. package/lib/types/client/settings-form.d.ts +9 -0
  12. package/lib/types/client/slash-dots.d.ts +57 -0
  13. package/lib/types/index.d.ts +4 -0
  14. package/lib/types/protocol.d.ts +42 -1
  15. package/lib/types/stats.d.ts +53 -4
  16. package/lib/types/store.d.ts +7 -2
  17. package/package.json +68 -26
  18. package/src/client/SkillHubSettingsCard.tsx +35 -10
  19. package/src/client/icons.tsx +61 -0
  20. package/src/client/index.tsx +13 -1
  21. package/src/client/locales.ts +14 -6
  22. package/src/client/panel/SkillDetailView.tsx +6 -2
  23. package/src/client/panel/SkillHubPanel.tsx +2 -2
  24. package/src/client/panel/SkillRow.tsx +8 -3
  25. package/src/client/panel/format.ts +11 -0
  26. package/src/client/panel/panel.module.css +1 -0
  27. package/src/client/settings-card.tsx +49 -1
  28. package/src/client/settings-form.ts +21 -0
  29. package/src/client/slash-dots.test.ts +145 -0
  30. package/src/client/slash-dots.tsx +190 -0
  31. package/src/index.ts +34 -3
  32. package/src/protocol.ts +47 -1
  33. package/src/routes.test.ts +3 -1
  34. package/src/routes.ts +2 -2
  35. package/src/stats.test.ts +273 -2
  36. package/src/stats.ts +166 -23
  37. package/src/store.test.ts +32 -0
  38. package/src/store.ts +58 -3
package/src/store.ts CHANGED
@@ -15,7 +15,7 @@
15
15
  import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'
16
16
  import { homedir } from 'node:os'
17
17
  import { dirname, join } from 'node:path'
18
- import type { DisabledSkill, HubConfig, MarketSourceRecord, RepoRoot, SkillTag, SourceRecord, TrashEntry } from './protocol.ts'
18
+ import type { DisabledSkill, HubConfig, MarketSourceRecord, RepoRoot, SkillStatsCheckpoint, SkillTag, SourceRecord, TrashEntry } from './protocol.ts'
19
19
 
20
20
  /** 默认场景名(系统预置的兜底场景,新技能自动归入)。 */
21
21
  export const DEFAULT_SCENE_NAME = '通用'
@@ -34,6 +34,8 @@ interface StoreFile {
34
34
  marketSources?: MarketSourceRecord[]
35
35
  /** Trashed skills (removed after upstream deletion, restorable). */
36
36
  trash?: TrashEntry[]
37
+ /** Usage-statistics incremental-scan checkpoint (frozen watermark + totals). */
38
+ skillStats?: SkillStatsCheckpoint
37
39
  }
38
40
 
39
41
  /** Resolve the DSH home directory (the filesystem provider's user-dsh root base). */
@@ -47,7 +49,7 @@ export function statePath(home = dshHome()): string {
47
49
  }
48
50
 
49
51
  /** Current sidecar schema version. Bump on breaking shape changes and add a migration below. */
50
- export const STORE_VERSION = 3
52
+ export const STORE_VERSION = 4
51
53
 
52
54
  /**
53
55
  * Business-rule failure the routes layer can map onto a 4xx status instead
@@ -74,7 +76,7 @@ export class StoreError extends Error {
74
76
  * Returns null when the file claims a newer schema than this plugin
75
77
  * understands, so the caller starts empty instead of risking data loss.
76
78
  */
77
- function migrateStore(parsed: unknown): { version: number; disabled: unknown; config?: unknown; tags?: unknown; sources?: unknown; marketSources?: unknown; trash?: unknown } | null {
79
+ function migrateStore(parsed: unknown): { version: number; disabled: unknown; config?: unknown; tags?: unknown; sources?: unknown; marketSources?: unknown; trash?: unknown; skillStats?: unknown } | null {
78
80
  if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) return null
79
81
  const record = parsed as Record<string, unknown>
80
82
  const version = typeof record.version === 'number' ? record.version : 0
@@ -120,6 +122,9 @@ function migrateStore(parsed: unknown): { version: number; disabled: unknown; co
120
122
  ...(sources !== undefined ? { sources } : {}),
121
123
  ...(marketSources !== undefined ? { marketSources } : {}),
122
124
  ...(trash !== undefined ? { trash } : {}),
125
+ // v4 (skillStats) is a pure addition — older files simply lack the field,
126
+ // and the loader validates its shape, so pass it through untouched.
127
+ ...(record.skillStats !== undefined ? { skillStats: record.skillStats } : {}),
123
128
  }
124
129
  }
125
130
 
@@ -131,6 +136,7 @@ export class SkillHubStore {
131
136
  private sourcesByRepo = new Map<string, SourceRecord>()
132
137
  private marketSources: MarketSourceRecord[] = []
133
138
  private trashByName = new Map<string, TrashEntry>()
139
+ private skillStats: SkillStatsCheckpoint | undefined = undefined
134
140
  private loaded = false
135
141
  /** Serializes persist runs: concurrent mutators must not let an earlier
136
142
  * snapshot overwrite a later one (rename is atomic, ordering is not). */
@@ -234,6 +240,34 @@ export class SkillHubStore {
234
240
  }
235
241
  }
236
242
  }
243
+ const savedStats = migrated.skillStats as Partial<SkillStatsCheckpoint> | null | undefined
244
+ if (savedStats !== null && typeof savedStats === 'object'
245
+ && typeof savedStats.frozenBefore === 'number' && typeof savedStats.lastFullReconcile === 'number'
246
+ && typeof savedStats.windowDays === 'number'
247
+ && typeof savedStats.frozenSessions === 'object' && savedStats.frozenSessions !== null) {
248
+ // Validate entry shapes too: a corrupt bucket degrades to a fresh
249
+ // checkpoint (one extra full reconciliation), never to bad counts.
250
+ const sessions: SkillStatsCheckpoint['frozenSessions'] = {}
251
+ for (const [id, entry] of Object.entries(savedStats.frozenSessions)) {
252
+ if (entry === null || typeof entry !== 'object' || typeof entry.createdAt !== 'number'
253
+ || typeof entry.counts !== 'object' || entry.counts === null) continue
254
+ const counts: Record<string, { count: number; lastUsed: number }> = {}
255
+ for (const [name, stat] of Object.entries(entry.counts)) {
256
+ if (stat !== null && typeof stat === 'object'
257
+ && typeof (stat as { count?: unknown }).count === 'number'
258
+ && typeof (stat as { lastUsed?: unknown }).lastUsed === 'number') {
259
+ counts[name] = { count: (stat as { count: number }).count, lastUsed: (stat as { lastUsed: number }).lastUsed }
260
+ }
261
+ }
262
+ sessions[id] = { createdAt: entry.createdAt, counts }
263
+ }
264
+ this.skillStats = {
265
+ windowDays: savedStats.windowDays,
266
+ frozenBefore: savedStats.frozenBefore,
267
+ frozenSessions: sessions,
268
+ lastFullReconcile: savedStats.lastFullReconcile,
269
+ }
270
+ }
237
271
  }
238
272
  } catch (error) {
239
273
  // Missing or unreadable state starts empty; never crash the plugin.
@@ -602,6 +636,26 @@ export class SkillHubStore {
602
636
  await this.persist()
603
637
  }
604
638
 
639
+ /** The persisted usage-statistics checkpoint (undefined until first saved). */
640
+ async getSkillStatsState(): Promise<SkillStatsCheckpoint | undefined> {
641
+ await this.ensureLoaded()
642
+ return this.skillStats !== undefined
643
+ ? { ...this.skillStats, frozenSessions: { ...this.skillStats.frozenSessions } }
644
+ : undefined
645
+ }
646
+
647
+ /** Persist a usage-statistics checkpoint (written at most ~once a day, on full reconciliations). */
648
+ async saveSkillStatsState(state: SkillStatsCheckpoint): Promise<void> {
649
+ await this.ensureLoaded()
650
+ this.skillStats = {
651
+ windowDays: state.windowDays,
652
+ frozenBefore: state.frozenBefore,
653
+ frozenSessions: { ...state.frozenSessions },
654
+ lastFullReconcile: state.lastFullReconcile,
655
+ }
656
+ await this.persist()
657
+ }
658
+
605
659
  private persist(): Promise<void> {
606
660
  // The payload is built inside the queued step (not here), so every
607
661
  // concurrent mutation made before a write actually lands is included in
@@ -615,6 +669,7 @@ export class SkillHubStore {
615
669
  ...(this.sourcesByRepo.size > 0 ? { sources: [...this.sourcesByRepo.values()] } : {}),
616
670
  ...(this.marketSources.length > 0 ? { marketSources: [...this.marketSources] } : {}),
617
671
  ...(this.trashByName.size > 0 ? { trash: [...this.trashByName.values()] } : {}),
672
+ ...(this.skillStats !== undefined ? { skillStats: this.skillStats } : {}),
618
673
  }
619
674
  const tmp = this.file + '.tmp'
620
675
  await mkdir(dirname(this.file), { recursive: true })