@ponydev/dsh-model-usage 1.1.0 → 1.2.0
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/CHANGELOG.md +10 -0
- package/README.md +1 -0
- package/lib/client.js +83 -1
- package/lib/index.js +57 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.2.0 — 2026-10-20
|
|
4
|
+
|
|
5
|
+
### 新增
|
|
6
|
+
- **检查更新**:设置 → 模型用量 标题右侧显示当前版本 `vX.Y.Z`,点击「检查更新」查询 npm registry 最新版本;有新版本时显示橙点与更新提示
|
|
7
|
+
- 弹窗展示 运行版本 / 最新版本 / 一键复制手工更新命令(`dsh plugin --profile web add <pkg>@<ver> --registry=...`)
|
|
8
|
+
- RPC `update-check`(10 分钟缓存;registry 固定 npmjs,不返回凭据)
|
|
9
|
+
|
|
10
|
+
### 说明
|
|
11
|
+
- 自动更新需 DSH profile 包管理器介入,本插件只提供「检查 + 命令复制」,执行后重启 DSH Web 生效
|
|
12
|
+
|
|
3
13
|
## 1.1.0 — 2026-10-20
|
|
4
14
|
|
|
5
15
|
### 新增
|
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
- 官方 Logo:DeepSeek / GLM(Z.ai) / Kimi / OpenRouter / MiniMax / OpenCode / OpenAI / Anthropic / Gemini / xAI / Qwen / Mistral / Groq / GitHub Copilot 等 25+
|
|
10
10
|
- 设置 → 模型用量:KPI、趋势图、供应商/模型聚合、请求日志、CSV 导出
|
|
11
11
|
- 设置 → 模型:页面底部显示各供应商额度
|
|
12
|
+
- 版本检查:设置 → 模型用量 右上角「检查更新」(对比 npm 最新版,可复制更新命令)
|
|
12
13
|
- 只用**真实数据**:额度来自服务商接口,用量来自本机会话日志(逐 token 一致),不做金额估算
|
|
13
14
|
|
|
14
15
|

