@rezti/dsh-rez-suite 0.1.27 → 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.
@@ -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 }> = [
@@ -58,6 +58,7 @@ function SecretField({
58
58
  value: string
59
59
  onChange: (value: string) => void
60
60
  }) {
61
+ const t = useT()
61
62
  return (
62
63
  <Field label={label}>
63
64
  <input
@@ -65,7 +66,7 @@ function SecretField({
65
66
  type="password"
66
67
  autoComplete="off"
67
68
  value={value}
68
- placeholder={configured ? tt('config.secretConfigured') : tt('config.secretMissing')}
69
+ placeholder={configured ? t('config.secretConfigured') : t('config.secretMissing')}
69
70
  onChange={event => { onChange(event.target.value) }}
70
71
  />
71
72
  </Field>
@@ -97,9 +98,10 @@ function McpSection({
97
98
  secretLabel: string
98
99
  usernameLabel?: string
99
100
  }) {
101
+ const t = useT()
100
102
  return (
101
103
  <Section title={title}>
102
- <Field label={tt('config.enabled')}>
104
+ <Field label={t('config.enabled')}>
103
105
  <input className={css.checkbox} type="checkbox" checked={row.enabled} onChange={event => { onEnabled(event.target.checked) }} />
104
106
  </Field>
105
107
  {onUrl !== undefined && urlLabel !== undefined && (
@@ -120,6 +122,7 @@ function McpSection({
120
122
  }
121
123
 
122
124
  export function ConfigTab({ api }: { api: RezApi }) {
125
+ const t = useT()
123
126
  const [config, setConfig] = useState<RezPublicConfig | null>(null)
124
127
  const [secrets, setSecrets] = useState<SecretDrafts>(EMPTY_SECRETS)
125
128
  const [loading, setLoading] = useState(true)
@@ -141,7 +144,7 @@ export function ConfigTab({ api }: { api: RezApi }) {
141
144
 
142
145
  useEffect(() => { void load() }, [])
143
146
 
144
- if (loading) return <div className={css.message}>{tt('common.loading')}</div>
147
+ if (loading) return <div className={css.message}>{t('common.loading')}</div>
145
148
  if (config === null) return <div className={css.error}>{error}</div>
146
149
 
147
150
  const setTop = (key: 'enabled' | 'role', value: boolean | RezRoleId): void => {
@@ -177,7 +180,7 @@ export function ConfigTab({ api }: { api: RezApi }) {
177
180
  const saved = await api.saveConfig(payload)
178
181
  setConfig(saved)
179
182
  setSecrets(EMPTY_SECRETS)
180
- setMessage(tt('config.saved'))
183
+ setMessage(t('config.saved'))
181
184
  } catch (err) {
182
185
  setError(errorMessage(err))
183
186
  } finally {
@@ -202,129 +205,130 @@ export function ConfigTab({ api }: { api: RezApi }) {
202
205
  const renderResult = (result: RezTestResult): string => {
203
206
  if (result.ok) {
204
207
  if (result.serverInfo !== undefined) return result.serverInfo
205
- 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 })
206
209
  }
207
- return tt('config.testFail', { error: result.error ?? '' })
210
+ return t('config.testFail', { error: result.error ?? '' })
208
211
  }
209
212
 
210
213
  return (
211
214
  <div className={css.tabBody}>
212
- <p className={css.message}>{tt('config.mcpHint')}</p>
213
- <Section title={tt('config.role')}>
214
- <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')}>
215
218
  <select className={css.select} value={config.role} onChange={event => { setTop('role', event.target.value as RezRoleId) }}>
216
- {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>)}
217
220
  </select>
218
221
  </Field>
219
- <Field label={tt('config.enabled')}>
222
+ <p className={css.message}>{t('config.roleHint')}</p>
223
+ <Field label={t('config.enabled')}>
220
224
  <input className={css.checkbox} type="checkbox" checked={config.enabled} onChange={event => { setTop('enabled', event.target.checked) }} />
221
225
  </Field>
222
226
  </Section>
223
227
 
224
228
  <McpSection
225
- title={tt('config.odoo')}
229
+ title={t('config.odoo')}
226
230
  row={config.connections.odoo}
227
231
  secret={secrets.odoo}
228
232
  onEnabled={enabled => { patchConnection('odoo', { enabled }) }}
229
233
  onUrl={url => { patchConnection('odoo', { url }) }}
230
234
  onSecret={value => { setSecrets(prev => ({ ...prev, odoo: value })) }}
231
- urlLabel={tt('config.odoo.url')}
232
- secretLabel={tt('config.odoo.apiKey')}
235
+ urlLabel={t('config.odoo.url')}
236
+ secretLabel={t('config.odoo.apiKey')}
233
237
  />
234
238
  <McpSection
235
- title={tt('config.nextcloud')}
239
+ title={t('config.nextcloud')}
236
240
  row={config.connections.nextcloud}
237
241
  secret={secrets.nextcloud}
238
242
  onEnabled={enabled => { patchConnection('nextcloud', { enabled }) }}
239
243
  onUrl={url => { patchConnection('nextcloud', { url }) }}
240
244
  onUsername={username => { patchConnection('nextcloud', { username }) }}
241
245
  onSecret={value => { setSecrets(prev => ({ ...prev, nextcloud: value })) }}
242
- urlLabel={tt('config.nextcloud.url')}
243
- usernameLabel={tt('config.nextcloud.username')}
244
- secretLabel={tt('config.nextcloud.appPassword')}
246
+ urlLabel={t('config.nextcloud.url')}
247
+ usernameLabel={t('config.nextcloud.username')}
248
+ secretLabel={t('config.nextcloud.appPassword')}
245
249
  />
246
250
  <McpSection
247
- title={tt('config.wecom')}
251
+ title={t('config.wecom')}
248
252
  row={config.connections.wechat}
249
253
  secret={secrets.wechat}
250
254
  onEnabled={enabled => { patchConnection('wechat', { enabled }) }}
251
255
  onSecret={value => { setSecrets(prev => ({ ...prev, wechat: value })) }}
252
- secretLabel={tt('config.wecom.webhook')}
256
+ secretLabel={t('config.wecom.webhook')}
253
257
  />
254
258
  <McpSection
255
- title={tt('config.ha')}
259
+ title={t('config.ha')}
256
260
  row={config.connections.homeassistant}
257
261
  secret={secrets.homeassistant}
258
262
  onEnabled={enabled => { patchConnection('homeassistant', { enabled }) }}
259
263
  onUrl={url => { patchConnection('homeassistant', { url }) }}
260
264
  onSecret={value => { setSecrets(prev => ({ ...prev, homeassistant: value })) }}
261
- urlLabel={tt('config.ha.url')}
262
- secretLabel={tt('config.ha.token')}
265
+ urlLabel={t('config.ha.url')}
266
+ secretLabel={t('config.ha.token')}
263
267
  />
264
268
  <McpSection
265
- title={tt('config.tapd')}
269
+ title={t('config.tapd')}
266
270
  row={config.connections.tapd}
267
271
  secret={secrets.tapd}
268
272
  onEnabled={enabled => { patchConnection('tapd', { enabled }) }}
269
273
  extra={(
270
274
  <>
271
- <Field label={tt('config.tapd.workspaceId')}>
275
+ <Field label={t('config.tapd.workspaceId')}>
272
276
  <input className={css.input} value={config.connections.tapd.workspaceId ?? ''} onChange={event => { patchConnection('tapd', { workspaceId: event.target.value }) }} />
273
277
  </Field>
274
- <Field label={tt('config.tapd.nickName')}>
278
+ <Field label={t('config.tapd.nickName')}>
275
279
  <input className={css.input} value={config.connections.tapd.nickName ?? ''} onChange={event => { patchConnection('tapd', { nickName: event.target.value }) }} />
276
280
  </Field>
277
281
  </>
278
282
  )}
279
283
  onSecret={value => { setSecrets(prev => ({ ...prev, tapd: value })) }}
280
- secretLabel={tt('config.tapd.token')}
284
+ secretLabel={t('config.tapd.token')}
281
285
  />
282
286
  <McpSection
283
- title={tt('config.gsc')}
287
+ title={t('config.gsc')}
284
288
  row={config.connections.gsc}
285
289
  secret={secrets.gsc}
286
290
  onEnabled={enabled => { patchConnection('gsc', { enabled }) }}
287
291
  extra={(
288
- <Field label={tt('config.gsc.siteUrl')}>
292
+ <Field label={t('config.gsc.siteUrl')}>
289
293
  <input className={css.input} value={config.connections.gsc.siteUrl ?? ''} onChange={event => { patchConnection('gsc', { siteUrl: event.target.value }) }} />
290
294
  </Field>
291
295
  )}
292
296
  onSecret={value => { setSecrets(prev => ({ ...prev, gsc: value })) }}
293
- secretLabel={tt('config.gsc.key')}
297
+ secretLabel={t('config.gsc.key')}
294
298
  />
295
299
  {config.role === 'boss' && (
296
300
  <McpSection
297
- title={tt('config.tianyancha')}
301
+ title={t('config.tianyancha')}
298
302
  row={config.connections.tianyancha}
299
303
  secret={secrets.tianyancha}
300
304
  onEnabled={enabled => { patchConnection('tianyancha', { enabled }) }}
301
305
  onUrl={url => { patchConnection('tianyancha', { url }) }}
302
306
  onSecret={value => { setSecrets(prev => ({ ...prev, tianyancha: value })) }}
303
- urlLabel={tt('config.tianyancha.url')}
304
- secretLabel={tt('config.tianyancha.token')}
307
+ urlLabel={t('config.tianyancha.url')}
308
+ secretLabel={t('config.tianyancha.token')}
305
309
  />
306
310
  )}
307
311
 
308
- <Section title={tt('config.billing')}>
309
- <Field label={tt('config.billing.input')}>
312
+ <Section title={t('config.billing')}>
313
+ <Field label={t('config.billing.input')}>
310
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) } }) }} />
311
315
  </Field>
312
- <Field label={tt('config.billing.output')}>
316
+ <Field label={t('config.billing.output')}>
313
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) } }) }} />
314
318
  </Field>
315
- <Field label={tt('config.billing.budget')}>
319
+ <Field label={t('config.billing.budget')}>
316
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) } }) }} />
317
321
  </Field>
