dsh-ollama-usage 0.1.12 → 0.1.14
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.en.md +12 -2
- package/README.md +12 -2
- package/lib/client.js +214 -102
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -28,7 +28,7 @@ plugin's own detail page, persisted credentials & snapshots, auto-refresh.
|
|
|
28
28
|
| 🎯 **Session usage (per 5 hours)** | Ollama Cloud's 5-hour session quota, pinned at the top showing "used X% · remaining Y%" |
|
|
29
29
|
| 📅 **Weekly usage** | This week's quota progress, shown next to the session usage |
|
|
30
30
|
| 🎨 **Sidebar dual bars** | Minimal, transparent style: session 5h in **light purple** + weekly in **deep purple**, each with a percentage, details on hover, refreshed every 60 seconds (auto-hidden on narrow layouts) |
|
|
31
|
-
| 🖥️ **Full panel on the plugin detail page** | Rendered between the description and component rows of the "Plugin list → dsh-ollama-usage" page: session/week usage bars, session and weekly reset countdowns, and the last 5 session-usage history entries |
|
|
31
|
+
| 🖥️ **Full panel on the plugin detail page** | Rendered between the description and component rows of the "Plugin list → dsh-ollama-usage" page: session/week usage bars, session and weekly reset countdowns, and the last 5 session-usage history entries; card-based sections with loading / error / success states |
|
|
32
32
|
| 💾 **Persistence** | The API Key and usage snapshot are written to `$DSH_HOME/storages/ollama-usage/usage.json` (mode 600, 24 history entries kept), restored automatically across conversations / restarts |
|
|
33
33
|
| 🔄 **Auto-refresh** | The host queries every 10 minutes (even when the page is closed); an invalidated Key (401) is cleared automatically with a prompt |
|
|
34
34
|
| 🔑 **Login guidance** | When not signed in, one click opens [ollama.com/settings](https://ollama.com/settings) to sign in and [settings/keys](https://ollama.com/settings/keys) to create an API Key; compatible with the legacy CLI's `~/.ollama/auth.json` |
|
|
@@ -62,6 +62,16 @@ dsh --profile web
|
|
|
62
62
|
3. On success it is persisted; expand the sidebar to see the dual bars at the bottom, and the panel shows details and history at any time;
|
|
63
63
|
4. When no longer needed, click **Clear stored Key** to remove the local credential (history is kept).
|
|
64
64
|
|
|
65
|
+
## 🖼️ 界面预览 / Screenshots
|
|
66
|
+
|
|
67
|
+
The screenshots below come from a throwaway local preview instance; the example data is for demonstration only:
|
|
68
|
+
|
|
69
|
+

|
|
70
|
+
|
|
71
|
+

|
|
72
|
+
|
|
73
|
+

|
|
74
|
+
|
|
65
75
|
## 🔒 数据与隐私 / Privacy
|
|
66
76
|
|
|
67
77
|
- Only a Bearer-carrying query request is sent to `https://ollama.com/api/balance`;
|
|
@@ -104,7 +114,7 @@ Since DSH 0.2.1 the compatibility gate prefers `engines.dsh`. This plugin declar
|
|
|
104
114
|
|
|
105
115
|
> Only versions that were **actually run** are listed; untested versions are omitted.
|
|
106
116
|
|
|
107
|
-
v0.1.
|
|
117
|
+
v0.1.14 targets DSH **≥0.1.5-rc.1** (the `@deepseek-ai/dsh-*` peer was relaxed to `>=0.1.7-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** (all RPC envelope probes pass; on 0.1.7-rc.2 the `/ollama-usage/snapshot` endpoint returns as measured; on 0.2.0-rc.1 it passes the new compatibility gate and loads normally; on 0.2.1-rc.1 the gate and RPC envelope probes pass, and 0.2.1-rc.2 passes the gate); for older DSH (0.1.0-rc.5 ~ rc.8) use the last compatible npm version, **0.1.5**.
|
|
108
118
|
|
|
109
119
|
## 🛠️ 开发 / Develop
|
|
110
120
|
|
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ plugin's own detail page, persisted credentials & snapshots, auto-refresh.*
|
|
|
31
31
|
| 🎯 **会话用量(每 5 小时)** | Ollama Cloud 的 5 小时会话配额,置顶展示「已用 X% · 剩余 Y%」 |
|
|
32
32
|
| 📅 **周用量(Weekly)** | 本周配额进度,与会话用量并排展示 |
|
|
33
33
|
| 🎨 **侧边栏双横条** | 极简透明风格:会话 5h **淡紫** + 周用量**深紫**,各带百分比数字,悬停显示详情,每 60 秒自动更新(窄栏自动隐藏) |
|
|
34
|
-
| 🖥️ **插件详情页完整面板** | 渲染在「插件列表 → dsh-ollama-usage」页面的描述与组件行之间:会话/周用量进度条、会话与周重置倒计时、最近 5
|
|
34
|
+
| 🖥️ **插件详情页完整面板** | 渲染在「插件列表 → dsh-ollama-usage」页面的描述与组件行之间:会话/周用量进度条、会话与周重置倒计时、最近 5 条会话用量历史;卡片化分区,带加载 / 错误 / 成功状态提示 |
|
|
35
35
|
| 💾 **持久化** | API Key 与用量快照写入 `$DSH_HOME/storages/ollama-usage/usage.json`(权限 600,保留 24 条历史),跨对话 / 重启自动恢复 |
|
|
36
36
|
| 🔄 **自动刷新** | Host 每 10 分钟自动查询(页面关闭也持续);Key 失效(401)自动清除并提示 |
|
|
37
37
|
| 🔑 **登录引导** | 未登录时一键打开 [ollama.com/settings](https://ollama.com/settings) 登录、到 [settings/keys](https://ollama.com/settings/keys) 创建 API Key;兼容旧版 CLI 的 `~/.ollama/auth.json` |
|
|
@@ -65,6 +65,16 @@ dsh --profile web
|
|
|
65
65
|
3. 检测成功后即持久化,展开侧边栏即可看到底部双横条,面板可随时查看详情与历史;
|
|
66
66
|
4. 不再需要时点 **清除已存 Key** 移除本地凭证(历史保留)。
|
|
67
67
|
|
|
68
|
+
## 🖼️ 界面预览 / Screenshots
|
|
69
|
+
|
|
70
|
+
以下截图来自本地一次性预览实例,示例数据仅用于演示:
|
|
71
|
+
|
|
72
|
+

|
|
73
|
+
|
|
74
|
+

|
|
75
|
+
|
|
76
|
+

|
|
77
|
+
|
|
68
78
|
## 🔒 数据与隐私 / Privacy
|
|
69
79
|
|
|
70
80
|
- 仅向 `https://ollama.com/api/balance` 发送带 Bearer 的查询请求;
|
|
@@ -107,7 +117,7 @@ DSH 0.2.1 起兼容门禁优先读取 `engines.dsh`。本插件声明 **`engines
|
|
|
107
117
|
|
|
108
118
|
> 只列出**实际跑过**的版本;未实测的不列。
|
|
109
119
|
|
|
110
|
-
v0.1.
|
|
120
|
+
v0.1.14 支持 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 上通过新的兼容性门禁并正常加载;0.2.1-rc.1 上兼容门禁与 RPC envelope 探针通过,0.2.1-rc.2 门禁通过);旧版 DSH(0.1.0-rc.5 ~ rc.8)请使用最后兼容的 npm 版本 **0.1.5**。
|
|
111
121
|
|
|
112
122
|
## 🛠️ 开发 / Develop
|
|
113
123
|
|
package/lib/client.js
CHANGED
|
@@ -6,12 +6,15 @@
|
|
|
6
6
|
* 进度条、会话与周用量重置倒计时、历史记录、API Key 配置、清除 Key)。
|
|
7
7
|
* 渲染在「插件列表 → dsh-ollama-usage」页面的描述与组件行之间 ——
|
|
8
8
|
* 配置不再位于设置面板(settings.section 已退役);
|
|
9
|
-
* - sidebar.footer.action:极简双横条(会话 5h
|
|
9
|
+
* - sidebar.footer.action:极简双横条(会话 5h / 周用量,紫为主的数据可视化强调色);
|
|
10
10
|
* - tool.view.cordis:Run 卡片内的同款面板。
|
|
11
11
|
*
|
|
12
12
|
* 数据通道:Host 在 webServer 注册的 /ollama-usage 前缀路由,经 Connection
|
|
13
|
-
* RPC 信封协议调用(check / snapshot / forget)
|
|
14
|
-
*
|
|
13
|
+
* RPC 信封协议调用(check / snapshot / forget)。
|
|
14
|
+
*
|
|
15
|
+
* 呈现层:手写 <style> 注入,颜色/字体/圆角/阴影/焦点环全部来自官方 --dsw-*
|
|
16
|
+
* 主题变量,亮暗主题自动成立。唯一保留的硬编码颜色是双横条的两档紫色,按规范
|
|
17
|
+
* 只作为「数据可视化强调色」用于横条填充,不扩散到任何通用控件。
|
|
15
18
|
*/
|
|
16
19
|
|
|
17
20
|
window.__ModuleLoader__.load({
|
|
@@ -25,52 +28,95 @@ window.__ModuleLoader__.load({
|
|
|
25
28
|
|
|
26
29
|
const RPC_CHANNEL = '/ollama-usage'
|
|
27
30
|
const CSS_TAG_ID = 'dsh-ollama-usage/client.css'
|
|
31
|
+
// 数据可视化强调色:仅用于侧栏双横条的填充(会话 5h 淡紫 / 周用量深紫),
|
|
32
|
+
// 属于插件品牌识别,不用于按钮、边框、文字等通用控件。
|
|
28
33
|
const SESSION_COLOR = '#b39df7'
|
|
29
34
|
const WEEKLY_COLOR = '#7c66e8'
|
|
30
35
|
|
|
31
36
|
const CSS = [
|
|
32
|
-
|
|
33
|
-
'.olusa-
|
|
34
|
-
'.olusa-
|
|
35
|
-
'.olusa-
|
|
36
|
-
'.olusa-
|
|
37
|
-
'.olusa-
|
|
38
|
-
'.olusa-
|
|
39
|
-
'.olusa-
|
|
40
|
-
'.olusa-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
'.olusa-
|
|
44
|
-
'.olusa-
|
|
45
|
-
'.olusa-
|
|
46
|
-
'.olusa-
|
|
47
|
-
'.olusa-
|
|
48
|
-
'.olusa-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
'.olusa-
|
|
52
|
-
'.olusa-
|
|
53
|
-
'.olusa-
|
|
54
|
-
'.olusa-
|
|
55
|
-
'.olusa-
|
|
56
|
-
'.olusa-
|
|
57
|
-
'.olusa-
|
|
58
|
-
'.olusa-
|
|
59
|
-
'.olusa-
|
|
60
|
-
'.olusa-
|
|
61
|
-
'.olusa-
|
|
62
|
-
'.olusa-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
'.olusa-
|
|
66
|
-
'.olusa-
|
|
67
|
-
'.olusa-
|
|
37
|
+
// ── 面板骨架 ────────────────────────────────────────────────
|
|
38
|
+
'.olusa-panel{display:flex;flex-direction:column;gap:12px;padding:14px 16px;border:1px solid var(--dsw-alias-border-l1);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:var(--dsw-font-xs-13);box-sizing:border-box}',
|
|
39
|
+
'.olusa-panel *{box-sizing:border-box}',
|
|
40
|
+
'.olusa-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;min-width:0}',
|
|
41
|
+
'.olusa-head-main{display:flex;flex-direction:column;gap:2px;min-width:0}',
|
|
42
|
+
'.olusa-title{font:var(--dsw-font-s-14);font-weight:600;color:var(--dsw-alias-label-primary);white-space:nowrap}',
|
|
43
|
+
'.olusa-sub{font:var(--dsw-font-xxxs-11);color:var(--dsw-alias-label-tertiary)}',
|
|
44
|
+
'.olusa-block{display:flex;flex-direction:column;gap:12px;min-width:0}',
|
|
45
|
+
'.olusa-ver{font:var(--dsw-font-xxxs-11);color:var(--dsw-alias-label-caption)}',
|
|
46
|
+
|
|
47
|
+
// ── 用量进度条 ──────────────────────────────────────────────
|
|
48
|
+
'.olusa-barblock{display:flex;flex-direction:column;gap:6px;min-width:0}',
|
|
49
|
+
'.olusa-barhead{display:flex;align-items:baseline;justify-content:space-between;gap:8px;min-width:0}',
|
|
50
|
+
'.olusa-barlabel{font:var(--dsw-font-xxs-strong-12);font-weight:500;color:var(--dsw-alias-label-primary)}',
|
|
51
|
+
'.olusa-barval{font:var(--dsw-font-xxxs-strong-11);font-weight:500;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;white-space:nowrap}',
|
|
52
|
+
'.olusa-bar{height:6px;border-radius:999px;background:color-mix(in srgb,var(--dsw-alias-label-primary) 10%,var(--dsw-alias-bg-layer-2));overflow:hidden}',
|
|
53
|
+
'.olusa-barfill{height:100%;border-radius:999px;transition:width .2s ease}',
|
|
54
|
+
|
|
55
|
+
// ── 说明 / 分区 / 列表 ──────────────────────────────────────
|
|
56
|
+
'.olusa-hint{margin:0;font:var(--dsw-font-xxxs-11);color:var(--dsw-alias-label-tertiary);line-height:16px}',
|
|
57
|
+
'.olusa-sec{display:flex;flex-direction:column;gap:2px;min-width:0}',
|
|
58
|
+
'.olusa-sec-title{margin:0 0 2px;font:var(--dsw-font-xxxs-strong-11);font-weight:600;color:var(--dsw-alias-label-secondary);letter-spacing:.02em}',
|
|
59
|
+
'.olusa-list{display:flex;flex-direction:column;min-width:0}',
|
|
60
|
+
'.olusa-row{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding:6px 0;border-bottom:1px solid var(--dsw-alias-border-l1);min-width:0;font:var(--dsw-font-xxs-12)}',
|
|
61
|
+
'.olusa-row:last-child{border-bottom:none}',
|
|
62
|
+
'.olusa-rowname{color:var(--dsw-alias-label-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}',
|
|
63
|
+
'.olusa-rowval{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;white-space:nowrap}',
|
|
64
|
+
'.olusa-link{color:var(--dsw-alias-link);text-decoration:none;border-radius:var(--dsw-radius-xs)}',
|
|
65
|
+
'.olusa-link:hover{text-decoration:underline}',
|
|
66
|
+
'.olusa-link:focus-visible{outline:var(--dsw-focus-ring-width,2px) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:2px}',
|
|
67
|
+
'.olusa-steps{margin:0;padding-left:16px;display:flex;flex-direction:column;gap:4px;font:var(--dsw-font-xxs-12);color:var(--dsw-alias-label-secondary);line-height:18px}',
|
|
68
|
+
|
|
69
|
+
// ── 输入 / 按钮 / 状态 ──────────────────────────────────────
|
|
70
|
+
'.olusa-input{flex:1;min-width:0;height:30px;padding:0 10px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-sm);font:var(--dsw-font-xs-13);transition:border-color .15s ease}',
|
|
71
|
+
'.olusa-input:hover{border-color:var(--dsw-alias-border-l3)}',
|
|
72
|
+
'.olusa-input:focus{outline:none;border-color:var(--dsw-alias-brand-primary)}',
|
|
73
|
+
'.olusa-input:focus-visible{outline:var(--dsw-focus-ring-width,2px) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:1px}',
|
|
74
|
+
'.olusa-input::placeholder{color:var(--dsw-alias-label-tertiary)}',
|
|
75
|
+
'.olusa-login-title{font:var(--dsw-font-xs-strong-13);font-weight:600;color:var(--dsw-alias-label-primary)}',
|
|
76
|
+
'.olusa-rowbtns{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-top:2px}',
|
|
77
|
+
'.olusa-btn{display:inline-flex;align-items:center;justify-content:center;height:28px;padding:0 12px;color:var(--dsw-alias-label-primary);background:transparent;border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-sm);font:var(--dsw-font-xxs-strong-12);font-weight:500;cursor:pointer;white-space:nowrap;transition:background-color .15s ease,border-color .15s ease,color .15s ease,opacity .15s ease}',
|
|
78
|
+
'.olusa-btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}',
|
|
79
|
+
'.olusa-btn:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active)}',
|
|
80
|
+
'.olusa-btn:focus-visible{outline:var(--dsw-focus-ring-width,2px) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:2px}',
|
|
81
|
+
'.olusa-btn:disabled{opacity:.5;cursor:not-allowed}',
|
|
82
|
+
'.olusa-btn-primary{background:var(--dsw-alias-button-primary-fill);border-color:transparent;color:var(--dsw-alias-label-primary-foreground)}',
|
|
83
|
+
'.olusa-btn-primary:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}',
|
|
84
|
+
'.olusa-btn-primary:active:not(:disabled){background:var(--dsw-alias-button-primary-hover)}',
|
|
85
|
+
'.olusa-btn-danger{color:var(--dsw-alias-state-error-primary)}',
|
|
86
|
+
'.olusa-btn-danger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger)}',
|
|
87
|
+
'.olusa-error{margin:0;font:var(--dsw-font-xxs-12);color:var(--dsw-alias-state-error-primary);line-height:18px;word-break:break-word}',
|
|
88
|
+
'.olusa-state{display:inline-flex;align-items:center;align-self:flex-start;padding:2px 8px;border-radius:999px;font:var(--dsw-font-xxxs-strong-11);font-weight:500;max-width:100%}',
|
|
89
|
+
'.olusa-state[data-tone=ok]{background:var(--dsw-alias-state-success-tertiary);color:var(--dsw-alias-state-success-primary);border:1px solid color-mix(in srgb,var(--dsw-alias-state-success-primary) 20%,transparent)}',
|
|
90
|
+
'.olusa-state[data-tone=err]{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent);color:var(--dsw-alias-state-error-primary);border:1px solid color-mix(in srgb,var(--dsw-alias-state-error-primary) 20%,transparent)}',
|
|
91
|
+
'.olusa-loading{display:flex;flex-direction:column;gap:8px}',
|
|
92
|
+
'.olusa-skeleton{height:10px;border-radius:var(--dsw-radius-xs);background:var(--dsw-alias-bg-skeleton)}',
|
|
93
|
+
'.olusa-skeleton.is-wide{width:56%}',
|
|
94
|
+
|
|
95
|
+
// ── 侧栏双横条 ──────────────────────────────────────────────
|
|
96
|
+
// 侧栏底部空间窄:用 5px 圆点把标签和填充色绑定,数值右对齐,进度条压到 5px;
|
|
97
|
+
// 填充色由行内 --olusa-fill 注入,达到 70%/90% 阈值时切换警告/错误语义色,
|
|
98
|
+
// 与面板里的用量配色保持一致。
|
|
99
|
+
'.olusa-bars{display:flex;flex-direction:column;gap:10px;padding:0;width:120px;min-width:120px;flex-shrink:0;box-sizing:border-box;background:transparent;border:none}',
|
|
100
|
+
'.olusa-barsrow{display:flex;flex-direction:column;gap:4px;min-width:0}',
|
|
101
|
+
'.olusa-barshead{display:flex;align-items:center;justify-content:space-between;gap:8px;min-width:0}',
|
|
102
|
+
'.olusa-barslabel{display:inline-flex;align-items:center;gap:5px;min-width:0;font:var(--dsw-font-xxxs-11);color:var(--dsw-alias-label-secondary);white-space:nowrap}',
|
|
103
|
+
'.olusa-barsdot{width:5px;height:5px;border-radius:999px;flex:none;background:var(--olusa-fill,var(--dsw-alias-brand-primary))}',
|
|
104
|
+
'.olusa-barsval{font:var(--dsw-font-xxxs-strong-11);font-weight:600;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-primary);white-space:nowrap;min-width:34px;text-align:right}',
|
|
105
|
+
'.olusa-minibar{height:5px;border-radius:999px;background:color-mix(in srgb,var(--dsw-alias-label-primary) 8%,transparent);overflow:hidden}',
|
|
106
|
+
'.olusa-minibarfill{height:100%;border-radius:999px;background:var(--olusa-fill,var(--dsw-alias-brand-primary));transition:width .3s cubic-bezier(.2,.7,.3,1),background-color .2s ease}',
|
|
107
|
+
'.olusa-barsrow[data-tone=warn] .olusa-barsval{color:var(--dsw-alias-state-warn-primary)}',
|
|
108
|
+
'.olusa-barsrow[data-tone=error] .olusa-barsval{color:var(--dsw-alias-state-error-primary)}',
|
|
109
|
+
'.olusa-barsempty{display:inline-flex;align-items:center;gap:6px;font:var(--dsw-font-xxxs-11);color:var(--dsw-alias-label-tertiary);white-space:nowrap}',
|
|
110
|
+
'.olusa-barsempty::before{content:"";width:5px;height:5px;border-radius:999px;flex:none;background:var(--dsw-alias-state-idle-primary)}',
|
|
111
|
+
|
|
112
|
+
'@media (prefers-reduced-motion:reduce){.olusa-barfill,.olusa-minibarfill{transition:none}}',
|
|
68
113
|
].join('\n')
|
|
69
114
|
|
|
70
115
|
function injectCss() {
|
|
71
116
|
if (typeof document === 'undefined') return
|
|
72
117
|
if (document.querySelector('style[data-plugin-css="' + CSS_TAG_ID + '"]') !== null) return
|
|
73
118
|
const tag = document.createElement('style')
|
|
119
|
+
tag.dataset.plugin = 'dsh-ollama-usage'
|
|
74
120
|
tag.dataset.pluginCss = CSS_TAG_ID
|
|
75
121
|
tag.textContent = CSS
|
|
76
122
|
;(document.head || document.documentElement).appendChild(tag)
|
|
@@ -78,10 +124,10 @@ window.__ModuleLoader__.load({
|
|
|
78
124
|
|
|
79
125
|
const pct = (v) => (typeof v === 'number' && Number.isFinite(v) ? Math.round(v * 10) / 10 : null)
|
|
80
126
|
const colorFor = (p) => {
|
|
81
|
-
if (p === null) return 'var(--dsw-alias-label-secondary
|
|
82
|
-
if (p < 70) return 'var(--dsw-alias-state-success-primary
|
|
83
|
-
if (p < 90) return 'var(--dsw-alias-state-warn-primary
|
|
84
|
-
return 'var(--dsw-alias-state-error-primary
|
|
127
|
+
if (p === null) return 'var(--dsw-alias-label-secondary)'
|
|
128
|
+
if (p < 70) return 'var(--dsw-alias-state-success-primary)'
|
|
129
|
+
if (p < 90) return 'var(--dsw-alias-state-warn-primary)'
|
|
130
|
+
return 'var(--dsw-alias-state-error-primary)'
|
|
85
131
|
}
|
|
86
132
|
const fmtTime = (iso) => { try {
|
|
87
133
|
const d = new Date(iso)
|
|
@@ -137,7 +183,7 @@ window.__ModuleLoader__.load({
|
|
|
137
183
|
const p = pct(props.percent)
|
|
138
184
|
const width = p === null ? 0 : Math.min(100, Math.max(0, p))
|
|
139
185
|
const remaining = p === null ? null : Math.round((100 - p) * 10) / 10
|
|
140
|
-
return React.createElement('div', { className: 'olusa-
|
|
186
|
+
return React.createElement('div', { className: 'olusa-barblock' },
|
|
141
187
|
React.createElement('div', { className: 'olusa-barhead' },
|
|
142
188
|
React.createElement('span', { className: 'olusa-barlabel' }, props.label),
|
|
143
189
|
React.createElement('span', { className: 'olusa-barval' }, p === null ? '暂无数据' : ('已用 ' + p + '% · 剩余 ' + remaining + '%'))
|
|
@@ -154,7 +200,8 @@ window.__ModuleLoader__.load({
|
|
|
154
200
|
React.createElement('div', { className: 'olusa-login-title' }, '尚未登录 Ollama 账号'),
|
|
155
201
|
React.createElement('ol', { className: 'olusa-steps' },
|
|
156
202
|
React.createElement('li', null,
|
|
157
|
-
|
|
203
|
+
'打开 ',
|
|
204
|
+
React.createElement('a', { className: 'olusa-link', href: 'https://ollama.com/settings', target: '_blank', rel: 'noreferrer' }, 'ollama.com/settings'),
|
|
158
205
|
' 并登录(或注册)Ollama 账号'
|
|
159
206
|
),
|
|
160
207
|
React.createElement('li', null,
|
|
@@ -162,14 +209,14 @@ window.__ModuleLoader__.load({
|
|
|
162
209
|
React.createElement('a', { className: 'olusa-link', href: 'https://ollama.com/settings/keys', target: '_blank', rel: 'noreferrer' }, 'settings/keys'),
|
|
163
210
|
' 创建一个 API Key'
|
|
164
211
|
),
|
|
165
|
-
React.createElement('li', null, '把 Key
|
|
212
|
+
React.createElement('li', null, '把 Key 粘贴到下方点击「检测」;或先在终端运行 ollama signin / ollama login,再点「重新检测本地凭证」')
|
|
166
213
|
),
|
|
167
214
|
React.createElement('input', { className: 'olusa-input', placeholder: '粘贴 API Key', value: props.tokenInput, onChange: (e) => props.onInput(e.target.value), spellCheck: false }),
|
|
168
215
|
React.createElement('div', { className: 'olusa-rowbtns' },
|
|
169
216
|
React.createElement('button', { className: 'olusa-btn olusa-btn-primary', disabled: props.busy || !props.tokenInput.trim(), onClick: () => props.onCheck(props.tokenInput.trim()) }, props.busy ? '检测中…' : '检测'),
|
|
170
217
|
React.createElement('button', { className: 'olusa-btn', disabled: props.busy, onClick: () => props.onRetryLocal() }, '重新检测本地凭证')
|
|
171
218
|
),
|
|
172
|
-
React.createElement('div', { className: 'olusa-hint' }, '
|
|
219
|
+
React.createElement('div', { className: 'olusa-hint' }, '检测成功后,Key 与用量快照会保存到 $DSH_HOME/storages/ollama-usage/usage.json(权限 600),跨对话 / 重启自动恢复并每 10 分钟自动刷新。')
|
|
173
220
|
)
|
|
174
221
|
}
|
|
175
222
|
|
|
@@ -180,34 +227,49 @@ window.__ModuleLoader__.load({
|
|
|
180
227
|
const models = Array.isArray(d.models) ? d.models.slice(0, 5) : []
|
|
181
228
|
const history = Array.isArray(props.history) ? props.history.slice(-5).reverse() : []
|
|
182
229
|
return React.createElement('div', { className: 'olusa-block' },
|
|
183
|
-
React.createElement(BarRow, { label: '会话用量(每 5 小时)', percent: d.sessionPercent,
|
|
230
|
+
React.createElement(BarRow, { label: '会话用量(每 5 小时)', percent: d.sessionPercent, resetText: sessionResetText ? '还有 ' + sessionResetText + ' 重置' : null }),
|
|
184
231
|
React.createElement(BarRow, { label: '本周用量(Weekly)', percent: d.weeklyPercent, resetText: weeklyResetText ? '还有 ' + weeklyResetText + ' 重置' : null }),
|
|
185
232
|
(models.length > 0
|
|
186
|
-
? React.createElement('div', { className: 'olusa-
|
|
187
|
-
React.createElement('div', { className: 'olusa-
|
|
188
|
-
|
|
189
|
-
React.createElement('
|
|
190
|
-
|
|
191
|
-
|
|
233
|
+
? React.createElement('div', { className: 'olusa-sec' },
|
|
234
|
+
React.createElement('div', { className: 'olusa-sec-title' }, '模型请求数(本周)'),
|
|
235
|
+
React.createElement('div', { className: 'olusa-list' },
|
|
236
|
+
models.map((m) => React.createElement('div', { className: 'olusa-row', key: m.name },
|
|
237
|
+
React.createElement('span', { className: 'olusa-rowname', title: m.name }, m.name),
|
|
238
|
+
React.createElement('span', { className: 'olusa-rowval' }, String(m.requestCount) + ' 次')
|
|
239
|
+
)))
|
|
240
|
+
)
|
|
192
241
|
: null),
|
|
193
242
|
(history.length > 0
|
|
194
|
-
? React.createElement('div', { className: 'olusa-
|
|
195
|
-
React.createElement('div', { className: 'olusa-
|
|
196
|
-
|
|
197
|
-
React.createElement('
|
|
198
|
-
|
|
199
|
-
|
|
243
|
+
? React.createElement('div', { className: 'olusa-sec' },
|
|
244
|
+
React.createElement('div', { className: 'olusa-sec-title' }, '最近记录(会话用量)'),
|
|
245
|
+
React.createElement('div', { className: 'olusa-list' },
|
|
246
|
+
history.map((h, i) => React.createElement('div', { className: 'olusa-row', key: String(i) },
|
|
247
|
+
React.createElement('span', { className: 'olusa-rowname' }, fmtTime(h.at)),
|
|
248
|
+
React.createElement('span', { className: 'olusa-rowval' }, (h.sessionPercent === null || h.sessionPercent === undefined) ? '—' : (h.sessionPercent + '%'))
|
|
249
|
+
)))
|
|
250
|
+
)
|
|
200
251
|
: null),
|
|
201
252
|
(props.updatedAt
|
|
202
253
|
? React.createElement('div', { className: 'olusa-hint' }, '上次更新:' + fmtTime(props.updatedAt) + '(每 10 分钟自动刷新)')
|
|
203
254
|
: null),
|
|
204
|
-
React.createElement('div', {
|
|
205
|
-
|
|
255
|
+
React.createElement('div', {
|
|
256
|
+
className: 'olusa-state',
|
|
257
|
+
'data-tone': props.persisted ? 'ok' : 'err',
|
|
258
|
+
title: props.persisted ? '$DSH_HOME/storages/ollama-usage/usage.json' : undefined,
|
|
259
|
+
}, props.persisted ? '✓ 已持久化到本地' : '⚠ 未能写入本地文件')
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** 加载骨架:首屏 snapshot 落定前占位,避免登录表单闪现。 */
|
|
264
|
+
function LoadingBody() {
|
|
265
|
+
return React.createElement('div', { className: 'olusa-loading', role: 'status', 'aria-live': 'polite' },
|
|
266
|
+
React.createElement('div', { className: 'olusa-skeleton is-wide' }),
|
|
267
|
+
React.createElement('div', { className: 'olusa-skeleton' })
|
|
206
268
|
)
|
|
207
269
|
}
|
|
208
270
|
|
|
209
271
|
function OllamaUsagePanel() {
|
|
210
|
-
const [phase, setPhase] = React.useState('
|
|
272
|
+
const [phase, setPhase] = React.useState('loading')
|
|
211
273
|
const [data, setData] = React.useState(null)
|
|
212
274
|
const [history, setHistory] = React.useState(null)
|
|
213
275
|
const [updatedAt, setUpdatedAt] = React.useState(null)
|
|
@@ -224,7 +286,8 @@ window.__ModuleLoader__.load({
|
|
|
224
286
|
setUpdatedAt(v.updatedAt || null)
|
|
225
287
|
if (typeof v.persisted === 'boolean') setPersisted(v.persisted)
|
|
226
288
|
setError('')
|
|
227
|
-
|
|
289
|
+
// 无 usage 快照时留在登录引导,避免“无错误却显示查询失败”的 usage/error 分支。
|
|
290
|
+
setPhase(v.usage ? 'usage' : 'login')
|
|
228
291
|
} else if (res && res.error && (res.error.code === 'no-token' || res.error.code === 'no-data')) {
|
|
229
292
|
setError('')
|
|
230
293
|
setPhase('login')
|
|
@@ -258,44 +321,54 @@ window.__ModuleLoader__.load({
|
|
|
258
321
|
setData(res.value.usage); setHistory(res.value.history || null); setUpdatedAt(res.value.updatedAt || null); setError(''); setPhase('usage')
|
|
259
322
|
// snapshot 数据读自磁盘文件(插件无内存缓存),必然已持久化;服务端字段优先,缺省按不变式置 true
|
|
260
323
|
setPersisted(typeof res.value.persisted === 'boolean' ? res.value.persisted : true)
|
|
324
|
+
} else {
|
|
325
|
+
// 首屏无数据(未登录 / 无快照)落到登录引导;已有数据时保持当前视图。
|
|
326
|
+
setPhase((current) => (current === 'loading' ? 'login' : current))
|
|
261
327
|
}
|
|
262
|
-
}).catch(() => {
|
|
328
|
+
}).catch(() => {
|
|
329
|
+
if (!disposed) setPhase((current) => (current === 'loading' ? 'login' : current))
|
|
330
|
+
})
|
|
263
331
|
refresh()
|
|
264
332
|
const dispose = ctx.interval(refresh, 60000)
|
|
265
333
|
return () => { disposed = true; dispose() }
|
|
266
334
|
}, [])
|
|
267
335
|
|
|
268
|
-
const body = phase === '
|
|
269
|
-
? React.createElement(
|
|
270
|
-
: phase === '
|
|
271
|
-
? React.createElement(
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
React.createElement('
|
|
336
|
+
const body = phase === 'loading'
|
|
337
|
+
? React.createElement(LoadingBody)
|
|
338
|
+
: phase === 'login'
|
|
339
|
+
? React.createElement(LoginBody, { tokenInput: tokenInput, onInput: setTokenInput, onCheck: doCheck, onRetryLocal: () => doCheck(''), busy: busy })
|
|
340
|
+
: phase === 'usage' && data
|
|
341
|
+
? React.createElement('div', { className: 'olusa-block' },
|
|
342
|
+
React.createElement(UsageBody, { data: data, history: history, updatedAt: updatedAt, persisted: persisted }),
|
|
343
|
+
(error ? React.createElement('div', { className: 'olusa-error' }, String(error)) : null),
|
|
344
|
+
React.createElement('div', { className: 'olusa-rowbtns' },
|
|
345
|
+
React.createElement('button', { className: 'olusa-btn olusa-btn-danger', disabled: busy, onClick: () => doForget() }, '清除已存 Key')
|
|
346
|
+
)
|
|
276
347
|
)
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
348
|
+
: React.createElement('div', { className: 'olusa-block' },
|
|
349
|
+
React.createElement('div', { className: 'olusa-error' }, String(error || '查询失败')),
|
|
350
|
+
React.createElement('div', { className: 'olusa-hint' },
|
|
351
|
+
'请确认已登录:',
|
|
352
|
+
React.createElement('a', { className: 'olusa-link', href: 'https://ollama.com/settings', target: '_blank', rel: 'noreferrer' }, 'ollama.com/settings'),
|
|
353
|
+
',或粘贴 API Key 重试。'
|
|
354
|
+
),
|
|
355
|
+
React.createElement('input', { className: 'olusa-input', placeholder: '粘贴 API Key', value: tokenInput, onChange: (e) => setTokenInput(e.target.value), spellCheck: false }),
|
|
356
|
+
React.createElement('div', { className: 'olusa-rowbtns' },
|
|
357
|
+
React.createElement('button', { className: 'olusa-btn olusa-btn-primary', disabled: busy || !tokenInput.trim(), onClick: () => doCheck(tokenInput.trim()) }, busy ? '检测中…' : '检测'),
|
|
358
|
+
React.createElement('button', { className: 'olusa-btn', disabled: busy, onClick: () => doCheck('') }, '重试')
|
|
359
|
+
)
|
|
289
360
|
)
|
|
290
|
-
)
|
|
291
361
|
|
|
292
362
|
return React.createElement('div', { className: 'olusa-panel' },
|
|
293
363
|
React.createElement('div', { className: 'olusa-head' },
|
|
294
|
-
React.createElement('
|
|
364
|
+
React.createElement('div', { className: 'olusa-head-main' },
|
|
365
|
+
React.createElement('span', { className: 'olusa-title' }, '⚡ Ollama 用量'),
|
|
366
|
+
React.createElement('span', { className: 'olusa-sub' }, '会话 5h · 周用量 · 每 10 分钟自动刷新')
|
|
367
|
+
),
|
|
295
368
|
React.createElement('button', { className: 'olusa-btn', disabled: busy, onClick: () => doCheck('') }, busy ? '查询中…' : '刷新')
|
|
296
369
|
),
|
|
297
370
|
body,
|
|
298
|
-
React.createElement('div', { className: 'olusa-ver' }, 'dsh-ollama-usage · v0.1.
|
|
371
|
+
React.createElement('div', { className: 'olusa-ver' }, 'dsh-ollama-usage · v0.1.14')
|
|
299
372
|
)
|
|
300
373
|
}
|
|
301
374
|
|
|
@@ -322,23 +395,62 @@ window.__ModuleLoader__.load({
|
|
|
322
395
|
|
|
323
396
|
const sp = data && typeof data.sessionPercent === 'number' ? Math.min(100, Math.max(0, data.sessionPercent)) : null
|
|
324
397
|
const wp = data && typeof data.weeklyPercent === 'number' ? Math.min(100, Math.max(0, data.weeklyPercent)) : null
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
398
|
+
const fmtPct = (p) => {
|
|
399
|
+
if (p === null) return '—'
|
|
400
|
+
const v = Math.round(p * 10) / 10
|
|
401
|
+
return (Number.isInteger(v) ? String(v) : v.toFixed(1)) + '%'
|
|
402
|
+
}
|
|
403
|
+
const resetNote = (iso) => {
|
|
404
|
+
const text = iso ? fmtCountdown(iso) : null
|
|
405
|
+
return text ? ' · ' + text + '后重置' : ''
|
|
406
|
+
}
|
|
407
|
+
const toneOf = (p) => (p !== null && p >= 90 ? 'error' : p !== null && p >= 70 ? 'warn' : undefined)
|
|
408
|
+
const fillOf = (p, base) => (p === null
|
|
409
|
+
? 'var(--dsw-alias-state-idle-primary)'
|
|
410
|
+
: p >= 90
|
|
411
|
+
? 'var(--dsw-alias-state-error-primary)'
|
|
412
|
+
: p >= 70
|
|
413
|
+
? 'var(--dsw-alias-state-warn-primary)'
|
|
414
|
+
: base)
|
|
415
|
+
// 极小百分比也要留下可见的圆头:0.2% 在 120px 容器里不足 1px。
|
|
416
|
+
const widthOf = (p) => (p === null || p <= 0 ? 0 : Math.max(p, 2))
|
|
417
|
+
const tip = sp === null && wp === null
|
|
418
|
+
? 'Ollama 用量:未登录,点击插件列表中的 dsh-ollama-usage 完成登录'
|
|
419
|
+
: 'Ollama 用量 · 会话 5h ' + fmtPct(sp) + resetNote(data && data.sessionResetAt) +
|
|
420
|
+
' · 本周 ' + fmtPct(wp) + resetNote(data && data.weeklyResetAt) +
|
|
421
|
+
(updatedAt ? ' · 更新于 ' + fmtTime(updatedAt) : '')
|
|
328
422
|
const rows = [
|
|
329
|
-
{ label: '会话5h', p: sp,
|
|
330
|
-
{ label: '周用量', p: wp,
|
|
423
|
+
{ label: '会话 5h', aria: '会话 5 小时用量', p: sp, base: SESSION_COLOR, resetAt: data && data.sessionResetAt },
|
|
424
|
+
{ label: '周用量', aria: '本周用量', p: wp, base: WEEKLY_COLOR, resetAt: data && data.weeklyResetAt },
|
|
331
425
|
]
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
React.createElement('
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
426
|
+
if (sp === null && wp === null) {
|
|
427
|
+
return React.createElement('div', { className: 'olusa-bars', title: tip, role: 'group', 'aria-label': 'Ollama 用量' },
|
|
428
|
+
React.createElement('span', { className: 'olusa-barsempty' }, 'Ollama 未登录'))
|
|
429
|
+
}
|
|
430
|
+
return React.createElement('div', { className: 'olusa-bars', title: tip, role: 'group', 'aria-label': 'Ollama Cloud 用量' },
|
|
431
|
+
rows.map((b) => {
|
|
432
|
+
const tone = toneOf(b.p)
|
|
433
|
+
const fill = fillOf(b.p, b.base)
|
|
434
|
+
return React.createElement('div', { className: 'olusa-barsrow', key: b.label, 'data-tone': tone },
|
|
435
|
+
React.createElement('div', { className: 'olusa-barshead' },
|
|
436
|
+
React.createElement('span', { className: 'olusa-barslabel' },
|
|
437
|
+
React.createElement('span', { className: 'olusa-barsdot', style: { '--olusa-fill': fill } }),
|
|
438
|
+
b.label
|
|
439
|
+
),
|
|
440
|
+
React.createElement('span', { className: 'olusa-barsval' }, fmtPct(b.p))
|
|
441
|
+
),
|
|
442
|
+
React.createElement('div', {
|
|
443
|
+
className: 'olusa-minibar',
|
|
444
|
+
role: 'progressbar',
|
|
445
|
+
'aria-label': b.aria,
|
|
446
|
+
'aria-valuemin': 0,
|
|
447
|
+
'aria-valuemax': 100,
|
|
448
|
+
'aria-valuenow': b.p === null ? 0 : Math.round(b.p),
|
|
449
|
+
},
|
|
450
|
+
React.createElement('div', { className: 'olusa-minibarfill', style: { width: widthOf(b.p) + '%', '--olusa-fill': fill } })
|
|
451
|
+
)
|
|
340
452
|
)
|
|
341
|
-
)
|
|
453
|
+
})
|
|
342
454
|
)
|
|
343
455
|
}
|
|
344
456
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-ollama-usage",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
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",
|