dsh-ollama-usage 0.1.10 → 0.1.11
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/README.md +21 -10
- package/lib/client.js +4 -3
- package/lib/index.js +45 -55
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -20,8 +20,8 @@ plugin's own detail page, persisted credentials & snapshots, auto-refresh.*
|
|
|
20
20
|
|
|
21
21
|
---
|
|
22
22
|
|
|
23
|
-
> **兼容性 / Compatibility**:v0.1.
|
|
24
|
-
> v0.1.
|
|
23
|
+
> **兼容性 / Compatibility**:v0.1.11 支持 DSH **≥0.1.5-rc.1**(`@deepseek-ai/dsh-*` peer 放宽为 `>=0.1.7-rc.1`),已在 **0.1.5-rc.1 / 0.1.5-rc.2 / 0.1.6-alpha.2 / 0.1.7-rc.1 / 0.1.7-rc.2 / 0.2.0-rc.1** 实测(RPC envelope 探针全过;0.1.7-rc.2 上 `/ollama-usage/snapshot` 实测返回;0.2.0-rc.1 上通过新的兼容性门禁并正常加载);旧版 DSH(0.1.0-rc.5 ~ rc.8)请使用最后兼容的 npm 版本 **0.1.5**。
|
|
24
|
+
> v0.1.11 targets DSH **≥0.1.5-rc.1**, verified on **0.1.5-rc.1 / 0.1.5-rc.2 / 0.1.6-alpha.2 / 0.1.7-rc.1 / 0.1.7-rc.2 / 0.2.0-rc.1**; for older DSH releases use the last compatible npm version **0.1.5**.
|
|
25
25
|
|
|
26
26
|
## ✨ 功能特性 Features
|
|
27
27
|
|
|
@@ -30,13 +30,20 @@ plugin's own detail page, persisted credentials & snapshots, auto-refresh.*
|
|
|
30
30
|
| 🎯 **会话用量(每 5 小时)** | Ollama Cloud 的 5 小时会话配额,置顶展示「已用 X% · 剩余 Y%」 |
|
|
31
31
|
| 📅 **周用量(Weekly)** | 本周配额进度,与会话用量并排展示 |
|
|
32
32
|
| 🎨 **侧边栏双横条** | 极简透明风格:会话 5h **淡紫** + 周用量**深紫**,各带百分比数字,悬停显示详情,每 60 秒自动更新(窄栏自动隐藏) |
|
|
33
|
-
| 🖥️ **插件详情页完整面板** | 渲染在「插件列表 → dsh-ollama-usage
|
|
33
|
+
| 🖥️ **插件详情页完整面板** | 渲染在「插件列表 → dsh-ollama-usage」页面的描述与组件行之间:会话/周用量进度条、会话与周重置倒计时、最近 5 条会话用量历史 |
|
|
34
34
|
| 💾 **持久化** | API Key 与用量快照写入 `$DSH_HOME/storages/ollama-usage/usage.json`(权限 600,保留 24 条历史),跨对话 / 重启自动恢复 |
|
|
35
35
|
| 🔄 **自动刷新** | Host 每 10 分钟自动查询(页面关闭也持续);Key 失效(401)自动清除并提示 |
|
|
36
36
|
| 🔑 **登录引导** | 未登录时一键打开 [ollama.com/settings](https://ollama.com/settings) 登录、到 [settings/keys](https://ollama.com/settings/keys) 创建 API Key;兼容旧版 CLI 的 `~/.ollama/auth.json` |
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
限额进度数据来源于 Ollama 官方接口 `GET https://ollama.com/api/balance`
|
|
39
|
+
(`included.session` / `included.weekly` 的 `remaining_percent` 与 `resets_at`),
|
|
40
|
+
与 [ollama.com/settings](https://ollama.com/settings) 页面一致。
|
|
41
|
+
|
|
42
|
+
> **v0.1.11 修复**:Ollama 在 2026-10 把 `GET /api/usage` 改为只返回按小时/天的
|
|
43
|
+
> 请求量分桶,不再包含 `limits.session` / `limits.weekly`,旧版本因此把两个百分比
|
|
44
|
+
> 解析成 `null` —— 面板显示「暂无数据」、侧边栏双横条显示「—」。
|
|
45
|
+
> 现在限额进度改由 `/api/balance` 提供(`已用 = 100 - remaining_percent`),
|
|
46
|
+
> 重置倒计时也直接使用接口下发的真实 `resets_at`。
|
|
40
47
|
|
|
41
48
|
## 📦 安装 Install
|
|
42
49
|
|
|
@@ -59,7 +66,7 @@ dsh --profile web
|
|
|
59
66
|
|
|
60
67
|
## 🔒 数据与隐私 Privacy
|
|
61
68
|
|
|
62
|
-
- 仅向 `https://ollama.com/api/
|
|
69
|
+
- 仅向 `https://ollama.com/api/balance` 发送带 Bearer 的查询请求;
|
|
63
70
|
- Key 以明文保存在本机 `$DSH_HOME/storages/ollama-usage/usage.json`(权限 600,
|
|
64
71
|
与 `~/.ollama/auth.json` 同等级信任),随时可清除。
|
|
65
72
|
|
|
@@ -69,7 +76,7 @@ dsh --profile web
|
|
|
69
76
|
┌──────────────────────────┐ ┌─────────────────────────────┐
|
|
70
77
|
│ browser (client bundle) │ RPC │ host (lib/index.js) │
|
|
71
78
|
│ lib/client.js │◄──────►│ webServer /ollama-usage │
|
|
72
|
-
│ · sidebar dual bars │ 信封协议 │ · fetch api/
|
|
79
|
+
│ · sidebar dual bars │ 信封协议 │ · fetch api/balance(Bearer)│
|
|
73
80
|
│ · settings panel │ │ · $DSH_HOME 持久化(0600) │
|
|
74
81
|
│ · login guidance │ │ · 10min auto-refresh │
|
|
75
82
|
└──────────────────────────┘ └─────────────────────────────┘
|
|
@@ -85,11 +92,15 @@ dsh --profile web
|
|
|
85
92
|
## 🧪 测试 Tests
|
|
86
93
|
|
|
87
94
|
```bash
|
|
88
|
-
|
|
95
|
+
npm test # bridge-smoke → balance-parse → rpc-envelope-alpha4
|
|
89
96
|
```
|
|
90
97
|
|
|
91
|
-
17
|
|
92
|
-
snapshot / forget / auth-state、非法信封与请求方法校验。
|
|
98
|
+
- `test/bridge-smoke.mjs`(17 项):路由注册、no-token / 401 分支、持久化读写与
|
|
99
|
+
清理、snapshot / forget / auth-state、非法信封与请求方法校验。
|
|
100
|
+
- `test/balance-parse.mjs`:`/api/balance` 解析回归(不联网,fetch 替身)。用真实
|
|
101
|
+
响应体驱动 `check`,断言 5h / 周百分比换算、`resets_at` 透传、持久化与历史追加,
|
|
102
|
+
以及无限额套餐 / 非 JSON / 401 / 403 / 网络异常各分支。
|
|
103
|
+
- `test/rpc-envelope-alpha4.mjs`:逐端点通过 alpha.4 的 Connection RPC 信封校验器。
|
|
93
104
|
|
|
94
105
|
## 🛠️ 开发 / 发布 Develop & Publish
|
|
95
106
|
|
package/lib/client.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 提供三处界面:
|
|
5
5
|
* - plugins.bundle.config(以包名为键)「Ollama 用量」:完整面板(会话 5h / 周用量
|
|
6
|
-
*
|
|
6
|
+
* 进度条、会话与周用量重置倒计时、历史记录、API Key 配置、清除 Key)。
|
|
7
7
|
* 渲染在「插件列表 → dsh-ollama-usage」页面的描述与组件行之间 ——
|
|
8
8
|
* 配置不再位于设置面板(settings.section 已退役);
|
|
9
9
|
* - sidebar.footer.action:极简双横条(会话 5h 淡紫 / 周用量深紫,透明背景);
|
|
@@ -175,11 +175,12 @@ window.__ModuleLoader__.load({
|
|
|
175
175
|
|
|
176
176
|
function UsageBody(props) {
|
|
177
177
|
const d = props.data
|
|
178
|
+
const sessionResetText = d.sessionResetAt ? fmtCountdown(d.sessionResetAt) : null
|
|
178
179
|
const weeklyResetText = d.weeklyResetAt ? fmtCountdown(d.weeklyResetAt) : null
|
|
179
180
|
const models = Array.isArray(d.models) ? d.models.slice(0, 5) : []
|
|
180
181
|
const history = Array.isArray(props.history) ? props.history.slice(-5).reverse() : []
|
|
181
182
|
return React.createElement('div', { className: 'olusa-block' },
|
|
182
|
-
React.createElement(BarRow, { label: '会话用量(每 5 小时)', percent: d.sessionPercent, em: true }),
|
|
183
|
+
React.createElement(BarRow, { label: '会话用量(每 5 小时)', percent: d.sessionPercent, em: true, resetText: sessionResetText ? '还有 ' + sessionResetText + ' 重置' : null }),
|
|
183
184
|
React.createElement(BarRow, { label: '本周用量(Weekly)', percent: d.weeklyPercent, resetText: weeklyResetText ? '还有 ' + weeklyResetText + ' 重置' : null }),
|
|
184
185
|
(models.length > 0
|
|
185
186
|
? React.createElement('div', { className: 'olusa-models' },
|
|
@@ -294,7 +295,7 @@ window.__ModuleLoader__.load({
|
|
|
294
295
|
React.createElement('button', { className: 'olusa-btn', disabled: busy, onClick: () => doCheck('') }, busy ? '查询中…' : '刷新')
|
|
295
296
|
),
|
|
296
297
|
body,
|
|
297
|
-
React.createElement('div', { className: 'olusa-ver' }, 'dsh-ollama-usage · v0.1.
|
|
298
|
+
React.createElement('div', { className: 'olusa-ver' }, 'dsh-ollama-usage · v0.1.11')
|
|
298
299
|
)
|
|
299
300
|
}
|
|
300
301
|
|
package/lib/index.js
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* 单一 Loader 行(见 cordis.patch.yml)挂载本模块,职责:
|
|
5
5
|
* 1. 读取 Ollama 凭证:优先持久化的 API Key(settings 面板检测成功后写入),
|
|
6
6
|
* 兼容旧版 CLI 的 ~/.ollama/auth.json;
|
|
7
|
-
* 2. 请求 https://ollama.com/api/
|
|
7
|
+
* 2. 请求 https://ollama.com/api/balance 获取余量:按会话(每 5 小时)与周
|
|
8
|
+
* 限额的「legacy plan」在 included.session / included.weekly 返回
|
|
9
|
+
* { remaining_percent, resets_at },换算成已用进度与重置时间;
|
|
8
10
|
* 3. 把 Key / 用量快照 / 历史记录持久化到
|
|
9
11
|
* $DSH_HOME/storages/ollama-usage/usage.json(权限 0600),跨对话/重启恢复;
|
|
10
12
|
* 4. 每 10 分钟自动刷新一次(浏览器页面关闭也持续,Key 失效自动清除);
|
|
@@ -23,7 +25,7 @@ import { resolveDshHome } from '@deepseek-ai/dsh-home-paths'
|
|
|
23
25
|
export const name = 'ollama-usage'
|
|
24
26
|
|
|
25
27
|
const RPC_CHANNEL = '/ollama-usage'
|
|
26
|
-
const
|
|
28
|
+
const BALANCE_URL = 'https://ollama.com/api/balance'
|
|
27
29
|
const FETCH_TIMEOUT_MS = 20000
|
|
28
30
|
const AUTO_REFRESH_MS = 10 * 60 * 1000
|
|
29
31
|
const HISTORY_CAP = 24
|
|
@@ -78,28 +80,45 @@ function readLocalToken() {
|
|
|
78
80
|
* 用量查询与解析
|
|
79
81
|
* ------------------------------------------------------------------ */
|
|
80
82
|
|
|
81
|
-
async function
|
|
82
|
-
const res = await fetch(
|
|
83
|
+
async function fetchBalance(token) {
|
|
84
|
+
const res = await fetch(BALANCE_URL, {
|
|
83
85
|
headers: { Authorization: 'Bearer ' + token },
|
|
84
86
|
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
85
87
|
})
|
|
86
88
|
return { status: res.status, body: await res.text() }
|
|
87
89
|
}
|
|
88
90
|
|
|
89
|
-
/**
|
|
90
|
-
|
|
91
|
+
/**
|
|
92
|
+
* included.session|weekly.remaining_percent 是 0~100 的「剩余百分比」
|
|
93
|
+
* (75 = 还剩 75%);面板展示的是已用进度,因此换算成 100 - remaining。
|
|
94
|
+
*/
|
|
95
|
+
function usedPercent(remaining) {
|
|
91
96
|
let n = null
|
|
92
|
-
if (typeof
|
|
93
|
-
else if (typeof
|
|
94
|
-
const c = Number(
|
|
97
|
+
if (typeof remaining === 'number' && Number.isFinite(remaining)) n = remaining
|
|
98
|
+
else if (typeof remaining === 'string' && remaining.trim() !== '') {
|
|
99
|
+
const c = Number(remaining)
|
|
95
100
|
if (Number.isFinite(c)) n = c
|
|
96
101
|
}
|
|
97
102
|
if (n === null) return null
|
|
98
|
-
|
|
99
|
-
|
|
103
|
+
return Math.round(Math.min(100, Math.max(0, 100 - n)) * 10) / 10
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function resetAt(limit) {
|
|
107
|
+
return typeof limit.resets_at === 'string' && limit.resets_at !== '' ? limit.resets_at : null
|
|
100
108
|
}
|
|
101
109
|
|
|
102
|
-
|
|
110
|
+
/**
|
|
111
|
+
* 解析 GET /api/balance(契约见 https://docs.ollama.com/api/balance)。
|
|
112
|
+
*
|
|
113
|
+
* 带会话(每 5 小时)/周限额的 legacy plan 返回
|
|
114
|
+
* included.session|weekly = { remaining_percent, resets_at }
|
|
115
|
+
* 按美元计费的套餐只返回 included.balance_usd / allowance_usd / period,
|
|
116
|
+
* 没有 5h/周进度可展示 —— 此时两个百分比保持 null,面板显示“暂无数据”。
|
|
117
|
+
*
|
|
118
|
+
* /api/usage 自 2026-10 起只返回按小时/天的请求量分桶(不再有 limits /
|
|
119
|
+
* activity / models 字段),因此限额进度一律以 /api/balance 为唯一来源。
|
|
120
|
+
*/
|
|
121
|
+
function parseBalance(body) {
|
|
103
122
|
let json = null
|
|
104
123
|
try {
|
|
105
124
|
json = JSON.parse(body)
|
|
@@ -109,57 +128,28 @@ function parseUsage(body, status) {
|
|
|
109
128
|
if (!json || typeof json !== 'object' || Array.isArray(json)) {
|
|
110
129
|
return { ok: false, message: '响应格式异常' }
|
|
111
130
|
}
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
const weeklyUsage = limits.weekly && typeof limits.weekly === 'object' ? toPercent(limits.weekly.usage) : null
|
|
116
|
-
// 模型请求数:真实响应放在 limits.weekly.models(有时 activity.models 也有),按优先级取
|
|
117
|
-
const weeklyModels = limits.weekly && Array.isArray(limits.weekly.models) ? limits.weekly.models : null
|
|
118
|
-
const sessionModels = limits.session && Array.isArray(limits.session.models) ? limits.session.models : null
|
|
119
|
-
const activityModels = Array.isArray(activity.models) ? activity.models : null
|
|
120
|
-
const rawModels =
|
|
121
|
-
(weeklyModels && weeklyModels.length ? weeklyModels : null) ||
|
|
122
|
-
(sessionModels && sessionModels.length ? sessionModels : null) ||
|
|
123
|
-
(activityModels || [])
|
|
124
|
-
const models = rawModels
|
|
125
|
-
.map((m) => ({ name: String((m && m.name) || ''), requestCount: Number((m && m.request_count)) || 0 }))
|
|
126
|
-
.filter((m) => m.name)
|
|
127
|
-
.sort((a, b) => b.requestCount - a.requestCount)
|
|
131
|
+
const included = json.included && typeof json.included === 'object' ? json.included : {}
|
|
132
|
+
const session = included.session && typeof included.session === 'object' ? included.session : null
|
|
133
|
+
const weekly = included.weekly && typeof included.weekly === 'object' ? included.weekly : null
|
|
128
134
|
return {
|
|
129
135
|
ok: true,
|
|
130
136
|
usage: {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
models,
|
|
137
|
+
sessionPercent: session ? usedPercent(session.remaining_percent) : null,
|
|
138
|
+
weeklyPercent: weekly ? usedPercent(weekly.remaining_percent) : null,
|
|
139
|
+
sessionResetAt: session ? resetAt(session) : null,
|
|
140
|
+
weeklyResetAt: weekly ? resetAt(weekly) : null,
|
|
141
|
+
// 模型请求数仍留在快照结构里(客户端按非空才渲染),等官方
|
|
142
|
+
// “usage breakdowns by model”(docs 标注 coming soon)上线后再填充。
|
|
143
|
+
models: [],
|
|
138
144
|
},
|
|
139
145
|
}
|
|
140
146
|
}
|
|
141
147
|
|
|
142
|
-
/* ------------------------------------------------------------------ *
|
|
143
|
-
* 周重置时间推算(API 不提供各限额重置时间,按配额语义推算)
|
|
144
|
-
* - weekly:下周一 00:00 UTC(last_4_weeks 周期锚点为周一)
|
|
145
|
-
* ------------------------------------------------------------------ */
|
|
146
|
-
|
|
147
|
-
function computeResetTimes(nowIso) {
|
|
148
|
-
const now = new Date(nowIso)
|
|
149
|
-
const daysUntilMonday = ((7 - now.getUTCDay()) % 7) + 1
|
|
150
|
-
const weeklyResetAt = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + daysUntilMonday)).toISOString()
|
|
151
|
-
return { weeklyResetAt }
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function withResetTimes(usage, nowIso) {
|
|
155
|
-
return Object.assign({}, usage, computeResetTimes(nowIso))
|
|
156
|
-
}
|
|
157
|
-
|
|
158
148
|
/** 用 token 查询并把结果写入持久化文件;返回 RpcResult({ ok, value|error })。 */
|
|
159
149
|
async function checkWithToken(token) {
|
|
160
150
|
let res
|
|
161
151
|
try {
|
|
162
|
-
res = await
|
|
152
|
+
res = await fetchBalance(token)
|
|
163
153
|
} catch (e) {
|
|
164
154
|
return fail('network', String((e && e.message) || e))
|
|
165
155
|
}
|
|
@@ -172,7 +162,7 @@ async function checkWithToken(token) {
|
|
|
172
162
|
if (res.status >= 400) {
|
|
173
163
|
return fail('http-error', res.body || ('HTTP ' + res.status))
|
|
174
164
|
}
|
|
175
|
-
const parsed =
|
|
165
|
+
const parsed = parseBalance(res.body)
|
|
176
166
|
if (!parsed.ok) {
|
|
177
167
|
return fail('internal', parsed.message)
|
|
178
168
|
}
|
|
@@ -182,7 +172,7 @@ async function checkWithToken(token) {
|
|
|
182
172
|
const history = Array.isArray(prev && prev.history) ? prev.history.slice() : []
|
|
183
173
|
history.push({ at: now, sessionPercent: parsed.usage.sessionPercent, weeklyPercent: parsed.usage.weeklyPercent })
|
|
184
174
|
const trimmed = history.length > HISTORY_CAP ? history.slice(history.length - HISTORY_CAP) : history
|
|
185
|
-
const usage =
|
|
175
|
+
const usage = parsed.usage
|
|
186
176
|
const persisted = writeFile({ token, updatedAt: now, usage, history: trimmed })
|
|
187
177
|
return {
|
|
188
178
|
ok: true,
|
|
@@ -231,7 +221,7 @@ async function dispatch(endpoint, payload) {
|
|
|
231
221
|
if (endpoint === 'snapshot') {
|
|
232
222
|
const rec = readFile()
|
|
233
223
|
if (!rec || !rec.usage) return fail('no-data', '暂无持久化的用量数据')
|
|
234
|
-
const usage =
|
|
224
|
+
const usage = rec.usage
|
|
235
225
|
// snapshot 数据读自磁盘文件(插件无内存缓存),必然已持久化;与 check 端点一致地报告 persisted
|
|
236
226
|
return { ok: true, value: { tokenPersisted: !!rec.token, persisted: true, updatedAt: rec.updatedAt, history: (rec.history || []).slice(-5), usage } }
|
|
237
227
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-ollama-usage",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "DeepSeek Harness 插件:可视化 Ollama Cloud 账号用量余量(每 5 小时会话用量 + 周用量),支持侧边栏双横条、插件详情页完整面板、API Key 与用量快照持久化、自动刷新与登录引导。Ollama Pro usage & remaining-quota visualization for DeepSeek Harness: 5-hour session + weekly quota bars in the sidebar footer, a full panel on the plugin's own detail page, persisted API key and usage snapshots, auto-refresh and login guidance.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -94,6 +94,6 @@
|
|
|
94
94
|
],
|
|
95
95
|
"license": "MIT",
|
|
96
96
|
"scripts": {
|
|
97
|
-
"test": "DSH_HOME=.tmp-dsh-home HOME=.tmp-home node test/bridge-smoke.mjs && DSH_HOME=.tmp-env-dsh-home HOME=.tmp-env-home node test/rpc-envelope-alpha4.mjs"
|
|
97
|
+
"test": "DSH_HOME=.tmp-dsh-home HOME=.tmp-home node test/bridge-smoke.mjs && DSH_HOME=.tmp-balance-dsh-home HOME=.tmp-balance-home node test/balance-parse.mjs && DSH_HOME=.tmp-env-dsh-home HOME=.tmp-env-home node test/rpc-envelope-alpha4.mjs"
|
|
98
98
|
}
|
|
99
99
|
}
|