dsh-opencode-go-usage 1.1.1 → 1.2.2
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 +2 -0
- package/README.zh.md +2 -0
- package/lib/client.js +55 -33
- package/lib/index.js +92 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
English | [中文](README.zh.md)
|
|
4
4
|
|
|
5
|
+
> ⚡ Get an **OpenCode GO plan**: [buycodingplan.com](https://buycodingplan.com/)
|
|
6
|
+
|
|
5
7
|
A [DSH](https://github.com/deepseek-ai/deepseek-harness) (DeepSeek Harness) plugin that watches your **OpenCode GO plan** quota — the $10/month subscription that gives you usage limits on open-source models (rolling 5-hour, weekly, and monthly windows).
|
|
6
8
|
|
|
7
9
|
## Features
|
package/README.zh.md
CHANGED
package/lib/client.js
CHANGED
|
@@ -25,19 +25,23 @@ window.__ModuleLoader__.load({
|
|
|
25
25
|
tag.dataset.plugin = "dsh-opencode-go-usage";
|
|
26
26
|
tag.dataset.pluginCss = CSS_ID;
|
|
27
27
|
tag.textContent = [
|
|
28
|
-
".ocg-widget{box-sizing:border-box;width:100%;min-width:0;padding:
|
|
28
|
+
".ocg-widget{box-sizing:border-box;width:100%;min-width:0;padding:6px 6px 4px;display:flex;flex-direction:column;gap:4px;border-top:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary)}",
|
|
29
29
|
".ocg-widget:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
30
30
|
".ocg-head{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:12px;font-weight:600;line-height:18px}",
|
|
31
31
|
".ocg-refresh{cursor:pointer;border:none;background:none;padding:0;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:18px}",
|
|
32
32
|
".ocg-refresh:hover{color:var(--dsw-alias-label-primary)}",
|
|
33
33
|
".ocg-row{display:flex;flex-direction:column;gap:3px;min-width:0}",
|
|
34
|
-
".ocg-row-label{display:flex;justify-content:space-between;gap:8px;font-size:11px;line-height:
|
|
34
|
+
".ocg-row-label{display:flex;justify-content:space-between;gap:8px;font-size:11px;line-height:15px;color:var(--dsw-alias-label-secondary)}",
|
|
35
35
|
".ocg-row-label b{font-weight:500;color:var(--dsw-alias-label-primary)}",
|
|
36
|
+
".ocg-meta{display:flex;align-items:center;gap:6px;min-width:0}",
|
|
37
|
+
".ocg-meta .ocg-track{flex:1;min-width:0}",
|
|
38
|
+
".ocg-meta>span{font-size:10px;line-height:14px;color:var(--dsw-alias-label-secondary);white-space:nowrap;flex:none}",
|
|
36
39
|
".ocg-track{box-sizing:border-box;height:5px;border-radius:3px;background:var(--dsw-alias-border-l2);overflow:hidden}",
|
|
37
40
|
".ocg-fill{height:100%;border-radius:3px;transition:width .4s ease}",
|
|
38
41
|
".ocg-err{font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
39
42
|
".ocg-rail{box-sizing:border-box;width:36px;height:36px;display:flex;align-items:center;justify-content:center;border-radius:8px;font-size:10px;font-weight:700;color:var(--dsw-alias-label-primary);cursor:default}",
|
|
40
|
-
".ocg-rail:hover{background:var(--dsw-alias-interactive-bg-hover)}"
|
|
43
|
+
".ocg-rail:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
44
|
+
".ocg-update{display:inline-block;padding:1px 6px;border-radius:8px;font-size:10px;line-height:16px;font-weight:600;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover);text-decoration:none;cursor:pointer;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:110px}"
|
|
41
45
|
].join("");
|
|
42
46
|
document.head.appendChild(tag);
|
|
43
47
|
}
|
|
@@ -48,31 +52,33 @@ window.__ModuleLoader__.load({
|
|
|
48
52
|
"window.rolling": "滚动窗口 (5h)",
|
|
49
53
|
"window.weekly": "周窗口",
|
|
50
54
|
"window.monthly": "月窗口",
|
|
51
|
-
"window.rolling.hint": "
|
|
52
|
-
"status.ok": "正常",
|
|
55
|
+
"window.rolling.hint": "5h",
|
|
53
56
|
"time.soon": "即将重置",
|
|
54
|
-
"time.minutes": "{n}
|
|
55
|
-
"time.hours": "{n}
|
|
56
|
-
"time.days": "{n}
|
|
57
|
+
"time.minutes": "{n}分",
|
|
58
|
+
"time.hours": "{n}小时{m}分",
|
|
59
|
+
"time.days": "{n}天{m}小时",
|
|
57
60
|
"refresh": "刷新",
|
|
58
61
|
"loading": "加载中…",
|
|
59
62
|
"rail.title": "OpenCode GO 月用量 {pct}%",
|
|
60
|
-
"rail.plain": "OpenCode GO"
|
|
63
|
+
"rail.plain": "OpenCode GO",
|
|
64
|
+
"update.available": "新版本 v{v}",
|
|
65
|
+
"update.title": "点击查看升级说明"
|
|
61
66
|
};
|
|
62
67
|
const en = {
|
|
63
68
|
"window.rolling": "Rolling (5h)",
|
|
64
69
|
"window.weekly": "Weekly",
|
|
65
70
|
"window.monthly": "Monthly",
|
|
66
|
-
"window.rolling.hint": "
|
|
67
|
-
"status.ok": "OK",
|
|
71
|
+
"window.rolling.hint": "5h",
|
|
68
72
|
"time.soon": "resets soon",
|
|
69
|
-
"time.minutes": "
|
|
70
|
-
"time.hours": "
|
|
71
|
-
"time.days": "
|
|
73
|
+
"time.minutes": "{n}m",
|
|
74
|
+
"time.hours": "{n}h{m}m",
|
|
75
|
+
"time.days": "{n}d{m}h",
|
|
72
76
|
"refresh": "Refresh",
|
|
73
77
|
"loading": "Loading…",
|
|
74
78
|
"rail.title": "OpenCode GO monthly {pct}%",
|
|
75
|
-
"rail.plain": "OpenCode GO"
|
|
79
|
+
"rail.plain": "OpenCode GO",
|
|
80
|
+
"update.available": "v{v} available",
|
|
81
|
+
"update.title": "Click for upgrade instructions"
|
|
76
82
|
};
|
|
77
83
|
|
|
78
84
|
// ── data helpers ──
|
|
@@ -95,11 +101,11 @@ window.__ModuleLoader__.load({
|
|
|
95
101
|
return t("time.days", { n: Math.floor(hours / 24), m: hours % 24 });
|
|
96
102
|
}
|
|
97
103
|
|
|
98
|
-
/** Fill color by usage percent. */
|
|
104
|
+
/** Fill color by usage percent — themed alias tokens (light/dark safe). */
|
|
99
105
|
function fillColor(percent) {
|
|
100
|
-
if (percent >= 90) return "
|
|
101
|
-
if (percent >= 70) return "
|
|
102
|
-
return "
|
|
106
|
+
if (percent >= 90) return "var(--dsw-alias-state-error-primary)";
|
|
107
|
+
if (percent >= 70) return "var(--dsw-alias-state-warn-primary)";
|
|
108
|
+
return "var(--dsw-alias-state-success-primary)";
|
|
103
109
|
}
|
|
104
110
|
|
|
105
111
|
// ── widget component ──
|
|
@@ -159,28 +165,34 @@ window.__ModuleLoader__.load({
|
|
|
159
165
|
rows.push(react_jsx_runtime.jsx("div", {
|
|
160
166
|
className: "ocg-row",
|
|
161
167
|
children: [
|
|
168
|
+
// Name + percentage on one line (percentage right-aligned).
|
|
162
169
|
react_jsx_runtime.jsx("div", {
|
|
163
170
|
className: "ocg-row-label",
|
|
164
171
|
children: [
|
|
165
172
|
react_jsx_runtime.jsx("b", { children: t(w.labelKey) }),
|
|
166
|
-
react_jsx_runtime.jsx("
|
|
173
|
+
react_jsx_runtime.jsx("b", { children: win.percent + "%" })
|
|
167
174
|
]
|
|
168
175
|
}),
|
|
176
|
+
// Track + countdown on one line (track flexes, countdown
|
|
177
|
+
// right-aligned small text; status replaces it when not ok).
|
|
169
178
|
react_jsx_runtime.jsx("div", {
|
|
170
|
-
className: "ocg-
|
|
171
|
-
children: react_jsx_runtime.jsx("div", {
|
|
172
|
-
className: "ocg-fill",
|
|
173
|
-
style: {
|
|
174
|
-
width: Math.max(0, Math.min(100, win.percent)) + "%",
|
|
175
|
-
background: fillColor(win.percent)
|
|
176
|
-
}
|
|
177
|
-
})
|
|
178
|
-
}),
|
|
179
|
-
react_jsx_runtime.jsx("div", {
|
|
180
|
-
className: "ocg-row-label",
|
|
179
|
+
className: "ocg-meta",
|
|
181
180
|
children: [
|
|
182
|
-
react_jsx_runtime.jsx("
|
|
183
|
-
|
|
181
|
+
react_jsx_runtime.jsx("div", {
|
|
182
|
+
className: "ocg-track",
|
|
183
|
+
children: react_jsx_runtime.jsx("div", {
|
|
184
|
+
className: "ocg-fill",
|
|
185
|
+
style: {
|
|
186
|
+
width: Math.max(0, Math.min(100, win.percent)) + "%",
|
|
187
|
+
background: fillColor(win.percent)
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
}),
|
|
191
|
+
react_jsx_runtime.jsx("span", {
|
|
192
|
+
children: win.status === "ok"
|
|
193
|
+
? (win.resetsAt ? timeUntil(win.resetsAt, t) : (w.hintKey ? t(w.hintKey) : ""))
|
|
194
|
+
: win.status
|
|
195
|
+
})
|
|
184
196
|
]
|
|
185
197
|
})
|
|
186
198
|
]
|
|
@@ -194,6 +206,16 @@ window.__ModuleLoader__.load({
|
|
|
194
206
|
className: "ocg-head",
|
|
195
207
|
children: [
|
|
196
208
|
react_jsx_runtime.jsx("span", { children: "OpenCode GO" }),
|
|
209
|
+
(data && data.update && data.update.available && data.update.latest
|
|
210
|
+
? react_jsx_runtime.jsx("a", {
|
|
211
|
+
className: "ocg-update",
|
|
212
|
+
href: "https://www.npmjs.com/package/dsh-opencode-go-usage",
|
|
213
|
+
target: "_blank",
|
|
214
|
+
rel: "noreferrer",
|
|
215
|
+
title: t("update.title"),
|
|
216
|
+
children: t("update.available", { v: data.update.latest })
|
|
217
|
+
})
|
|
218
|
+
: null),
|
|
197
219
|
react_jsx_runtime.jsx("button", {
|
|
198
220
|
className: "ocg-refresh",
|
|
199
221
|
onClick: () => setStamp(stamp + 1),
|
package/lib/index.js
CHANGED
|
@@ -23,9 +23,12 @@
|
|
|
23
23
|
* settings.
|
|
24
24
|
*
|
|
25
25
|
* Settings namespace `dsh-opencode-go-usage`:
|
|
26
|
-
* - apiKeyEnv:
|
|
27
|
-
* - baseUrl:
|
|
28
|
-
* - cacheMs:
|
|
26
|
+
* - apiKeyEnv: credential ref / env var for the API key (default OPENCODE_GO_API_KEY)
|
|
27
|
+
* - baseUrl: GO gateway base (default https://opencode.ai/zen/go)
|
|
28
|
+
* - cacheMs: host-side cache TTL (default 30000)
|
|
29
|
+
* - updateCheck: check npm for newer versions (default true); result shows
|
|
30
|
+
* in the widget and the command — the plugin never installs
|
|
31
|
+
* itself, upgrading stays an explicit user action.
|
|
29
32
|
*/
|
|
30
33
|
|
|
31
34
|
import { readFileSync } from "node:fs";
|
|
@@ -43,12 +46,43 @@ export const namespace = "dsh-opencode-go-usage";
|
|
|
43
46
|
const DEFAULT_BASE_URL = "https://opencode.ai/zen/go";
|
|
44
47
|
const DEFAULT_API_KEY_ENV = "OPENCODE_GO_API_KEY";
|
|
45
48
|
const DEFAULT_CACHE_MS = 30_000;
|
|
49
|
+
/** npm package name this plugin is published under (update checks). */
|
|
50
|
+
const NPM_PACKAGE = "dsh-opencode-go-usage";
|
|
51
|
+
/** How often the update check may hit the npm registry (ms). */
|
|
52
|
+
const UPDATE_CHECK_INTERVAL_MS = 24 * 3600_000;
|
|
53
|
+
|
|
54
|
+
/** The version this running copy was installed as (read once at load). */
|
|
55
|
+
const CURRENT_VERSION = readPackageVersion();
|
|
56
|
+
|
|
57
|
+
/** Read `version` from the installed package.json next to this file. */
|
|
58
|
+
function readPackageVersion() {
|
|
59
|
+
try {
|
|
60
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
61
|
+
if (typeof pkg?.version === "string") return pkg.version;
|
|
62
|
+
} catch {
|
|
63
|
+
// fall through
|
|
64
|
+
}
|
|
65
|
+
return "0.0.0";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Compare two `x.y.z` version strings; returns true when `a` is newer than `b`. */
|
|
69
|
+
function isNewer(a, b) {
|
|
70
|
+
const pa = String(a).split("-")[0].split(".").map(Number);
|
|
71
|
+
const pb = String(b).split("-")[0].split(".").map(Number);
|
|
72
|
+
for (let i = 0; i < 3; i++) {
|
|
73
|
+
const na = pa[i] ?? 0;
|
|
74
|
+
const nb = pb[i] ?? 0;
|
|
75
|
+
if (na !== nb) return na > nb;
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
46
79
|
|
|
47
80
|
/** Settings schema for this plugin's namespace. */
|
|
48
81
|
export const Config = z.object({
|
|
49
82
|
apiKeyEnv: z.string().default(DEFAULT_API_KEY_ENV),
|
|
50
83
|
baseUrl: z.string().default(DEFAULT_BASE_URL),
|
|
51
|
-
cacheMs: z.number().default(DEFAULT_CACHE_MS)
|
|
84
|
+
cacheMs: z.number().default(DEFAULT_CACHE_MS),
|
|
85
|
+
updateCheck: z.boolean().default(true)
|
|
52
86
|
});
|
|
53
87
|
|
|
54
88
|
/**
|
|
@@ -150,10 +184,50 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
150
184
|
apiKeyEnv: stored.apiKeyEnv,
|
|
151
185
|
baseUrl: stored.baseUrl,
|
|
152
186
|
cacheMs: stored.cacheMs,
|
|
187
|
+
updateCheck: stored.updateCheck !== false,
|
|
153
188
|
locale
|
|
154
189
|
};
|
|
155
190
|
};
|
|
156
191
|
|
|
192
|
+
// ── update check (npm) ─────────────────────────────────────────────────
|
|
193
|
+
// Checks the registry at most once per UPDATE_CHECK_INTERVAL_MS; failures
|
|
194
|
+
// are silent. The plugin only reports — upgrading stays a user action
|
|
195
|
+
// (`dsh plugin --profile web add dsh-opencode-go-usage@latest`).
|
|
196
|
+
/** @type {{ at: number, latest: string | null } | null} */
|
|
197
|
+
let updateState = null;
|
|
198
|
+
const checkForUpdate = async (config) => {
|
|
199
|
+
const now = Date.now();
|
|
200
|
+
if (updateState !== null && now - updateState.at < UPDATE_CHECK_INTERVAL_MS) return updateState;
|
|
201
|
+
const snapshot = { at: now, latest: null };
|
|
202
|
+
try {
|
|
203
|
+
const res = await fetch(`https://registry.npmjs.org/${NPM_PACKAGE}/latest`, {
|
|
204
|
+
signal: AbortSignal.timeout(10_000)
|
|
205
|
+
});
|
|
206
|
+
if (res.ok) {
|
|
207
|
+
const json = await res.json();
|
|
208
|
+
if (typeof json?.version === "string" && isNewer(json.version, CURRENT_VERSION)) {
|
|
209
|
+
snapshot.latest = json.version;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
} catch {
|
|
213
|
+
// offline / registry hiccup — keep the previous result, refresh later
|
|
214
|
+
snapshot.at = updateState?.at ?? now;
|
|
215
|
+
snapshot.latest = updateState?.latest ?? null;
|
|
216
|
+
}
|
|
217
|
+
updateState = snapshot;
|
|
218
|
+
return snapshot;
|
|
219
|
+
};
|
|
220
|
+
const updateInfoOf = (config) => {
|
|
221
|
+
const st = updateState;
|
|
222
|
+
if (!config.updateCheck || st === null || st.latest === null) {
|
|
223
|
+
return { current: CURRENT_VERSION, available: false, latest: null };
|
|
224
|
+
}
|
|
225
|
+
return { current: CURRENT_VERSION, available: true, latest: st.latest };
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// Kick off the first check shortly after boot (non-blocking).
|
|
229
|
+
void checkForUpdate(readConfig()).catch(() => {});
|
|
230
|
+
|
|
157
231
|
// Host-side cache: one in-flight promise + a TTL, so several open tabs or
|
|
158
232
|
// the command never hammer the gateway.
|
|
159
233
|
/** @type {{ at: number, promise: Promise<unknown> } | null} */
|
|
@@ -180,8 +254,12 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
180
254
|
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
181
255
|
res.setHeader("cache-control", "no-store");
|
|
182
256
|
try {
|
|
257
|
+
const config = readConfig();
|
|
258
|
+
// Trigger a background refresh when the interval elapsed (never
|
|
259
|
+
// block the response on the registry).
|
|
260
|
+
void checkForUpdate(config).catch(() => {});
|
|
183
261
|
const data = await quotaOnce();
|
|
184
|
-
res.end(JSON.stringify(data));
|
|
262
|
+
res.end(JSON.stringify({ ...data, update: updateInfoOf(config) }));
|
|
185
263
|
} catch (error) {
|
|
186
264
|
res.statusCode = 502;
|
|
187
265
|
res.end(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
|
|
@@ -235,8 +313,16 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
235
313
|
description: "show OpenCode GO plan usage (rolling/weekly/monthly windows)",
|
|
236
314
|
handler: async () => {
|
|
237
315
|
try {
|
|
316
|
+
const config = readConfig();
|
|
238
317
|
const data = await quotaOnce();
|
|
239
|
-
|
|
318
|
+
let text = renderUsageText(data, config.locale);
|
|
319
|
+
const update = updateInfoOf(config);
|
|
320
|
+
if (update.available && update.latest !== null) {
|
|
321
|
+
text += config.locale === "en"
|
|
322
|
+
? `\n\nUpdate available: v${update.latest} — run \`dsh plugin --profile web add ${NPM_PACKAGE}@latest\` to upgrade.`
|
|
323
|
+
: `\n\n发现新版本 v${update.latest} — 执行 \`dsh plugin --profile web add ${NPM_PACKAGE}@latest\` 升级。`;
|
|
324
|
+
}
|
|
325
|
+
return { kind: "success", text };
|
|
240
326
|
} catch (error) {
|
|
241
327
|
return { kind: "error", text: error instanceof Error ? error.message : String(error) };
|
|
242
328
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-opencode-go-usage",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "DSH (DeepSeek Harness) plugin: OpenCode GO plan quota widget in the sidebar, same-origin usage proxy, and a /opencode-go chat command",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|