dsh-opencode-go-usage 1.0.1 → 1.1.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/lib/client.js +58 -20
- package/lib/index.js +22 -11
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
// (react / react/jsx-runtime are in the static table). It registers a widget
|
|
7
7
|
// into the sidebar's `sidebar.footer.action` list slot and polls the
|
|
8
8
|
// same-origin proxy route owned by the host half — the GO API key never
|
|
9
|
-
// enters the browser.
|
|
9
|
+
// enters the browser. UI strings are bilingual via the DSH locale service
|
|
10
|
+
// (dictionaries registered under the `dsh-opencode-go-usage` namespace).
|
|
10
11
|
window.__ModuleLoader__.load({
|
|
11
12
|
id: "dsh-opencode-go-usage",
|
|
12
13
|
factory: (require) => {
|
|
@@ -41,24 +42,57 @@ window.__ModuleLoader__.load({
|
|
|
41
42
|
document.head.appendChild(tag);
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
// ── locale dictionaries ──
|
|
46
|
+
const NS = "dsh-opencode-go-usage";
|
|
47
|
+
const zh = {
|
|
48
|
+
"window.rolling": "滚动窗口",
|
|
49
|
+
"window.weekly": "周窗口",
|
|
50
|
+
"window.monthly": "月窗口",
|
|
51
|
+
"window.rolling.hint": "5 小时",
|
|
52
|
+
"status.ok": "正常",
|
|
53
|
+
"time.soon": "即将重置",
|
|
54
|
+
"time.minutes": "{n} 分后重置",
|
|
55
|
+
"time.hours": "{n} 小时 {m} 分后",
|
|
56
|
+
"time.days": "{n} 天 {m} 小时后",
|
|
57
|
+
"refresh": "刷新",
|
|
58
|
+
"loading": "加载中…",
|
|
59
|
+
"rail.title": "OpenCode GO 月用量 {pct}%",
|
|
60
|
+
"rail.plain": "OpenCode GO"
|
|
61
|
+
};
|
|
62
|
+
const en = {
|
|
63
|
+
"window.rolling": "Rolling (5h)",
|
|
64
|
+
"window.weekly": "Weekly",
|
|
65
|
+
"window.monthly": "Monthly",
|
|
66
|
+
"window.rolling.hint": "5 hours",
|
|
67
|
+
"status.ok": "OK",
|
|
68
|
+
"time.soon": "resets soon",
|
|
69
|
+
"time.minutes": "in {n} min",
|
|
70
|
+
"time.hours": "in {n}h {m}m",
|
|
71
|
+
"time.days": "in {n}d {m}h",
|
|
72
|
+
"refresh": "Refresh",
|
|
73
|
+
"loading": "Loading…",
|
|
74
|
+
"rail.title": "OpenCode GO monthly {pct}%",
|
|
75
|
+
"rail.plain": "OpenCode GO"
|
|
76
|
+
};
|
|
77
|
+
|
|
44
78
|
// ── data helpers ──
|
|
45
79
|
const WINDOWS = [
|
|
46
|
-
{ key: "rolling",
|
|
47
|
-
{ key: "weekly",
|
|
48
|
-
{ key: "monthly",
|
|
80
|
+
{ key: "rolling", labelKey: "window.rolling", hintKey: "window.rolling.hint" },
|
|
81
|
+
{ key: "weekly", labelKey: "window.weekly", hintKey: null },
|
|
82
|
+
{ key: "monthly", labelKey: "window.monthly", hintKey: null }
|
|
49
83
|
];
|
|
50
84
|
|
|
51
|
-
/** Short human relative time until `iso
|
|
52
|
-
function timeUntil(iso) {
|
|
85
|
+
/** Short human relative time until `iso`, localized. */
|
|
86
|
+
function timeUntil(iso, t) {
|
|
53
87
|
const target = new Date(iso).getTime();
|
|
54
88
|
if (!Number.isFinite(target)) return "";
|
|
55
89
|
const diff = target - Date.now();
|
|
56
|
-
if (diff <= 0) return "
|
|
90
|
+
if (diff <= 0) return t("time.soon");
|
|
57
91
|
const mins = Math.floor(diff / 60000);
|
|
58
|
-
if (mins < 60) return
|
|
92
|
+
if (mins < 60) return t("time.minutes", { n: mins });
|
|
59
93
|
const hours = Math.floor(mins / 60);
|
|
60
|
-
if (hours < 24) return hours
|
|
61
|
-
return Math.floor(hours / 24)
|
|
94
|
+
if (hours < 24) return t("time.hours", { n: hours, m: mins % 60 });
|
|
95
|
+
return t("time.days", { n: Math.floor(hours / 24), m: hours % 24 });
|
|
62
96
|
}
|
|
63
97
|
|
|
64
98
|
/** Fill color by usage percent. */
|
|
@@ -70,10 +104,12 @@ window.__ModuleLoader__.load({
|
|
|
70
104
|
|
|
71
105
|
// ── widget component ──
|
|
72
106
|
/**
|
|
73
|
-
* @param {{ wide?: boolean }} props - owner share from the
|
|
74
|
-
* (`sidebar.footer.action` is rendered with `{ wide }`)
|
|
107
|
+
* @param {{ wide?: boolean, t?: Function }} props - owner share from the
|
|
108
|
+
* sidebar (`sidebar.footer.action` is rendered with `{ wide }`) plus the
|
|
109
|
+
* locale translate seat declared by this registration.
|
|
75
110
|
*/
|
|
76
111
|
function Widget(props) {
|
|
112
|
+
const t = props.t || ((key) => key);
|
|
77
113
|
const [data, setData] = react.useState(null);
|
|
78
114
|
const [error, setError] = react.useState(null);
|
|
79
115
|
const [stamp, setStamp] = react.useState(0);
|
|
@@ -110,7 +146,7 @@ window.__ModuleLoader__.load({
|
|
|
110
146
|
if (!props.wide) {
|
|
111
147
|
return react_jsx_runtime.jsx("div", {
|
|
112
148
|
className: "ocg-rail",
|
|
113
|
-
title: error ? ("OpenCode GO: " + error) : (pct === null ? "
|
|
149
|
+
title: error ? ("OpenCode GO: " + error) : (pct === null ? t("rail.plain") : t("rail.title", { pct: pct })),
|
|
114
150
|
children: pct === null ? "GO" : pct + "%"
|
|
115
151
|
});
|
|
116
152
|
}
|
|
@@ -126,8 +162,8 @@ window.__ModuleLoader__.load({
|
|
|
126
162
|
react_jsx_runtime.jsx("div", {
|
|
127
163
|
className: "ocg-row-label",
|
|
128
164
|
children: [
|
|
129
|
-
react_jsx_runtime.jsx("b", { children: w.
|
|
130
|
-
react_jsx_runtime.jsx("span", { children: (win.resetsAt ? timeUntil(win.resetsAt) : w.
|
|
165
|
+
react_jsx_runtime.jsx("b", { children: t(w.labelKey) }),
|
|
166
|
+
react_jsx_runtime.jsx("span", { children: (win.resetsAt ? timeUntil(win.resetsAt, t) : (w.hintKey ? t(w.hintKey) : "")) })
|
|
131
167
|
]
|
|
132
168
|
}),
|
|
133
169
|
react_jsx_runtime.jsx("div", {
|
|
@@ -143,7 +179,7 @@ window.__ModuleLoader__.load({
|
|
|
143
179
|
react_jsx_runtime.jsx("div", {
|
|
144
180
|
className: "ocg-row-label",
|
|
145
181
|
children: [
|
|
146
|
-
react_jsx_runtime.jsx("span", { children: win.status === "ok" ? "
|
|
182
|
+
react_jsx_runtime.jsx("span", { children: win.status === "ok" ? t("status.ok") : win.status }),
|
|
147
183
|
react_jsx_runtime.jsx("span", { children: win.percent + "%" })
|
|
148
184
|
]
|
|
149
185
|
})
|
|
@@ -161,26 +197,28 @@ window.__ModuleLoader__.load({
|
|
|
161
197
|
react_jsx_runtime.jsx("button", {
|
|
162
198
|
className: "ocg-refresh",
|
|
163
199
|
onClick: () => setStamp(stamp + 1),
|
|
164
|
-
children: "
|
|
200
|
+
children: t("refresh")
|
|
165
201
|
})
|
|
166
202
|
]
|
|
167
203
|
}),
|
|
168
204
|
error ? react_jsx_runtime.jsx("div", { className: "ocg-err", children: error }) : null,
|
|
169
|
-
!error && data === null ? react_jsx_runtime.jsx("div", { className: "ocg-err", children: "
|
|
205
|
+
!error && data === null ? react_jsx_runtime.jsx("div", { className: "ocg-err", children: t("loading") }) : null,
|
|
170
206
|
rows.length > 0 ? react_jsx_runtime.jsx(react.Fragment, { children: rows }) : null
|
|
171
207
|
]
|
|
172
208
|
});
|
|
173
209
|
}
|
|
174
210
|
|
|
175
211
|
// ── cordis plugin entry ──
|
|
176
|
-
const inject = ["slots"];
|
|
212
|
+
const inject = ["slots", "locale"];
|
|
177
213
|
|
|
178
214
|
function apply(ctx) {
|
|
215
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-opencode-go-usage: dictionaries");
|
|
179
216
|
ctx.effect(() => ctx.slots.register({
|
|
180
217
|
name: "sidebar.footer.action",
|
|
181
218
|
id: "dsh-opencode-go-usage",
|
|
182
219
|
order: 0,
|
|
183
|
-
label: "OpenCode GO
|
|
220
|
+
label: "OpenCode GO",
|
|
221
|
+
locale: NS
|
|
184
222
|
}, Widget), "dsh-opencode-go-usage: widget registration");
|
|
185
223
|
}
|
|
186
224
|
|
package/lib/index.js
CHANGED
|
@@ -75,21 +75,24 @@ async function fetchUsage(baseUrl, apiKey) {
|
|
|
75
75
|
return await res.json();
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
/** Human-friendly chat output. */
|
|
79
|
-
function renderUsageText(data) {
|
|
78
|
+
/** Human-friendly chat output, localized (zh | en). */
|
|
79
|
+
function renderUsageText(data, lang) {
|
|
80
|
+
const zh = lang !== "en";
|
|
80
81
|
const u = data?.usage;
|
|
81
82
|
if (!u || typeof u !== "object") return "OpenCode GO: unexpected response shape.";
|
|
82
|
-
const lines = ["OpenCode GO 套餐用量:"];
|
|
83
|
+
const lines = [zh ? "OpenCode GO 套餐用量:" : "OpenCode GO plan usage:"];
|
|
83
84
|
const defs = [
|
|
84
|
-
["rolling", "滚动窗口 (5h)"],
|
|
85
|
-
["weekly", "周窗口"],
|
|
86
|
-
["monthly", "月窗口"]
|
|
85
|
+
["rolling", zh ? "滚动窗口 (5h)" : "Rolling (5h)"],
|
|
86
|
+
["weekly", zh ? "周窗口" : "Weekly"],
|
|
87
|
+
["monthly", zh ? "月窗口" : "Monthly"]
|
|
87
88
|
];
|
|
88
89
|
for (const [key, label] of defs) {
|
|
89
90
|
const w = u[key];
|
|
90
91
|
if (!w || typeof w.percent !== "number") continue;
|
|
91
|
-
const status = w.status === "ok" ? "正常" : w.status;
|
|
92
|
-
const resets = w.resetsAt
|
|
92
|
+
const status = w.status === "ok" ? (zh ? "正常" : "ok") : w.status;
|
|
93
|
+
const resets = w.resetsAt
|
|
94
|
+
? (zh ? `重置于 ${new Date(w.resetsAt).toLocaleString("zh-CN")}` : `resets at ${new Date(w.resetsAt).toLocaleString("en-US")}`)
|
|
95
|
+
: "";
|
|
93
96
|
lines.push(` ${label}: ${w.percent}% (${status})${resets ? `, ${resets}` : ""}`);
|
|
94
97
|
}
|
|
95
98
|
return lines.join("\n");
|
|
@@ -140,10 +143,14 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
140
143
|
|
|
141
144
|
const readConfig = () => {
|
|
142
145
|
const stored = ctx.settings.get(namespace) ?? {};
|
|
146
|
+
// DSH's own locale preference ("locale.preference": zh | en); absent
|
|
147
|
+
// falls back to zh (the browser decides when the user never picked).
|
|
148
|
+
const locale = ctx.settings.get("locale")?.preference ?? "zh";
|
|
143
149
|
return {
|
|
144
150
|
apiKeyEnv: stored.apiKeyEnv,
|
|
145
151
|
baseUrl: stored.baseUrl,
|
|
146
|
-
cacheMs: stored.cacheMs
|
|
152
|
+
cacheMs: stored.cacheMs,
|
|
153
|
+
locale
|
|
147
154
|
};
|
|
148
155
|
};
|
|
149
156
|
|
|
@@ -157,7 +164,11 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
157
164
|
if (quotaCache !== null && now - quotaCache.at < config.cacheMs) return quotaCache.promise;
|
|
158
165
|
const promise = (async () => {
|
|
159
166
|
const key = await resolveApiKey(ctx, config.apiKeyEnv);
|
|
160
|
-
if (key === null)
|
|
167
|
+
if (key === null) {
|
|
168
|
+
throw new Error(config.locale === "en"
|
|
169
|
+
? `OpenCode GO: no API key (set credential ${config.apiKeyEnv})`
|
|
170
|
+
: `OpenCode GO: 未配置 API key(在 credential ${config.apiKeyEnv} 中设置)`);
|
|
171
|
+
}
|
|
161
172
|
return await fetchUsage(config.baseUrl, key);
|
|
162
173
|
})();
|
|
163
174
|
quotaCache = { at: now, promise };
|
|
@@ -225,7 +236,7 @@ export function apply(ctx, rawConfig = {}) {
|
|
|
225
236
|
handler: async () => {
|
|
226
237
|
try {
|
|
227
238
|
const data = await quotaOnce();
|
|
228
|
-
return { kind: "success", text: renderUsageText(data) };
|
|
239
|
+
return { kind: "success", text: renderUsageText(data, readConfig().locale) };
|
|
229
240
|
} catch (error) {
|
|
230
241
|
return { kind: "error", text: error instanceof Error ? error.message : String(error) };
|
|
231
242
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-opencode-go-usage",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
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",
|