@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.
- package/lib/client.d.ts +4 -0
- package/lib/client.js +360 -189
- package/lib/index.js +7 -3
- package/lib/style.css +47 -0
- package/package.json +5 -5
- 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 +8 -0
- package/src/client/panel/AuditTab.tsx +20 -19
- package/src/client/panel/ConfigTab.tsx +45 -41
- 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 +2 -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 +6 -0
- package/templates/staff/legal/MEMORY.md +1 -1
|
@@ -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 }> = [
|
|
@@ -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 ?
|
|
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={
|
|
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}>{
|
|
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(
|
|
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
|
|
208
|
+
return t('config.testOk', { tools: result.toolCount ?? 0, latency: result.latencyMs ?? 0 })
|
|
206
209
|
}
|
|
207
|
-
return
|
|
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}>{
|
|
213
|
-
<Section title={
|
|
214
|
-
<Field label={
|
|
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}>{
|
|
219
|
+
{ROLES.map(role => <option key={role.value} value={role.value}>{t(role.label as never)}</option>)}
|
|
217
220
|
</select>
|
|
218
221
|
</Field>
|
|
219
|
-
<
|
|
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={
|
|
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={
|
|
232
|
-
secretLabel={
|
|
235
|
+
urlLabel={t('config.odoo.url')}
|
|
236
|
+
secretLabel={t('config.odoo.apiKey')}
|
|
233
237
|
/>
|
|
234
238
|
<McpSection
|
|
235
|
-
title={
|
|
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={
|
|
243
|
-
usernameLabel={
|
|
244
|
-
secretLabel={
|
|
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={
|
|
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={
|
|
256
|
+
secretLabel={t('config.wecom.webhook')}
|
|
253
257
|
/>
|
|
254
258
|
<McpSection
|
|
255
|
-
title={
|
|
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={
|
|
262
|
-
secretLabel={
|
|
265
|
+
urlLabel={t('config.ha.url')}
|
|
266
|
+
secretLabel={t('config.ha.token')}
|
|
263
267
|
/>
|
|
264
268
|
<McpSection
|
|
265
|
-
title={
|
|
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={
|
|
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={
|
|
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={
|
|
284
|
+
secretLabel={t('config.tapd.token')}
|
|
281
285
|
/>
|
|
282
286
|
<McpSection
|
|
283
|
-
title={
|
|
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={
|
|
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={
|
|
297
|
+
secretLabel={t('config.gsc.key')}
|
|
294
298
|
/>
|
|
295
299
|
{config.role === 'boss' && (
|
|
296
300
|
<McpSection
|
|
297
|
-
title={
|
|
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={
|
|
304
|
-
secretLabel={
|
|
307
|
+
urlLabel={t('config.tianyancha.url')}
|
|
308
|
+
secretLabel={t('config.tianyancha.token')}
|
|
305
309
|
/>
|
|
306
310
|
)}
|
|
307
311
|
|
|
308
|
-
<Section title={
|
|
309
|
-
<Field label={
|
|
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={
|
|
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={
|
|
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() }}>{
|
|
322
|
-
<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>
|
|
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}>{
|
|
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,
|
|
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
|
}
|
|
@@ -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(
|
|
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(
|
|
44
|
+
throw new Error(timeoutMessage)
|
|
39
45
|
}
|
|
40
46
|
|
|
41
|
-
export function UpgradeButton({
|
|
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
|
-
?
|
|
70
|
+
? t('upgrade.running')
|
|
59
71
|
: info?.outdated === true
|
|
60
|
-
?
|
|
61
|
-
:
|
|
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(
|
|
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(
|
|
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(
|
|
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={
|
|
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
|
你是雷泽智能老板的**产品简报官**,不是固件工程师,不是项目经理堆 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。用户用中文就中文、用英文就英文。
|