@ponydev/dsh-model-usage 1.1.0 → 1.2.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.2.1 — 2026-10-20
4
+
5
+ ### 变更(UI 对齐 DSH 设置页规范)
6
+ - 版本/检查更新改为**原生设置行**(16px 标签 + 44px 行高 + 细分割线 + 右侧控件)
7
+ - 设置内容去掉重复大标题(section 名称已由设置导航提供)
8
+ - 卡片圆角统一 12px、列表改为无卡片底的 44px 行 + 细线(对齐 通用设置/模型 页)
9
+ - 字号规范化:正文 12px、行标签 16px、卡片标签 12px,去掉 10.5px 等非规范字号
10
+ - 分组标题由 10.5px 大写间距改为 13px/600 常规字重
11
+
12
+ ## 1.2.0 — 2026-10-20
13
+
14
+ ### 新增
15
+ - **检查更新**:设置 → 模型用量 标题右侧显示当前版本 `vX.Y.Z`,点击「检查更新」查询 npm registry 最新版本;有新版本时显示橙点与更新提示
16
+ - 弹窗展示 运行版本 / 最新版本 / 一键复制手工更新命令(`dsh plugin --profile web add <pkg>@<ver> --registry=...`)
17
+ - RPC `update-check`(10 分钟缓存;registry 固定 npmjs,不返回凭据)
18
+
19
+ ### 说明
20
+ - 自动更新需 DSH profile 包管理器介入,本插件只提供「检查 + 命令复制」,执行后重启 DSH Web 生效
21
+
3
22
  ## 1.1.0 — 2026-10-20
4
23
 
