@rezti/dsh-rez-suite 0.1.26 → 0.1.33

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 (39) hide show
  1. package/README.md +1 -1
  2. package/lib/client.d.ts +7 -0
  3. package/lib/client.js +388 -187
  4. package/lib/index.d.ts +4 -0
  5. package/lib/index.js +85 -7
  6. package/lib/style.css +47 -0
  7. package/package.json +7 -7
  8. package/src/boss/mount.ts +2 -0
  9. package/src/client/assets/rez-mark.png +0 -0
  10. package/src/client/brand-mark.ts +4 -0
  11. package/src/client/brand.module.css +51 -0
  12. package/src/client/brand.ts +135 -0
  13. package/src/client/index.ts +11 -0
  14. package/src/client/locales.ts +14 -0
  15. package/src/client/panel/AuditTab.tsx +20 -19
  16. package/src/client/panel/ConfigTab.tsx +57 -39
  17. package/src/client/panel/StatusTab.tsx +11 -10
  18. package/src/client/panel/WeixinTab.tsx +9 -8
  19. package/src/client/panel/helpers.ts +20 -5
  20. package/src/client/settings-card.tsx +48 -42
  21. package/src/client/upgrade-button.tsx +24 -12
  22. package/src/index.ts +11 -0
  23. package/src/observe-mcp.ts +5 -2
  24. package/src/presets.ts +1 -0
  25. package/src/protocol.ts +2 -0
  26. package/src/store.ts +6 -0
  27. package/src/tianyancha.ts +79 -0
  28. package/templates/boss/money/AGENTS.md +2 -0
  29. package/templates/boss/ops/AGENTS.md +2 -0
  30. package/templates/boss/product/AGENTS.md +4 -0
  31. package/templates/staff/finance/.agents/skills/staff-finance/SKILL.md +1 -1
  32. package/templates/staff/finance/AGENTS.md +6 -0
  33. package/templates/staff/finance/MEMORY.md +1 -1
  34. package/templates/staff/hr/.agents/skills/staff-hr/SKILL.md +1 -1
  35. package/templates/staff/hr/AGENTS.md +6 -0
  36. package/templates/staff/hr/MEMORY.md +1 -1
  37. package/templates/staff/legal/.agents/skills/staff-legal/SKILL.md +1 -1
  38. package/templates/staff/legal/AGENTS.md +10 -0
  39. package/templates/staff/legal/MEMORY.md +1 -1
@@ -5,10 +5,11 @@
5
5
  import { useEffect, useState } from 'react'
6
6
  import type { RezApi } from '../api.ts'
7
7
  import type { RezAuditResponse } from '../../protocol.ts'
8
- import { errorMessage, tt } from './helpers.ts'
8
+ import { errorMessage, useT } from './helpers.ts'
9
9
  import css from './panel.module.css'
10
10
 
