dsh-skill-hub 0.2.4 → 0.2.6
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/lib/client.js +153 -9
- package/lib/client.js.map +1 -1
- package/lib/index.js +215 -36
- package/lib/types/client/SkillHubSettingsCard.d.ts +3 -1
- package/lib/types/client/locales.d.ts +5 -1
- package/lib/types/client/settings-card.d.ts +20 -0
- package/lib/types/client/settings-form.d.ts +9 -0
- package/lib/types/index.d.ts +4 -0
- package/lib/types/protocol.d.ts +44 -1
- package/lib/types/stats.d.ts +53 -4
- package/lib/types/store.d.ts +7 -2
- package/package.json +1 -1
- package/src/client/SkillHubSettingsCard.tsx +43 -4
- package/src/client/locales.ts +9 -1
- package/src/client/panel/SkillHubPanel.tsx +1 -1
- package/src/client/panel/panel.module.css +1 -0
- package/src/client/settings-card.module.css +1 -0
- package/src/client/settings-card.tsx +51 -1
- package/src/client/settings-form.ts +21 -0
- package/src/index.ts +34 -3
- package/src/protocol.ts +49 -1
- package/src/routes.test.ts +4 -1
- package/src/routes.ts +4 -4
- 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/lib/index.js
CHANGED
|
@@ -53,27 +53,39 @@ const HUB_CONFIG_DEFAULTS = {
|
|
|
53
53
|
announceToAgent: true,
|
|
54
54
|
showUseCount: true,
|
|
55
55
|
showUseTime: true,
|
|
56
|
-
showGroupSummary: true
|
|
56
|
+
showGroupSummary: true,
|
|
57
|
+
statsWindowDays: 14,
|
|
58
|
+
statsScanMinutes: 5
|
|
57
59
|
};
|
|
58
60
|
/**
|
|
59
61
|
* Resolve the effective hub config: saved sidecar overrides win over the
|
|
60
62
|
* cordis composition entry (the web card owns runtime config), missing
|
|
61
63
|
* booleans fall back to HUB_CONFIG_DEFAULTS, and dot colors pass through
|
|
62
|
-
* (saved first, then base) only when set.
|
|
64
|
+
* (saved first, then base) only when set. Numeric stats knobs are clamped to
|
|
65
|
+
* their sane ranges (window ≥ 0, scan interval ≥ 1 minute).
|
|
63
66
|
*/
|
|
64
67
|
function resolveHubConfig(saved, base = {}) {
|
|
65
68
|
const dotModelColor = saved.dotModelColor !== void 0 ? saved.dotModelColor : base.dotModelColor;
|
|
66
69
|
const dotUserColor = saved.dotUserColor !== void 0 ? saved.dotUserColor : base.dotUserColor;
|
|
70
|
+
const windowDays = clampNumber(saved.statsWindowDays ?? base.statsWindowDays, 0) ?? HUB_CONFIG_DEFAULTS.statsWindowDays;
|
|
71
|
+
const scanMinutes = clampNumber(saved.statsScanMinutes ?? base.statsScanMinutes, 1) ?? HUB_CONFIG_DEFAULTS.statsScanMinutes;
|
|
67
72
|
return {
|
|
68
73
|
enabled: saved.enabled ?? base.enabled ?? HUB_CONFIG_DEFAULTS.enabled,
|
|
69
74
|
announceToAgent: saved.announceToAgent ?? base.announceToAgent ?? HUB_CONFIG_DEFAULTS.announceToAgent,
|
|
70
75
|
showUseCount: saved.showUseCount ?? base.showUseCount ?? HUB_CONFIG_DEFAULTS.showUseCount,
|
|
71
76
|
showUseTime: saved.showUseTime ?? base.showUseTime ?? HUB_CONFIG_DEFAULTS.showUseTime,
|
|
72
77
|
showGroupSummary: saved.showGroupSummary ?? base.showGroupSummary ?? HUB_CONFIG_DEFAULTS.showGroupSummary,
|
|
78
|
+
statsWindowDays: windowDays,
|
|
79
|
+
statsScanMinutes: scanMinutes,
|
|
73
80
|
...dotModelColor !== void 0 ? { dotModelColor } : {},
|
|
74
81
|
...dotUserColor !== void 0 ? { dotUserColor } : {}
|
|
75
82
|
};
|
|
76
83
|
}
|
|
84
|
+
/** Clamp a numeric override into a valid value; undefined/invalid stays undefined. */
|
|
85
|
+
function clampNumber(value, min) {
|
|
86
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < min) return void 0;
|
|
87
|
+
return Math.floor(value);
|
|
88
|
+
}
|
|
77
89
|
/** HEX color validation shared by host routes and the settings card. */
|
|
78
90
|
const HEX_COLOR_RE = /^#[0-9a-f]{6}$/i;
|
|
79
91
|
//#endregion
|
|
@@ -129,7 +141,7 @@ function migrateStore(parsed) {
|
|
|
129
141
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
130
142
|
const record = parsed;
|
|
131
143
|
const version = typeof record.version === "number" ? record.version : 0;
|
|
132
|
-
if (version >
|
|
144
|
+
if (version > 4) return null;
|
|
133
145
|
const disabled = Array.isArray(record.disabled) ? record.disabled : [];
|
|
134
146
|
const config = typeof record.config === "object" && record.config !== null && !Array.isArray(record.config) ? record.config : void 0;
|
|
135
147
|
const tags = Array.isArray(record.tags) ? record.tags : void 0;
|
|
@@ -153,13 +165,14 @@ function migrateStore(parsed) {
|
|
|
153
165
|
}));
|
|
154
166
|
}
|
|
155
167
|
return {
|
|
156
|
-
version:
|
|
168
|
+
version: 4,
|
|
157
169
|
disabled,
|
|
158
170
|
...config !== void 0 ? { config } : {},
|
|
159
171
|
...tags !== void 0 ? { tags } : {},
|
|
160
172
|
...sources !== void 0 ? { sources } : {},
|
|
161
173
|
...marketSources !== void 0 ? { marketSources } : {},
|
|
162
|
-
...trash !== void 0 ? { trash } : {}
|
|
174
|
+
...trash !== void 0 ? { trash } : {},
|
|
175
|
+
...record.skillStats !== void 0 ? { skillStats: record.skillStats } : {}
|
|
163
176
|
};
|
|
164
177
|
}
|
|
165
178
|
/** Sidecar state owner. */
|
|
@@ -171,6 +184,7 @@ var SkillHubStore = class {
|
|
|
171
184
|
sourcesByRepo = /* @__PURE__ */ new Map();
|
|
172
185
|
marketSources = [];
|
|
173
186
|
trashByName = /* @__PURE__ */ new Map();
|
|
187
|
+
skillStats = void 0;
|
|
174
188
|
loaded = false;
|
|
175
189
|
/** Serializes persist runs: concurrent mutators must not let an earlier
|
|
176
190
|
* snapshot overwrite a later one (rename is atomic, ordering is not). */
|
|
@@ -250,6 +264,28 @@ var SkillHubStore = class {
|
|
|
250
264
|
});
|
|
251
265
|
}
|
|
252
266
|
}
|
|
267
|
+
const savedStats = migrated.skillStats;
|
|
268
|
+
if (savedStats !== null && typeof savedStats === "object" && typeof savedStats.frozenBefore === "number" && typeof savedStats.lastFullReconcile === "number" && typeof savedStats.windowDays === "number" && typeof savedStats.frozenSessions === "object" && savedStats.frozenSessions !== null) {
|
|
269
|
+
const sessions = {};
|
|
270
|
+
for (const [id, entry] of Object.entries(savedStats.frozenSessions)) {
|
|
271
|
+
if (entry === null || typeof entry !== "object" || typeof entry.createdAt !== "number" || typeof entry.counts !== "object" || entry.counts === null) continue;
|
|
272
|
+
const counts = {};
|
|
273
|
+
for (const [name, stat] of Object.entries(entry.counts)) if (stat !== null && typeof stat === "object" && typeof stat.count === "number" && typeof stat.lastUsed === "number") counts[name] = {
|
|
274
|
+
count: stat.count,
|
|
275
|
+
lastUsed: stat.lastUsed
|
|
276
|
+
};
|
|
277
|
+
sessions[id] = {
|
|
278
|
+
createdAt: entry.createdAt,
|
|
279
|
+
counts
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
this.skillStats = {
|
|
283
|
+
windowDays: savedStats.windowDays,
|
|
284
|
+
frozenBefore: savedStats.frozenBefore,
|
|
285
|
+
frozenSessions: sessions,
|
|
286
|
+
lastFullReconcile: savedStats.lastFullReconcile
|
|
287
|
+
};
|
|
288
|
+
}
|
|
253
289
|
}
|
|
254
290
|
} catch (error) {
|
|
255
291
|
if (error.code !== "ENOENT") console.warn("[dsh-skill-hub] sidecar state unreadable, starting empty:", error instanceof Error ? error.message : error);
|
|
@@ -594,16 +630,36 @@ var SkillHubStore = class {
|
|
|
594
630
|
if (!this.trashByName.delete(name)) return;
|
|
595
631
|
await this.persist();
|
|
596
632
|
}
|
|
633
|
+
/** The persisted usage-statistics checkpoint (undefined until first saved). */
|
|
634
|
+
async getSkillStatsState() {
|
|
635
|
+
await this.ensureLoaded();
|
|
636
|
+
return this.skillStats !== void 0 ? {
|
|
637
|
+
...this.skillStats,
|
|
638
|
+
frozenSessions: { ...this.skillStats.frozenSessions }
|
|
639
|
+
} : void 0;
|
|
640
|
+
}
|
|
641
|
+
/** Persist a usage-statistics checkpoint (written at most ~once a day, on full reconciliations). */
|
|
642
|
+
async saveSkillStatsState(state) {
|
|
643
|
+
await this.ensureLoaded();
|
|
644
|
+
this.skillStats = {
|
|
645
|
+
windowDays: state.windowDays,
|
|
646
|
+
frozenBefore: state.frozenBefore,
|
|
647
|
+
frozenSessions: { ...state.frozenSessions },
|
|
648
|
+
lastFullReconcile: state.lastFullReconcile
|
|
649
|
+
};
|
|
650
|
+
await this.persist();
|
|
651
|
+
}
|
|
597
652
|
persist() {
|
|
598
653
|
const run = this.writeChain.then(async () => {
|
|
599
654
|
const payload = {
|
|
600
|
-
version:
|
|
655
|
+
version: 4,
|
|
601
656
|
disabled: [...this.entries.values()],
|
|
602
657
|
config: this.config,
|
|
603
658
|
...this.tagsById.size > 0 ? { tags: [...this.tagsById.values()] } : {},
|
|
604
659
|
...this.sourcesByRepo.size > 0 ? { sources: [...this.sourcesByRepo.values()] } : {},
|
|
605
660
|
...this.marketSources.length > 0 ? { marketSources: [...this.marketSources] } : {},
|
|
606
|
-
...this.trashByName.size > 0 ? { trash: [...this.trashByName.values()] } : {}
|
|
661
|
+
...this.trashByName.size > 0 ? { trash: [...this.trashByName.values()] } : {},
|
|
662
|
+
...this.skillStats !== void 0 ? { skillStats: this.skillStats } : {}
|
|
607
663
|
};
|
|
608
664
|
const tmp = this.file + ".tmp";
|
|
609
665
|
await mkdir(dirname(this.file), { recursive: true });
|
|
@@ -1904,6 +1960,7 @@ async function buildCatalog(deps, cwd) {
|
|
|
1904
1960
|
const diagnostics = [...await scanDiagnostics("user-dsh", home), ...await scanDiagnostics("user-agents", home)];
|
|
1905
1961
|
return {
|
|
1906
1962
|
ok: true,
|
|
1963
|
+
pluginVersion: CURRENT_VERSION,
|
|
1907
1964
|
complete,
|
|
1908
1965
|
skills,
|
|
1909
1966
|
disabled,
|
|
@@ -2246,6 +2303,7 @@ function makeRoutes(deps) {
|
|
|
2246
2303
|
if (req.method === "GET") {
|
|
2247
2304
|
writeJson(res, 200, {
|
|
2248
2305
|
ok: true,
|
|
2306
|
+
pluginVersion: CURRENT_VERSION,
|
|
2249
2307
|
config: configOf(deps),
|
|
2250
2308
|
saved: savedOf(deps)
|
|
2251
2309
|
});
|
|
@@ -2302,6 +2360,7 @@ function makeRoutes(deps) {
|
|
|
2302
2360
|
} else config = await deps.updateConfig(patch);
|
|
2303
2361
|
writeJson(res, 200, {
|
|
2304
2362
|
ok: true,
|
|
2363
|
+
pluginVersion: CURRENT_VERSION,
|
|
2305
2364
|
config,
|
|
2306
2365
|
saved: savedOf(deps)
|
|
2307
2366
|
});
|
|
@@ -3011,6 +3070,9 @@ function makeRoutes(deps) {
|
|
|
3011
3070
|
}
|
|
3012
3071
|
//#endregion
|
|
3013
3072
|
//#region src/stats.ts
|
|
3073
|
+
/** Fallback freeze horizon when no rolling window is configured (14 days). */
|
|
3074
|
+
const STATS_FREEZE_AFTER_MS = 336 * 60 * 60 * 1e3;
|
|
3075
|
+
const DAY_MS = 1440 * 60 * 1e3;
|
|
3014
3076
|
/** Collect per-skill invocation counts and last-used times from one session. */
|
|
3015
3077
|
function countSkillInvocations(events) {
|
|
3016
3078
|
const stats = /* @__PURE__ */ new Map();
|
|
@@ -3041,31 +3103,100 @@ function countSkillInvocations(events) {
|
|
|
3041
3103
|
}
|
|
3042
3104
|
return stats;
|
|
3043
3105
|
}
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3106
|
+
function isFrozen(record, watermark) {
|
|
3107
|
+
const created = record.header.createdAt;
|
|
3108
|
+
return typeof created === "number" && created > 0 && created < watermark;
|
|
3109
|
+
}
|
|
3110
|
+
function mergeInto(totals, counted) {
|
|
3111
|
+
for (const [name, stat] of counted) {
|
|
3112
|
+
const total = totals[name];
|
|
3113
|
+
if (total === void 0) totals[name] = { ...stat };
|
|
3114
|
+
else {
|
|
3115
|
+
total.count += stat.count;
|
|
3116
|
+
if (stat.lastUsed > total.lastUsed) total.lastUsed = stat.lastUsed;
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
function toSorted(totals) {
|
|
3121
|
+
return Object.entries(totals).map(([name, stat]) => ({
|
|
3122
|
+
name,
|
|
3123
|
+
count: stat.count,
|
|
3124
|
+
lastUsed: stat.lastUsed
|
|
3125
|
+
})).sort((x, y) => x.name.localeCompare(y.name));
|
|
3126
|
+
}
|
|
3127
|
+
/** Effective freeze watermark for the configured rolling window (0 = all history → freeze horizon). */
|
|
3128
|
+
function watermarkFor(windowDays, nowMs) {
|
|
3129
|
+
return windowDays > 0 ? nowMs - windowDays * DAY_MS : nowMs - STATS_FREEZE_AFTER_MS;
|
|
3130
|
+
}
|
|
3131
|
+
/**
|
|
3132
|
+
* Whether a session's usage counts toward the configured window. With no
|
|
3133
|
+
* window (0) everything counts — full history; with a window, only sessions
|
|
3134
|
+
* created inside it do. Distinct from the freeze watermark, which is purely a
|
|
3135
|
+
* re-read optimization.
|
|
3136
|
+
*/
|
|
3137
|
+
function inWindow(createdAt, windowDays, nowMs) {
|
|
3138
|
+
if (windowDays <= 0) return true;
|
|
3139
|
+
return typeof createdAt === "number" && createdAt > 0 && createdAt >= nowMs - windowDays * DAY_MS;
|
|
3140
|
+
}
|
|
3141
|
+
/**
|
|
3142
|
+
* One pass over the corpus. Runs either a full reconciliation (rebuilds the
|
|
3143
|
+
* per-session cache and advances the watermark — mutates the checkpoint) or a
|
|
3144
|
+
* cheap incremental scan (re-reads everything at or after the watermark and
|
|
3145
|
+
* merges over the cached sessions — leaves the checkpoint untouched). Totals
|
|
3146
|
+
* always apply the CURRENT window filter over the cached sessions, so a
|
|
3147
|
+
* window shrink takes effect immediately even before the next reconciliation.
|
|
3148
|
+
*/
|
|
3149
|
+
async function scan(query, checkpoint, nowMs, windowDays) {
|
|
3047
3150
|
const sessions = await query.listSessions();
|
|
3151
|
+
const cutoff = watermarkFor(windowDays, nowMs);
|
|
3152
|
+
if (nowMs - checkpoint.lastFullReconcile >= 864e5 || checkpoint.windowDays !== windowDays) {
|
|
3153
|
+
const cache = {};
|
|
3154
|
+
const totals = {};
|
|
3155
|
+
for (const record of sessions) {
|
|
3156
|
+
let counted;
|
|
3157
|
+
try {
|
|
3158
|
+
counted = countSkillInvocations((await query.readSession(record.header.id)).events);
|
|
3159
|
+
} catch {
|
|
3160
|
+
continue;
|
|
3161
|
+
}
|
|
3162
|
+
const created = record.header.createdAt;
|
|
3163
|
+
if (isFrozen(record, cutoff) && counted.size > 0 && typeof created === "number") cache[record.header.id] = {
|
|
3164
|
+
createdAt: created,
|
|
3165
|
+
counts: Object.fromEntries(counted)
|
|
3166
|
+
};
|
|
3167
|
+
if (inWindow(created, windowDays, nowMs)) mergeInto(totals, counted);
|
|
3168
|
+
}
|
|
3169
|
+
checkpoint.frozenSessions = cache;
|
|
3170
|
+
checkpoint.frozenBefore = cutoff;
|
|
3171
|
+
checkpoint.windowDays = windowDays;
|
|
3172
|
+
checkpoint.lastFullReconcile = nowMs;
|
|
3173
|
+
return {
|
|
3174
|
+
stats: toSorted(totals),
|
|
3175
|
+
mutated: true
|
|
3176
|
+
};
|
|
3177
|
+
}
|
|
3178
|
+
const recent = {};
|
|
3048
3179
|
for (const record of sessions) {
|
|
3049
|
-
|
|
3180
|
+
if (isFrozen(record, checkpoint.frozenBefore)) continue;
|
|
3050
3181
|
try {
|
|
3051
|
-
|
|
3182
|
+
mergeInto(recent, countSkillInvocations((await query.readSession(record.header.id)).events));
|
|
3052
3183
|
} catch {
|
|
3053
3184
|
continue;
|
|
3054
3185
|
}
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
}
|
|
3186
|
+
}
|
|
3187
|
+
const totals = {};
|
|
3188
|
+
for (const [id, entry] of Object.entries(checkpoint.frozenSessions)) {
|
|
3189
|
+
if (!inWindow(entry.createdAt, windowDays, nowMs)) {
|
|
3190
|
+
delete checkpoint.frozenSessions[id];
|
|
3191
|
+
continue;
|
|
3062
3192
|
}
|
|
3193
|
+
mergeInto(totals, new Map(Object.entries(entry.counts)));
|
|
3063
3194
|
}
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
}
|
|
3195
|
+
mergeInto(totals, new Map(Object.entries(recent)));
|
|
3196
|
+
return {
|
|
3197
|
+
stats: toSorted(totals),
|
|
3198
|
+
mutated: false
|
|
3199
|
+
};
|
|
3069
3200
|
}
|
|
3070
3201
|
/**
|
|
3071
3202
|
* Wrap a query in a stale-while-revalidate cache: responses never wait for a
|
|
@@ -3074,19 +3205,44 @@ async function readSkillStats(query) {
|
|
|
3074
3205
|
* single background rescan refreshes them — the panel's next poll picks the
|
|
3075
3206
|
* fresh numbers. A full scan decompresses every session log and can take
|
|
3076
3207
|
* seconds, so it must never sit on the request path.
|
|
3208
|
+
*
|
|
3209
|
+
* Two scaling mechanisms keep this sane as history grows:
|
|
3210
|
+
* - the rescan is incremental (per-session checkpoint, see module doc);
|
|
3211
|
+
* - the effective TTL adapts to the measured scan duration, so a heavier
|
|
3212
|
+
* corpus automatically lowers the rescan cadence instead of burning CPU
|
|
3213
|
+
* on every poll interval.
|
|
3077
3214
|
*/
|
|
3078
|
-
function createSkillStatsReader(query, ttlMs = 3e5) {
|
|
3215
|
+
function createSkillStatsReader(query, ttlMs = 3e5, options = {}) {
|
|
3216
|
+
const checkpoint = options.checkpoint ?? {
|
|
3217
|
+
windowDays: 0,
|
|
3218
|
+
frozenBefore: 0,
|
|
3219
|
+
frozenSessions: {},
|
|
3220
|
+
lastFullReconcile: 0
|
|
3221
|
+
};
|
|
3222
|
+
const now = options.now ?? (() => Date.now());
|
|
3079
3223
|
let cached;
|
|
3080
3224
|
let cachedAt = 0;
|
|
3081
3225
|
let refreshing = null;
|
|
3226
|
+
let lastScanDurationMs = 0;
|
|
3082
3227
|
return async () => {
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3228
|
+
const startedAt = now();
|
|
3229
|
+
const base = typeof ttlMs === "function" ? ttlMs() : ttlMs;
|
|
3230
|
+
const ttl = Math.max(base, lastScanDurationMs * 3);
|
|
3231
|
+
if (cached !== void 0 && startedAt - cachedAt < ttl) return cached;
|
|
3232
|
+
if (refreshing === null) {
|
|
3233
|
+
const windowDays = options.windowDays?.() ?? 0;
|
|
3234
|
+
refreshing = scan(query, checkpoint, startedAt, windowDays).then(({ stats, mutated }) => {
|
|
3235
|
+
cached = stats;
|
|
3236
|
+
cachedAt = now();
|
|
3237
|
+
lastScanDurationMs = Math.max(0, cachedAt - startedAt);
|
|
3238
|
+
if (mutated) options.onCheckpoint?.({
|
|
3239
|
+
...checkpoint,
|
|
3240
|
+
frozenSessions: { ...checkpoint.frozenSessions }
|
|
3241
|
+
});
|
|
3242
|
+
}).catch(() => {}).finally(() => {
|
|
3243
|
+
refreshing = null;
|
|
3244
|
+
});
|
|
3245
|
+
}
|
|
3090
3246
|
return cached ?? [];
|
|
3091
3247
|
};
|
|
3092
3248
|
}
|
|
@@ -3106,7 +3262,9 @@ const Config = z.object({
|
|
|
3106
3262
|
enabled: z.boolean().default(HUB_CONFIG_DEFAULTS.enabled),
|
|
3107
3263
|
showUseCount: z.boolean().default(HUB_CONFIG_DEFAULTS.showUseCount),
|
|
3108
3264
|
showUseTime: z.boolean().default(HUB_CONFIG_DEFAULTS.showUseTime),
|
|
3109
|
-
showGroupSummary: z.boolean().default(HUB_CONFIG_DEFAULTS.showGroupSummary)
|
|
3265
|
+
showGroupSummary: z.boolean().default(HUB_CONFIG_DEFAULTS.showGroupSummary),
|
|
3266
|
+
statsWindowDays: z.number().min(0).max(3650).default(HUB_CONFIG_DEFAULTS.statsWindowDays),
|
|
3267
|
+
statsScanMinutes: z.number().min(1).max(1440).default(HUB_CONFIG_DEFAULTS.statsScanMinutes)
|
|
3110
3268
|
});
|
|
3111
3269
|
/**
|
|
3112
3270
|
* Settings namespace hosting the hub's runtime config. Since dsh rc.7 the
|
|
@@ -3124,7 +3282,9 @@ const HubSettingsSchema = z.object({
|
|
|
3124
3282
|
showUseTime: z.boolean().default(HUB_CONFIG_DEFAULTS.showUseTime),
|
|
3125
3283
|
showGroupSummary: z.boolean().default(HUB_CONFIG_DEFAULTS.showGroupSummary),
|
|
3126
3284
|
dotModelColor: z.string().pattern(HEX_COLOR_RE),
|
|
3127
|
-
dotUserColor: z.string().pattern(HEX_COLOR_RE)
|
|
3285
|
+
dotUserColor: z.string().pattern(HEX_COLOR_RE),
|
|
3286
|
+
statsWindowDays: z.number().min(0).max(3650).default(HUB_CONFIG_DEFAULTS.statsWindowDays),
|
|
3287
|
+
statsScanMinutes: z.number().min(1).max(1440).default(HUB_CONFIG_DEFAULTS.statsScanMinutes)
|
|
3128
3288
|
});
|
|
3129
3289
|
/** Order of the announcement section within the tool-guidance band. */
|
|
3130
3290
|
const SECTION_ORDER = 152;
|
|
@@ -3211,8 +3371,27 @@ function apply(ctx, config) {
|
|
|
3211
3371
|
}
|
|
3212
3372
|
})();
|
|
3213
3373
|
ctx.inject(["sessionQuery"], (sctx) => {
|
|
3214
|
-
|
|
3215
|
-
|
|
3374
|
+
(async () => {
|
|
3375
|
+
const saved = await store.getSkillStatsState().catch(() => void 0);
|
|
3376
|
+
const scanMinutes = () => {
|
|
3377
|
+
const value = current().statsScanMinutes;
|
|
3378
|
+
return typeof value === "number" && value >= 1 ? Math.floor(value) : HUB_CONFIG_DEFAULTS.statsScanMinutes;
|
|
3379
|
+
};
|
|
3380
|
+
const windowDays = () => {
|
|
3381
|
+
const value = current().statsWindowDays;
|
|
3382
|
+
return typeof value === "number" && value >= 0 ? Math.floor(value) : HUB_CONFIG_DEFAULTS.statsWindowDays;
|
|
3383
|
+
};
|
|
3384
|
+
stats = createSkillStatsReader(sctx.sessionQuery, () => scanMinutes() * 6e4, {
|
|
3385
|
+
checkpoint: saved,
|
|
3386
|
+
windowDays,
|
|
3387
|
+
onCheckpoint: (next) => {
|
|
3388
|
+
store.saveSkillStatsState(next).catch((error) => {
|
|
3389
|
+
ctx.logger.warn("[dsh-skill-hub] persisting skill-stats checkpoint failed", error);
|
|
3390
|
+
});
|
|
3391
|
+
}
|
|
3392
|
+
});
|
|
3393
|
+
sync();
|
|
3394
|
+
})();
|
|
3216
3395
|
});
|
|
3217
3396
|
}
|
|
3218
3397
|
//#endregion
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Registered into the official `settings.plugin.item` slot keyed by that
|
|
6
6
|
* namespace, so the plugin shows up in Settings → 插件 on dsh rc.7+.
|
|
7
7
|
*/
|
|
8
|
-
import type
|
|
8
|
+
import { type ReactElement } from 'react';
|
|
9
9
|
import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client';
|
|
10
10
|
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
11
11
|
import { type CardShell, type FieldState, type FormScope } from './settings-form.ts';
|
|
@@ -20,6 +20,8 @@ export interface SkillHubSettingsState extends CardShell {
|
|
|
20
20
|
showUseCount: FieldState;
|
|
21
21
|
showUseTime: FieldState;
|
|
22
22
|
showGroupSummary: FieldState;
|
|
23
|
+
statsWindowDays: FieldState;
|
|
24
|
+
statsScanMinutes: FieldState;
|
|
23
25
|
}
|
|
24
26
|
/** The business face the card's slot registration injects. */
|
|
25
27
|
export interface SkillHubSettingsCardFace {
|
|
@@ -180,7 +180,7 @@ export declare const zh: {
|
|
|
180
180
|
readonly 'form.success': "已创建:";
|
|
181
181
|
readonly 'form.error': "创建失败:";
|
|
182
182
|
readonly 'err.dismiss': "知道了";
|
|
183
|
-
readonly 'settings.title': "
|
|
183
|
+
readonly 'settings.title': "技能中枢";
|
|
184
184
|
readonly 'settings.description': "管理 dsh 本地技能目录:浏览、分组、来源同步与新建。";
|
|
185
185
|
readonly 'settings.enabled': "启用插件";
|
|
186
186
|
readonly 'settings.enabledHint': "关闭后技能中枢的路由、入口与公告全部下线。";
|
|
@@ -196,6 +196,10 @@ export declare const zh: {
|
|
|
196
196
|
readonly 'settings.showUseTimeHint': "在技能名行右侧显示相对时间(如「3 天前」)。";
|
|
197
197
|
readonly 'settings.showGroupSummary': "显示分组汇总";
|
|
198
198
|
readonly 'settings.showGroupSummaryHint': "在分组标题后汇总调用次数与最近调用时间。";
|
|
199
|
+
readonly 'settings.statsWindowDays': "统计窗口(天)";
|
|
200
|
+
readonly 'settings.statsWindowDaysHint': "只统计最近 N 天的使用次数,默认 14 天;0 = 全部历史。改动立即生效。";
|
|
201
|
+
readonly 'settings.statsScanMinutes': "自动统计间隔(分钟)";
|
|
202
|
+
readonly 'settings.statsScanMinutesHint': "后台扫描会话日志的间隔,最小 1 分钟;扫描耗时会自动拉长间隔。";
|
|
199
203
|
readonly 'settings.inherit': "继承";
|
|
200
204
|
readonly 'settings.on': "开";
|
|
201
205
|
readonly 'settings.off': "关";
|
|
@@ -16,6 +16,8 @@ export interface PluginSettingsCardProps {
|
|
|
16
16
|
titleKey: HubKey;
|
|
17
17
|
/** Locale key of the card description. */
|
|
18
18
|
descriptionKey: HubKey;
|
|
19
|
+
/** 插件自身版本(可选):显示在标题旁的小徽标;缺省不渲染。 */
|
|
20
|
+
version?: string;
|
|
19
21
|
/** The form shell state. */
|
|
20
22
|
state: CardShell;
|
|
21
23
|
onSave: () => void;
|
|
@@ -80,3 +82,21 @@ export interface ColorFieldProps {
|
|
|
80
82
|
onReset: () => void;
|
|
81
83
|
}
|
|
82
84
|
export declare function ColorField(props: ColorFieldProps): ReactElement;
|
|
85
|
+
/** One staged numeric field: a numeric draft text input with inherit/reset semantics. */
|
|
86
|
+
export interface NumberFieldProps {
|
|
87
|
+
id: string;
|
|
88
|
+
label: string;
|
|
89
|
+
hint: string;
|
|
90
|
+
/** Placeholder shown while the field inherits its default. */
|
|
91
|
+
inheritLabel: string;
|
|
92
|
+
overriddenLabel: string;
|
|
93
|
+
resetLabel: string;
|
|
94
|
+
disabled: boolean;
|
|
95
|
+
/** Effective number when overridden; empty string means inherit. */
|
|
96
|
+
text: string;
|
|
97
|
+
overridden: boolean;
|
|
98
|
+
invalid?: boolean;
|
|
99
|
+
onEdit: (text: string) => void;
|
|
100
|
+
onReset: () => void;
|
|
101
|
+
}
|
|
102
|
+
export declare function NumberField(props: NumberFieldProps): ReactElement;
|
|
@@ -23,6 +23,15 @@ export interface FieldSpec {
|
|
|
23
23
|
export declare function booleanField(field: string): FieldSpec;
|
|
24
24
|
/** A #rrggbb color field, edited through hex draft text. */
|
|
25
25
|
export declare function colorField(field: string): FieldSpec;
|
|
26
|
+
/**
|
|
27
|
+
* A numeric field with range clamping. Integer by default: a fractional draft
|
|
28
|
+
* is truncated so "5.5 分钟" cannot sneak past an integer-only schema.
|
|
29
|
+
*/
|
|
30
|
+
export declare function numberField(field: string, options?: {
|
|
31
|
+
min?: number;
|
|
32
|
+
max?: number;
|
|
33
|
+
integer?: boolean;
|
|
34
|
+
}): FieldSpec;
|
|
26
35
|
/** Card-level state the chrome renders. */
|
|
27
36
|
export interface CardShell {
|
|
28
37
|
available: boolean;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -25,6 +25,10 @@ export interface Config {
|
|
|
25
25
|
showUseTime?: boolean;
|
|
26
26
|
/** Show group-header usage summaries (count + last used). Default true. */
|
|
27
27
|
showGroupSummary?: boolean;
|
|
28
|
+
/** 统计滚动窗口天数:只统计最近 N 天的使用;0 = 全部历史。默认 0。 */
|
|
29
|
+
statsWindowDays?: number;
|
|
30
|
+
/** 自动统计扫描间隔(分钟,最小 1)。默认 5。 */
|
|
31
|
+
statsScanMinutes?: number;
|
|
28
32
|
}
|
|
29
33
|
export declare const Config: z<Config>;
|
|
30
34
|
/**
|
package/lib/types/protocol.d.ts
CHANGED
|
@@ -81,6 +81,8 @@ export interface DiagnosticEntry {
|
|
|
81
81
|
/** GET /api/skill-hub/catalog */
|
|
82
82
|
export interface CatalogResponse {
|
|
83
83
|
ok: true;
|
|
84
|
+
/** 已安装插件自身的版本号(package.json version),面板标题旁显示。 */
|
|
85
|
+
pluginVersion: string;
|
|
84
86
|
/** Whether discovery completed within a stable catalog revision. */
|
|
85
87
|
complete: boolean;
|
|
86
88
|
/** Sorted winning summaries of every enabled skill (all roots + providers). */
|
|
@@ -179,6 +181,30 @@ export interface StatsResponse {
|
|
|
179
181
|
/** Sorted per-skill invocation counts. */
|
|
180
182
|
stats: SkillStat[];
|
|
181
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* Persisted incremental-scan checkpoint for the usage statistics (sidecar
|
|
186
|
+
* `skillStats` field). Sessions created before `frozenBefore` are treated as
|
|
187
|
+
* finalized: their per-session counts live in `frozenSessions` (only sessions
|
|
188
|
+
* with at least one invocation are kept) and they are not re-read on
|
|
189
|
+
* incremental scans. A daily full reconciliation rebuilds the cache and
|
|
190
|
+
* advances the watermark, so a resumed old session is eventually re-counted.
|
|
191
|
+
*/
|
|
192
|
+
export interface SkillStatsCheckpoint {
|
|
193
|
+
/** The rolling-window configuration this checkpoint was built for (0 = all history). */
|
|
194
|
+
windowDays: number;
|
|
195
|
+
/** Watermark: every session with header.createdAt < this value is frozen. */
|
|
196
|
+
frozenBefore: number;
|
|
197
|
+
/** Per-session counts of finalized sessions, keyed by session id. */
|
|
198
|
+
frozenSessions: Record<string, {
|
|
199
|
+
createdAt: number;
|
|
200
|
+
counts: Record<string, {
|
|
201
|
+
count: number;
|
|
202
|
+
lastUsed: number;
|
|
203
|
+
}>;
|
|
204
|
+
}>;
|
|
205
|
+
/** Epoch ms of the last full reconciliation (drives the daily cadence). */
|
|
206
|
+
lastFullReconcile: number;
|
|
207
|
+
}
|
|
182
208
|
/** JSON error body shared by every route. */
|
|
183
209
|
export interface ErrorResponse {
|
|
184
210
|
error: string;
|
|
@@ -199,6 +225,10 @@ export interface HubConfig {
|
|
|
199
225
|
showUseTime?: boolean;
|
|
200
226
|
/** Show group-header usage summaries (count + last used). Default true. */
|
|
201
227
|
showGroupSummary?: boolean;
|
|
228
|
+
/** 统计滚动窗口天数:只统计最近 N 天的使用;0 = 全部历史。默认 0。 */
|
|
229
|
+
statsWindowDays?: number;
|
|
230
|
+
/** 自动统计扫描间隔(分钟,最小 1)。默认 5。 */
|
|
231
|
+
statsScanMinutes?: number;
|
|
202
232
|
}
|
|
203
233
|
/**
|
|
204
234
|
* The resolved shape of the hub's settings namespace (schema defaults, then
|
|
@@ -216,6 +246,10 @@ export type HubSettingsValue = {
|
|
|
216
246
|
dotModelColor?: string;
|
|
217
247
|
/** User-invocable dot color (#rrggbb); absent means the panel default. */
|
|
218
248
|
dotUserColor?: string;
|
|
249
|
+
/** 统计滚动窗口天数(0 = 全部历史)。 */
|
|
250
|
+
statsWindowDays?: number;
|
|
251
|
+
/** 自动统计扫描间隔(分钟)。 */
|
|
252
|
+
statsScanMinutes?: number;
|
|
219
253
|
};
|
|
220
254
|
/**
|
|
221
255
|
* Hub config defaults — the single source every layer reads: the cordis
|
|
@@ -228,12 +262,15 @@ export declare const HUB_CONFIG_DEFAULTS: {
|
|
|
228
262
|
readonly showUseCount: true;
|
|
229
263
|
readonly showUseTime: true;
|
|
230
264
|
readonly showGroupSummary: true;
|
|
265
|
+
readonly statsWindowDays: 14;
|
|
266
|
+
readonly statsScanMinutes: 5;
|
|
231
267
|
};
|
|
232
268
|
/**
|
|
233
269
|
* Resolve the effective hub config: saved sidecar overrides win over the
|
|
234
270
|
* cordis composition entry (the web card owns runtime config), missing
|
|
235
271
|
* booleans fall back to HUB_CONFIG_DEFAULTS, and dot colors pass through
|
|
236
|
-
* (saved first, then base) only when set.
|
|
272
|
+
* (saved first, then base) only when set. Numeric stats knobs are clamped to
|
|
273
|
+
* their sane ranges (window ≥ 0, scan interval ≥ 1 minute).
|
|
237
274
|
*/
|
|
238
275
|
export declare function resolveHubConfig(saved: Partial<HubConfig>, base?: Partial<HubConfig>): HubConfig;
|
|
239
276
|
/** HEX color validation shared by host routes and the settings card. */
|
|
@@ -241,6 +278,8 @@ export declare const HEX_COLOR_RE: RegExp;
|
|
|
241
278
|
/** GET /api/skill-hub/config */
|
|
242
279
|
export interface ConfigResponse {
|
|
243
280
|
ok: true;
|
|
281
|
+
/** 已安装插件自身的版本号(package.json version),设置卡标题旁显示。 */
|
|
282
|
+
pluginVersion: string;
|
|
244
283
|
/** Effective configuration (saved overrides merged over the defaults). */
|
|
245
284
|
config: HubConfig;
|
|
246
285
|
/** Raw user overrides persisted in the sidecar (absent fields inherit defaults). */
|
|
@@ -262,6 +301,10 @@ export interface ConfigRequest {
|
|
|
262
301
|
dotModelColor?: string | null;
|
|
263
302
|
/** Set the dot color; null clears the saved override so it re-inherits the default. */
|
|
264
303
|
dotUserColor?: string | null;
|
|
304
|
+
/** 统计滚动窗口天数(0 = 全部历史);null 清除覆盖回默认。 */
|
|
305
|
+
statsWindowDays?: number | null;
|
|
306
|
+
/** 自动统计扫描间隔(分钟);null 清除覆盖回默认。 */
|
|
307
|
+
statsScanMinutes?: number | null;
|
|
265
308
|
}
|
|
266
309
|
/** One market source: a tracked upstream repo plus its pinned version. */
|
|
267
310
|
export interface MarketSourceRecord {
|