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.
- package/README.md +10 -14
- package/README.zh.md +9 -12
- package/lib/client.js +493 -177
- package/lib/client.js.map +1 -1
- package/lib/index.js +213 -36
- package/lib/types/client/SkillHubSettingsCard.d.ts +4 -6
- package/lib/types/client/icons.d.ts +25 -0
- package/lib/types/client/locales.d.ts +7 -3
- package/lib/types/client/panel/format.d.ts +10 -0
- package/lib/types/client/settings-card.d.ts +18 -0
- package/lib/types/client/settings-form.d.ts +9 -0
- package/lib/types/client/slash-dots.d.ts +57 -0
- package/lib/types/index.d.ts +4 -0
- package/lib/types/protocol.d.ts +42 -1
- package/lib/types/stats.d.ts +53 -4
- package/lib/types/store.d.ts +7 -2
- package/package.json +68 -26
- package/src/client/SkillHubSettingsCard.tsx +35 -10
- package/src/client/icons.tsx +61 -0
- package/src/client/index.tsx +13 -1
- package/src/client/locales.ts +14 -6
- package/src/client/panel/SkillDetailView.tsx +6 -2
- package/src/client/panel/SkillHubPanel.tsx +2 -2
- package/src/client/panel/SkillRow.tsx +8 -3
- package/src/client/panel/format.ts +11 -0
- package/src/client/panel/panel.module.css +1 -0
- package/src/client/settings-card.tsx +49 -1
- package/src/client/settings-form.ts +21 -0
- package/src/client/slash-dots.test.ts +145 -0
- package/src/client/slash-dots.tsx +190 -0
- package/src/index.ts +34 -3
- package/src/protocol.ts +47 -1
- package/src/routes.test.ts +3 -1
- package/src/routes.ts +2 -2
- package/src/stats.test.ts +273 -2
- package/src/stats.ts +166 -23
- package/src/store.test.ts +32 -0
- 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 =
|
|
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 })
|