@ruoyang/dsh-plugin-deepseek-balance 1.0.0 → 1.0.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.
Files changed (3) hide show
  1. package/README.md +30 -4
  2. package/client.js +140 -27
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -14,6 +14,7 @@ calls.
14
14
  | Badge text | total balance in the primary currency, e.g. `¥4.94` |
15
15
  | Hover panel | per-currency totals, granted vs topped-up split, whether the account is callable, and the query time |
16
16
  | Interaction | click to refresh, drag to reposition, automatic refresh every 60 seconds |
17
+ | Language | every label comes from this plugin's own locale namespace, so the badge follows the UI language (English / Chinese) |
17
18
 
18
19
  The badge itself is a pill pinned to the lower-right corner; drag it anywhere and it stays there for
19
20
  the session.
@@ -43,6 +44,20 @@ In the Web UI: **Settings → Plugins**, then install by the package name above.
43
44
  Through an agent session, the equivalent is the `plugin_manager` tool with
44
45
  `action: install_bundle` and that package name as `target`.
45
46
 
47
+ ### If you install through a registry mirror
48
+
49
+ `registry.npmmirror.com` and other read-only mirrors lag behind npmjs.org, and a package they have
50
+ not synced yet answers **404** — which looks exactly like "no such package". If your npm or pnpm is
51
+ pointed at a mirror (common on machines set up for fast installs), either wait for the sync or send
52
+ this scope to the official registry:
53
+
54
+ ```sh
55
+ npm config set @ruoyang:registry https://registry.npmjs.org/
56
+ ```
57
+
58
+ A scope key is a different setting from `registry`, so it also survives an inherited
59
+ `npm_config_registry` environment variable, which outranks every `.npmrc`.
60
+
46
61
  A new bundle activates immediately; if the badge does not appear, reload the page once so the
47
62
  browser picks up the new client module.
48
63
 
@@ -88,9 +103,18 @@ A 404 on that path means the row did not mount — check that the bundle is enab
88
103
 
89
104
  ## Known limitations
90
105
 
91
- - Visible badge text is currently Chinese only; it does not yet go through the Client locale service,
92
- so an English UI still shows Chinese labels in the hover panel.
93
106
  - The badge position resets on page reload; it is not persisted.
107
+ - Only the two languages the harness ships (English, Chinese) have dictionaries; a third language
108
+ falls back to the harness's own chain and then to the key, so add a dictionary before adding a
109
+ language.
110
+
111
+ ## Language
112
+
113
+ Strings live in the plugin's own locale namespace and reach the component through the `t` seat the
114
+ slot framework injects for a registration that names a `locale` namespace, so switching
115
+ **Settings → General → Language** re-words the badge live. Host-side failures arrive as a
116
+ machine-readable `code` that the badge translates; an unrecognised code falls back to the host's own
117
+ message so a novel failure stays legible.
94
118
 
95
119
  ## 中文
96
120
 
@@ -125,8 +149,10 @@ curl "http://127.0.0.1:3080/deepseek-balance/api?force=1" # 绕过宿主半 20
125
149
  返回 `NO_KEY` = 没配密钥,`HTTP_401` = 密钥被拒,`TRANSPORT` = 宿主机连不上
126
150
  `api.deepseek.com`;404 = 行没挂上,去 **设置 → 插件** 看是否启用。
127
151
 
128
- **已知限制**:徽标内的可见文字目前是硬编码中文,还没接 Client locale 服务,所以英文界面下悬停
129
- 面板仍是中文;徽标位置刷新页面后重置。
152
+ **已知限制**:徽标位置在刷新页面后重置(不做持久化);内置字典只有 harness 自带的两种语言
153
+ (中/英),加第三种语言前需要先补字典。
154
+
155
+ 徽标文字走本插件自己的 locale 命名空间,**设置 → 通用 → 语言** 切换后会实时改文案。
130
156
 
131
157
  ## Releasing (maintainer)
132
158
 
package/client.js CHANGED
@@ -10,8 +10,14 @@
10
10
  * build step is involved. `id` must equal the package name. React comes from the
11
11
  * browser module table via `require`.
12
12
  *
13
- * The factory itself stays side-effect free: the stylesheet, the interval and the
14
- * slot registration are all created inside `apply` and owned by that fiber.
13
+ * Every visible string lives in this plugin's own locale namespace and reaches
14
+ * the component through the `t` seat the slot framework injects for a
15
+ * registration that names a `locale` namespace, so the badge follows the active
16
+ * language instead of being Chinese-only.
17
+ *
18
+ * The factory itself stays side-effect free: the stylesheet, the dictionaries,
19
+ * the interval and the slot registration are all created inside `apply` and
20
+ * owned by that fiber.
15
21
  */
