@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.
- package/README.md +1 -1
- package/lib/client.d.ts +7 -0
- package/lib/client.js +388 -187
- package/lib/index.d.ts +4 -0
- package/lib/index.js +85 -7
- package/lib/style.css +47 -0
- package/package.json +7 -7
- package/src/boss/mount.ts +2 -0
- package/src/client/assets/rez-mark.png +0 -0
- package/src/client/brand-mark.ts +4 -0
- package/src/client/brand.module.css +51 -0
- package/src/client/brand.ts +135 -0
- package/src/client/index.ts +11 -0
- package/src/client/locales.ts +14 -0
- package/src/client/panel/AuditTab.tsx +20 -19
- package/src/client/panel/ConfigTab.tsx +57 -39
- package/src/client/panel/StatusTab.tsx +11 -10
- package/src/client/panel/WeixinTab.tsx +9 -8
- package/src/client/panel/helpers.ts +20 -5
- package/src/client/settings-card.tsx +48 -42
- package/src/client/upgrade-button.tsx +24 -12
- package/src/index.ts +11 -0
- package/src/observe-mcp.ts +5 -2
- package/src/presets.ts +1 -0
- package/src/protocol.ts +2 -0
- package/src/store.ts +6 -0
- package/src/tianyancha.ts +79 -0
- package/templates/boss/money/AGENTS.md +2 -0
- package/templates/boss/ops/AGENTS.md +2 -0
- package/templates/boss/product/AGENTS.md +4 -0
- package/templates/staff/finance/.agents/skills/staff-finance/SKILL.md +1 -1
- package/templates/staff/finance/AGENTS.md +6 -0
- package/templates/staff/finance/MEMORY.md +1 -1
- package/templates/staff/hr/.agents/skills/staff-hr/SKILL.md +1 -1
- package/templates/staff/hr/AGENTS.md +6 -0
- package/templates/staff/hr/MEMORY.md +1 -1
- package/templates/staff/legal/.agents/skills/staff-legal/SKILL.md +1 -1
- package/templates/staff/legal/AGENTS.md +10 -0
- 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,
|
|
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(
|
|
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}>{
|
|
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() }}>{
|
|
47
|
-
<button type="button" className={css.button + ' ' + css.dangerButton} onClick={() => { void reset() }}>{
|
|
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}>{
|
|
52
|
-
<div className={css.stat}><div className={css.cardLabel}>{
|
|
53
|
-
<div className={css.stat}><div className={css.cardLabel}>{
|
|
54
|
-
<div className={css.stat}><div className={css.cardLabel}>{
|
|
55
|
-
<div className={css.stat}><div className={css.cardLabel}>{
|
|
56
|
-
<div className={css.stat}><div className={css.cardLabel}>{
|
|
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>{
|
|
63
|
-
<th>{
|
|
64
|
-
<th>{
|
|
65
|
-
<th>{
|
|
66
|
-
<th>{
|
|
67
|
-
<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}>{
|
|
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}>{
|
|
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,
|
|
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 ?
|
|
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={
|
|
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}>{
|
|
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(
|
|
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
|
|
208
|
+
return t('config.testOk', { tools: result.toolCount ?? 0, latency: result.latencyMs ?? 0 })
|
|
204
209
|
}
|
|
205
|
-
return
|
|
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}>{
|
|
211
|
-
<Section title={
|
|
212
|
-
<Field label={
|
|
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}>{
|
|
219
|
+
{ROLES.map(role => <option key={role.value} value={role.value}>{t(role.label as never)}</option>)}
|
|
215
220
|
</select>
|
|
216
221
|
</Field>
|
|
217
|
-
<
|
|
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={
|
|
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={
|
|
230
|
-
secretLabel={
|
|
235
|
+
urlLabel={t('config.odoo.url')}
|
|
236
|
+
secretLabel={t('config.odoo.apiKey')}
|
|
231
237
|
/>
|
|
232
238
|
<McpSection
|
|
233
|
-
title={
|
|
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={
|
|
241
|
-
usernameLabel={
|
|
242
|
-
secretLabel={
|
|
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={
|
|
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={
|
|
256
|
+
secretLabel={t('config.wecom.webhook')}
|
|
251
257
|
/>
|
|
252
258
|
<McpSection
|
|
253
|
-
title={
|
|
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={
|
|
260
|
-
secretLabel={
|
|
265
|
+
urlLabel={t('config.ha.url')}
|
|
266
|
+
secretLabel={t('config.ha.token')}
|
|
261
267
|
/>
|
|
262
268
|
<McpSection
|
|
263
|
-
title={
|
|
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={
|
|
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={
|
|
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={
|
|
284
|
+
secretLabel={t('config.tapd.token')}
|
|
279
285
|
/>
|
|
280
286
|
<McpSection
|
|
281
|
-
title={
|
|
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={
|
|
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={
|
|
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={
|
|
295
|
-
<Field label={
|
|
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={
|
|
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={
|
|
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() }}>{
|
|
308
|
-
<button type="button" className={css.button} disabled={testing} onClick={() => { void test() }}>{testing ?
|
|
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}>{
|
|
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,
|
|
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}>{
|
|
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}>{
|
|
37
|
-
<button type="button" className={css.button} onClick={() => { void load() }}>{
|
|
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}>{
|
|
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>{
|
|
46
|
-
<th>{
|
|
47
|
-
<th>{
|
|
48
|
-
<th>{
|
|
49
|
-
<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,
|
|
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}>{
|
|
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={
|
|
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={
|
|
83
|
-
<button type="button" className={css.button} disabled={busy} onClick={() => { void verify() }}>{
|
|
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 ?
|
|
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() }}>{
|
|
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 ?
|
|
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
|
-
*
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
-
<
|
|
29
|
-
<div className={css.
|
|
30
|
-
{
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
56
|
-
<
|
|
57
|
-
<
|
|
58
|
-
<div className={css.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
}
|