@goodandready/dsh-cost-meter 0.8.2 → 0.8.4
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 +241 -43
- package/lib/index.js +43 -1
- package/lib/updater.js +270 -0
- package/package.json +7 -4
- package/docs/README.ru.md +0 -162
- package/docs/README.zh.md +0 -150
- package/docs/deployment/public-release.md +0 -9
- package/docs/design/DESIGN.md +0 -117
- package/docs/plans/findings.md +0 -13
- package/docs/plans/progress.md +0 -12
- package/docs/plans/task_plan.md +0 -31
- package/docs/testing/projection-lifecycle.md +0 -28
- package/docs/testing/public-route.md +0 -12
package/lib/client.js
CHANGED
|
@@ -11,8 +11,13 @@ window.__ModuleLoader__.load({
|
|
|
11
11
|
const h = React.createElement
|
|
12
12
|
|
|
13
13
|
const NS = 'dsh-cost-meter'
|
|
14
|
+
// Plugins page row seat (DSH 0.1.6-alpha.2): key = '<package name>#<row id>'.
|
|
15
|
+
const PKG = '@goodandready/dsh-cost-meter'
|
|
16
|
+
const ROW_ID = 'dsh-cost-meter'
|
|
17
|
+
const ROW_CONFIG_KEY = PKG + '#' + ROW_ID
|
|
14
18
|
const STATE_PATH = '/dsh-cost-meter/state'
|
|
15
19
|
const REFRESH_PATH = '/dsh-cost-meter/refresh'
|
|
20
|
+
const UPDATE_PATH = '/dsh-cost-meter/update'
|
|
16
21
|
const DAY = 1440
|
|
17
22
|
|
|
18
23
|
// ----------------------------------------------------------- localization
|
|
@@ -22,6 +27,13 @@ window.__ModuleLoader__.load({
|
|
|
22
27
|
subtitle: 'Model rates, peak discount windows and session spend',
|
|
23
28
|
'header.title': 'Cost Meter',
|
|
24
29
|
'header.sub': 'Token spend monitoring, automatic tariff resolution and discount windows',
|
|
30
|
+
'update.checking': 'Checking for updates…',
|
|
31
|
+
'update.up_to_date': 'Up to date',
|
|
32
|
+
'update.available': 'Update available: v{latestVersion} (current: v{currentVersion})',
|
|
33
|
+
'update.btn': 'Update Now',
|
|
34
|
+
'update.updating': 'Updating…',
|
|
35
|
+
'update.done': 'Successfully updated to v{version}! Please restart DSH.',
|
|
36
|
+
'update.failed': 'Update failed: {error}',
|
|
25
37
|
'badge.catalog_ok': 'Catalog: {count} models',
|
|
26
38
|
'badge.catalog_empty': 'Catalog: loading…',
|
|
27
39
|
'badge.catalog_off': 'Catalog: disabled',
|
|
@@ -93,6 +105,13 @@ window.__ModuleLoader__.load({
|
|
|
93
105
|
subtitle: '模型费率、高峰优惠窗口与会话消费统计',
|
|
94
106
|
'header.title': '成本计量器 (Cost Meter)',
|
|
95
107
|
'header.sub': 'Token 消费监控、费率自动匹配与折扣窗口管理',
|
|
108
|
+
'update.checking': '正在检查更新…',
|
|
109
|
+
'update.up_to_date': '已是最新版本',
|
|
110
|
+
'update.available': '发现新版本 v{latestVersion}(当前版本 v{currentVersion})',
|
|
111
|
+
'update.btn': '立即更新',
|
|
112
|
+
'update.updating': '正在更新…',
|
|
113
|
+
'update.done': '更新成功至 v{version}!请重启 DSH 服务生效。',
|
|
114
|
+
'update.failed': '更新失败:{error}',
|
|
96
115
|
'badge.catalog_ok': '目录:{count} 个模型',
|
|
97
116
|
'badge.catalog_empty': '目录:加载中…',
|
|
98
117
|
'badge.catalog_off': '目录:已禁用',
|
|
@@ -200,19 +219,22 @@ window.__ModuleLoader__.load({
|
|
|
200
219
|
.dcm-grid-2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px; }
|
|
201
220
|
.dcm-field { display: flex; flex-direction: column; gap: 6px; }
|
|
202
221
|
.dcm-row-check { display: flex; align-items: center; gap: 10px; padding: 4px 0; }
|
|
203
|
-
.dcm-check { width: 16px; height: 16px; accent-color: var(--dsw-alias-state-brand-primary
|
|
222
|
+
.dcm-check { width: 16px; height: 16px; accent-color: var(--dsw-alias-state-brand-primary); cursor: pointer; }
|
|
204
223
|
.dcm-label { color: var(--dsw-alias-label-primary); font-size: 13px; font-weight: 500; }
|
|
205
224
|
.dcm-hint { color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 1.4; }
|
|
206
225
|
|
|
207
226
|
.dcm-input { height: 34px; border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-3); color: var(--dsw-alias-label-primary); border-radius: 8px; padding: 0 12px; font-size: 13px; width: 100%; box-sizing: border-box; }
|
|
208
|
-
.dcm-input:focus { outline: none; border-color: var(--dsw-alias-state-brand-primary
|
|
209
|
-
.dcm-input-err { border-color: var(--dsw-alias-state-error-primary
|
|
227
|
+
.dcm-input:focus { outline: none; border-color: var(--dsw-alias-state-brand-primary); }
|
|
228
|
+
.dcm-input-err { border-color: var(--dsw-alias-state-error-primary); }
|
|
229
|
+
|
|
230
|
+
.dcm-chevron { display: inline-flex; align-items: center; justify-content: center; transition: transform .16s ease; color: var(--dsw-alias-label-tertiary); }
|
|
231
|
+
.dcm-chevron-open { transform: rotate(180deg); }
|
|
210
232
|
|
|
211
233
|
.dcm-badges { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-top: 4px; }
|
|
212
234
|
.dcm-badge { font-size: 11px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--dsw-alias-border-l2); display: inline-flex; align-items: center; gap: 4px; font-weight: 500; }
|
|
213
|
-
.dcm-badge-ok { border-color: var(--dsw-alias-state-success-primary); color: var(--dsw-alias-state-success-primary); background:
|
|
214
|
-
.dcm-badge-warn { border-color: var(--dsw-alias-state-warning-primary); color: var(--dsw-alias-state-warning-primary); background:
|
|
215
|
-
.dcm-badge-bad { border-color: var(--dsw-alias-state-error-primary); color: var(--dsw-alias-state-error-primary); background:
|
|
235
|
+
.dcm-badge-ok { border-color: var(--dsw-alias-state-success-primary); color: var(--dsw-alias-state-success-primary); background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 8%, transparent); }
|
|
236
|
+
.dcm-badge-warn { border-color: var(--dsw-alias-state-warning-primary); color: var(--dsw-alias-state-warning-primary); background: color-mix(in srgb, var(--dsw-alias-state-warning-primary) 8%, transparent); }
|
|
237
|
+
.dcm-badge-bad { border-color: var(--dsw-alias-state-error-primary); color: var(--dsw-alias-state-error-primary); background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 8%, transparent); }
|
|
216
238
|
|
|
217
239
|
.dcm-btn { appearance: none; font: inherit; cursor: pointer; border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; padding: 6px 14px; font-size: 13px; background: var(--dsw-alias-bg-layer-3); color: var(--dsw-alias-label-primary); font-weight: 500; display: inline-flex; align-items: center; justify-content: center; gap: 6px; transition: all .15s ease; }
|
|
218
240
|
.dcm-btn:hover:not(:disabled) { background: var(--dsw-alias-bg-layer-4, var(--dsw-alias-bg-layer-3)); }
|
|
@@ -222,10 +244,10 @@ window.__ModuleLoader__.load({
|
|
|
222
244
|
.dcm-btn-sm { padding: 4px 10px; font-size: 12px; border-radius: 6px; }
|
|
223
245
|
|
|
224
246
|
.dcm-alert { padding: 10px 14px; border-radius: 8px; font-size: 13px; line-height: 1.4; }
|
|
225
|
-
.dcm-alert-ok { background:
|
|
226
|
-
.dcm-alert-err { background:
|
|
227
|
-
.dcm-tip-box { padding: 8px 12px; border-radius: 8px; background:
|
|
228
|
-
.dcm-cache-box { padding: 7px 11px; border-radius: 8px; background:
|
|
247
|
+
.dcm-alert-ok { background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 10%, transparent); color: var(--dsw-alias-state-success-primary); }
|
|
248
|
+
.dcm-alert-err { background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent); color: var(--dsw-alias-state-error-primary); }
|
|
249
|
+
.dcm-tip-box { padding: 8px 12px; border-radius: 8px; background: color-mix(in srgb, var(--dsw-alias-state-brand-primary) 8%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-state-brand-primary) 20%, transparent); color: var(--dsw-alias-label-primary); font-size: 12px; line-height: 1.4; display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
250
|
+
.dcm-cache-box { padding: 7px 11px; border-radius: 8px; background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 8%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-state-success-primary) 22%, transparent); color: var(--dsw-alias-state-success-primary); font-size: 11.5px; font-weight: 500; line-height: 1.4; font-variant-numeric: tabular-nums; }
|
|
229
251
|
|
|
230
252
|
.dcm-stat-box { padding: 10px 12px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; background: var(--dsw-alias-bg-layer-3); display: flex; flex-direction: column; gap: 2px; }
|
|
231
253
|
.dcm-stat-val { font-size: 14px; font-weight: 600; color: var(--dsw-alias-label-primary); font-variant-numeric: tabular-nums; }
|
|
@@ -234,13 +256,13 @@ window.__ModuleLoader__.load({
|
|
|
234
256
|
.dcm-foot { display: flex; justify-content: flex-end; align-items: center; gap: 10px; padding-top: 6px; }
|
|
235
257
|
|
|
236
258
|
/* Native dot-indicator chip */
|
|
237
|
-
.dcm-chip { display: inline-flex; align-items: center; gap: 6px; height: 24px; padding: 0 9px; border-radius: 7px; border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-2
|
|
238
|
-
.dcm-chip:hover { border-color: var(--dsw-alias-border-l3, var(--dsw-alias-label-tertiary)); background: var(--dsw-alias-bg-layer-3
|
|
259
|
+
.dcm-chip { display: inline-flex; align-items: center; gap: 6px; height: 24px; padding: 0 9px; border-radius: 7px; border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); cursor: pointer; font: inherit; font-size: 12px; font-variant-numeric: tabular-nums; line-height: 24px; transition: all .15s ease; user-select: none; }
|
|
260
|
+
.dcm-chip:hover { border-color: var(--dsw-alias-border-l3, var(--dsw-alias-label-tertiary)); background: var(--dsw-alias-bg-layer-3); }
|
|
239
261
|
.dcm-dot { width: 6.5px; height: 6.5px; border-radius: 50%; display: inline-block; flex-shrink: 0; }
|
|
240
|
-
.dcm-dot-ok { background: var(--dsw-alias-state-success-primary
|
|
241
|
-
.dcm-dot-warn { background: var(--dsw-alias-state-warning-primary
|
|
242
|
-
.dcm-dot-err { background: var(--dsw-alias-state-error-primary
|
|
243
|
-
.dcm-dot-idle { background: var(--dsw-alias-label-tertiary
|
|
262
|
+
.dcm-dot-ok { background: var(--dsw-alias-state-success-primary); box-shadow: 0 0 5px color-mix(in srgb, var(--dsw-alias-state-success-primary) 50%, transparent); }
|
|
263
|
+
.dcm-dot-warn { background: var(--dsw-alias-state-warning-primary); box-shadow: 0 0 5px color-mix(in srgb, var(--dsw-alias-state-warning-primary) 40%, transparent); }
|
|
264
|
+
.dcm-dot-err { background: var(--dsw-alias-state-error-primary); box-shadow: 0 0 6px color-mix(in srgb, var(--dsw-alias-state-error-primary) 70%, transparent); animation: dcm-pulse 1.8s infinite ease-in-out; }
|
|
265
|
+
.dcm-dot-idle { background: var(--dsw-alias-label-tertiary); }
|
|
244
266
|
|
|
245
267
|
.dcm-chip-timer { opacity: 0.72; font-size: 11px; padding-left: 2px; border-left: 1px solid var(--dsw-alias-border-l2); }
|
|
246
268
|
@media (max-width: 680px) {
|
|
@@ -249,7 +271,7 @@ window.__ModuleLoader__.load({
|
|
|
249
271
|
|
|
250
272
|
@keyframes dcm-pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.4; transform: scale(0.85); } }
|
|
251
273
|
|
|
252
|
-
.dcm-panel { position: absolute; top: 30px; right: 0; z-index: 40; width: 336px; padding: 14px 16px; border-radius: 12px; border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-base)); box-shadow: 0 10px 32px
|
|
274
|
+
.dcm-panel { position: absolute; top: 30px; right: 0; z-index: 40; width: 336px; padding: 14px 16px; border-radius: 12px; border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-base)); box-shadow: var(--dsw-alias-shadow-layer-3, 0 10px 32px var(--dsw-alias-bg-base)); font-size: 12px; line-height: 1.5; color: var(--dsw-alias-label-primary); text-align: left; font-variant-numeric: tabular-nums; }
|
|
253
275
|
.dcm-panel-title { font-weight: 600; padding-bottom: 2px; font-size: 13.5px; display: flex; justify-content: space-between; align-items: baseline; }
|
|
254
276
|
.dcm-panel-note { color: var(--dsw-alias-label-tertiary); font-size: 12px; }
|
|
255
277
|
.dcm-hr { height: 1px; background: var(--dsw-alias-border-l2); margin: 10px -16px; border: 0; }
|
|
@@ -264,7 +286,7 @@ window.__ModuleLoader__.load({
|
|
|
264
286
|
.dcm-cost { text-align: right; padding: 3.5px 0; font-weight: 500; color: var(--dsw-alias-label-primary); }
|
|
265
287
|
.dcm-section-lbl { color: var(--dsw-alias-label-tertiary); padding-bottom: 4px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
266
288
|
.dcm-total { display: flex; justify-content: space-between; font-weight: 600; font-size: 13.5px; }
|
|
267
|
-
.dcm-accordion-btn { appearance: none; background: transparent; border: 0; color: var(--dsw-alias-state-brand-primary
|
|
289
|
+
.dcm-accordion-btn { appearance: none; background: transparent; border: 0; color: var(--dsw-alias-state-brand-primary); font: inherit; font-size: 11.5px; cursor: pointer; padding: 2px 0; text-align: left; }
|
|
268
290
|
.dcm-accordion-btn:hover { text-decoration: underline; }
|
|
269
291
|
`
|
|
270
292
|
|
|
@@ -318,51 +340,83 @@ window.__ModuleLoader__.load({
|
|
|
318
340
|
}
|
|
319
341
|
const ErrorBoundary = createErrorBoundary()
|
|
320
342
|
|
|
343
|
+
function getSettingsScope(ctx) {
|
|
344
|
+
return (ctx && typeof ctx.get === 'function' ? ctx.get('settingsScope') : null) || (ctx && ctx.settingsScope) || (ctx && ctx.services && ctx.services.settingsScope) || null
|
|
345
|
+
}
|
|
346
|
+
|
|
321
347
|
function refreshMirrorUntilVisible(ctx) {
|
|
348
|
+
const s = getSettingsScope(ctx)
|
|
322
349
|
const visible = () => {
|
|
323
350
|
try {
|
|
324
|
-
const s = (ctx?.get && ctx.get('lanSettings')) || (ctx?.get && ctx.get('settingsScope')) || ctx?.settingsScope
|
|
325
351
|
const view = s?.describe?.()?.getSnapshot?.()?.view
|
|
326
352
|
return !!view && Array.isArray(view.namespaces) && view.namespaces.some((row) => row.ns === NS)
|
|
327
|
-
} catch (
|
|
353
|
+
} catch (e) {
|
|
354
|
+
if (ctx?.logger?.debug) ctx.logger.debug('[dsh-cost-meter] mirror check error: ' + (e?.message || e))
|
|
328
355
|
return false
|
|
329
356
|
}
|
|
330
357
|
}
|
|
331
|
-
if (visible()) return () => {}
|
|
358
|
+
if (!s || visible()) return () => {}
|
|
332
359
|
let tries = 0
|
|
333
360
|
const timer = setInterval(() => {
|
|
334
|
-
if (visible() || tries >= 15) {
|
|
361
|
+
if (visible() || tries >= 15) {
|
|
362
|
+
if (!visible() && tries >= 15 && ctx?.logger?.warn) {
|
|
363
|
+
ctx.logger.warn('[dsh-cost-meter] settings mirror did not appear after 15 attempts')
|
|
364
|
+
}
|
|
365
|
+
clearInterval(timer)
|
|
366
|
+
return
|
|
367
|
+
}
|
|
335
368
|
tries += 1
|
|
336
369
|
try {
|
|
337
|
-
const s = (ctx?.get && ctx.get('lanSettings')) || (ctx?.get && ctx.get('settingsScope')) || ctx?.settingsScope
|
|
338
370
|
s?.describe?.()?.load?.()
|
|
339
|
-
} catch (
|
|
371
|
+
} catch (e) {
|
|
372
|
+
if (ctx?.logger?.debug) ctx.logger.debug('[dsh-cost-meter] settings load error: ' + (e?.message || e))
|
|
373
|
+
}
|
|
340
374
|
}, 1000)
|
|
341
375
|
return () => clearInterval(timer)
|
|
342
376
|
}
|
|
343
377
|
|
|
344
|
-
|
|
378
|
+
let ChevronIcon = null
|
|
379
|
+
try {
|
|
380
|
+
const primitives = require('@deepseek-ai/dsh-client-ui-primitives')
|
|
381
|
+
ChevronIcon = primitives && (primitives.IconChevronDownOutline14 || primitives.IconChevronDownOutline)
|
|
382
|
+
} catch (e) {
|
|
383
|
+
ChevronIcon = null
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function FallbackChevron(props) {
|
|
345
387
|
return React.createElement(
|
|
346
388
|
'svg',
|
|
347
389
|
{
|
|
348
|
-
width:
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
color: 'var(--dsw-alias-label-secondary)',
|
|
354
|
-
},
|
|
390
|
+
width: 14,
|
|
391
|
+
height: 14,
|
|
392
|
+
viewBox: '0 0 14 14',
|
|
393
|
+
fill: 'none',
|
|
394
|
+
'aria-hidden': true,
|
|
355
395
|
},
|
|
356
396
|
React.createElement('path', {
|
|
357
|
-
d: '
|
|
397
|
+
d: 'M3.5 5.25L7 8.75L10.5 5.25',
|
|
358
398
|
stroke: 'currentColor',
|
|
359
|
-
strokeWidth: 1.
|
|
399
|
+
strokeWidth: 1.5,
|
|
360
400
|
strokeLinecap: 'round',
|
|
361
401
|
strokeLinejoin: 'round',
|
|
362
402
|
}),
|
|
363
403
|
)
|
|
364
404
|
}
|
|
365
405
|
|
|
406
|
+
function Chevron(props) {
|
|
407
|
+
const icon = ChevronIcon
|
|
408
|
+
? React.createElement(ChevronIcon, { 'aria-hidden': true })
|
|
409
|
+
: React.createElement(FallbackChevron, null)
|
|
410
|
+
return React.createElement(
|
|
411
|
+
'span',
|
|
412
|
+
{
|
|
413
|
+
className: 'dcm-chevron' + (props.open ? ' dcm-chevron-open' : ''),
|
|
414
|
+
style: { marginLeft: 'auto' },
|
|
415
|
+
},
|
|
416
|
+
icon,
|
|
417
|
+
)
|
|
418
|
+
}
|
|
419
|
+
|
|
366
420
|
function isValidTimeZone(tz) {
|
|
367
421
|
if (!tz || typeof tz !== 'string') return false
|
|
368
422
|
try {
|
|
@@ -389,7 +443,8 @@ window.__ModuleLoader__.load({
|
|
|
389
443
|
const ctx = props.ctx
|
|
390
444
|
const locale = useActiveLocale(ctx)
|
|
391
445
|
const t = props.t || makeT(locale === 'zh' ? zh : en, en)
|
|
392
|
-
const
|
|
446
|
+
const page = !!(props && props.view === 'page')
|
|
447
|
+
const [open, setOpen] = React.useState(!!page)
|
|
393
448
|
const [draft, setDraft] = React.useState(null)
|
|
394
449
|
const [saving, setSaving] = React.useState(false)
|
|
395
450
|
const [err, setErr] = React.useState('')
|
|
@@ -400,7 +455,72 @@ window.__ModuleLoader__.load({
|
|
|
400
455
|
const [syncing, setSyncing] = React.useState(false)
|
|
401
456
|
const [syncMsg, setSyncMsg] = React.useState('')
|
|
402
457
|
|
|
403
|
-
|
|
458
|
+
// Plugin in-app updater state
|
|
459
|
+
const [updateState, setUpdateState] = React.useState({
|
|
460
|
+
checking: false,
|
|
461
|
+
updating: false,
|
|
462
|
+
currentVersion: '0.8.3',
|
|
463
|
+
latestVersion: '',
|
|
464
|
+
updateAvailable: false,
|
|
465
|
+
canAutoUpdate: true,
|
|
466
|
+
error: '',
|
|
467
|
+
notice: '',
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
const checkUpdate = React.useCallback(async () => {
|
|
471
|
+
setUpdateState((s) => ({ ...s, checking: true, error: '' }))
|
|
472
|
+
try {
|
|
473
|
+
const res = await fetch(UPDATE_PATH)
|
|
474
|
+
if (!res.ok) throw new Error('HTTP ' + res.status)
|
|
475
|
+
const data = await res.json().catch(() => ({}))
|
|
476
|
+
setUpdateState((s) => ({
|
|
477
|
+
...s,
|
|
478
|
+
checking: false,
|
|
479
|
+
currentVersion: data.currentVersion || s.currentVersion,
|
|
480
|
+
latestVersion: data.latestVersion || '',
|
|
481
|
+
updateAvailable: Boolean(data.updateAvailable),
|
|
482
|
+
canAutoUpdate: data.canAutoUpdate !== false,
|
|
483
|
+
}))
|
|
484
|
+
} catch (e) {
|
|
485
|
+
setUpdateState((s) => ({ ...s, checking: false }))
|
|
486
|
+
}
|
|
487
|
+
}, [])
|
|
488
|
+
|
|
489
|
+
React.useEffect(() => {
|
|
490
|
+
if (open) checkUpdate()
|
|
491
|
+
}, [open, checkUpdate])
|
|
492
|
+
|
|
493
|
+
async function handleTriggerUpdate() {
|
|
494
|
+
if (updateState.updating) return
|
|
495
|
+
setUpdateState((s) => ({ ...s, updating: true, error: '', notice: '' }))
|
|
496
|
+
try {
|
|
497
|
+
const res = await fetch(UPDATE_PATH, {
|
|
498
|
+
method: 'POST',
|
|
499
|
+
headers: { 'x-dsh-plugin-update': '1' },
|
|
500
|
+
})
|
|
501
|
+
const data = await res.json().catch(() => ({}))
|
|
502
|
+
if (!res.ok || data.ok === false || data.error) {
|
|
503
|
+
throw new Error(data.error || ('HTTP ' + res.status))
|
|
504
|
+
}
|
|
505
|
+
const newVer = data.updatedVersion || updateState.latestVersion || updateState.currentVersion
|
|
506
|
+
setUpdateState((s) => ({
|
|
507
|
+
...s,
|
|
508
|
+
updating: false,
|
|
509
|
+
updateAvailable: false,
|
|
510
|
+
currentVersion: newVer,
|
|
511
|
+
notice: t('update.done', { version: newVer }),
|
|
512
|
+
}))
|
|
513
|
+
setTimeout(() => checkUpdate(), 2000)
|
|
514
|
+
} catch (err) {
|
|
515
|
+
setUpdateState((s) => ({
|
|
516
|
+
...s,
|
|
517
|
+
updating: false,
|
|
518
|
+
error: t('update.failed', { error: String(err.message || err) }),
|
|
519
|
+
}))
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const settingsScope = getSettingsScope(ctx)
|
|
404
524
|
const scope = React.useMemo(
|
|
405
525
|
() => (settingsScope ? settingsScope.bind({ namespace: NS }) : undefined),
|
|
406
526
|
[settingsScope],
|
|
@@ -535,9 +655,15 @@ window.__ModuleLoader__.load({
|
|
|
535
655
|
const catalogHasErr = diagState && Boolean(diagState.catalogError)
|
|
536
656
|
const openRouterEnabled = draft ? Boolean(draft.useOpenRouter) : true
|
|
537
657
|
|
|
538
|
-
|
|
658
|
+
// Row seat (plugins.row.config): the host page draws title/icon/crumb and the
|
|
659
|
+
// padding, so the summary is a one-liner and the page drops our card chrome.
|
|
660
|
+
if (props && props.view === 'summary') {
|
|
661
|
+
return React.createElement('span', { className: 'dcm-sub' }, t('header.sub'))
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
return React.createElement(page ? 'div' : 'li', { className: page ? 'dcm-page' : 'dcm-card' },
|
|
539
665
|
React.createElement('button', {
|
|
540
|
-
type: 'button', className: 'dcm-head', 'aria-expanded': open,
|
|
666
|
+
type: 'button', className: 'dcm-head', style: page ? { display: 'none' } : undefined, 'aria-expanded': page ? true : open,
|
|
541
667
|
onClick: () => setOpen((v) => !v),
|
|
542
668
|
},
|
|
543
669
|
React.createElement('div', { style: { flex: 1, display: 'flex', flexDirection: 'column', gap: 2 } },
|
|
@@ -566,6 +692,38 @@ window.__ModuleLoader__.load({
|
|
|
566
692
|
: status !== 'ready'
|
|
567
693
|
? React.createElement('div', { className: 'dcm-alert dcm-alert-err' }, t('settings.unavailable'))
|
|
568
694
|
: React.createElement(React.Fragment, null,
|
|
695
|
+
// In-app Update Bar
|
|
696
|
+
React.createElement('div', {
|
|
697
|
+
className: 'dcm-section-card',
|
|
698
|
+
style: { padding: '10px 14px', flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' },
|
|
699
|
+
},
|
|
700
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, fontSize: 13 } },
|
|
701
|
+
React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)', fontWeight: 500 } },
|
|
702
|
+
'v' + updateState.currentVersion
|
|
703
|
+
),
|
|
704
|
+
updateState.checking
|
|
705
|
+
? React.createElement('span', { style: { color: 'var(--dsw-alias-label-tertiary)', fontSize: 12 } }, t('update.checking'))
|
|
706
|
+
: updateState.updateAvailable
|
|
707
|
+
? React.createElement('span', { className: 'dcm-badge dcm-badge-warn' },
|
|
708
|
+
t('update.available', { latestVersion: updateState.latestVersion, currentVersion: updateState.currentVersion })
|
|
709
|
+
)
|
|
710
|
+
: React.createElement('span', { className: 'dcm-badge dcm-badge-ok' },
|
|
711
|
+
'✓ ' + t('update.up_to_date')
|
|
712
|
+
)
|
|
713
|
+
),
|
|
714
|
+
updateState.updateAvailable
|
|
715
|
+
? React.createElement('button', {
|
|
716
|
+
type: 'button',
|
|
717
|
+
className: 'dcm-btn dcm-btn-primary',
|
|
718
|
+
disabled: updateState.updating,
|
|
719
|
+
onClick: handleTriggerUpdate,
|
|
720
|
+
style: { padding: '4px 10px', fontSize: 12 },
|
|
721
|
+
}, updateState.updating ? t('update.updating') : t('update.btn'))
|
|
722
|
+
: null
|
|
723
|
+
),
|
|
724
|
+
updateState.notice ? React.createElement('div', { className: 'dcm-alert dcm-alert-ok' }, '✓ ' + updateState.notice) : null,
|
|
725
|
+
updateState.error ? React.createElement('div', { className: 'dcm-alert dcm-alert-err' }, updateState.error) : null,
|
|
726
|
+
|
|
569
727
|
// Section 1: General Parameters
|
|
570
728
|
React.createElement('div', { className: 'dcm-section-card' },
|
|
571
729
|
React.createElement('div', { className: 'dcm-section-title' }, t('section.general')),
|
|
@@ -855,7 +1013,7 @@ window.__ModuleLoader__.load({
|
|
|
855
1013
|
|
|
856
1014
|
React.useEffect(() => { ensureStyles() }, [])
|
|
857
1015
|
|
|
858
|
-
const settingsScope = (ctx
|
|
1016
|
+
const settingsScope = getSettingsScope(ctx)
|
|
859
1017
|
const settingsSnap = React.useSyncExternalStore
|
|
860
1018
|
? React.useSyncExternalStore(
|
|
861
1019
|
(cb) => (settingsScope && typeof settingsScope.subscribe === 'function' ? settingsScope.subscribe(cb) : () => {}),
|
|
@@ -960,7 +1118,7 @@ window.__ModuleLoader__.load({
|
|
|
960
1118
|
? 'dcm-dot dcm-dot-err'
|
|
961
1119
|
: (flat ? 'dcm-dot dcm-dot-idle' : (cheapNow ? 'dcm-dot dcm-dot-ok' : 'dcm-dot dcm-dot-warn'))
|
|
962
1120
|
|
|
963
|
-
const activeCell = { textAlign: 'right', padding: '3px 6px', borderRadius: 4, background: '
|
|
1121
|
+
const activeCell = { textAlign: 'right', padding: '3px 6px', borderRadius: 4, background: 'color-mix(in srgb, var(--dsw-alias-state-brand-primary) 12%, transparent)', color: 'var(--dsw-alias-state-brand-primary)', fontWeight: 600 }
|
|
964
1122
|
|
|
965
1123
|
const rateRow = (label, key) => flat
|
|
966
1124
|
? [
|
|
@@ -1203,11 +1361,43 @@ window.__ModuleLoader__.load({
|
|
|
1203
1361
|
}
|
|
1204
1362
|
|
|
1205
1363
|
function apply(ctx) {
|
|
1206
|
-
|
|
1207
|
-
try {
|
|
1364
|
+
const registerDictionaries = () => {
|
|
1365
|
+
try {
|
|
1366
|
+
// ctx.locale.register(NS, { en, zh }) is canonical form in DSH
|
|
1367
|
+
return ctx.locale.register(NS, { en, zh })
|
|
1368
|
+
} catch (err) {
|
|
1369
|
+
if (ctx?.logger?.warn) {
|
|
1370
|
+
ctx.logger.warn(`[dsh-cost-meter] locale register failed: ${err?.message || err}`)
|
|
1371
|
+
}
|
|
1372
|
+
return () => {}
|
|
1373
|
+
}
|
|
1208
1374
|
}
|
|
1209
1375
|
|
|
1210
|
-
|
|
1376
|
+
if (ctx.locale && typeof ctx.locale.register === 'function') {
|
|
1377
|
+
if (typeof ctx.effect === 'function') {
|
|
1378
|
+
ctx.effect(() => {
|
|
1379
|
+
const undo = registerDictionaries()
|
|
1380
|
+
return () => {
|
|
1381
|
+
try {
|
|
1382
|
+
if (typeof undo === 'function') undo()
|
|
1383
|
+
} catch (e) {
|
|
1384
|
+
if (ctx?.logger?.debug) ctx.logger.debug('[dsh-cost-meter] locale undo error: ' + (e?.message || e))
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
}, 'dsh-cost-meter: locale dictionaries')
|
|
1388
|
+
} else {
|
|
1389
|
+
registerDictionaries()
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
if (typeof ctx.effect === 'function') {
|
|
1394
|
+
ctx.effect(
|
|
1395
|
+
() => refreshMirrorUntilVisible(ctx),
|
|
1396
|
+
'dsh-cost-meter: refresh settings mirror',
|
|
1397
|
+
)
|
|
1398
|
+
} else {
|
|
1399
|
+
refreshMirrorUntilVisible(ctx)
|
|
1400
|
+
}
|
|
1211
1401
|
|
|
1212
1402
|
if (ctx.slots && typeof ctx.slots.inject === 'function') {
|
|
1213
1403
|
ctx.slots.inject('conversation.session.header.utilities', () =>
|
|
@@ -1217,6 +1407,14 @@ window.__ModuleLoader__.load({
|
|
|
1217
1407
|
),
|
|
1218
1408
|
)
|
|
1219
1409
|
|
|
1410
|
+
// Row seat first (the seat the current core renders), legacy seat kept below.
|
|
1411
|
+
ctx.slots.inject('plugins.row.config', () =>
|
|
1412
|
+
ctx.slots.register(
|
|
1413
|
+
{ name: 'plugins.row.config', key: ROW_CONFIG_KEY, locale: NS, order: 20, inject: () => ({ ctx }) },
|
|
1414
|
+
(p) => React.createElement(ErrorBoundary, null, React.createElement(CostMeterCard, { ...p, ctx: (p && p.ctx) || ctx })),
|
|
1415
|
+
),
|
|
1416
|
+
)
|
|
1417
|
+
|
|
1220
1418
|
ctx.slots.inject('settings.plugin.item', () =>
|
|
1221
1419
|
ctx.slots.register(
|
|
1222
1420
|
{ name: 'settings.plugin.item', key: NS, locale: NS, order: 20, inject: () => ({ ctx }) },
|
package/lib/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { registerPluginUpdater } from './updater.js'
|
|
1
2
|
// dsh-cost-meter — host half.
|
|
2
3
|
//
|
|
3
4
|
// The harness prices nothing on purpose: pi-ai carries a `cost` field but
|
|
@@ -20,12 +21,38 @@ import { dirname, join } from 'node:path'
|
|
|
20
21
|
import z from '@deepseek-ai/schemastery'
|
|
21
22
|
import { z as zod } from 'zod'
|
|
22
23
|
|
|
23
|
-
export const name = 'dsh-cost-meter'
|
|
24
|
+
export const name = '@goodandready/dsh-cost-meter'
|
|
24
25
|
export const inject = ['webServer']
|
|
25
26
|
|
|
26
27
|
const NS = 'dsh-cost-meter'
|
|
27
28
|
const STATE_PATH = '/dsh-cost-meter/state'
|
|
28
29
|
const REFRESH_PATH = '/dsh-cost-meter/refresh'
|
|
30
|
+
export function isLoopback(value) {
|
|
31
|
+
const address = value?.toLowerCase().replace(/^\[|\]$/g, '')
|
|
32
|
+
return address === 'localhost' || address === 'localhost.' || address === '::1'
|
|
33
|
+
|| address?.startsWith('127.') === true
|
|
34
|
+
|| address?.startsWith('::ffff:127.') === true
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function isTrustedCaller(request) {
|
|
38
|
+
const remote = request?.socket?.remoteAddress || request?.connection?.remoteAddress
|
|
39
|
+
if (isLoopback(remote)) return true
|
|
40
|
+
const site = request?.headers?.['sec-fetch-site']
|
|
41
|
+
if (site === 'same-origin' || site === 'same-site') return true
|
|
42
|
+
if (site === 'cross-site') return false
|
|
43
|
+
const origin = request?.headers?.['origin']
|
|
44
|
+
const host = request?.headers?.['host']
|
|
45
|
+
if (origin && host) {
|
|
46
|
+
try {
|
|
47
|
+
const url = new URL(origin)
|
|
48
|
+
if (url.host === host) return true
|
|
49
|
+
} catch (e) {
|
|
50
|
+
/* invalid url */
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
|
|
29
56
|
const CATALOG_URL = 'https://openrouter.ai/api/v1/models'
|
|
30
57
|
const CACHE_FILE = join(process.env.DSH_HOME || join(homedir(), '.dsh'), 'storages', 'dsh-cost-meter-catalog.json')
|
|
31
58
|
|
|
@@ -532,6 +559,11 @@ export function apply(ctx, config) {
|
|
|
532
559
|
res.end(JSON.stringify({ error: 'method not allowed' }))
|
|
533
560
|
return
|
|
534
561
|
}
|
|
562
|
+
if (!isTrustedCaller(req)) {
|
|
563
|
+
res.writeHead(403, { 'Content-Type': 'application/json' })
|
|
564
|
+
res.end(JSON.stringify({ error: 'forbidden source origin' }))
|
|
565
|
+
return
|
|
566
|
+
}
|
|
535
567
|
const now = Date.now()
|
|
536
568
|
const COOLDOWN_MS = 30000
|
|
537
569
|
if (now - lastManualRefreshAt < COOLDOWN_MS) {
|
|
@@ -627,4 +659,14 @@ export function apply(ctx, config) {
|
|
|
627
659
|
}),
|
|
628
660
|
'dsh-cost-meter: state route',
|
|
629
661
|
)
|
|
662
|
+
|
|
663
|
+
// Route 3: /dsh-cost-meter/update (in-app one-click updater)
|
|
664
|
+
if (ctx.webServer?.register) {
|
|
665
|
+
ctx.effect(() => registerPluginUpdater(ctx, {
|
|
666
|
+
endpoint: '/dsh-cost-meter/update',
|
|
667
|
+
packageName: name,
|
|
668
|
+
manifestUrl: new URL('../package.json', import.meta.url),
|
|
669
|
+
}), 'dsh-cost-meter: updater')
|
|
670
|
+
}
|
|
671
|
+
|
|
630
672
|
}
|