16
22
  window.__ModuleLoader__.load({
17
23
  id: '@ruoyang/dsh-plugin-deepseek-balance',
@@ -26,6 +32,72 @@ window.__ModuleLoader__.load({
26
32
  const SYMBOLS = { CNY: '¥', USD: '$' }
27
33
  /** Id of the one style node this fiber owns. */
28
34
  const STYLE_ID = 'dsh-balance-styles'
35
+ /**
36
+ * Locale namespace this plugin owns. It is also the namespace named on the
37
+ * slot registration, which is what makes the framework inject `t`.
38
+ */
39
+ const NS = 'deepseek-balance'
40
+
41
+ /**
42
+ * Dictionaries for every built-in locale. Registration requires the complete
43
+ * set, and both dictionaries carry the same keys.
44
+ */
45
+ const DICTS = {
46
+ en: {
47
+ 'pill.loading': 'Balance …',
48
+ 'pill.unavailable': 'Balance unavailable',
49
+ 'pill.title': 'DeepSeek balance · click to refresh · drag to move',
50
+ 'panel.totalIn': '{currency} total',
51
+ 'panel.grantSplit': ' granted / topped up',
52
+ 'panel.balance': 'Balance',
53
+ 'panel.noData': 'no data',
54
+ 'panel.callable': 'Callable',
55
+ 'panel.yes': 'yes',
56
+ 'panel.no': 'no',
57
+ 'panel.updated': 'Updated',
58
+ 'panel.state': 'State',
59
+ 'panel.querying': 'Querying …',
60
+ 'panel.error': 'Error',
61
+ 'panel.credential': 'Credential',
62
+ 'hint': 'Click to refresh · drag to move',
63
+ 'error.fallback': 'Balance temporarily unavailable',
64
+ 'error.NO_KEY': 'No DEEPSEEK_API_KEY credential — configure it under Settings → Models',
65
+ 'error.TRANSPORT': 'The host could not reach api.deepseek.com',
66
+ 'error.BAD_JSON': 'The balance response was not valid JSON',
67
+ 'error.TIMEOUT': 'The balance query timed out',
68
+ 'error.NO_OUTPUT': 'The balance query returned nothing parseable',
69
+ 'error.HTTP_401': 'The API key was rejected',
70
+ 'error.HTTP_403': 'This API key is not permitted',
71
+ 'error.HTTP_429': 'Rate limited — try again shortly',
72
+ },
73
+ zh: {
74
+ 'pill.loading': '余额 …',
75
+ 'pill.unavailable': '余额不可用',
76
+ 'pill.title': 'DeepSeek 余额 · 点击刷新 · 拖动移动',
77
+ 'panel.totalIn': '{currency} 总余额',
78
+ 'panel.grantSplit': ' 赠金 / 充值',
79
+ 'panel.balance': '余额',
80
+ 'panel.noData': '无数据',
81
+ 'panel.callable': '可调用',
82
+ 'panel.yes': '是',
83
+ 'panel.no': '否',
84
+ 'panel.updated': '更新于',
85
+ 'panel.state': '状态',
86
+ 'panel.querying': '查询中 …',
87
+ 'panel.error': '错误',
88
+ 'panel.credential': '凭据',
89
+ 'hint': '点击刷新 · 按住拖动可移动位置',
90
+ 'error.fallback': '余额暂不可用',
91
+ 'error.NO_KEY': '未找到 DEEPSEEK_API_KEY 凭据 —— 请在 设置 → Models 里配置',
92
+ 'error.TRANSPORT': '宿主机连不上 api.deepseek.com',
93
+ 'error.BAD_JSON': '余额响应不是合法 JSON',
94
+ 'error.TIMEOUT': '余额查询超时',
95
+ 'error.NO_OUTPUT': '余额查询没有返回可解析的结果',
96
+ 'error.HTTP_401': 'API key 被拒绝',
97
+ 'error.HTTP_403': '这个 API key 没有权限',
98
+ 'error.HTTP_429': '被限流了 —— 稍后再试',
99
+ },
100
+ }
29
101
 
30
102
  const CSS = [
31
103
  '.dsh-balance-root{position:fixed;z-index:60;pointer-events:auto;user-select:none;',
@@ -92,6 +164,44 @@ window.__ModuleLoader__.load({
92
164
  return pad(date.getHours()) + ':' + pad(date.getMinutes()) + ':' + pad(date.getSeconds())
93
165
  }
94
166
 
167
+ /**
168
+ * Stand-in used only when the slot framework injects no `t` seat.
169
+ *
170
+ * The framework throws rather than omitting the seat for a registration that
171
+ * names a locale namespace, so this is a guard against a future change on a
172
+ * harness this plugin deliberately declares no version dependency on: a
173
+ * missing seat degrades the wording instead of blanking the badge.
174
+ */
175
+ function fallbackTranslate(key) {
176
+ return DICTS.zh[key] !== undefined ? DICTS.zh[key] : key
177
+ }
178
+
179
+ /** Take the injected translate seat, falling back when it is absent. */
180
+ function translatorOf(props) {
181
+ if (props !== null && typeof props === 'object' && typeof props.t === 'function') return props.t
182
+ return fallbackTranslate
183
+ }
184
+
185
+ /**
186
+ * Wording for a failed snapshot.
187
+ *
188
+ * The host half already answers a machine-readable `code`; translating that
189
+ * keeps the badge in the active language, while an unrecognised code falls
190
+ * back to the host's own message so a novel failure is still legible. An
191
+ * unknown key resolves to the key itself, which is how the miss is detected.
192
+ */
193
+ function failureText(t, payload) {
194
+ if (payload !== null && typeof payload === 'object' && typeof payload.code === 'string' && payload.code !== '') {
195
+ const key = 'error.' + payload.code
196
+ const text = t(key)
197
+ if (text !== key) return text
198
+ }
199
+ if (payload !== null && typeof payload === 'object' && typeof payload.message === 'string' && payload.message !== '') {
200
+ return payload.message
201
+ }
202
+ return t('error.fallback')
203
+ }
204
+
95
205
  /** One key/value line of the hover panel. */
96
206
  function row(key, value, extraClass) {
97
207
  return React.createElement(
@@ -111,34 +221,33 @@ window.__ModuleLoader__.load({
111
221
  * and freshness. The failure branch names the credential so a missing key is
112
222
  * obvious without opening logs.
113
223
  */
114
- function panelRows(data, failure, phase) {
224
+ function panelRows(t, data, failure, phase) {
115
225
  const rows = []
116
226
  if (data !== null) {
117
227
  if (data.infos.length === 0) {
118
- rows.push(row('余额', '无数据'))
228
+ rows.push(row(t('panel.balance'), t('panel.noData')))
119
229
  } else {
120
230
  for (const info of data.infos) {
121
231
  const symbol = symbolOf(info.currency)
122
- rows.push(row(info.currency + ' 总余额', symbol + info.total))
123
- rows.push(row(' 赠金 / 充值', symbol + info.granted + ' / ' + symbol + info.toppedUp))
232
+ rows.push(row(t('panel.totalIn', { currency: info.currency }), symbol + info.total))
233
+ rows.push(row(t('panel.grantSplit'), symbol + info.granted + ' / ' + symbol + info.toppedUp))
124
234
  }
125
235
  }
126
- rows.push(row('可调用', data.isAvailable === true ? '是' : '否'))
127
- rows.push(row('更新于', formatTime(data.at)))
236
+ rows.push(row(t('panel.callable'), data.isAvailable === true ? t('panel.yes') : t('panel.no')))
237
+ rows.push(row(t('panel.updated'), formatTime(data.at)))
128
238
  } else if (phase === 'loading') {
129
- rows.push(row('状态', '查询中 …'))
130
- rows.push(row('凭据', 'DEEPSEEK_API_KEY'))
239
+ rows.push(row(t('panel.state'), t('panel.querying')))
240
+ rows.push(row(t('panel.credential'), 'DEEPSEEK_API_KEY'))
131
241
  } else {
132
- rows.push(row('错误', failure === null ? '余额暂不可用' : failure, 'dsh-balance-err'))
133
- rows.push(row('凭据', 'DEEPSEEK_API_KEY'))
242
+ rows.push(row(t('panel.error'), failure === null ? t('error.fallback') : failure, 'dsh-balance-err'))
243
+ rows.push(row(t('panel.credential'), 'DEEPSEEK_API_KEY'))
134
244
  }
135
- rows.push(React.createElement('div', { className: 'dsh-balance-hint', key: 'hint' },
136
- '点击刷新 · 按住拖动可移动位置'))
245
+ rows.push(React.createElement('div', { className: 'dsh-balance-hint', key: 'hint' }, t('hint')))
137
246
  return rows
138
247
  }
139
248
 
140
249
  return {
141
- inject: ['slots', 'timer'],
250
+ inject: ['slots', 'timer', 'locale'],
142
251
  apply(ctx) {
143
252
  ctx.effect(() => {
144
253
  if (document.getElementById(STYLE_ID) === null) {
@@ -150,10 +259,13 @@ window.__ModuleLoader__.load({
150
259
  return () => { document.getElementById(STYLE_ID)?.remove() }
151
260
  }, 'deepseek-balance: styles')
152
261
 
262
+ ctx.effect(() => ctx.locale.register(NS, DICTS), 'deepseek-balance: dictionaries')
263
+
153
264
  // Defined inside apply so the component closes over this fiber's context
154
265
  // and can own its own interval. One mount per client run keeps the
155
266
  // component identity stable across re-renders.
156
- function BalanceBadge() {
267
+ function BalanceBadge(props) {
268
+ const t = translatorOf(props)
157
269
  const [snapshot, setSnapshot] = React.useState({ phase: 'loading' })
158
270
  const [busy, setBusy] = React.useState(false)
159
271
  const [position, setPosition] = React.useState({ right: 22, bottom: 22 })
@@ -215,17 +327,16 @@ window.__ModuleLoader__.load({
215
327
  const data = snapshot.phase === 'ready' && snapshot.data !== null && snapshot.data.ok === true
216
328
  ? snapshot.data
217
329
  : null
218
- const failure = snapshot.phase === 'error'
219
- ? snapshot.message
220
- : (snapshot.phase === 'ready' && snapshot.data !== null && snapshot.data.ok !== true
221
- ? snapshot.data.message
222
- : null)
330
+ const failed = snapshot.phase === 'error'
331
+ ? { message: snapshot.message }
332
+ : (snapshot.phase === 'ready' && snapshot.data !== null && snapshot.data.ok !== true ? snapshot.data : null)
333
+ const failure = failed === null ? null : failureText(t, failed)
223
334
 
224
335
  let tone = 'idle'
225
- let label = '余额 …'
226
- if (snapshot.phase === 'error' || failure !== null) {
336
+ let label = t('pill.loading')
337
+ if (failure !== null) {
227
338
  tone = 'error'
228
- label = '余额不可用'
339
+ label = t('pill.unavailable')
229
340
  } else if (data !== null) {
230
341
  tone = data.isAvailable === true ? 'ok' : 'warn'
231
342
  label = symbolOf(data.currency) + data.total
@@ -237,12 +348,12 @@ window.__ModuleLoader__.load({
237
348
  className: 'dsh-balance-root dsh-balance-tone-' + tone,
238
349
  style: { right: position.right + 'px', bottom: position.bottom + 'px' },
239
350
  },
240
- React.createElement('div', { className: 'dsh-balance-panel' }, panelRows(data, failure, snapshot.phase)),
351
+ React.createElement('div', { className: 'dsh-balance-panel' }, panelRows(t, data, failure, snapshot.phase)),
241
352
  React.createElement(
242
353
  'div',
243
354
  {
244
355
  className: 'dsh-balance-pill',
245
- title: 'DeepSeek 余额 · 点击刷新 · 拖动移动',
356
+ title: t('pill.title'),
246
357
  onPointerDown,
247
358
  onPointerMove,
248
359
  onPointerUp,
@@ -256,7 +367,9 @@ window.__ModuleLoader__.load({
256
367
  }
257
368
 
258
369
  ctx.effect(() => ctx.slots.inject('shell.overlay', () => ctx.slots.register(
259
- { name: 'shell.overlay', id: 'deepseek-balance', order: 200 },
370
+ // Naming the locale namespace is what makes the framework inject the
371
+ // `t` seat (and re-render every outlet when the language changes).
372
+ { name: 'shell.overlay', id: 'deepseek-balance', order: 200, locale: NS },
260
373
  BalanceBadge,
261
374
  )), 'deepseek-balance: overlay badge')
262
375
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ruoyang/dsh-plugin-deepseek-balance",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Floating DeepSeek API balance badge for the DeepSeek Harness Web UI, backed by the harness's own DEEPSEEK_API_KEY credential",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -42,6 +42,7 @@
42
42
  "platform": "web",
43
43
  "immediately": true,
44
44
  "inject": [
45
+ "@deepseek-ai/dsh-client-locale",
45
46
  "@deepseek-ai/dsh-client-ui-layout"
46
47
  ]
47
48
  }