5
24
  ### 新增
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
  ![侧边栏额度卡片](https://raw.githubusercontent.com/PonyDevAI/dsh-model-usage/main/docs/screenshots/01-sidebar.png)
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',
@@ -115,7 +123,7 @@ window.__ModuleLoader__.load({
115
123
  .dmu-footcard .metric b{font-weight:650}
116
124
  .dmu-mwin{margin-top:8px}
117
125
  .dmu-mwin:first-child{margin-top:9px}
118
- .dmu-mwin .t{display:flex;font-size:10.5px;color:var(--dsw-alias-label-secondary,#637182);margin-bottom:3px}
126
+ .dmu-mwin .t{display:flex;font-size:11.5px;color:var(--dsw-alias-label-secondary,#637182);margin-bottom:3px}
119
127
  .dmu-mwin .t b{margin-left:auto;color:var(--dsw-alias-label-primary,#191f28);font-variant-numeric:tabular-nums}
120
128
  .dmu-mwin .track{height:6px;border-radius:4px;background:var(--dsw-alias-bg-base,#e4e8ee);overflow:hidden}
121
129
  .dmu-mwin .fill{height:100%;border-radius:4px;transition:width .4s ease}
@@ -142,58 +150,64 @@ window.__ModuleLoader__.load({
142
150
  .dmu-mbody .dmu-pcard:first-child{margin-top:0}
143
151
  .dmu-mbody .dmu-kicker{margin:12px 0 0}
144
152
  .dmu-sumhead{display:flex;align-items:center;gap:8px}
145
- .dmu-sumhead .k{font-size:10.5px;font-weight:700;letter-spacing:1px;color:var(--dsw-alias-label-tertiary,#98a2b3)}
153
+ .dmu-sumhead .k{font-size:12px;font-weight:600;letter-spacing:0;color:var(--dsw-alias-label-tertiary,#98a2b3)}
146
154
  .dmu-summrow{display:flex;align-items:flex-end;gap:26px;margin-top:11px}
147
- .dmu-sumstat .k{font-size:10.5px;color:var(--dsw-alias-label-secondary,#637182)}
155
+ .dmu-sumstat .k{font-size:12px;color:var(--dsw-alias-label-secondary,#637182)}
148
156
  .dmu-sumstat .v{margin-top:3px;font-size:16px;font-weight:700;color:var(--dsw-alias-label-primary,#191f28);font-variant-numeric:tabular-nums;line-height:1}
149
157
  .dmu-spark{display:flex;align-items:flex-end;gap:3px;height:22px;width:120px;margin-left:auto}
150
158
  .dmu-spark i{flex:1;background:var(--dsw-alias-state-business-primary,#4176E6);border-radius:2px 2px 0 0;min-height:2px;display:block;opacity:.85}
151
- .dmu-sumtext{margin-top:9px;font-size:10.5px;color:var(--dsw-alias-label-tertiary,#98a2b3)}
152
- .dmu-kicker{font-size:10.5px;font-weight:700;letter-spacing:1px;color:var(--dsw-alias-label-tertiary,#98a2b3);margin:6px 0 6px}
153
- .dmu-pcard{background:var(--dsw-alias-settings-card-fill,var(--dsw-alias-bg-layer-1,#fff));border:1px solid var(--dsw-alias-settings-card-stroke,var(--dsw-alias-border-l1,#e3e7ed));border-radius:var(--dsw-radius-lg,14px);padding:13px 15px}
159
+ .dmu-sumtext{margin-top:10px;font-size:12px;color:var(--dsw-alias-label-tertiary,#98a2b3)}
160
+ .dmu-kicker{font-size:13px;font-weight:600;letter-spacing:0;color:var(--dsw-alias-label-secondary,#637182);margin:20px 0 8px}
161
+ .dmu-pcard{background:var(--dsw-alias-settings-card-fill,var(--dsw-alias-bg-layer-1,#fff));border:1px solid var(--dsw-alias-settings-card-stroke,var(--dsw-alias-border-l1,#e3e7ed));border-radius:12px;padding:14px 16px}
154
162
  .dmu-pcard.crit{border-top:2px solid var(--dsw-alias-state-error-primary,#f04438)}
155
163
  .dmu-pcard .ph{display:flex;align-items:center;gap:8px;font-size:var(--dsh-content-font-size-secondary,13px);font-weight:600;color:var(--dsw-alias-label-primary,#191f28);white-space:nowrap}
156
164
  .dmu-pcard .ph .dmu-pill{white-space:nowrap}
157
- .dmu-pcard .route{font-size:10.5px;color:var(--dsw-alias-label-tertiary,#98a2b3);font-weight:400;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:26%}
165
+ .dmu-pcard .route{font-size:12px;color:var(--dsw-alias-label-tertiary,#98a2b3);font-weight:400;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:26%}
158
166
  .dmu-big{margin-top:11px;font-size:28px;font-weight:700;letter-spacing:-.5px;line-height:1;color:var(--dsw-alias-label-primary,#191f28);font-variant-numeric:tabular-nums}
159
167
  .dmu-big small{font-size:13px;font-weight:600;color:var(--dsw-alias-label-secondary,#637182);margin-left:3px;letter-spacing:0}
160
- .dmu-cap{margin-top:6px;font-size:11px;color:var(--dsw-alias-label-secondary,#637182)}
168
+ .dmu-cap{margin-top:6px;font-size:12px;color:var(--dsw-alias-label-secondary,#637182)}
161
169
  .dmu-seg{display:flex;height:10px;border-radius:var(--dsw-radius-sm,6px);overflow:hidden;margin-top:12px;background:var(--dsw-alias-bg-base,#e4e8ee)}
162
170
  .dmu-seg span{height:100%}
163
171
  .dmu-legend{display:flex;gap:14px;margin-top:8px;font-size:10.5px;color:var(--dsw-alias-label-secondary,#637182)}
164
172
  .dmu-legend i{width:8px;height:8px;border-radius:3px;display:inline-block;margin-right:5px;vertical-align:-1px}
165
173
  .dmu-pcard .winrow{margin-top:12px;display:flex;flex-direction:column;gap:12px}
166
- .dmu-pcard .win .t{display:flex;align-items:baseline;font-size:11.5px;color:var(--dsw-alias-label-secondary,#637182);margin-bottom:5px}
174
+ .dmu-pcard .win .t{display:flex;align-items:baseline;font-size:12.5px;color:var(--dsw-alias-label-secondary,#637182);margin-bottom:5px}
167
175
  .dmu-pcard .win .t .lb{font-weight:600;color:var(--dsw-alias-label-primary,#191f28);margin-right:8px}
168
176
  .dmu-pcard .win .t b{margin-left:auto;font-size:12.5px;color:var(--dsw-alias-label-primary,#191f28);font-variant-numeric:tabular-nums}
169
177
  .dmu-pcard .win .track{height:10px;border-radius:var(--dsw-radius-sm,6px);background:var(--dsw-alias-bg-base,#e4e8ee);overflow:hidden}
170
178
  .dmu-pcard .win .fill{height:100%;border-radius:var(--dsw-radius-sm,6px);transition:width .4s ease}
171
- .dmu-pcard .win .f{display:flex;margin-top:5px;font-size:10.5px;color:var(--dsw-alias-label-tertiary,#98a2b3)}
179
+ .dmu-pcard .win .f{display:flex;margin-top:6px;font-size:12px;color:var(--dsw-alias-label-tertiary,#98a2b3)}
172
180
  .dmu-pcard .win .f .wh{margin-left:auto;font-weight:600}
173
- .dmu-errline{display:flex;gap:8px;align-items:flex-start;font-size:11px;color:var(--dsw-alias-state-error-primary,#f04438);background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#f04438) 8%,transparent);border:1px solid color-mix(in srgb,var(--dsw-alias-state-error-primary,#f04438) 25%,transparent);padding:7px 10px;border-radius:var(--dsw-radius-md,10px);margin-top:10px;word-break:break-all}
174
- .dmu-warnline{color:var(--dsw-alias-state-warn-primary,#f79009);font-size:11px;margin-top:10px}
175
- .dmu-mutedline{color:var(--dsw-alias-label-tertiary,#98a2b3);font-size:11px;margin-top:10px}
176
- .dmu-settings{padding:4px 2px}
181
+ .dmu-errline{display:flex;gap:8px;align-items:flex-start;font-size:12px;color:var(--dsw-alias-state-error-primary,#f04438);background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#f04438) 8%,transparent);border:1px solid color-mix(in srgb,var(--dsw-alias-state-error-primary,#f04438) 25%,transparent);padding:7px 10px;border-radius:var(--dsw-radius-md,10px);margin-top:10px;word-break:break-all}
182
+ .dmu-warnline{color:var(--dsw-alias-state-warn-primary,#f79009);font-size:12px;margin-top:10px}
183
+ .dmu-mutedline{color:var(--dsw-alias-label-secondary,#637182);font-size:12px;margin-top:10px}
184
+ .dmu-settings{padding:0}
185
+ .dmu-setrow{display:flex;align-items:center;gap:12px;min-height:44px;padding:10px 0;border-bottom:1px solid var(--dsw-alias-border-l1,#e3e7ed)}
186
+ .dmu-setrow .label{font-size:16px;line-height:22px;color:var(--dsw-alias-label-primary,#191f28);white-space:nowrap}
187
+ .dmu-setrow .sub{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary,#637182);margin-top:2px}
188
+ .dmu-setrow .after{margin-left:auto;display:flex;align-items:center;gap:10px}
189
+ .dmu-setrow .ver{font-size:14px;line-height:22px;color:var(--dsw-alias-label-secondary,#637182);font-variant-numeric:tabular-nums}
190
+ .dmu-main-tabs{margin:18px 0 0}
177
191
  .dmu-settings h2{font-size:16px;font-weight:650;margin:0 0 2px;color:var(--dsw-alias-label-primary,#191f28)}
178
- .dmu-settings .desc{font-size:11.5px;color:var(--dsw-alias-label-tertiary,#98a2b3);margin-bottom:14px}
192
+ .dmu-settings .desc{font-size:12px;color:var(--dsw-alias-label-secondary,#637182);margin-bottom:16px}
179
193
  .dmu-settings .list{display:flex;flex-direction:column;gap:12px}
180
194
  .dmu-empty{padding:18px 12px;text-align:center;font-size:11.5px;color:var(--dsw-alias-label-tertiary,#98a2b3)}
181
195
  .dmu-settings,.dmu-mbody{--dmu-violet:#7c5cf0}
182
196
  @media (prefers-reduced-motion:reduce){.dmu-pcard .win .fill,.dmu-mwin .fill{transition:none}}
183
197
  .dmu-kpis{display:flex;gap:10px;flex-wrap:wrap}
184
- .dmu-kpi{flex:1;min-width:120px;background:var(--dsw-alias-settings-card-fill,var(--dsw-alias-bg-layer-1,#fff));border:1px solid var(--dsw-alias-settings-card-stroke,var(--dsw-alias-border-l1,#e3e7ed));border-radius:var(--dsw-radius-lg,14px);padding:12px 14px}
185
- .dmu-kpi .k{font-size:10.5px;color:var(--dsw-alias-label-tertiary,#98a2b3)}
186
- .dmu-kpi .v{margin-top:5px;font-size:22px;font-weight:700;letter-spacing:-.3px;color:var(--dsw-alias-label-primary,#191f28);font-variant-numeric:tabular-nums}
198
+ .dmu-kpi{flex:1;min-width:120px;background:var(--dsw-alias-settings-card-fill,var(--dsw-alias-bg-layer-1,#fff));border:1px solid var(--dsw-alias-settings-card-stroke,var(--dsw-alias-border-l1,#e3e7ed));border-radius:12px;padding:14px 16px}
199
+ .dmu-kpi .k{font-size:12px;color:var(--dsw-alias-label-secondary,#637182)}
200
+ .dmu-kpi .v{margin-top:6px;font-size:20px;font-weight:600;letter-spacing:-.2px;color:var(--dsw-alias-label-primary,#191f28);font-variant-numeric:tabular-nums}
187
201
  .dmu-kpi .v small{font-size:11px;font-weight:600;color:var(--dsw-alias-label-secondary,#637182);margin-left:2px}
188
- .dmu-chart{background:var(--dsw-alias-settings-card-fill,var(--dsw-alias-bg-layer-1,#fff));border:1px solid var(--dsw-alias-settings-card-stroke,var(--dsw-alias-border-l1,#e3e7ed));border-radius:var(--dsw-radius-lg,14px);padding:14px 16px 10px;margin-top:12px}
202
+ .dmu-chart{background:var(--dsw-alias-settings-card-fill,var(--dsw-alias-bg-layer-1,#fff));border:1px solid var(--dsw-alias-settings-card-stroke,var(--dsw-alias-border-l1,#e3e7ed));border-radius:12px;padding:16px 18px 12px;margin-top:16px}
189
203
  .dmu-chart .bars{display:flex;align-items:flex-end;gap:3px;height:120px}
190
204
  .dmu-chart .bar{flex:1;border-radius:3px 3px 1px 1px;background:var(--dsw-alias-state-business-primary,#4176E6);min-height:2px;position:relative}
191
205
  .dmu-chart .bar:hover{opacity:.75}
192
206
  .dmu-chart .labels{display:flex;gap:3px;margin-top:5px}
193
- .dmu-chart .labels span{flex:1;text-align:center;font-size:9px;color:var(--dsw-alias-label-tertiary,#98a2b3);overflow:visible;white-space:nowrap}
194
- .dmu-table{width:100%;border-collapse:collapse;font-size:12px;margin-top:12px;background:var(--dsw-alias-settings-card-fill,var(--dsw-alias-bg-layer-1,#fff));border:1px solid var(--dsw-alias-settings-card-stroke,var(--dsw-alias-border-l1,#e3e7ed));border-radius:var(--dsw-radius-lg,14px);overflow:hidden}
195
- .dmu-table th{ text-align:left;font-size:10.5px;font-weight:600;color:var(--dsw-alias-label-tertiary,#98a2b3);padding:9px 12px;border-bottom:1px solid var(--dsw-alias-border-l1,#e3e7ed)}
196
- .dmu-table td{padding:9px 12px;border-bottom:1px solid var(--dsw-alias-border-l1,#e3e7ed);color:var(--dsw-alias-label-primary,#191f28);font-variant-numeric:tabular-nums}
207
+ .dmu-chart .labels span{flex:1;text-align:center;font-size:10px;color:var(--dsw-alias-label-tertiary,#98a2b3);overflow:visible;white-space:nowrap}
208
+ .dmu-table{width:100%;border-collapse:collapse;font-size:13px;margin-top:16px}
209
+ .dmu-table th{text-align:left;font-size:12px;font-weight:500;color:var(--dsw-alias-label-secondary,#637182);padding:0 12px 8px;border-bottom:1px solid var(--dsw-alias-border-l1,#e3e7ed)}
210
+ .dmu-table td{height:44px;padding:0 12px;border-bottom:1px solid var(--dsw-alias-border-l1,#e3e7ed);color:var(--dsw-alias-label-primary,#191f28);font-variant-numeric:tabular-nums}
197
211
  .dmu-table tr:last-child td{border-bottom:none}
198
212
  .dmu-table .num{text-align:right;font-variant-numeric:tabular-nums}
199
213
  .dmu-table th.num{text-align:right}
@@ -335,6 +349,7 @@ window.__ModuleLoader__.load({
335
349
  sessionProvider: (sessionId) => unwrap('session-provider', { sessionId }),
336
350
  stats: (opts) => unwrap('stats', opts || {}),
337
351
  config: () => unwrap('config', {}),
352
+ updateCheck: (force) => unwrap('update-check', { force: !!force }),
338
353
  }
339
354
  }
340
355
 
@@ -1111,6 +1126,79 @@ window.__ModuleLoader__.load({
1111
1126
  summaryOf({ providers: [p] }, tr))))))
1112
1127
  }
1113
1128
 
1129
+ // ---------------------------------------------------------------- 版本 / 检查更新
1130
+ function VersionBadge({ api, tr }) {
1131
+ const [info, setInfo] = useState(null)
1132
+ const [busy, setBusy] = useState(false)
1133
+ const [open, setOpen] = useState(false)
1134
+ const [copied, setCopied] = useState(false)
1135
+ useEffect(() => {
1136
+ let alive = true
1137
+ api.updateCheck(false).then((v) => { if (alive) setInfo(v) }).catch(() => {})
1138
+ return () => { alive = false }
1139
+ }, [])
1140
+ const check = async () => {
1141
+ setBusy(true)
1142
+ try {
1143
+ setInfo(await api.updateCheck(true))
1144
+ } catch (e) {
1145
+ setInfo({ current: '—', latest: null, error: String((e && e.message) || e), updateAvailable: false })
1146
+ } finally {
1147
+ setBusy(false)
1148
+ setOpen(true)
1149
+ }
1150
+ }
1151
+ const copy = () => {
1152
+ try {
1153
+ navigator.clipboard.writeText((info && info.command) || '').then(() => {
1154
+ setCopied(true)
1155
+ setTimeout(() => setCopied(false), 1500)
1156
+ })
1157
+ } catch {}
1158
+ }
1159
+ const stat = (label, value) => h('div', { className: 'dmu-mstat' },
1160
+ h('div', { className: 'k' }, label),
1161
+ h('div', { className: 'v' }, value))
1162
+ const cmdStyle = {
1163
+ flex: 1, fontSize: 12, fontFamily: 'var(--dsh-font-mono,monospace)',
1164
+ background: 'var(--dsw-alias-bg-base,#f2f4f7)', padding: '10px 12px',
1165
+ borderRadius: 10, wordBreak: 'break-all', whiteSpace: 'pre-wrap', margin: 0,
1166
+ }
1167
+ return [
1168
+ info && info.updateAvailable
1169
+ ? h('span', { key: 'dot', style: { width: 6, height: 6, borderRadius: 999, background: 'var(--dsw-alias-state-warn-primary,#f79009)' } })
1170
+ : null,
1171
+ h('span', { key: 'ver', className: 'ver' }, 'v' + ((info && info.current) || '—')),
1172
+ h(P.Button, { key: 'btn', variant: 'outline', size: 'sm', onClick: check, disabled: busy },
1173
+ busy ? tr('checkingUpdate') : tr('checkUpdate')),
1174
+ open && info && h(P.Modal, {
1175
+ key: 'modal',
1176
+ open: true,
1177
+ onClose: () => setOpen(false),
1178
+ title: tr('checkUpdate'),
1179
+ closeLabel: tr('close'),
1180
+ footer: h('div', { className: 'dmu-row', style: { justifyContent: 'flex-end' } },
1181
+ h(P.Button, { variant: 'primary', size: 'sm', onClick: () => setOpen(false) }, tr('close'))),
1182
+ },
1183
+ h('div', { style: { display: 'flex', flexDirection: 'column', gap: 14 } },
1184
+ h('div', { className: 'dmu-row', style: { gap: 28 } },
1185
+ stat(tr('currentVersion'), info.current ? 'v' + info.current : '—'),
1186
+ stat(tr('latestVersion'), info.latest ? 'v' + info.latest : '—')),
1187
+ info.error
1188
+ ? h('div', { className: 'dmu-warnline', style: { margin: 0 } }, tr('updateFailed', { msg: info.error }))
1189
+ : info.updateAvailable
1190
+ ? h('div', { className: 'dmu-warnline', style: { margin: 0 } }, tr('updateAvailableHint', { v: 'v' + info.latest }))
1191
+ : h('div', { className: 'dmu-mutedline', style: { margin: 0 } }, tr('upToDate')),
1192
+ h('div', null,
1193
+ h('div', { style: { fontSize: 13, fontWeight: 600, color: 'var(--dsw-alias-label-primary,#191f28)', marginBottom: 4 } }, tr('manualUpdateTitle')),
1194
+ h('div', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary,#637182)', marginBottom: 8 } }, tr('manualUpdateNote')),
1195
+ h('div', { style: { display: 'flex', gap: 10, alignItems: 'flex-start' } },
1196
+ h('code', { style: cmdStyle }, info.command || ''),
1197
+ h(P.Button, { variant: 'outline', size: 'sm', onClick: copy },
1198
+ copied ? tr('copied') : tr('copyCommand')))))),
1199
+ ]
1200
+ }
1201
+
1114
1202
  // ---------------------------------------------------------------- 入口 2:设置页
1115
1203
  function SettingsEntry(props) {
1116
1204
  const { api } = props
@@ -1122,7 +1210,9 @@ window.__ModuleLoader__.load({
1122
1210
  { value: 'log', label: tr('tabLog'), id: 'dmu-tab-log', panelId: 'dmu-panel-log' },
1123
1211
  ]
1124
1212
  return h('div', { className: 'dmu-settings' },
1125
- h('h2', null, tr('nav')),
1213
+ h('div', { className: 'dmu-setrow' },
1214
+ h('div', null, h('div', { className: 'label' }, tr('versionLabel'))),
1215
+ h('div', { className: 'after' }, h(VersionBadge, { api, tr }))),
1126
1216
  h(P.SegmentedTabs, {
1127
1217
  items: tabs,
1128
1218
  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 ASSETS_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'assets', 'logos')
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ponydev/dsh-model-usage",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "DSH Web 插件:实时显示模型供应商额度与用量(DeepSeek / GLM Coding Plan / OpenRouter / Kimi Coding Plan + 自定义适配器)",
5
5
  "keywords": [
6
6
  "dsh",