dsh-plugin-usage 0.1.0 → 0.1.1

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.
@@ -1,6 +1,9 @@
1
1
  // Auto-generated module entry wrapping the shared client body
2
2
  // (source: src/client/index.js). Do not edit directly.
3
- export function apply(ctx) {
3
+ // The shared body returns a plugin descriptor ({ inject, apply }) for the
4
+ // dynamic-plugin runner; the static composition loader calls module apply()
5
+ // directly, so this entry unwraps the descriptor and runs its apply.
6
+ const _usagePlugin = (() => {
4
7
  // ============================================================================
5
8
  // Token 用量统计 — settings.section Client UI(纯 JS,动态插件沙箱)
6
9
  // 可用全局:React / ctx / styles / host / console;浏览器全局 Date / Intl 等。
@@ -960,7 +963,28 @@ const CSS = `
960
963
  font-size: 10px;
961
964
  color: var(--dsw-alias-label-tertiary);
962
965
  }
963
- `;
966
+ `
967
+
968
+ // RPC bridge: dynamic-plugin runs provide a `host` global; static (bundle)
969
+ // installs provide ctx.get('connection').rpc. Keep one UI source for both.
970
+ function hostCall(method, args) {
971
+ if (typeof host !== 'undefined' && host && typeof host.call === 'function') {
972
+ return Promise.resolve(host.call(method, args))
973
+ }
974
+ const connection = ctx.get('connection')
975
+ if (connection === undefined || connection.rpc === undefined) {
976
+ return Promise.reject(new Error('connection service unavailable'))
977
+ }
978
+ const name = String(method)
979
+ const slash = name.indexOf('/')
980
+ const channel = slash === -1 ? name : name.slice(0, slash)
981
+ const endpoint = slash === -1 ? name : name.slice(slash + 1)
982
+ return connection.rpc.call('/' + channel.replace(/^\/+/, ''), endpoint, args).then((res) => {
983
+ if (res && res.ok) return res.value
984
+ throw new Error((res && res.error && res.error.message) || ('RPC ' + method + ' failed'))
985
+ })
986
+ }
987
+ ;
964
988
  const h = React.createElement
965
989
 
966
990
  /* ---------------------------------------------------------------------------
@@ -2098,7 +2122,7 @@ function StatsDashboard(props) {
2098
2122
  React.useEffect(() => {
2099
2123
  let current = true
2100
2124
  setStatus((s) => (s === 'ready' && data !== null ? 'refreshing' : 'loading'))
2101
- host.call('usage/query', {
2125
+ hostCall('usage/query', {
2102
2126
  from: eff.from,
2103
2127
  to: eff.to,
2104
2128
  models: models.slice(),
@@ -2123,7 +2147,7 @@ function StatsDashboard(props) {
2123
2147
  // 价格表:进入页面拉一次 + 每次刷新同步
2124
2148
  React.useEffect(() => {
2125
2149
  let current = true
2126
- host.call('usage/prices').then((p) => {
2150
+ hostCall('usage/prices').then((p) => {
2127
2151
  if (current && p && p.prices) setPrices(p)
2128
2152
  }, () => {})
2129
2153
  return () => { current = false }
@@ -2193,7 +2217,18 @@ function StatsDashboard(props) {
2193
2217
  * 注册:settings.section 页面
2194
2218
  * ------------------------------------------------------------------------- */
2195
2219
  function apply(ctx) {
2196
- ctx.effect(() => styles.insert(CSS), 'usage: styles')
2220
+ ctx.effect(() => {
2221
+ if (typeof styles !== 'undefined' && styles && typeof styles.insert === 'function') {
2222
+ return styles.insert(CSS)
2223
+ }
2224
+ if (typeof document !== 'undefined') {
2225
+ const el = document.createElement('style')
2226
+ el.setAttribute('data-plugin', 'dsh-plugin-usage')
2227
+ el.textContent = CSS
2228
+ document.head.appendChild(el)
2229
+ return () => { el.remove() }
2230
+ }
2231
+ }, 'usage: styles')
2197
2232
  ctx.effect(() => ctx.locale.register(NS, { zh: ZH, en: EN }), 'usage: dictionaries')
2198
2233
  const t0 = ctx.locale ? ctx.locale.bind(NS) : fallbackT
2199
2234
  const slots = ctx.get('slots')
@@ -2206,4 +2241,8 @@ function apply(ctx) {
2206
2241
 
2207
2242
  return { inject: ['locale'], apply }
2208
2243
 
2244
+ })();
2245
+ export const inject = _usagePlugin.inject ?? [];
2246
+ export function apply(ctx) {
2247
+ return _usagePlugin.apply(ctx);
2209
2248
  }
package/lib/host/index.js CHANGED
@@ -351,50 +351,75 @@ export function apply(ctx) {
351
351
  };
352
352
  }
353
353
  // ------------------------------------------------------------------- RPC
354
- harness.handle(RPC_QUERY, (args) => query(args || {}));
355
- harness.handle(RPC_PRICES, (args) => {
356
- const incoming = (args && args.prices) || null;
357
- if (incoming && typeof incoming === 'object') {
358
- for (const [model, p] of Object.entries(incoming)) {
359
- if (p && typeof p === 'object') {
360
- prices[model] = {
361
- inputPerM: Number(p.inputPerM) || 0,
362
- outputPerM: Number(p.outputPerM) || 0,
363
- cacheReadPerM: Number(p.cacheReadPerM) || 0,
364
- cacheWritePerM: Number(p.cacheWritePerM) || 0,
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
- if (args && typeof args.currency === 'string')
370
- currency = args.currency;
371
- return { prices: Object.assign({}, prices), currency };
372
- });
373
- // ------------------------------------------------- client source loader
374
- // The full dashboard UI lives in src/client/index.js (single source of
375
- // truth). A dynamic plugin cannot import files, so the host reads the file
376
- // and serves it to the browser half, which evaluates it with new Function.
377
- const CLIENT_SOURCE_CANDIDATES = [
378
- '/Users/ruanbw/projects/usage/src/client/index.js',
379
- '/Users/ruanbw/projects/usage/lib/client/index.js',
380
- ];
381
- harness.handle('usage/client-source', async () => {
382
- const fs = ctx.get('fs');
383
- if (fs === undefined)
384
- return { ok: false, error: 'fs service unavailable' };
385
- for (const p of CLIENT_SOURCE_CANDIDATES) {
386
- try {
387
- const target = await fs.resolve(p);
388
- const body = await fs.readText(target);
389
- if (typeof body === 'string' && body.length > 0)
390
- return { ok: true, body };
391
- }
392
- catch (error) {
393
- // try the next candidate
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
- return { ok: false, error: 'client source not found (tried: ' + CLIENT_SOURCE_CANDIDATES.join(', ') + ')' };
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.0",
4
+ "version": "0.1.1",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",