|
package/lib/client.js
CHANGED
|
@@ -44,6 +44,10 @@ window.__ModuleLoader__.load({
|
|
|
44
44
|
shortAccount: '账号', remainCompact: '剩 {n}%', remainPrefix: '剩 ', remainSuffix: '', statsError: '统计加载失败', retry: '重试', emptyData: '暂无数据', emptyUsage: '今天还没有调用记录',
|
|
45
45
|
kpiRequests: '模型调用', kpiTokens: '消耗 Tokens', kpiCacheHit: '缓存命中率',
|
|
46
46
|
limitMcp: '每月 MCP 工具', limitMonthly: '每月限额', remainCalls: '剩余 {n}/{m} 次',
|
|
47
|
+
versionLabel: '版本', currentVersion: '运行版本', latestVersion: '最新版本', checkUpdate: '检查更新', checkingUpdate: '检查中…', upToDate: '已是最新版本',
|
|
48
|
+
updateAvailableHint: '发现新版本 {v}', updateFailed: '检查失败:{msg}',
|
|
49
|
+
manualUpdateTitle: '手工更新', manualUpdateNote: '插件由 DSH profile 管理:在终端执行以下命令,然后重启 DSH Web。',
|
|
50
|
+
copyCommand: '复制命令', copied: '已复制',
|
|
47
51
|
groupCurrent: '当前会话在用', groupSub: '订阅窗口', groupApi: 'API 余额', groupWallet: '账号钱包', groupOther: '其他',
|
|
48
52
|
trendTitle: '使用趋势',
|
|
49
53
|
thProvider: '供应商', thModel: '模型', thRequests: '请求数', thTokens: 'Tokens',
|
|
@@ -80,6 +84,10 @@ window.__ModuleLoader__.load({
|
|
|
80
84
|
shortAccount: 'Account', remainCompact: '{n}% left', remainPrefix: '', remainSuffix: ' left', statsError: 'Failed to load stats', retry: 'Retry', emptyData: 'No data', emptyUsage: 'No calls today yet',
|
|
81
85
|
kpiRequests: 'Model calls', kpiTokens: 'Tokens', kpiCacheHit: 'Cache hit rate',
|
|
82
86
|
limitMcp: 'MCP tools / month', limitMonthly: 'Monthly', remainCalls: '{n}/{m} calls left',
|
|
87
|
+
versionLabel: 'Version', currentVersion: 'Running', latestVersion: 'Latest', checkUpdate: 'Check for updates', checkingUpdate: 'Checking…', upToDate: 'Up to date',
|
|
88
|
+
updateAvailableHint: 'New version {v}', updateFailed: 'Check failed: {msg}',
|
|
89
|
+
manualUpdateTitle: 'Manual update', manualUpdateNote: 'Plugins are managed by the DSH profile: run this in a terminal, then restart DSH Web.',
|
|
90
|
+
copyCommand: 'Copy command', copied: 'Copied',
|
|
83
91
|
groupCurrent: 'In use this session', groupSub: 'Subscription windows', groupApi: 'API balance', groupWallet: 'Account wallet', groupOther: 'Other',
|
|
84
92
|
trendTitle: 'Usage trend',
|
|
85
93
|
thProvider: 'Provider', thModel: 'Model', thRequests: 'Requests', thTokens: 'Tokens',
|
|
@@ -335,6 +343,7 @@ window.__ModuleLoader__.load({
|
|
|
335
343
|
sessionProvider: (sessionId) => unwrap('session-provider', { sessionId }),
|
|
336
344
|
stats: (opts) => unwrap('stats', opts || {}),
|
|
337
345
|
config: () => unwrap('config', {}),
|
|
346
|
+
updateCheck: (force) => unwrap('update-check', { force: !!force }),
|
|
338
347
|
}
|
|
339
348
|
}
|
|
340
349
|
|
|
@@ -1111,6 +1120,77 @@ window.__ModuleLoader__.load({
|
|
|
1111
1120
|
summaryOf({ providers: [p] }, tr))))))
|
|
1112
1121
|
}
|
|
1113
1122
|
|
|
1123
|
+
// ---------------------------------------------------------------- 版本 / 检查更新
|
|
1124
|
+
function VersionBadge({ api, tr }) {
|
|
1125
|
+
const [info, setInfo] = useState(null)
|
|
1126
|
+
const [busy, setBusy] = useState(false)
|
|
1127
|
+
const [open, setOpen] = useState(false)
|
|
1128
|
+
const [copied, setCopied] = useState(false)
|
|
1129
|
+
useEffect(() => {
|
|
1130
|
+
let alive = true
|
|
1131
|
+
api.updateCheck(false).then((v) => { if (alive) setInfo(v) }).catch(() => {})
|
|
1132
|
+
return () => { alive = false }
|
|
1133
|
+
}, [])
|
|
1134
|
+
const check = async () => {
|
|
1135
|
+
setBusy(true)
|
|
1136
|
+
try {
|
|
1137
|
+
setInfo(await api.updateCheck(true))
|
|
1138
|
+
} catch (e) {
|
|
1139
|
+
setInfo({ current: '—', latest: null, error: String((e && e.message) || e), updateAvailable: false })
|
|
1140
|
+
} finally {
|
|
1141
|
+
setBusy(false)
|
|
1142
|
+
setOpen(true)
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
const copy = () => {
|
|
1146
|
+
try {
|
|
1147
|
+
navigator.clipboard.writeText((info && info.command) || '').then(() => {
|
|
1148
|
+
setCopied(true)
|
|
1149
|
+
setTimeout(() => setCopied(false), 1500)
|
|
1150
|
+
})
|
|
1151
|
+
} catch {}
|
|
1152
|
+
}
|
|
1153
|
+
const stat = (label, value) => h('div', { className: 'dmu-mstat' },
|
|
1154
|
+
h('div', { className: 'k' }, label),
|
|
1155
|
+
h('div', { className: 'v' }, value))
|
|
1156
|
+
const cmdStyle = {
|
|
1157
|
+
flex: 1, fontSize: 11, fontFamily: 'var(--dsh-font-mono,monospace)',
|
|
1158
|
+
background: 'var(--dsw-alias-bg-base,#f2f4f7)', padding: '8px 10px',
|
|
1159
|
+
borderRadius: 8, wordBreak: 'break-all', whiteSpace: 'pre-wrap', margin: 0,
|
|
1160
|
+
}
|
|
1161
|
+
const dotColor = info && info.updateAvailable ? 'var(--dsw-alias-state-warn-primary,#f79009)' : 'var(--dsw-alias-label-tertiary,#98a2b3)'
|
|
1162
|
+
return h('span', { className: 'dmu-row', style: { gap: 8 } },
|
|
1163
|
+
info && info.updateAvailable ? h('span', { style: { width: 6, height: 6, borderRadius: 999, background: dotColor } }) : null,
|
|
1164
|
+
h('span', { style: { fontSize: 11, color: 'var(--dsw-alias-label-tertiary,#98a2b3)' } },
|
|
1165
|
+
'v' + ((info && info.current) || '—')),
|
|
1166
|
+
h(P.Button, { variant: 'ghost', size: 'sm', onClick: check, disabled: busy },
|
|
1167
|
+
busy ? tr('checkingUpdate') : tr('checkUpdate')),
|
|
1168
|
+
open && info && h(P.Modal, {
|
|
1169
|
+
open: true,
|
|
1170
|
+
onClose: () => setOpen(false),
|
|
1171
|
+
title: tr('checkUpdate'),
|
|
1172
|
+
closeLabel: tr('close'),
|
|
1173
|
+
footer: h('div', { className: 'dmu-row', style: { justifyContent: 'flex-end' } },
|
|
1174
|
+
h(P.Button, { variant: 'primary', size: 'sm', onClick: () => setOpen(false) }, tr('close'))),
|
|
1175
|
+
},
|
|
1176
|
+
h('div', { style: { display: 'flex', flexDirection: 'column', gap: 10 } },
|
|
1177
|
+
h('div', { className: 'dmu-row', style: { gap: 28 } },
|
|
1178
|
+
stat(tr('currentVersion'), info.current ? 'v' + info.current : '—'),
|
|
1179
|
+
stat(tr('latestVersion'), info.latest ? 'v' + info.latest : '—')),
|
|
1180
|
+
info.error
|
|
1181
|
+
? h('div', { className: 'dmu-warnline', style: { margin: 0 } }, tr('updateFailed', { msg: info.error }))
|
|
1182
|
+
: info.updateAvailable
|
|
1183
|
+
? h('div', { className: 'dmu-warnline', style: { margin: 0 } }, tr('updateAvailableHint', { v: 'v' + info.latest }))
|
|
1184
|
+
: h('div', { className: 'dmu-mutedline', style: { margin: 0 } }, tr('upToDate')),
|
|
1185
|
+
h('div', null,
|
|
1186
|
+
h('div', { style: { fontSize: 11, color: 'var(--dsw-alias-label-tertiary,#98a2b3)', marginBottom: 4 } }, tr('manualUpdateTitle')),
|
|
1187
|
+
h('div', { style: { fontSize: 11, color: 'var(--dsw-alias-label-secondary,#637182)', marginBottom: 6 } }, tr('manualUpdateNote')),
|
|
1188
|
+
h('div', { style: { display: 'flex', gap: 8, alignItems: 'flex-start' } },
|
|
1189
|
+
h('code', { style: cmdStyle }, info.command || ''),
|
|
1190
|
+
h(P.Button, { variant: 'outline', size: 'sm', onClick: copy },
|
|
1191
|
+
copied ? tr('copied') : tr('copyCommand')))))))
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1114
1194
|
// ---------------------------------------------------------------- 入口 2:设置页
|
|
1115
1195
|
function SettingsEntry(props) {
|
|
1116
1196
|
const { api } = props
|
|
@@ -1122,7 +1202,9 @@ window.__ModuleLoader__.load({
|
|
|
1122
1202
|
{ value: 'log', label: tr('tabLog'), id: 'dmu-tab-log', panelId: 'dmu-panel-log' },
|
|
1123
1203
|
]
|
|
1124
1204
|
return h('div', { className: 'dmu-settings' },
|
|
1125
|
-
h('
|
|
1205
|
+
h('div', { className: 'dmu-row', style: { gap: 10, marginBottom: 10 } },
|
|
1206
|
+
h('h2', { style: { margin: 0 } }, tr('nav')),
|
|
1207
|
+
h('span', { style: { marginLeft: 'auto' } }, h(VersionBadge, { api, tr }))),
|
|
1126
1208
|
h(P.SegmentedTabs, {
|
|
1127
1209
|
items: tabs,
|
|
1128
1210
|
value: tab,
|
package/lib/index.js
CHANGED
|
@@ -5,14 +5,67 @@
|
|
|
5
5
|
// (不走 connection.rpc.handle:其内部对 connection fiber 的 webServer 访问
|
|
6
6
|
// 在 web 组合下会抛 without inject —— 参见 dsh-api-gateway 的同类做法)
|
|
7
7
|
// - 凭据只在 Host 进程内存中出现;TTL 缓存 + 单飞 + 指数退避
|
|
8
|
-
import { adapterFor, safeMsg } from './adapters.js'
|
|
8
|
+
import { adapterFor, fetchT, safeMsg } from './adapters.js'
|
|
9
9
|
import { createLedger } from './ledger.js'
|
|
10
10
|
import { dirname, join } from 'node:path'
|
|
11
11
|
import { fileURLToPath } from 'node:url'
|
|
12
12
|
import { readFile } from 'node:fs/promises'
|
|
13
|
+
import { readFileSync } from 'node:fs'
|
|
13
14
|
import { homedir } from 'node:os'
|
|
14
15
|
|
|
15
|
-
const
|
|
16
|
+
const PKG_DIR = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
17
|
+
const ASSETS_DIR = join(PKG_DIR, 'assets', 'logos')
|
|
18
|
+
const REGISTRY = 'https://registry.npmjs.org/'
|
|
19
|
+
const PKG_NAME = '@ponydev/dsh-model-usage'
|
|
20
|
+
|
|
21
|
+
/** 自身版本(读 package.json;失败返回 '0.0.0') */
|
|
22
|
+
let SELF_VERSION = '0.0.0'
|
|
23
|
+
try {
|
|
24
|
+
SELF_VERSION = JSON.parse(readFileSync(join(PKG_DIR, 'package.json'), 'utf8')).version || '0.0.0'
|
|
25
|
+
} catch {}
|
|
26
|
+
|
|
27
|
+
/** 检查 npm 最新版本(10 分钟缓存;force 跳过缓存) */
|
|
28
|
+
let updateCache = { at: 0, data: null }
|
|
29
|
+
async function checkUpdate(force) {
|
|
30
|
+
const now = Date.now()
|
|
31
|
+
if (!force && updateCache.data && now - updateCache.at < 10 * 60 * 1000) return updateCache.data
|
|
32
|
+
const url = REGISTRY + PKG_NAME.replace('/', '%2F') + '/latest'
|
|
33
|
+
let latest = null
|
|
34
|
+
let error = null
|
|
35
|
+
try {
|
|
36
|
+
const res = await fetchT(url, { headers: { accept: 'application/json' } }, 8000)
|
|
37
|
+
if (res.ok) {
|
|
38
|
+
const body = await res.json()
|
|
39
|
+
latest = typeof body.version === 'string' ? body.version : null
|
|
40
|
+
if (!latest) error = 'registry 未返回 version'
|
|
41
|
+
} else {
|
|
42
|
+
error = 'HTTP ' + res.status
|
|
43
|
+
}
|
|
44
|
+
} catch (e) {
|
|
45
|
+
error = safeMsg(e)
|
|
46
|
+
}
|
|
47
|
+
const data = {
|
|
48
|
+
current: SELF_VERSION,
|
|
49
|
+
latest,
|
|
50
|
+
updateAvailable: !!(latest && latest !== SELF_VERSION && cmpVersion(latest, SELF_VERSION) > 0),
|
|
51
|
+
registry: REGISTRY,
|
|
52
|
+
name: PKG_NAME,
|
|
53
|
+
command: 'dsh plugin --profile web add ' + PKG_NAME + (latest ? '@' + latest : '') + ' --registry=' + REGISTRY,
|
|
54
|
+
error,
|
|
55
|
+
checkedAt: now,
|
|
56
|
+
}
|
|
57
|
+
if (latest) updateCache = { at: now, data }
|
|
58
|
+
return data
|
|
59
|
+
}
|
|
60
|
+
function cmpVersion(a, b) {
|
|
61
|
+
const pa = String(a).split(/[.-]/).map((x) => parseInt(x, 10) || 0)
|
|
62
|
+
const pb = String(b).split(/[.-]/).map((x) => parseInt(x, 10) || 0)
|
|
63
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
64
|
+
const x = pa[i] || 0, y = pb[i] || 0
|
|
65
|
+
if (x !== y) return x > y ? 1 : -1
|
|
66
|
+
}
|
|
67
|
+
return 0
|
|
68
|
+
}
|
|
16
69
|
|
|
17
70
|
/** 用户自定义 logo 目录($DSH_HOME/model-usage/logos) */
|
|
18
71
|
function userLogosDir() {
|
|
@@ -176,6 +229,8 @@ export function apply(ctx, rawConfig) {
|
|
|
176
229
|
return ok(publicConfig())
|
|
177
230
|
case 'stats':
|
|
178
231
|
return ok(ledger.stats({ days: payload && payload.days }))
|
|
232
|
+
case 'update-check':
|
|
233
|
+
return ok(await checkUpdate(!!(payload && payload.force)))
|
|
179
234
|
default:
|
|
180
235
|
return fail('model-usage/unknown-endpoint', 'unknown endpoint: ' + String(endpoint))
|
|
181
236
|
}
|