11
11
  export function AuditTab({ api }: { api: RezApi }) {
12
+ const t = useT()
12
13
  const [audit, setAudit] = useState<RezAuditResponse | null>(null)
13
14
  const [error, setError] = useState('')
14
15
  const [loading, setLoading] = useState(true)
@@ -28,7 +29,7 @@ export function AuditTab({ api }: { api: RezApi }) {
28
29
  useEffect(() => { void load() }, [])
29
30
 
30
31
  const reset = async (): Promise<void> => {
31
- if (!window.confirm(tt('audit.resetConfirm'))) return
32
+ if (!window.confirm(t('audit.resetConfirm'))) return
32
33
  try {
33
34
  await api.resetAudit()
34
35
  await load()
@@ -37,38 +38,38 @@ export function AuditTab({ api }: { api: RezApi }) {
37
38
  }
38
39
  }
39
40
 
40
- if (loading) return <div className={css.message}>{tt('common.loading')}</div>
41
+ if (loading) return <div className={css.message}>{t('common.loading')}</div>
41
42
  if (audit === null) return <div className={css.error}>{error}</div>
42
43
 
43
44
  return (
44
45
  <div className={css.tabBody}>
45
46
  <div className={css.toolbar}>
46
- <button type="button" className={css.button} onClick={() => { void load() }}>{tt('common.refresh')}</button>
47
- <button type="button" className={css.button + ' ' + css.dangerButton} onClick={() => { void reset() }}>{tt('audit.reset')}</button>
47
+ <button type="button" className={css.button} onClick={() => { void load() }}>{t('common.refresh')}</button>
48
+ <button type="button" className={css.button + ' ' + css.dangerButton} onClick={() => { void reset() }}>{t('audit.reset')}</button>
48
49
  </div>
49
50
  {error !== '' && <div className={css.error}>{error}</div>}
50
51
  <div className={css.cards}>
51
- <div className={css.stat}><div className={css.cardLabel}>{tt('audit.totalCalls')}</div><div className={css.cardValue}>{audit.totalCalls}</div></div>
52
- <div className={css.stat}><div className={css.cardLabel}>{tt('audit.inputTokens')}</div><div className={css.cardValue}>{audit.totalInputTokens}</div></div>
53
- <div className={css.stat}><div className={css.cardLabel}>{tt('audit.outputTokens')}</div><div className={css.cardValue}>{audit.totalOutputTokens}</div></div>
54
- <div className={css.stat}><div className={css.cardLabel}>{tt('audit.totalCost')}</div><div className={css.cardValue}>{audit.totalCost.toFixed(4)}</div></div>
55
- <div className={css.stat}><div className={css.cardLabel}>{tt('audit.monthCost')}</div><div className={css.cardValue}>{audit.monthCost.toFixed(4)}</div></div>
56
- <div className={css.stat}><div className={css.cardLabel}>{tt('audit.budget')}</div><div className={css.cardValue}>{audit.monthlyBudget.toFixed(2)}</div></div>
52
+ <div className={css.stat}><div className={css.cardLabel}>{t('audit.totalCalls')}</div><div className={css.cardValue}>{audit.totalCalls}</div></div>
53
+ <div className={css.stat}><div className={css.cardLabel}>{t('audit.inputTokens')}</div><div className={css.cardValue}>{audit.totalInputTokens}</div></div>
54
+ <div className={css.stat}><div className={css.cardLabel}>{t('audit.outputTokens')}</div><div className={css.cardValue}>{audit.totalOutputTokens}</div></div>
55
+ <div className={css.stat}><div className={css.cardLabel}>{t('audit.totalCost')}</div><div className={css.cardValue}>{audit.totalCost.toFixed(4)}</div></div>
56
+ <div className={css.stat}><div className={css.cardLabel}>{t('audit.monthCost')}</div><div className={css.cardValue}>{audit.monthCost.toFixed(4)}</div></div>
57
+ <div className={css.stat}><div className={css.cardLabel}>{t('audit.budget')}</div><div className={css.cardValue}>{audit.monthlyBudget.toFixed(2)}</div></div>
57
58
  </div>
58
59
  <div className={css.tableWrap}>
59
60
  <table className={css.table}>
60
61
  <thead>
61
62
  <tr>
62
- <th>{tt('audit.col.time')}</th>
63
- <th>{tt('audit.col.server')}</th>
64
- <th>{tt('audit.col.tool')}</th>
65
- <th>{tt('audit.col.tokens')}</th>
66
- <th>{tt('audit.col.cost')}</th>
67
- <th>{tt('audit.col.status')}</th>
63
+ <th>{t('audit.col.time')}</th>
64
+ <th>{t('audit.col.server')}</th>
65
+ <th>{t('audit.col.tool')}</th>
66
+ <th>{t('audit.col.tokens')}</th>
67
+ <th>{t('audit.col.cost')}</th>
68
+ <th>{t('audit.col.status')}</th>
68
69
  </tr>
69
70
  </thead>
70
71
  <tbody>
71
- {audit.recent.length === 0 && <tr><td colSpan={6}>{tt('audit.empty')}</td></tr>}
72
+ {audit.recent.length === 0 && <tr><td colSpan={6}>{t('audit.empty')}</td></tr>}
72
73
  {audit.recent.map(row => (
73
74
  <tr key={row.id}>
74
75
  <td>{new Date(row.ts).toLocaleString()}</td>
@@ -76,7 +77,7 @@ export function AuditTab({ api }: { api: RezApi }) {
76
77
  <td>{row.tool}</td>
77
78
  <td>{row.inputTokens + row.outputTokens}</td>
78
79
  <td>{row.cost.toFixed(4)}</td>
79
- <td>{row.ok ? <span className={css.badge + ' ' + css.badgeOk}>{tt('audit.ok')}</span> : <span className={css.badge + ' ' + css.badgeFail}>{tt('audit.fail')}</span>}</td>
80
+ <td>{row.ok ? <span className={css.badge + ' ' + css.badgeOk}>{t('audit.ok')}</span> : <span className={css.badge + ' ' + css.badgeFail}>{t('audit.fail')}</span>}</td>
80
81
  </tr>
81
82
  ))}
82
83
  </tbody>
@@ -6,7 +6,7 @@
6
6
  import { useEffect, useState, type ReactNode } from 'react'
7
7
  import type { RezApi } from '../api.ts'
8
8
  import type { RezPublicConfig, RezPublicConnection, RezRoleId, RezTestResult } from '../../protocol.ts'
9
- import { errorMessage, tt } from './helpers.ts'
9
+ import { errorMessage, useT } from './helpers.ts'
10
10
  import css from './panel.module.css'
11
11
 
12
12
  const ROLES: Array<{ value: RezRoleId; label: string }> = [
@@ -24,9 +24,10 @@ type SecretDrafts = {
24
24
  homeassistant: string
25
25
  tapd: string
26
26
  gsc: string
27
+ tianyancha: string
27
28
  }
28
29
 
29
- const EMPTY_SECRETS: SecretDrafts = { odoo: '', nextcloud: '', wechat: '', homeassistant: '', tapd: '', gsc: '' }
30
+ const EMPTY_SECRETS: SecretDrafts = { odoo: '', nextcloud: '', wechat: '', homeassistant: '', tapd: '', gsc: '', tianyancha: '' }
30
31
 
31
32
  function Field({ label, children }: { label: string; children: ReactNode }) {
32
33
  return (
@@ -57,6 +58,7 @@ function SecretField({
57
58
  value: string
58
59
  onChange: (value: string) => void
59
60
  }) {
61
+ const t = useT()
60
62
  return (
61
63
  <Field label={label}>
62
64
  <input
@@ -64,7 +66,7 @@ function SecretField({
64
66
  type="password"
65
67
  autoComplete="off"
66
68
  value={value}
67
- placeholder={configured ? tt('config.secretConfigured') : tt('config.secretMissing')}
69
+ placeholder={configured ? t('config.secretConfigured') : t('config.secretMissing')}
68
70
  onChange={event => { onChange(event.target.value) }}
69
71
  />
70
72
  </Field>
@@ -96,9 +98,10 @@ function McpSection({
96
98
  secretLabel: string
97
99
  usernameLabel?: string
98
100
  }) {
101
+ const t = useT()
99
102
  return (
100
103
  <Section title={title}>
101
- <Field label={tt('config.enabled')}>
104
+ <Field label={t('config.enabled')}>
102
105
  <input className={css.checkbox} type="checkbox" checked={row.enabled} onChange={event => { onEnabled(event.target.checked) }} />
103
106
  </Field>
104
107
  {onUrl !== undefined && urlLabel !== undefined && (
@@ -119,6 +122,7 @@ function McpSection({
119
122
  }
120
123
 
121
124
  export function ConfigTab({ api }: { api: RezApi }) {
125
+ const t = useT()
122
126
  const [config, setConfig] = useState<RezPublicConfig | null>(null)
123
127
  const [secrets, setSecrets] = useState<SecretDrafts>(EMPTY_SECRETS)
124
128
  const [loading, setLoading] = useState(true)
@@ -140,7 +144,7 @@ export function ConfigTab({ api }: { api: RezApi }) {
140
144
 
141
145
  useEffect(() => { void load() }, [])
142
146
 
143
- if (loading) return <div className={css.message}>{tt('common.loading')}</div>
147
+ if (loading) return <div className={css.message}>{t('common.loading')}</div>
144
148
  if (config === null) return <div className={css.error}>{error}</div>
145
149
 
146
150
  const setTop = (key: 'enabled' | 'role', value: boolean | RezRoleId): void => {
@@ -170,12 +174,13 @@ export function ConfigTab({ api }: { api: RezApi }) {
170
174
  homeassistant: { ...config.connections.homeassistant, secret: secrets.homeassistant || undefined },
171
175
  tapd: { ...config.connections.tapd, secret: secrets.tapd || undefined },
172
176
  gsc: { ...config.connections.gsc, secret: secrets.gsc || undefined },
177
+ tianyancha: { ...config.connections.tianyancha, secret: secrets.tianyancha || undefined },
173
178
  },
174
179
  }
175
180
  const saved = await api.saveConfig(payload)
176
181
  setConfig(saved)
177
182
  setSecrets(EMPTY_SECRETS)
178
- setMessage(tt('config.saved'))
183
+ setMessage(t('config.saved'))
179
184
  } catch (err) {
180
185
  setError(errorMessage(err))
181
186
  } finally {
@@ -200,117 +205,130 @@ export function ConfigTab({ api }: { api: RezApi }) {
200
205
  const renderResult = (result: RezTestResult): string => {
201
206
  if (result.ok) {
202
207
  if (result.serverInfo !== undefined) return result.serverInfo
203
- return tt('config.testOk', { tools: result.toolCount ?? 0, latency: result.latencyMs ?? 0 })
208
+ return t('config.testOk', { tools: result.toolCount ?? 0, latency: result.latencyMs ?? 0 })
204
209
  }
205
- return tt('config.testFail', { error: result.error ?? '' })
210
+ return t('config.testFail', { error: result.error ?? '' })
206
211
  }
207
212
 
208
213
  return (
209
214
  <div className={css.tabBody}>
210
- <p className={css.message}>{tt('config.mcpHint')}</p>
211
- <Section title={tt('config.role')}>
212
- <Field label={tt('config.role')}>
215
+ <p className={css.message}>{t('config.mcpHint')}</p>
216
+ <Section title={t('config.role')}>
217
+ <Field label={t('config.role')}>
213
218
  <select className={css.select} value={config.role} onChange={event => { setTop('role', event.target.value as RezRoleId) }}>
214
- {ROLES.map(role => <option key={role.value} value={role.value}>{tt(role.label as never)}</option>)}
219
+ {ROLES.map(role => <option key={role.value} value={role.value}>{t(role.label as never)}</option>)}
215
220
  </select>
216
221
  </Field>
217
- <Field label={tt('config.enabled')}>
222
+ <p className={css.message}>{t('config.roleHint')}</p>
223
+ <Field label={t('config.enabled')}>
218
224
  <input className={css.checkbox} type="checkbox" checked={config.enabled} onChange={event => { setTop('enabled', event.target.checked) }} />
219
225
  </Field>
220
226
  </Section>
221
227
 
222
228
  <McpSection
223
- title={tt('config.odoo')}
229
+ title={t('config.odoo')}
224
230
  row={config.connections.odoo}
225
231
  secret={secrets.odoo}
226
232
  onEnabled={enabled => { patchConnection('odoo', { enabled }) }}
227
233
  onUrl={url => { patchConnection('odoo', { url }) }}
228
234
  onSecret={value => { setSecrets(prev => ({ ...prev, odoo: value })) }}
229
- urlLabel={tt('config.odoo.url')}
230
- secretLabel={tt('config.odoo.apiKey')}
235
+ urlLabel={t('config.odoo.url')}
236
+ secretLabel={t('config.odoo.apiKey')}
231
237
  />
232
238
  <McpSection
233
- title={tt('config.nextcloud')}
239
+ title={t('config.nextcloud')}
234
240
  row={config.connections.nextcloud}
235
241
  secret={secrets.nextcloud}
236
242
  onEnabled={enabled => { patchConnection('nextcloud', { enabled }) }}
237
243
  onUrl={url => { patchConnection('nextcloud', { url }) }}
238
244
  onUsername={username => { patchConnection('nextcloud', { username }) }}
239
245
  onSecret={value => { setSecrets(prev => ({ ...prev, nextcloud: value })) }}
240
- urlLabel={tt('config.nextcloud.url')}
241
- usernameLabel={tt('config.nextcloud.username')}
242
- secretLabel={tt('config.nextcloud.appPassword')}
246
+ urlLabel={t('config.nextcloud.url')}
247
+ usernameLabel={t('config.nextcloud.username')}
248
+ secretLabel={t('config.nextcloud.appPassword')}
243
249
  />
244
250
  <McpSection
245
- title={tt('config.wecom')}
251
+ title={t('config.wecom')}
246
252
  row={config.connections.wechat}
247
253
  secret={secrets.wechat}
248
254
  onEnabled={enabled => { patchConnection('wechat', { enabled }) }}
249
255
  onSecret={value => { setSecrets(prev => ({ ...prev, wechat: value })) }}
250
- secretLabel={tt('config.wecom.webhook')}
256
+ secretLabel={t('config.wecom.webhook')}
251
257
  />
252
258
  <McpSection
253
- title={tt('config.ha')}
259
+ title={t('config.ha')}
254
260
  row={config.connections.homeassistant}
255
261
  secret={secrets.homeassistant}
256
262
  onEnabled={enabled => { patchConnection('homeassistant', { enabled }) }}
257
263
  onUrl={url => { patchConnection('homeassistant', { url }) }}
258
264
  onSecret={value => { setSecrets(prev => ({ ...prev, homeassistant: value })) }}
259
- urlLabel={tt('config.ha.url')}
260
- secretLabel={tt('config.ha.token')}
265
+ urlLabel={t('config.ha.url')}
266
+ secretLabel={t('config.ha.token')}
261
267
  />
262
268
  <McpSection
263
- title={tt('config.tapd')}
269
+ title={t('config.tapd')}
264
270
  row={config.connections.tapd}
265
271
  secret={secrets.tapd}
266
272
  onEnabled={enabled => { patchConnection('tapd', { enabled }) }}
267
273
  extra={(
268
274
  <>
269
- <Field label={tt('config.tapd.workspaceId')}>
275
+ <Field label={t('config.tapd.workspaceId')}>
270
276
  <input className={css.input} value={config.connections.tapd.workspaceId ?? ''} onChange={event => { patchConnection('tapd', { workspaceId: event.target.value }) }} />
271
277
  </Field>
272
- <Field label={tt('config.tapd.nickName')}>
278
+ <Field label={t('config.tapd.nickName')}>
273
279
  <input className={css.input} value={config.connections.tapd.nickName ?? ''} onChange={event => { patchConnection('tapd', { nickName: event.target.value }) }} />
274
280
  </Field>
275
281
  </>
276
282
  )}
277
283
  onSecret={value => { setSecrets(prev => ({ ...prev, tapd: value })) }}
278
- secretLabel={tt('config.tapd.token')}
284
+ secretLabel={t('config.tapd.token')}
279
285
  />
280
286
  <McpSection
281
- title={tt('config.gsc')}
287
+ title={t('config.gsc')}
282
288
  row={config.connections.gsc}
283
289
  secret={secrets.gsc}
284
290
  onEnabled={enabled => { patchConnection('gsc', { enabled }) }}
285
291
  extra={(
286
- <Field label={tt('config.gsc.siteUrl')}>
292
+ <Field label={t('config.gsc.siteUrl')}>
287
293
  <input className={css.input} value={config.connections.gsc.siteUrl ?? ''} onChange={event => { patchConnection('gsc', { siteUrl: event.target.value }) }} />
288
294
  </Field>
289
295
  )}
290
296
  onSecret={value => { setSecrets(prev => ({ ...prev, gsc: value })) }}
291
- secretLabel={tt('config.gsc.key')}
297
+ secretLabel={t('config.gsc.key')}
292
298
  />
299
+ {config.role === 'boss' && (
300
+ <McpSection
301
+ title={t('config.tianyancha')}
302
+ row={config.connections.tianyancha}
303
+ secret={secrets.tianyancha}
304
+ onEnabled={enabled => { patchConnection('tianyancha', { enabled }) }}
305
+ onUrl={url => { patchConnection('tianyancha', { url }) }}
306
+ onSecret={value => { setSecrets(prev => ({ ...prev, tianyancha: value })) }}
307
+ urlLabel={t('config.tianyancha.url')}
308
+ secretLabel={t('config.tianyancha.token')}
309
+ />
310
+ )}
293
311
 
294
- <Section title={tt('config.billing')}>
295
- <Field label={tt('config.billing.input')}>
312
+ <Section title={t('config.billing')}>
313
+ <Field label={t('config.billing.input')}>
296
314
  <input className={css.input} type="number" step="0.0001" value={config.billing.inputCostPer1k} onChange={event => { setConfig(prev => prev === null ? prev : { ...prev, billing: { ...prev.billing, inputCostPer1k: Number(event.target.value) } }) }} />
297
315
  </Field>
298
- <Field label={tt('config.billing.output')}>
316
+ <Field label={t('config.billing.output')}>
299
317
  <input className={css.input} type="number" step="0.0001" value={config.billing.outputCostPer1k} onChange={event => { setConfig(prev => prev === null ? prev : { ...prev, billing: { ...prev.billing, outputCostPer1k: Number(event.target.value) } }) }} />
300
318
  </Field>
301
- <Field label={tt('config.billing.budget')}>
319
+ <Field label={t('config.billing.budget')}>
302
320
  <input className={css.input} type="number" step="0.01" value={config.billing.monthlyBudget} onChange={event => { setConfig(prev => prev === null ? prev : { ...prev, billing: { ...prev.billing, monthlyBudget: Number(event.target.value) } }) }} />
303
321
  </Field>
304
322
  </Section>
305
323
 
306
324
  <div className={css.toolbar}>
307
- <button type="button" className={css.button + ' ' + css.primaryButton} disabled={saving} onClick={() => { void save() }}>{tt('config.save')}</button>
308
- <button type="button" className={css.button} disabled={testing} onClick={() => { void test() }}>{testing ? tt('config.testing') : tt('config.test')}</button>
325
+ <button type="button" className={css.button + ' ' + css.primaryButton} disabled={saving} onClick={() => { void save() }}>{t('config.save')}</button>
326
+ <button type="button" className={css.button} disabled={testing} onClick={() => { void test() }}>{testing ? t('config.testing') : t('config.test')}</button>
309
327
  </div>
310
328
  {message !== '' && <div className={css.message}>{message}</div>}
311
329
  {error !== '' && <div className={css.error}>{error}</div>}
312
330
  {results.map(result => (
313
- <div key={result.server} className={result.ok ? css.message : css.error}>{tt('config.testResult', { server: result.server, status: renderResult(result) })}</div>
331
+ <div key={result.server} className={result.ok ? css.message : css.error}>{t('config.testResult', { server: result.server, status: renderResult(result) })}</div>
314
332
  ))}
315
333
  </div>
316
334
  )
@@ -5,10 +5,11 @@
5
5
  import { useEffect, useState } from 'react'
6
6
  import type { RezApi } from '../api.ts'
7
7
  import type { RezStatusResponse } from '../../protocol.ts'
8
- import { errorMessage, tt } from './helpers.ts'
8
+ import { errorMessage, useT } from './helpers.ts'
9
9
  import css from './panel.module.css'
10
10
 
11
11
  export function StatusTab({ api }: { api: RezApi }) {
12
+ const t = useT()
12
13
  const [status, setStatus] = useState<RezStatusResponse | null>(null)
13
14
  const [error, setError] = useState('')
14
15
  const [loading, setLoading] = useState(true)
@@ -27,26 +28,26 @@ export function StatusTab({ api }: { api: RezApi }) {
27
28
 
28
29
  useEffect(() => { void load() }, [])
29
30
 
30
- if (loading) return <div className={css.message}>{tt('common.loading')}</div>
31
+ if (loading) return <div className={css.message}>{t('common.loading')}</div>
31
32
  if (status === null) return <div className={css.error}>{error}</div>
32
33
 
33
34
  return (
34
35
  <div className={css.tabBody}>
35
36
  <div className={css.toolbar}>
36
- <span className={css.message}>{tt('status.total', { role: status.role, count: status.totalRegisteredTools })}</span>
37
- <button type="button" className={css.button} onClick={() => { void load() }}>{tt('common.refresh')}</button>
37
+ <span className={css.message}>{t('status.total', { role: status.role, count: status.totalRegisteredTools })}</span>
38
+ <button type="button" className={css.button} onClick={() => { void load() }}>{t('common.refresh')}</button>
38
39
  </div>
39
- <p className={css.message}>{tt('status.hint')}</p>
40
+ <p className={css.message}>{t('status.hint')}</p>
40
41
  {error !== '' && <div className={css.error}>{error}</div>}
41
42
  <div className={css.tableWrap}>
42
43
  <table className={css.table}>
43
44
  <thead>
44
45
  <tr>
45
- <th>{tt('status.col.server')}</th>
46
- <th>{tt('status.col.state')}</th>
47
- <th>{tt('status.col.tools')}</th>
48
- <th>{tt('status.col.registered')}</th>
49
- <th>{tt('status.col.error')}</th>
46
+ <th>{t('status.col.server')}</th>
47
+ <th>{t('status.col.state')}</th>
48
+ <th>{t('status.col.tools')}</th>
49
+ <th>{t('status.col.registered')}</th>
50
+ <th>{t('status.col.error')}</th>
50
51
  </tr>
51
52
  </thead>
52
53
  <tbody>
@@ -5,10 +5,11 @@
5
5
  import { useEffect, useState } from 'react'
6
6
  import type { RezApi } from '../api.ts'
7
7
  import type { RezWeixinStatus } from '../../protocol.ts'
8
- import { errorMessage, tt } from './helpers.ts'
8
+ import { errorMessage, useT } from './helpers.ts'
9
9
  import css from './panel.module.css'
10
10
 
11
11
  export function WeixinTab({ api }: { api: RezApi }) {
12
+ const t = useT()
12
13
  const [status, setStatus] = useState<RezWeixinStatus | null>(null)
13
14
  const [error, setError] = useState('')
14
15
  const [code, setCode] = useState('')
@@ -68,32 +69,32 @@ export function WeixinTab({ api }: { api: RezApi }) {
68
69
 
69
70
  return (
70
71
  <div className={css.tabBody}>
71
- <p className={css.message}>{tt('weixin.intro')}</p>
72
+ <p className={css.message}>{t('weixin.intro')}</p>
72
73
  {error !== '' && <div className={css.error}>{error}</div>}
73
74
  {status !== null && <p className={css.message}>{status.hint}</p>}
74
75
  {status?.qrDataUrl !== undefined && (
75
- <img className={css.qr} src={status.qrDataUrl} alt={tt('weixin.qrAlt')} />
76
+ <img className={css.qr} src={status.qrDataUrl} alt={t('weixin.qrAlt')} />
76
77
  )}
77
78
  {status?.qrContent !== undefined && (
78
79
  <p className={css.message}><a href={status.qrContent} target="_blank" rel="noreferrer">{status.qrContent}</a></p>
79
80
  )}
80
81
  {status?.phase === 'need_verify' && (
81
82
  <div className={css.toolbar}>
82
- <input className={css.input} value={code} onChange={event => { setCode(event.target.value) }} placeholder={tt('weixin.verifyPlaceholder')} />
83
- <button type="button" className={css.button} disabled={busy} onClick={() => { void verify() }}>{tt('weixin.verify')}</button>
83
+ <input className={css.input} value={code} onChange={event => { setCode(event.target.value) }} placeholder={t('weixin.verifyPlaceholder')} />
84
+ <button type="button" className={css.button} disabled={busy} onClick={() => { void verify() }}>{t('weixin.verify')}</button>
84
85
  </div>
85
86
  )}
86
87
  <div className={css.toolbar}>
87
88
  <button type="button" className={css.button} disabled={busy} onClick={() => { void login() }}>
88
- {status?.loggedIn === true ? tt('weixin.reconnect') : tt('weixin.login')}
89
+ {status?.loggedIn === true ? t('weixin.reconnect') : t('weixin.login')}
89
90
  </button>
90
91
  {status?.loggedIn === true && (
91
- <button type="button" className={css.button} disabled={busy} onClick={() => { void logout() }}>{tt('weixin.logout')}</button>
92
+ <button type="button" className={css.button} disabled={busy} onClick={() => { void logout() }}>{t('weixin.logout')}</button>
92
93
  )}
93
94
  </div>
94
95
  {status !== null && (
95
96
  <p className={css.message}>
96
- {status.listening ? tt('weixin.listening') : status.loggedIn ? tt('weixin.loggedIn') : tt('weixin.loggedOut')}
97
+ {status.listening ? t('weixin.listening') : status.loggedIn ? t('weixin.loggedIn') : t('weixin.loggedOut')}
97
98
  </p>
98
99
  )}
99
100
  </div>
@@ -1,18 +1,33 @@
1
1
  /**
2
- * Shared panel helpers: active-language dictionary pick and error extraction.
2
+ * Official DSH locale seat for ReZ UI copy.
3
+ *
4
+ * Slot components receive `t` from `locale: 'dsh-rez-suite'`. Fallback copy
5
+ * is Chinese only — do not read `<html lang>`. The Host stores zh/en in
6
+ * `locale.preference`, which does not necessarily match the document language.
3
7
  */
4
8
 
5
- import { en, t, zh, type RezKey } from '../locales.ts'
9
+ import { createContext, useContext } from 'react'
10
+ import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'
11
+ import { t as fill, zh, type RezKey } from '../locales.ts'
6
12
 
7
13
  export type TranslateValues = Record<string, string | number>
14
+ export type RezTranslate = TranslateNS<'dsh-rez-suite'>
8
15
 
16
+ export const RezT = createContext<RezTranslate | null>(null)
17
+
18
+ /** Fallback for tests / code outside a slot. Prefer {@link useT}. */
9
19
  export function dictionary(): Record<string, string> {
10
- const lang = typeof document !== 'undefined' ? document.documentElement.lang : 'zh'
11
- return lang.toLowerCase().startsWith('en') ? { ...en } : { ...zh }
20
+ return { ...zh }
12
21
  }
13
22
 
14
23
  export function tt(key: RezKey, values?: TranslateValues): string {
15
- return t(dictionary(), key, values)
24
+ return fill(dictionary(), key, values)
25
+ }
26
+
27
+ export function useT(): RezTranslate {
28
+ const injected = useContext(RezT)
29
+ if (injected) return injected
30
+ return (key, params) => fill(dictionary(), key as RezKey, params as TranslateValues | undefined)
16
31
  }
17
32
 
18
33
  export function errorMessage(error: unknown): string {
@@ -1,11 +1,14 @@
1
1
  /**
2
2
  * ReZ settings pages: one accordion card in 设置 → 插件 → 插件配置,
3
3
  * plus a dedicated Plugins tab for the same content.
4
+ *
5
+ * `t` is the official locale seat (zh/en follows 设置 → 语言).
4
6
  */
5
7
 
6
8
  import { useMemo, useState } from 'react'
9
+ import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'
7
10
  import { RezApi } from './api.ts'
8
- import { tt } from './panel/helpers.ts'
11
+ import { RezT } from './panel/helpers.ts'
9
12
  import { AuditTab } from './panel/AuditTab.tsx'
10
13
  import { ConfigTab } from './panel/ConfigTab.tsx'
11
14
  import { StatusTab } from './panel/StatusTab.tsx'
@@ -14,55 +17,58 @@ import css from './panel/panel.module.css'
14
17
 
15
18
  export type RezTab = 'config' | 'weixin' | 'status' | 'audit'
16
19
 
17
- const TABS: ReadonlyArray<{ id: RezTab; label: () => string }> = [
18
- { id: 'config', label: () => tt('tab.config') },
19
- { id: 'weixin', label: () => tt('tab.weixin') },
20
- { id: 'status', label: () => tt('tab.status') },
21
- { id: 'audit', label: () => tt('tab.audit') },
22
- ]
23
-
24
- export function RezSettingsPage() {
20
+ export function RezSettingsPage({ t }: { t: TranslateNS<'dsh-rez-suite'> }) {
25
21
  const api = useMemo(() => new RezApi(), [])
26
22
  const [activeTab, setActiveTab] = useState<RezTab>('config')
23
+ const tabs: ReadonlyArray<{ id: RezTab; label: string }> = [
24
+ { id: 'config', label: t('tab.config') },
25
+ { id: 'weixin', label: t('tab.weixin') },
26
+ { id: 'status', label: t('tab.status') },
27
+ { id: 'audit', label: t('tab.audit') },
28
+ ]
27
29
  return (
28
- <div className={css.shell}>
29
- <div className={css.tabBar} role="tablist">
30
- {TABS.map(tab => (
31
- <button
32
- key={tab.id}
33
- type="button"
34
- role="tab"
35
- aria-selected={activeTab === tab.id}
36
- data-active={activeTab === tab.id ? '' : undefined}
37
- className={css.tab}
38
- onClick={() => { setActiveTab(tab.id) }}
39
- >
40
- {tab.label()}
41
- </button>
42
- ))}
30
+ <RezT.Provider value={t}>
31
+ <div className={css.shell}>
32
+ <div className={css.tabBar} role="tablist">
33
+ {tabs.map(tab => (
34
+ <button
35
+ key={tab.id}
36
+ type="button"
37
+ role="tab"
38
+ aria-selected={activeTab === tab.id}
39
+ data-active={activeTab === tab.id ? '' : undefined}
40
+ className={css.tab}
41
+ onClick={() => { setActiveTab(tab.id) }}
42
+ >
43
+ {tab.label}
44
+ </button>
45
+ ))}
46
+ </div>
47
+ {activeTab === 'config' && <ConfigTab api={api} />}
48
+ {activeTab === 'weixin' && <WeixinTab api={api} />}
49
+ {activeTab === 'status' && <StatusTab api={api} />}
50
+ {activeTab === 'audit' && <AuditTab api={api} />}
43
51
  </div>
44
- {activeTab === 'config' && <ConfigTab api={api} />}
45
- {activeTab === 'weixin' && <WeixinTab api={api} />}
46
- {activeTab === 'status' && <StatusTab api={api} />}
47
- {activeTab === 'audit' && <AuditTab api={api} />}
48
- </div>
52
+ </RezT.Provider>
49
53
  )
50
54
  }
51
55
 
52
- export function RezPluginCard() {
56
+ export function RezPluginCard({ t }: { t: TranslateNS<'dsh-rez-suite'> }) {
53
57
  const [open, setOpen] = useState(false)
54
58
  return (
55
- <li className={css.card + (open ? ' ' + css.cardOpen : '')}>
56
- <button type="button" className={css.header} onClick={() => { setOpen(!open) }}>
57
- <div className={css.headText}>
58
- <div className={css.name}>{tt('settings.title')}</div>
59
- <div className={css.description}>{tt('settings.description')}</div>
60
- </div>
61
- <svg className={css.chevron + (open ? ' ' + css.chevronOpen : '')} viewBox="0 0 14 14" width="14" height="14" aria-hidden="true">
62
- <path d="M3 5l4 4 4-4" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
63
- </svg>
64
- </button>
65
- {open ? <div className={css.body}><RezSettingsPage /></div> : null}
66
- </li>
59
+ <RezT.Provider value={t}>
60
+ <li className={css.card + (open ? ' ' + css.cardOpen : '')}>
61
+ <button type="button" className={css.header} onClick={() => { setOpen(!open) }}>
62
+ <div className={css.headText}>
63
+ <div className={css.name}>{t('settings.title')}</div>
64
+ <div className={css.description}>{t('settings.description')}</div>
65
+ </div>
66
+ <svg className={css.chevron + (open ? ' ' + css.chevronOpen : '')} viewBox="0 0 14 14" width="14" height="14" aria-hidden="true">
67
+ <path d="M3 5l4 4 4-4" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
68
+ </svg>
69
+ </button>
70
+ {open ? <div className={css.body}><RezSettingsPage t={t} /></div> : null}
71
+ </li>
72
+ </RezT.Provider>
67
73
  )
68
74
  }