dsh-plugin-usage 0.1.0 → 0.1.2
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/index.js +67 -6
- package/lib/host/index.js +66 -41
- package/package.json +1 -1
package/lib/client/index.js
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
// Auto-generated module entry wrapping the shared client body
|
|
2
2
|
// (source: src/client/index.js). Do not edit directly.
|
|
3
|
-
|
|
3
|
+
// Static composition loads bundles as scripts: the file must self-register
|
|
4
|
+
// via window.__ModuleLoader__.load({ id, factory }). The shared body returns
|
|
5
|
+
// a plugin descriptor ({ inject, apply }) for the dynamic-plugin runner;
|
|
6
|
+
// this entry unwraps it, forwards the loader-provided React, and exposes
|
|
7
|
+
// apply/inject for the cordis loader.
|
|
8
|
+
window.__ModuleLoader__.load({
|
|
9
|
+
id: "dsh-plugin-usage",
|
|
10
|
+
factory: (require) => {
|
|
11
|
+
var module = { exports: {} };
|
|
12
|
+
var exports = module.exports;
|
|
13
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
14
|
+
const React = require("react");
|
|
15
|
+
let _usageCtx = null;
|
|
16
|
+
const _usagePlugin = (() => {
|
|
4
17
|
// ============================================================================
|
|
5
18
|
// Token 用量统计 — settings.section Client UI(纯 JS,动态插件沙箱)
|
|
6
19
|
// 可用全局:React / ctx / styles / host / console;浏览器全局 Date / Intl 等。
|
|
@@ -960,7 +973,36 @@ const CSS = `
|
|
|
960
973
|
font-size: 10px;
|
|
961
974
|
color: var(--dsw-alias-label-tertiary);
|
|
962
975
|
}
|
|
963
|
-
|
|
976
|
+
`
|
|
977
|
+
|
|
978
|
+
// RPC bridge: dynamic-plugin runs provide a `host` global; static (bundle)
|
|
979
|
+
// installs provide ctx.get('connection').rpc. Keep one UI source for both.
|
|
980
|
+
function hostCall(method, args) {
|
|
981
|
+
if (typeof host !== 'undefined' && host && typeof host.call === 'function') {
|
|
982
|
+
return Promise.resolve(host.call(method, args))
|
|
983
|
+
}
|
|
984
|
+
// Static bundle installs have no ctx/host globals: the entry apply() stores
|
|
985
|
+
// the real context in _usageCtx (declared by the build wrapper).
|
|
986
|
+
const scope = (typeof ctx !== 'undefined' && ctx && typeof ctx.get === 'function')
|
|
987
|
+
? ctx
|
|
988
|
+
: (typeof _usageCtx !== 'undefined' ? _usageCtx : null)
|
|
989
|
+
if (scope === null || typeof scope.get !== 'function') {
|
|
990
|
+
return Promise.reject(new Error('context unavailable'))
|
|
991
|
+
}
|
|
992
|
+
const connection = scope.get('connection')
|
|
993
|
+
if (connection === undefined || connection.rpc === undefined) {
|
|
994
|
+
return Promise.reject(new Error('connection service unavailable'))
|
|
995
|
+
}
|
|
996
|
+
const name = String(method)
|
|
997
|
+
const slash = name.indexOf('/')
|
|
998
|
+
const channel = slash === -1 ? name : name.slice(0, slash)
|
|
999
|
+
const endpoint = slash === -1 ? name : name.slice(slash + 1)
|
|
1000
|
+
return connection.rpc.call('/' + channel.replace(/^\/+/, ''), endpoint, args).then((res) => {
|
|
1001
|
+
if (res && res.ok) return res.value
|
|
1002
|
+
throw new Error((res && res.error && res.error.message) || ('RPC ' + method + ' failed'))
|
|
1003
|
+
})
|
|
1004
|
+
}
|
|
1005
|
+
;
|
|
964
1006
|
const h = React.createElement
|
|
965
1007
|
|
|
966
1008
|
/* ---------------------------------------------------------------------------
|
|
@@ -2098,7 +2140,7 @@ function StatsDashboard(props) {
|
|
|
2098
2140
|
React.useEffect(() => {
|
|
2099
2141
|
let current = true
|
|
2100
2142
|
setStatus((s) => (s === 'ready' && data !== null ? 'refreshing' : 'loading'))
|
|
2101
|
-
|
|
2143
|
+
hostCall('usage/query', {
|
|
2102
2144
|
from: eff.from,
|
|
2103
2145
|
to: eff.to,
|
|
2104
2146
|
models: models.slice(),
|
|
@@ -2123,7 +2165,7 @@ function StatsDashboard(props) {
|
|
|
2123
2165
|
// 价格表:进入页面拉一次 + 每次刷新同步
|
|
2124
2166
|
React.useEffect(() => {
|
|
2125
2167
|
let current = true
|
|
2126
|
-
|
|
2168
|
+
hostCall('usage/prices').then((p) => {
|
|
2127
2169
|
if (current && p && p.prices) setPrices(p)
|
|
2128
2170
|
}, () => {})
|
|
2129
2171
|
return () => { current = false }
|
|
@@ -2193,7 +2235,18 @@ function StatsDashboard(props) {
|
|
|
2193
2235
|
* 注册:settings.section 页面
|
|
2194
2236
|
* ------------------------------------------------------------------------- */
|
|
2195
2237
|
function apply(ctx) {
|
|
2196
|
-
ctx.effect(() =>
|
|
2238
|
+
ctx.effect(() => {
|
|
2239
|
+
if (typeof styles !== 'undefined' && styles && typeof styles.insert === 'function') {
|
|
2240
|
+
return styles.insert(CSS)
|
|
2241
|
+
}
|
|
2242
|
+
if (typeof document !== 'undefined') {
|
|
2243
|
+
const el = document.createElement('style')
|
|
2244
|
+
el.setAttribute('data-plugin', 'dsh-plugin-usage')
|
|
2245
|
+
el.textContent = CSS
|
|
2246
|
+
document.head.appendChild(el)
|
|
2247
|
+
return () => { el.remove() }
|
|
2248
|
+
}
|
|
2249
|
+
}, 'usage: styles')
|
|
2197
2250
|
ctx.effect(() => ctx.locale.register(NS, { zh: ZH, en: EN }), 'usage: dictionaries')
|
|
2198
2251
|
const t0 = ctx.locale ? ctx.locale.bind(NS) : fallbackT
|
|
2199
2252
|
const slots = ctx.get('slots')
|
|
@@ -2206,4 +2259,12 @@ function apply(ctx) {
|
|
|
2206
2259
|
|
|
2207
2260
|
return { inject: ['locale'], apply }
|
|
2208
2261
|
|
|
2209
|
-
}
|
|
2262
|
+
})();
|
|
2263
|
+
exports.apply = function apply(ctx) {
|
|
2264
|
+
_usageCtx = ctx;
|
|
2265
|
+
return _usagePlugin.apply(ctx);
|
|
2266
|
+
};
|
|
2267
|
+
exports.inject = _usagePlugin.inject ?? [];
|
|
2268
|
+
return module.exports;
|
|
2269
|
+
}
|
|
2270
|
+
});
|
package/lib/host/index.js
CHANGED
|
@@ -351,50 +351,75 @@ export function apply(ctx) {
|
|
|
351
351
|
};
|
|
352
352
|
}
|
|
353
353
|
// ------------------------------------------------------------------- RPC
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
354
|
+
// Static (bundle) installs expose the generic Connection RPC bridge (the
|
|
355
|
+
// browser half calls `connection.rpc.call('/usage', endpoint, args)`);
|
|
356
|
+
// dynamic-plugin runs expose `harness` directly. Support both from one file.
|
|
357
|
+
const usageHandlers = new Map();
|
|
358
|
+
const usageHarness = (() => {
|
|
359
|
+
if (typeof harness !== 'undefined' && harness && typeof harness.handle === 'function')
|
|
360
|
+
return harness;
|
|
361
|
+
const connection = ctx.get('connection');
|
|
362
|
+
if (connection === undefined || connection.rpc === undefined)
|
|
363
|
+
return null;
|
|
364
|
+
connection.rpc.handle('/usage', (endpoint, args) => {
|
|
365
|
+
const fn = usageHandlers.get(endpoint);
|
|
366
|
+
if (fn === undefined)
|
|
367
|
+
return { ok: false, error: `unknown endpoint: ${endpoint}` };
|
|
368
|
+
return fn(args);
|
|
369
|
+
}, { authority: 'loopback' });
|
|
370
|
+
return {
|
|
371
|
+
handle(method, fn) {
|
|
372
|
+
const name = String(method);
|
|
373
|
+
usageHandlers.set(name.indexOf('/') === -1 ? name : name.slice(name.indexOf('/') + 1), fn);
|
|
374
|
+
},
|
|
375
|
+
};
|
|
376
|
+
})();
|
|
377
|
+
if (usageHarness !== null) {
|
|
378
|
+
usageHarness.handle(RPC_QUERY, (args) => query(args || {}));
|
|
379
|
+
usageHarness.handle(RPC_PRICES, (args) => {
|
|
380
|
+
const incoming = (args && args.prices) || null;
|
|
381
|
+
if (incoming && typeof incoming === 'object') {
|
|
382
|
+
for (const [model, p] of Object.entries(incoming)) {
|
|
383
|
+
if (p && typeof p === 'object') {
|
|
384
|
+
prices[model] = {
|
|
385
|
+
inputPerM: Number(p.inputPerM) || 0,
|
|
386
|
+
outputPerM: Number(p.outputPerM) || 0,
|
|
387
|
+
cacheReadPerM: Number(p.cacheReadPerM) || 0,
|
|
388
|
+
cacheWritePerM: Number(p.cacheWritePerM) || 0,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
366
391
|
}
|
|
367
392
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
393
|
+
if (args && typeof args.currency === 'string')
|
|
394
|
+
currency = args.currency;
|
|
395
|
+
return { prices: Object.assign({}, prices), currency };
|
|
396
|
+
});
|
|
397
|
+
// ------------------------------------------------- client source loader
|
|
398
|
+
// The full dashboard UI lives in src/client/index.js (single source of
|
|
399
|
+
// truth). A dynamic plugin cannot import files, so the host reads the file
|
|
400
|
+
// and serves it to the browser half, which evaluates it with new Function.
|
|
401
|
+
const CLIENT_SOURCE_CANDIDATES = [
|
|
402
|
+
'/Users/ruanbw/projects/usage/src/client/index.js',
|
|
403
|
+
'/Users/ruanbw/projects/usage/lib/client/index.js',
|
|
404
|
+
];
|
|
405
|
+
usageHarness.handle('usage/client-source', async () => {
|
|
406
|
+
const fs = ctx.get('fs');
|
|
407
|
+
if (fs === undefined)
|
|
408
|
+
return { ok: false, error: 'fs service unavailable' };
|
|
409
|
+
for (const p of CLIENT_SOURCE_CANDIDATES) {
|
|
410
|
+
try {
|
|
411
|
+
const target = await fs.resolve(p);
|
|
412
|
+
const body = await fs.readText(target);
|
|
413
|
+
if (typeof body === 'string' && body.length > 0)
|
|
414
|
+
return { ok: true, body };
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
// try the next candidate
|
|
418
|
+
}
|
|
394
419
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
}
|
|
420
|
+
return { ok: false, error: 'client source not found (tried: ' + CLIENT_SOURCE_CANDIDATES.join(', ') + ')' };
|
|
421
|
+
});
|
|
422
|
+
}
|
|
398
423
|
// ---------------------------------------------------------------- startup
|
|
399
424
|
runBackfill();
|
|
400
425
|
// Live sessions every tick; persisted sessions on a slow cadence to keep
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-usage",
|
|
3
3
|
"description": "Token usage statistics for DeepSeek Harness — daily/custom-range views, model filters, call counts and cost, inspired by CC Switch statistics.",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.2",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|