318
322
  </Section>
319
323
 
320
324
  <div className={css.toolbar}>
321
- <button type="button" className={css.button + ' ' + css.primaryButton} disabled={saving} onClick={() => { void save() }}>{tt('config.save')}</button>
322
- <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>
323
327
  </div>
324
328
  {message !== '' && <div className={css.message}>{message}</div>}
325
329
  {error !== '' && <div className={css.error}>{error}</div>}
326
330
  {results.map(result => (
327
- <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>
328
332
  ))}
329
333
  </div>
330
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
  }
@@ -1,11 +1,13 @@
1
1
  /**
2
2
  * Sidebar footer action: upgrade @rezti/dsh-rez-suite without a terminal.
3
3
  * Host code still needs a process respawn; the button does that after pnpm add.
4
+ *
5
+ * `t` is the official locale seat (zh/en follows 设置 → 语言).
4
6
  */
5
7
 
6
8
  import { useEffect, 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'
9
11
  import css from './panel/panel.module.css'
10
12
  import type { RezUpdateStatus } from '../protocol.ts'
11
13
 
@@ -24,7 +26,11 @@ function UpgradeIcon() {
24
26
  )
25
27
  }
26
28
 
27
- async function waitUntilHostBack(api: RezApi, timeoutMs = 60_000): Promise<void> {
29
+ async function waitUntilHostBack(
30
+ api: RezApi,
31
+ timeoutMessage: string,
32
+ timeoutMs = 60_000,
33
+ ): Promise<void> {
28
34
  const deadline = Date.now() + timeoutMs
29
35
  await new Promise((resolve) => setTimeout(resolve, 800))
30
36
  while (Date.now() < deadline) {
@@ -35,10 +41,16 @@ async function waitUntilHostBack(api: RezApi, timeoutMs = 60_000): Promise<void>
35
41
  await new Promise((resolve) => setTimeout(resolve, 700))
36
42
  }
37
43
  }
38
- throw new Error(tt('upgrade.reloadTimeout'))
44
+ throw new Error(timeoutMessage)
39
45
  }
