@goodandready/dsh-cost-meter 0.8.2 → 0.8.3
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 +219 -40
- 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
|
@@ -13,6 +13,7 @@ window.__ModuleLoader__.load({
|
|
|
13
13
|
const NS = 'dsh-cost-meter'
|
|
14
14
|
const STATE_PATH = '/dsh-cost-meter/state'
|
|
15
15
|
const REFRESH_PATH = '/dsh-cost-meter/refresh'
|
|
16
|
+
const UPDATE_PATH = '/dsh-cost-meter/update'
|
|
16
17
|
const DAY = 1440
|
|
17
18
|
|
|
18
19
|
// ----------------------------------------------------------- localization
|
|
@@ -22,6 +23,13 @@ window.__ModuleLoader__.load({
|
|
|
22
23
|
subtitle: 'Model rates, peak discount windows and session spend',
|
|
23
24
|
'header.title': 'Cost Meter',
|
|
24
25
|
'header.sub': 'Token spend monitoring, automatic tariff resolution and discount windows',
|
|
26
|
+
'update.checking': 'Checking for updates…',
|
|
27
|
+
'update.up_to_date': 'Up to date',
|
|
28
|
+
'update.available': 'Update available: v{latestVersion} (current: v{currentVersion})',
|
|
29
|
+
'update.btn': 'Update Now',
|
|
30
|
+
'update.updating': 'Updating…',
|
|
31
|
+
'update.done': 'Successfully updated to v{version}! Please restart DSH.',
|
|
32
|
+
'update.failed': 'Update failed: {error}',
|
|
25
33
|
'badge.catalog_ok': 'Catalog: {count} models',
|
|
26
34
|
'badge.catalog_empty': 'Catalog: loading…',
|
|
27
35
|
'badge.catalog_off': 'Catalog: disabled',
|
|
@@ -93,6 +101,13 @@ window.__ModuleLoader__.load({
|
|
|
93
101
|
subtitle: '模型费率、高峰优惠窗口与会话消费统计',
|
|
94
102
|
'header.title': '成本计量器 (Cost Meter)',
|
|
95
103
|
'header.sub': 'Token 消费监控、费率自动匹配与折扣窗口管理',
|
|
104
|
+
'update.checking': '正在检查更新…',
|
|
105
|
+
'update.up_to_date': '已是最新版本',
|
|
106
|
+
'update.available': '发现新版本 v{latestVersion}(当前版本 v{currentVersion})',
|
|
107
|
+
'update.btn': '立即更新',
|
|
108
|
+
'update.updating': '正在更新…',
|
|
109
|
+
'update.done': '更新成功至 v{version}!请重启 DSH 服务生效。',
|
|
110
|
+
'update.failed': '更新失败:{error}',
|
|
96
111
|
'badge.catalog_ok': '目录:{count} 个模型',
|
|
97
112
|
'badge.catalog_empty': '目录:加载中…',
|
|
98
113
|
'badge.catalog_off': '目录:已禁用',
|
|
@@ -200,19 +215,22 @@ window.__ModuleLoader__.load({
|
|
|
200
215
|
.dcm-grid-2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px; }
|
|
201
216
|
.dcm-field { display: flex; flex-direction: column; gap: 6px; }
|
|
202
217
|
.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
|
|
218
|
+
.dcm-check { width: 16px; height: 16px; accent-color: var(--dsw-alias-state-brand-primary); cursor: pointer; }
|
|
204
219
|
.dcm-label { color: var(--dsw-alias-label-primary); font-size: 13px; font-weight: 500; }
|
|
205
220
|
.dcm-hint { color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 1.4; }
|
|
206
221
|
|
|
207
222
|
.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
|
|
223
|
+
.dcm-input:focus { outline: none; border-color: var(--dsw-alias-state-brand-primary); }
|
|
224
|
+
.dcm-input-err { border-color: var(--dsw-alias-state-error-primary); }
|
|
225
|
+
|
|
226
|
+
.dcm-chevron { display: inline-flex; align-items: center; justify-content: center; transition: transform .16s ease; color: var(--dsw-alias-label-tertiary); }
|
|
227
|
+
.dcm-chevron-open { transform: rotate(180deg); }
|
|
210
228
|
|
|
211
229
|
.dcm-badges { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-top: 4px; }
|
|
212
230
|
.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:
|
|
231
|
+
.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); }
|
|
232
|
+
.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); }
|
|
233
|
+
.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
234
|
|
|
217
235
|
.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
236
|
.dcm-btn:hover:not(:disabled) { background: var(--dsw-alias-bg-layer-4, var(--dsw-alias-bg-layer-3)); }
|
|
@@ -222,10 +240,10 @@ window.__ModuleLoader__.load({
|
|
|
222
240
|
.dcm-btn-sm { padding: 4px 10px; font-size: 12px; border-radius: 6px; }
|
|
223
241
|
|
|
224
242
|
.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:
|
|
243
|
+
.dcm-alert-ok { background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 10%, transparent); color: var(--dsw-alias-state-success-primary); }
|
|
244
|
+
.dcm-alert-err { background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent); color: var(--dsw-alias-state-error-primary); }
|
|
245
|
+
.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; }
|
|
246
|
+
.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
247
|
|
|
230
248
|
.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
249
|
.dcm-stat-val { font-size: 14px; font-weight: 600; color: var(--dsw-alias-label-primary); font-variant-numeric: tabular-nums; }
|
|
@@ -234,13 +252,13 @@ window.__ModuleLoader__.load({
|
|
|
234
252
|
.dcm-foot { display: flex; justify-content: flex-end; align-items: center; gap: 10px; padding-top: 6px; }
|
|
235
253
|
|
|
236
254
|
/* 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
|
|
255
|
+
.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; }
|
|
256
|
+
.dcm-chip:hover { border-color: var(--dsw-alias-border-l3, var(--dsw-alias-label-tertiary)); background: var(--dsw-alias-bg-layer-3); }
|
|
239
257
|
.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
|
|
258
|
+
.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); }
|
|
259
|
+
.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); }
|
|
260
|
+
.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; }
|
|
261
|
+
.dcm-dot-idle { background: var(--dsw-alias-label-tertiary); }
|
|
244
262
|
|
|
245
263
|
.dcm-chip-timer { opacity: 0.72; font-size: 11px; padding-left: 2px; border-left: 1px solid var(--dsw-alias-border-l2); }
|
|
246
264
|
@media (max-width: 680px) {
|
|
@@ -249,7 +267,7 @@ window.__ModuleLoader__.load({
|
|
|
249
267
|
|
|
250
268
|
@keyframes dcm-pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.4; transform: scale(0.85); } }
|
|
251
269
|
|
|
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
|
|
270
|
+
.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
271
|
.dcm-panel-title { font-weight: 600; padding-bottom: 2px; font-size: 13.5px; display: flex; justify-content: space-between; align-items: baseline; }
|
|
254
272
|
.dcm-panel-note { color: var(--dsw-alias-label-tertiary); font-size: 12px; }
|
|
255
273
|
.dcm-hr { height: 1px; background: var(--dsw-alias-border-l2); margin: 10px -16px; border: 0; }
|
|
@@ -264,7 +282,7 @@ window.__ModuleLoader__.load({
|
|
|
264
282
|
.dcm-cost { text-align: right; padding: 3.5px 0; font-weight: 500; color: var(--dsw-alias-label-primary); }
|
|
265
283
|
.dcm-section-lbl { color: var(--dsw-alias-label-tertiary); padding-bottom: 4px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
266
284
|
.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
|
|
285
|
+
.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
286
|
.dcm-accordion-btn:hover { text-decoration: underline; }
|
|
269
287
|
`
|
|
270
288
|
|
|
@@ -318,51 +336,83 @@ window.__ModuleLoader__.load({
|
|
|
318
336
|
}
|
|
319
337
|
const ErrorBoundary = createErrorBoundary()
|
|
320
338
|
|
|
339
|
+
function getSettingsScope(ctx) {
|
|
340
|
+
return (ctx && typeof ctx.get === 'function' ? ctx.get('settingsScope') : null) || (ctx && ctx.settingsScope) || (ctx && ctx.services && ctx.services.settingsScope) || null
|
|
341
|
+
}
|
|
342
|
+
|
|
321
343
|
function refreshMirrorUntilVisible(ctx) {
|
|
344
|
+
const s = getSettingsScope(ctx)
|
|
322
345
|
const visible = () => {
|
|
323
346
|
try {
|
|
324
|
-
const s = (ctx?.get && ctx.get('lanSettings')) || (ctx?.get && ctx.get('settingsScope')) || ctx?.settingsScope
|
|
325
347
|
const view = s?.describe?.()?.getSnapshot?.()?.view
|
|
326
348
|
return !!view && Array.isArray(view.namespaces) && view.namespaces.some((row) => row.ns === NS)
|
|
327
|
-
} catch (
|
|
349
|
+
} catch (e) {
|
|
350
|
+
if (ctx?.logger?.debug) ctx.logger.debug('[dsh-cost-meter] mirror check error: ' + (e?.message || e))
|
|
328
351
|
return false
|
|
329
352
|
}
|
|
330
353
|
}
|
|
331
|
-
if (visible()) return () => {}
|
|
354
|
+
if (!s || visible()) return () => {}
|
|
332
355
|
let tries = 0
|
|
333
356
|
const timer = setInterval(() => {
|
|
334
|
-
if (visible() || tries >= 15) {
|
|
357
|
+
if (visible() || tries >= 15) {
|
|
358
|
+
if (!visible() && tries >= 15 && ctx?.logger?.warn) {
|
|
359
|
+
ctx.logger.warn('[dsh-cost-meter] settings mirror did not appear after 15 attempts')
|
|
360
|
+
}
|
|
361
|
+
clearInterval(timer)
|
|
362
|
+
return
|
|
363
|
+
}
|
|
335
364
|
tries += 1
|
|
336
365
|
try {
|
|
337
|
-
const s = (ctx?.get && ctx.get('lanSettings')) || (ctx?.get && ctx.get('settingsScope')) || ctx?.settingsScope
|
|
338
366
|
s?.describe?.()?.load?.()
|
|
339
|
-
} catch (
|
|
367
|
+
} catch (e) {
|
|
368
|
+
if (ctx?.logger?.debug) ctx.logger.debug('[dsh-cost-meter] settings load error: ' + (e?.message || e))
|
|
369
|
+
}
|
|
340
370
|
}, 1000)
|
|
341
371
|
return () => clearInterval(timer)
|
|
342
372
|
}
|
|
343
373
|
|
|
344
|
-
|
|
374
|
+
let ChevronIcon = null
|
|
375
|
+
try {
|
|
376
|
+
const primitives = require('@deepseek-ai/dsh-client-ui-primitives')
|
|
377
|
+
ChevronIcon = primitives && (primitives.IconChevronDownOutline14 || primitives.IconChevronDownOutline)
|
|
378
|
+
} catch (e) {
|
|
379
|
+
ChevronIcon = null
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function FallbackChevron(props) {
|
|
345
383
|
return React.createElement(
|
|
346
384
|
'svg',
|
|
347
385
|
{
|
|
348
|
-
width:
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
color: 'var(--dsw-alias-label-secondary)',
|
|
354
|
-
},
|
|
386
|
+
width: 14,
|
|
387
|
+
height: 14,
|
|
388
|
+
viewBox: '0 0 14 14',
|
|
389
|
+
fill: 'none',
|
|
390
|
+
'aria-hidden': true,
|
|
355
391
|
},
|
|
356
392
|
React.createElement('path', {
|
|
357
|
-
d: '
|
|
393
|
+
d: 'M3.5 5.25L7 8.75L10.5 5.25',
|
|
358
394
|
stroke: 'currentColor',
|
|
359
|
-
strokeWidth: 1.
|
|
395
|
+
strokeWidth: 1.5,
|
|
360
396
|
strokeLinecap: 'round',
|
|
361
397
|
strokeLinejoin: 'round',
|
|
362
398
|
}),
|
|
363
399
|
)
|
|
364
400
|
}
|
|
365
401
|
|
|
402
|
+
function Chevron(props) {
|
|
403
|
+
const icon = ChevronIcon
|
|
404
|
+
? React.createElement(ChevronIcon, { 'aria-hidden': true })
|
|
405
|
+
: React.createElement(FallbackChevron, null)
|
|
406
|
+
return React.createElement(
|
|
407
|
+
'span',
|
|
408
|
+
{
|
|
409
|
+
className: 'dcm-chevron' + (props.open ? ' dcm-chevron-open' : ''),
|
|
410
|
+
style: { marginLeft: 'auto' },
|
|
411
|
+
},
|
|
412
|
+
icon,
|
|
413
|
+
)
|
|
414
|
+
}
|
|
415
|
+
|
|
366
416
|
function isValidTimeZone(tz) {
|
|
367
417
|
if (!tz || typeof tz !== 'string') return false
|
|
368
418
|
try {
|
|
@@ -400,7 +450,72 @@ window.__ModuleLoader__.load({
|
|
|
400
450
|
const [syncing, setSyncing] = React.useState(false)
|
|
401
451
|
const [syncMsg, setSyncMsg] = React.useState('')
|
|
402
452
|
|
|
403
|
-
|
|
453
|
+
// Plugin in-app updater state
|
|
454
|
+
const [updateState, setUpdateState] = React.useState({
|
|
455
|
+
checking: false,
|
|
456
|
+
updating: false,
|
|
457
|
+
currentVersion: '0.8.3',
|
|
458
|
+
latestVersion: '',
|
|
459
|
+
updateAvailable: false,
|
|
460
|
+
canAutoUpdate: true,
|
|
461
|
+
error: '',
|
|
462
|
+
notice: '',
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
const checkUpdate = React.useCallback(async () => {
|
|
466
|
+
setUpdateState((s) => ({ ...s, checking: true, error: '' }))
|
|
467
|
+
try {
|
|
468
|
+
const res = await fetch(UPDATE_PATH)
|
|
469
|
+
if (!res.ok) throw new Error('HTTP ' + res.status)
|
|
470
|
+
const data = await res.json().catch(() => ({}))
|
|
471
|
+
setUpdateState((s) => ({
|
|
472
|
+
...s,
|
|
473
|
+
checking: false,
|
|
474
|
+
currentVersion: data.currentVersion || s.currentVersion,
|
|
475
|
+
latestVersion: data.latestVersion || '',
|
|
476
|
+
updateAvailable: Boolean(data.updateAvailable),
|
|
477
|
+
canAutoUpdate: data.canAutoUpdate !== false,
|
|
478
|
+
}))
|
|
479
|
+
} catch (e) {
|
|
480
|
+
setUpdateState((s) => ({ ...s, checking: false }))
|
|
481
|
+
}
|
|
482
|
+
}, [])
|
|
483
|
+
|
|
484
|
+
React.useEffect(() => {
|
|
485
|
+
if (open) checkUpdate()
|
|
486
|
+
}, [open, checkUpdate])
|
|
487
|
+
|
|
488
|
+
async function handleTriggerUpdate() {
|
|
489
|
+
if (updateState.updating) return
|
|
490
|
+
setUpdateState((s) => ({ ...s, updating: true, error: '', notice: '' }))
|
|
491
|
+
try {
|
|
492
|
+
const res = await fetch(UPDATE_PATH, {
|
|
493
|
+
method: 'POST',
|
|
494
|
+
headers: { 'x-dsh-plugin-update': '1' },
|
|
495
|
+
})
|
|
496
|
+
const data = await res.json().catch(() => ({}))
|
|
497
|
+
if (!res.ok || data.ok === false || data.error) {
|
|
498
|
+
throw new Error(data.error || ('HTTP ' + res.status))
|
|
499
|
+
}
|
|
500
|
+
const newVer = data.updatedVersion || updateState.latestVersion || updateState.currentVersion
|
|
501
|
+
setUpdateState((s) => ({
|
|
502
|
+
...s,
|
|
503
|
+
updating: false,
|
|
504
|
+
updateAvailable: false,
|
|
505
|
+
currentVersion: newVer,
|
|
506
|
+
notice: t('update.done', { version: newVer }),
|
|
507
|
+
}))
|
|
508
|
+
setTimeout(() => checkUpdate(), 2000)
|
|
509
|
+
} catch (err) {
|
|
510
|
+
setUpdateState((s) => ({
|
|
511
|
+
...s,
|
|
512
|
+
updating: false,
|
|
513
|
+
error: t('update.failed', { error: String(err.message || err) }),
|
|
514
|
+
}))
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const settingsScope = getSettingsScope(ctx)
|
|
404
519
|
const scope = React.useMemo(
|
|
405
520
|
() => (settingsScope ? settingsScope.bind({ namespace: NS }) : undefined),
|
|
406
521
|
[settingsScope],
|
|
@@ -566,6 +681,38 @@ window.__ModuleLoader__.load({
|
|
|
566
681
|
: status !== 'ready'
|
|
567
682
|
? React.createElement('div', { className: 'dcm-alert dcm-alert-err' }, t('settings.unavailable'))
|
|
568
683
|
: React.createElement(React.Fragment, null,
|
|
684
|
+
// In-app Update Bar
|
|
685
|
+
React.createElement('div', {
|
|
686
|
+
className: 'dcm-section-card',
|
|
687
|
+
style: { padding: '10px 14px', flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' },
|
|
688
|
+
},
|
|
689
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, fontSize: 13 } },
|
|
690
|
+
React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)', fontWeight: 500 } },
|
|
691
|
+
'v' + updateState.currentVersion
|
|
692
|
+
),
|
|
693
|
+
updateState.checking
|
|
694
|
+
? React.createElement('span', { style: { color: 'var(--dsw-alias-label-tertiary)', fontSize: 12 } }, t('update.checking'))
|
|
695
|
+
: updateState.updateAvailable
|
|
696
|
+
? React.createElement('span', { className: 'dcm-badge dcm-badge-warn' },
|
|
697
|
+
t('update.available', { latestVersion: updateState.latestVersion, currentVersion: updateState.currentVersion })
|
|
698
|
+
)
|
|
699
|
+
: React.createElement('span', { className: 'dcm-badge dcm-badge-ok' },
|
|
700
|
+
'✓ ' + t('update.up_to_date')
|
|
701
|
+
)
|
|
702
|
+
),
|
|
703
|
+
updateState.updateAvailable
|
|
704
|
+
? React.createElement('button', {
|
|
705
|
+
type: 'button',
|
|
706
|
+
className: 'dcm-btn dcm-btn-primary',
|
|
707
|
+
disabled: updateState.updating,
|
|
708
|
+
onClick: handleTriggerUpdate,
|
|
709
|
+
style: { padding: '4px 10px', fontSize: 12 },
|
|
710
|
+
}, updateState.updating ? t('update.updating') : t('update.btn'))
|
|
711
|
+
: null
|
|
712
|
+
),
|
|
713
|
+
updateState.notice ? React.createElement('div', { className: 'dcm-alert dcm-alert-ok' }, '✓ ' + updateState.notice) : null,
|
|
714
|
+
updateState.error ? React.createElement('div', { className: 'dcm-alert dcm-alert-err' }, updateState.error) : null,
|
|
715
|
+
|
|
569
716
|
// Section 1: General Parameters
|
|
570
717
|
React.createElement('div', { className: 'dcm-section-card' },
|
|
571
718
|
React.createElement('div', { className: 'dcm-section-title' }, t('section.general')),
|
|
@@ -855,7 +1002,7 @@ window.__ModuleLoader__.load({
|
|
|
855
1002
|
|
|
856
1003
|
React.useEffect(() => { ensureStyles() }, [])
|
|
857
1004
|
|
|
858
|
-
const settingsScope = (ctx
|
|
1005
|
+
const settingsScope = getSettingsScope(ctx)
|
|
859
1006
|
const settingsSnap = React.useSyncExternalStore
|
|
860
1007
|
? React.useSyncExternalStore(
|
|
861
1008
|
(cb) => (settingsScope && typeof settingsScope.subscribe === 'function' ? settingsScope.subscribe(cb) : () => {}),
|
|
@@ -960,7 +1107,7 @@ window.__ModuleLoader__.load({
|
|
|
960
1107
|
? 'dcm-dot dcm-dot-err'
|
|
961
1108
|
: (flat ? 'dcm-dot dcm-dot-idle' : (cheapNow ? 'dcm-dot dcm-dot-ok' : 'dcm-dot dcm-dot-warn'))
|
|
962
1109
|
|
|
963
|
-
const activeCell = { textAlign: 'right', padding: '3px 6px', borderRadius: 4, background: '
|
|
1110
|
+
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
1111
|
|
|
965
1112
|
const rateRow = (label, key) => flat
|
|
966
1113
|
? [
|
|
@@ -1203,11 +1350,43 @@ window.__ModuleLoader__.load({
|
|
|
1203
1350
|
}
|
|
1204
1351
|
|
|
1205
1352
|
function apply(ctx) {
|
|
1206
|
-
|
|
1207
|
-
try {
|
|
1353
|
+
const registerDictionaries = () => {
|
|
1354
|
+
try {
|
|
1355
|
+
// ctx.locale.register(NS, { en, zh }) is canonical form in DSH
|
|
1356
|
+
return ctx.locale.register(NS, { en, zh })
|
|
1357
|
+
} catch (err) {
|
|
1358
|
+
if (ctx?.logger?.warn) {
|
|
1359
|
+
ctx.logger.warn(`[dsh-cost-meter] locale register failed: ${err?.message || err}`)
|
|
1360
|
+
}
|
|
1361
|
+
return () => {}
|
|
1362
|
+
}
|
|
1208
1363
|
}
|
|
1209
1364
|
|
|
1210
|
-
|
|
1365
|
+
if (ctx.locale && typeof ctx.locale.register === 'function') {
|
|
1366
|
+
if (typeof ctx.effect === 'function') {
|
|
1367
|
+
ctx.effect(() => {
|
|
1368
|
+
const undo = registerDictionaries()
|
|
1369
|
+
return () => {
|
|
1370
|
+
try {
|
|
1371
|
+
if (typeof undo === 'function') undo()
|
|
1372
|
+
} catch (e) {
|
|
1373
|
+
if (ctx?.logger?.debug) ctx.logger.debug('[dsh-cost-meter] locale undo error: ' + (e?.message || e))
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
}, 'dsh-cost-meter: locale dictionaries')
|
|
1377
|
+
} else {
|
|
1378
|
+
registerDictionaries()
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
if (typeof ctx.effect === 'function') {
|
|
1383
|
+
ctx.effect(
|
|
1384
|
+
() => refreshMirrorUntilVisible(ctx),
|
|
1385
|
+
'dsh-cost-meter: refresh settings mirror',
|
|
1386
|
+
)
|
|
1387
|
+
} else {
|
|
1388
|
+
refreshMirrorUntilVisible(ctx)
|
|
1389
|
+
}
|
|
1211
1390
|
|
|
1212
1391
|
if (ctx.slots && typeof ctx.slots.inject === 'function') {
|
|
1213
1392
|
ctx.slots.inject('conversation.session.header.utilities', () =>
|
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
|
}
|