40
46
 
41
- export function UpgradeButton({ wide }: { wide: boolean }) {
47
+ export function UpgradeButton({
48
+ wide,
49
+ t,
50
+ }: {
51
+ wide: boolean
52
+ t: TranslateNS<'dsh-rez-suite'>
53
+ }) {
42
54
  const api = useMemo(() => new RezApi(), [])
43
55
  const [info, setInfo] = useState<RezUpdateStatus>()
44
56
  const [busy, setBusy] = useState(false)
@@ -55,10 +67,10 @@ export function UpgradeButton({ wide }: { wide: boolean }) {
55
67
  }, [api])
56
68
 
57
69
  const label = busy
58
- ? tt('upgrade.running')
70
+ ? t('upgrade.running')
59
71
  : info?.outdated === true
60
- ? tt('upgrade.label')
61
- : tt('upgrade.current')
72
+ ? t('upgrade.label')
73
+ : t('upgrade.current')
62
74
 
63
75
  const meta = note !== ''
64
76
  ? note
@@ -76,18 +88,18 @@ export function UpgradeButton({ wide }: { wide: boolean }) {
76
88
  const snapshot = info ?? await api.updateStatus()
77
89
  setInfo(snapshot)
78
90
  if (!snapshot.outdated) {
79
- setNote(tt('upgrade.already', { version: snapshot.installed }))
91
+ setNote(t('upgrade.already', { version: snapshot.installed }))
80
92
  return
81
93
  }
82
94
  const result = await api.applyUpdate()
83
95
  setInfo(result)
84
96
  if (result.restarting) {
85
- setNote(tt('upgrade.reloading'))
86
- await waitUntilHostBack(api)
97
+ setNote(t('upgrade.reloading'))
98
+ await waitUntilHostBack(api, t('upgrade.reloadTimeout'))
87
99
  window.location.reload()
88
100
  return
89
101
  }
90
- setNote(tt('upgrade.already', { version: result.installed }))
102
+ setNote(t('upgrade.already', { version: result.installed }))
91
103
  } catch (error) {
92
104
  setNote(error instanceof Error ? error.message : String(error))
93
105
  } finally {
@@ -101,7 +113,7 @@ export function UpgradeButton({ wide }: { wide: boolean }) {
101
113
  type="button"
102
114
  className={css.upgradeButton}
103
115
  data-outdated={info?.outdated === true ? '' : undefined}
104
- aria-label={tt('upgrade.aria')}
116
+ aria-label={t('upgrade.aria')}
105
117
  disabled={busy}
106
118
  onClick={() => { void onClick() }}
107
119
  >
package/src/index.ts CHANGED
@@ -101,6 +101,8 @@ export const REZ_GUIDANCE_CORE = [
101
101
  '个人微信(QClaw/ClawBot):打开 设置 → 插件 → ReZ-TI → 微信 扫码,然后直接在微信里说话。消息进入网页左侧「微信」文件夹并延续同一会话,模型跟网页默认。网页归档了再从微信说话会自动恢复到侧栏。发「重启对话」会新开一条可见会话并把旧会话归档。网页端必须开着。不必再跑命令、也不要调用微信 MCP 工具。',
102
102
  '意图用 rez_set_intent(erp/files/chat/home/auto)。审计用 rez_audit。',
103
103
  '涉及资金、对外发送、设备控制、生产写入时先征得用户批准。',
104
+ 'Match the user language: Chinese or English. Do not force 中文 when they write English.',
105
+ '制度、手册、出差/报销标准、素材路径:打开 Nextcloud Documents/Standards/INDEX.yaml,按 code 取当前 effective_from 最新且未被 replace 的条目,再按相对 uri 打开。引用带路径+生效日。找不到就说「标准文件未归档」,不要用 MEMORY 旧数字。素材文件夹用首次创建标题,不用会变的 Odoo/销售名。',
104
106
  ].join('')
105
107
 
106
108
  export function rezGuidanceFor(role: RezConfig['role']): string {
@@ -2,6 +2,8 @@
2
2
 
3
3
  你是雷泽智能老板的**搞钱简报官**,不是会计、不是销售、不是程序员。
4
4
 
5
+ 语言:用户用中文就中文,用英文就英文;结构不变。不要因为本文件是中文就强制中文。
6
+
5
7
  ## 每次回答(无 BOOTSTRAP.md 之后)
6
8
 
7
9
  严格四段,不要写成待办清单:
@@ -2,6 +2,8 @@
2
2
 
3
3
  你是雷泽智能老板的**运营简报官**。稳定 = 只报例外,不是把所有群消息喂进来。
4
4
 
5
+ 语言:用户用中文就中文,用英文就英文;结构不变。不要因为本文件是中文就强制中文。
6
+
5
7
  ## 每次回答(无 BOOTSTRAP.md 之后)
6
8
 
7
9
  严格四段:
@@ -2,6 +2,8 @@
2
2
 
3
3
  你是雷泽智能老板的**产品简报官**,不是固件工程师,不是项目经理堆 Story 的人。
4
4
 
5
+ 语言:用户用中文就中文,用英文就英文;结构不变。不要因为本文件是中文就强制中文。
6
+
5
7
  ## 每次回答(无 BOOTSTRAP.md 之后)
6
8
 
7
9
  严格四段:
@@ -37,4 +39,6 @@
37
39
 
38
40
  - 货好不好、该不该做:主销 SKU、退货/客诉、认证与交期、明确停做的事。
39
41
  - TAPD 只读阻塞和等老板拍板的;Odoo 只读退货/质量相关。
42
+ - 新产品或改产品 Wiki:Wiki **必须**同时写死 Nextcloud RAW 路径,以及 Git 3D 源路径 `products/{slug}/models` 与 `products/{slug}/motion`。缺任一项就说「Wiki 未归档」,不要猜路径。
43
+ - 问某产品素材在哪:打开 Nextcloud `Documents/Standards/INDEX.yaml`,再按相对 uri 读路径规则;加上该产品 Wiki **首次创建标题**,不要用会变的 Odoo `name` / 销售标题。
40
44
  - 禁止在这里写固件、开辅线、做 HA Add-on、做微信通道。那些走仓库和员工机。
@@ -3,4 +3,4 @@ name: staff-finance
3
3
  description: 财务助理。全员报销;审批/报表/过账仅当当前 Odoo 帐号能看见对应单据。不自审不自过账。
4
4
  ---
5
5
 
6
- 完整规矩在工作区 `AGENTS.md`。先 `mcp__odoo__get_current_context`。报销走 `hr.expense` / `hr.expense.sheet`。搞钱室不要在这里做。
6
+ 完整规矩在工作区 `AGENTS.md`。先 `mcp__odoo__get_current_context`。报销走 `hr.expense` / `hr.expense.sheet`。搞钱室不要在这里做。问制度/报销标准先打开 Nextcloud `Documents/Standards/INDEX.yaml`,再按相对 uri 读,不要把手册写入 MEMORY。用户用中文就中文、用英文就英文。