@rezti/dsh-rez-suite 0.1.4 → 0.1.6
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/README.zh.md +1 -1
- package/lib/client.d.ts +29 -155
- package/lib/client.js +365 -468
- package/lib/index.d.ts +20 -0
- package/lib/index.js +92 -13
- package/lib/style.css +82 -111
- package/package.json +9 -7
- package/src/client/index.ts +33 -21
- package/src/client/locales.ts +25 -7
- package/src/client/panel/AuditTab.tsx +6 -6
- package/src/client/panel/ConfigTab.tsx +141 -86
- package/src/client/panel/panel.module.css +89 -118
- package/src/client/settings-card.tsx +68 -0
- package/src/index.ts +37 -11
- package/src/protocol.ts +23 -0
- package/src/routes.ts +32 -4
- package/src/store.ts +37 -1
- package/src/client/mount.tsx +0 -97
- package/src/client/panel/RezPanel.tsx +0 -53
- package/src/client/panel/controller.ts +0 -42
- package/src/client/sidebar-entry.ts +0 -80
package/src/client/locales.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
export const zh = {
|
|
6
6
|
'entry.label': 'Rez',
|
|
7
7
|
'entry.tooltip': 'Rez Suite 配置与审计面板',
|
|
8
|
+
'settings.title': 'ReZ-TI',
|
|
9
|
+
'settings.description': '公司 MCP(Odoo / Nextcloud / 企微 / HA)与个人微信扫码通道。',
|
|
8
10
|
'panel.title': 'Rez Suite',
|
|
9
11
|
'tab.config': '配置',
|
|
10
12
|
'tab.weixin': '微信',
|
|
@@ -25,16 +27,23 @@ export const zh = {
|
|
|
25
27
|
'config.env': '环境变量 (JSON)',
|
|
26
28
|
'config.headers': 'Headers (JSON)',
|
|
27
29
|
'config.jsonInvalid': 'JSON 格式错误:{error}',
|
|
30
|
+
'config.mcpHint': '四个公司 MCP 在这里配。密钥写入 ~/.dsh/.credentials.yaml(REZ_*)。新填的密钥会立刻拉起连接;改 URL 后如果已经连上,需要重启 dsh web。',
|
|
31
|
+
'config.secretConfigured': '已配置(留空则保持)',
|
|
32
|
+
'config.secretMissing': '未配置',
|
|
28
33
|
'config.odoo': 'Odoo',
|
|
29
|
-
'config.odoo.url': 'Odoo URL',
|
|
30
|
-
'config.odoo.apiKey': 'API
|
|
34
|
+
'config.odoo.url': 'Odoo MCP URL',
|
|
35
|
+
'config.odoo.apiKey': 'API Token',
|
|
31
36
|
'config.nextcloud': 'Nextcloud',
|
|
32
|
-
'config.nextcloud.url': 'Nextcloud
|
|
37
|
+
'config.nextcloud.url': 'Nextcloud 主机',
|
|
33
38
|
'config.nextcloud.username': '用户名',
|
|
34
39
|
'config.nextcloud.appPassword': 'App Password',
|
|
35
40
|
'config.wecom': '企业微信',
|
|
36
41
|
'config.wecom.botId': 'Bot ID',
|
|
37
42
|
'config.wecom.secret': 'Secret',
|
|
43
|
+
'config.wecom.webhook': '群机器人 Webhook',
|
|
44
|
+
'config.ha': 'Home Assistant',
|
|
45
|
+
'config.ha.url': 'HA MCP URL',
|
|
46
|
+
'config.ha.token': '长期访问令牌',
|
|
38
47
|
'config.fs': '本地文件 (fs-mcp-server)',
|
|
39
48
|
'config.fs.root': '根目录',
|
|
40
49
|
'config.sqlite': 'SQLite (sqlite-mcp-server)',
|
|
@@ -99,6 +108,8 @@ export type RezKey = keyof typeof zh
|
|
|
99
108
|
export const en: Record<RezKey, string> = {
|
|
100
109
|
'entry.label': 'Rez',
|
|
101
110
|
'entry.tooltip': 'Rez Suite configuration and audit panel',
|
|
111
|
+
'settings.title': 'ReZ-TI',
|
|
112
|
+
'settings.description': 'Company MCP (Odoo / Nextcloud / WeCom / HA) and personal WeChat scan-and-chat.',
|
|
102
113
|
'panel.title': 'Rez Suite',
|
|
103
114
|
'tab.config': 'Config',
|
|
104
115
|
'tab.weixin': 'WeChat',
|
|
@@ -118,16 +129,23 @@ export const en: Record<RezKey, string> = {
|
|
|
118
129
|
'config.env': 'Env vars (JSON)',
|
|
119
130
|
'config.headers': 'Headers (JSON)',
|
|
120
131
|
'config.jsonInvalid': 'Invalid JSON: {error}',
|
|
132
|
+
'config.mcpHint': 'Company MCP lives here. Secrets go to ~/.dsh/.credentials.yaml (REZ_*). A newly saved secret starts the connection immediately; URL changes after a live connection need a dsh web restart.',
|
|
133
|
+
'config.secretConfigured': 'Configured (leave blank to keep)',
|
|
134
|
+
'config.secretMissing': 'Not configured',
|
|
121
135
|
'config.odoo': 'Odoo',
|
|
122
|
-
'config.odoo.url': 'Odoo URL',
|
|
123
|
-
'config.odoo.apiKey': 'API
|
|
136
|
+
'config.odoo.url': 'Odoo MCP URL',
|
|
137
|
+
'config.odoo.apiKey': 'API token',
|
|
124
138
|
'config.nextcloud': 'Nextcloud',
|
|
125
|
-
'config.nextcloud.url': 'Nextcloud
|
|
139
|
+
'config.nextcloud.url': 'Nextcloud host',
|
|
126
140
|
'config.nextcloud.username': 'Username',
|
|
127
|
-
'config.nextcloud.appPassword': 'App
|
|
141
|
+
'config.nextcloud.appPassword': 'App password',
|
|
128
142
|
'config.wecom': 'WeCom',
|
|
129
143
|
'config.wecom.botId': 'Bot ID',
|
|
130
144
|
'config.wecom.secret': 'Secret',
|
|
145
|
+
'config.wecom.webhook': 'Group bot webhook',
|
|
146
|
+
'config.ha': 'Home Assistant',
|
|
147
|
+
'config.ha.url': 'HA MCP URL',
|
|
148
|
+
'config.ha.token': 'Long-lived access token',
|
|
131
149
|
'config.fs': 'Local files (fs-mcp-server)',
|
|
132
150
|
'config.fs.root': 'Root directory',
|
|
133
151
|
'config.sqlite': 'SQLite (sqlite-mcp-server)',
|
|
@@ -48,12 +48,12 @@ export function AuditTab({ api }: { api: RezApi }) {
|
|
|
48
48
|
</div>
|
|
49
49
|
{error !== '' && <div className={css.error}>{error}</div>}
|
|
50
50
|
<div className={css.cards}>
|
|
51
|
-
<div className={css.
|
|
52
|
-
<div className={css.
|
|
53
|
-
<div className={css.
|
|
54
|
-
<div className={css.
|
|
55
|
-
<div className={css.
|
|
56
|
-
<div className={css.
|
|
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>
|
|
57
57
|
</div>
|
|
58
58
|
<div className={css.tableWrap}>
|
|
59
59
|
<table className={css.table}>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Config tab:
|
|
3
|
-
*
|
|
2
|
+
* Config tab: company MCP connections (Odoo / Nextcloud / WeCom / HA),
|
|
3
|
+
* role, and billing. Secrets are write-only and land in credentials.yaml.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { useEffect, useState, type ReactNode } from 'react'
|
|
7
7
|
import type { RezApi } from '../api.ts'
|
|
8
|
-
import type {
|
|
8
|
+
import type { RezPublicConfig, RezPublicConnection, RezRoleId, RezTestResult } from '../../protocol.ts'
|
|
9
9
|
import { errorMessage, tt } from './helpers.ts'
|
|
10
10
|
import css from './panel.module.css'
|
|
11
11
|
|
|
@@ -16,6 +16,15 @@ const ROLES: Array<{ value: RezRoleId; label: string }> = [
|
|
|
16
16
|
{ value: 'operations', label: 'role.operations' },
|
|
17
17
|
]
|
|
18
18
|
|
|
19
|
+
type SecretDrafts = {
|
|
20
|
+
odoo: string
|
|
21
|
+
nextcloud: string
|
|
22
|
+
wechat: string
|
|
23
|
+
homeassistant: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const EMPTY_SECRETS: SecretDrafts = { odoo: '', nextcloud: '', wechat: '', homeassistant: '' }
|
|
27
|
+
|
|
19
28
|
function Field({ label, children }: { label: string; children: ReactNode }) {
|
|
20
29
|
return (
|
|
21
30
|
<div className={css.field}>
|
|
@@ -34,47 +43,88 @@ function Section({ title, children }: { title: string; children: ReactNode }) {
|
|
|
34
43
|
)
|
|
35
44
|
}
|
|
36
45
|
|
|
37
|
-
function
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
function SecretField({
|
|
47
|
+
label,
|
|
48
|
+
configured,
|
|
49
|
+
value,
|
|
50
|
+
onChange,
|
|
51
|
+
}: {
|
|
52
|
+
label: string
|
|
53
|
+
configured: boolean
|
|
54
|
+
value: string
|
|
55
|
+
onChange: (value: string) => void
|
|
56
|
+
}) {
|
|
57
|
+
return (
|
|
58
|
+
<Field label={label}>
|
|
59
|
+
<input
|
|
60
|
+
className={css.input}
|
|
61
|
+
type="password"
|
|
62
|
+
autoComplete="off"
|
|
63
|
+
value={value}
|
|
64
|
+
placeholder={configured ? tt('config.secretConfigured') : tt('config.secretMissing')}
|
|
65
|
+
onChange={event => { onChange(event.target.value) }}
|
|
66
|
+
/>
|
|
67
|
+
</Field>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function McpSection({
|
|
72
|
+
title,
|
|
73
|
+
row,
|
|
74
|
+
secret,
|
|
75
|
+
onEnabled,
|
|
76
|
+
onUrl,
|
|
77
|
+
onUsername,
|
|
78
|
+
onSecret,
|
|
79
|
+
urlLabel,
|
|
80
|
+
secretLabel,
|
|
81
|
+
usernameLabel,
|
|
82
|
+
}: {
|
|
83
|
+
title: string
|
|
84
|
+
row: RezPublicConnection
|
|
85
|
+
secret: string
|
|
86
|
+
onEnabled: (enabled: boolean) => void
|
|
87
|
+
onUrl?: (url: string) => void
|
|
88
|
+
onUsername?: (username: string) => void
|
|
89
|
+
onSecret: (secret: string) => void
|
|
90
|
+
urlLabel?: string
|
|
91
|
+
secretLabel: string
|
|
92
|
+
usernameLabel?: string
|
|
93
|
+
}) {
|
|
94
|
+
return (
|
|
95
|
+
<Section title={title}>
|
|
96
|
+
<Field label={tt('config.enabled')}>
|
|
97
|
+
<input className={css.checkbox} type="checkbox" checked={row.enabled} onChange={event => { onEnabled(event.target.checked) }} />
|
|
98
|
+
</Field>
|
|
99
|
+
{onUrl !== undefined && urlLabel !== undefined && (
|
|
100
|
+
<Field label={urlLabel}>
|
|
101
|
+
<input className={css.input} value={row.url ?? ''} onChange={event => { onUrl(event.target.value) }} />
|
|
102
|
+
</Field>
|
|
103
|
+
)}
|
|
104
|
+
{onUsername !== undefined && usernameLabel !== undefined && (
|
|
105
|
+
<Field label={usernameLabel}>
|
|
106
|
+
<input className={css.input} value={row.username ?? ''} onChange={event => { onUsername(event.target.value) }} />
|
|
107
|
+
</Field>
|
|
108
|
+
)}
|
|
109
|
+
<SecretField label={secretLabel} configured={row.secretConfigured} value={secret} onChange={onSecret} />
|
|
110
|
+
<p className={css.message}>{row.secretRef}</p>
|
|
111
|
+
</Section>
|
|
112
|
+
)
|
|
45
113
|
}
|
|
46
114
|
|
|
47
115
|
export function ConfigTab({ api }: { api: RezApi }) {
|
|
48
116
|
const [config, setConfig] = useState<RezPublicConfig | null>(null)
|
|
117
|
+
const [secrets, setSecrets] = useState<SecretDrafts>(EMPTY_SECRETS)
|
|
49
118
|
const [loading, setLoading] = useState(true)
|
|
50
119
|
const [saving, setSaving] = useState(false)
|
|
51
120
|
const [testing, setTesting] = useState(false)
|
|
52
121
|
const [message, setMessage] = useState('')
|
|
53
122
|
const [error, setError] = useState('')
|
|
54
123
|
const [results, setResults] = useState<RezTestResult[]>([])
|
|
55
|
-
const [argsText, setArgsText] = useState<Record<string, string>>({})
|
|
56
|
-
const [envText, setEnvText] = useState<Record<string, string>>({})
|
|
57
|
-
const [headerText, setHeaderText] = useState<Record<string, string>>({})
|
|
58
|
-
|
|
59
|
-
const initTexts = (next: RezPublicConfig): void => {
|
|
60
|
-
const args: Record<string, string> = {}
|
|
61
|
-
const envs: Record<string, string> = {}
|
|
62
|
-
const headers: Record<string, string> = {}
|
|
63
|
-
for (const [id, server] of Object.entries(next.servers)) {
|
|
64
|
-
args[id] = (server.args ?? []).join('\n')
|
|
65
|
-
envs[id] = JSON.stringify(server.env ?? {}, null, 2)
|
|
66
|
-
headers[id] = JSON.stringify(server.headers ?? {}, null, 2)
|
|
67
|
-
}
|
|
68
|
-
setArgsText(args)
|
|
69
|
-
setEnvText(envs)
|
|
70
|
-
setHeaderText(headers)
|
|
71
|
-
}
|
|
72
124
|
|
|
73
125
|
const load = async (): Promise<void> => {
|
|
74
126
|
try {
|
|
75
|
-
|
|
76
|
-
setConfig(next)
|
|
77
|
-
initTexts(next)
|
|
127
|
+
setConfig(await api.getConfig())
|
|
78
128
|
} catch (err) {
|
|
79
129
|
setError(errorMessage(err))
|
|
80
130
|
} finally {
|
|
@@ -91,10 +141,10 @@ export function ConfigTab({ api }: { api: RezApi }) {
|
|
|
91
141
|
setConfig(prev => prev === null ? prev : { ...prev, [key]: value })
|
|
92
142
|
}
|
|
93
143
|
|
|
94
|
-
const
|
|
144
|
+
const patchConnection = (id: keyof RezPublicConfig['connections'], patch: Partial<RezPublicConnection>): void => {
|
|
95
145
|
setConfig(prev => prev === null ? prev : {
|
|
96
146
|
...prev,
|
|
97
|
-
|
|
147
|
+
connections: { ...prev.connections, [id]: { ...prev.connections[id], ...patch } },
|
|
98
148
|
})
|
|
99
149
|
}
|
|
100
150
|
|
|
@@ -103,9 +153,20 @@ export function ConfigTab({ api }: { api: RezApi }) {
|
|
|
103
153
|
setMessage('')
|
|
104
154
|
setError('')
|
|
105
155
|
try {
|
|
106
|
-
const
|
|
156
|
+
const payload = {
|
|
157
|
+
enabled: config.enabled,
|
|
158
|
+
role: config.role,
|
|
159
|
+
billing: config.billing,
|
|
160
|
+
connections: {
|
|
161
|
+
odoo: { ...config.connections.odoo, secret: secrets.odoo || undefined },
|
|
162
|
+
nextcloud: { ...config.connections.nextcloud, secret: secrets.nextcloud || undefined },
|
|
163
|
+
wechat: { ...config.connections.wechat, secret: secrets.wechat || undefined },
|
|
164
|
+
homeassistant: { ...config.connections.homeassistant, secret: secrets.homeassistant || undefined },
|
|
165
|
+
},
|
|
166
|
+
}
|
|
167
|
+
const saved = await api.saveConfig(payload)
|
|
107
168
|
setConfig(saved)
|
|
108
|
-
|
|
169
|
+
setSecrets(EMPTY_SECRETS)
|
|
109
170
|
setMessage(tt('config.saved'))
|
|
110
171
|
} catch (err) {
|
|
111
172
|
setError(errorMessage(err))
|
|
@@ -129,27 +190,16 @@ export function ConfigTab({ api }: { api: RezApi }) {
|
|
|
129
190
|
}
|
|
130
191
|
|
|
131
192
|
const renderResult = (result: RezTestResult): string => {
|
|
132
|
-
if (result.ok)
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const commitArgs = (id: string): void => {
|
|
137
|
-
updateServer(id, { args: argsText[id] === '' ? [] : argsText[id].split('\n').map(line => line.trim()).filter(Boolean) })
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const commitRecord = (id: string, kind: 'env' | 'headers'): void => {
|
|
141
|
-
const text = kind === 'env' ? envText[id] : headerText[id]
|
|
142
|
-
try {
|
|
143
|
-
const record = parseRecord(text ?? '{}')
|
|
144
|
-
updateServer(id, kind === 'env' ? { env: record } : { headers: record })
|
|
145
|
-
setError('')
|
|
146
|
-
} catch (err) {
|
|
147
|
-
setError(tt('config.jsonInvalid', { error: errorMessage(err) }))
|
|
193
|
+
if (result.ok) {
|
|
194
|
+
if (result.serverInfo !== undefined) return result.serverInfo
|
|
195
|
+
return tt('config.testOk', { tools: result.toolCount ?? 0, latency: result.latencyMs ?? 0 })
|
|
148
196
|
}
|
|
197
|
+
return tt('config.testFail', { error: result.error ?? '' })
|
|
149
198
|
}
|
|
150
199
|
|
|
151
200
|
return (
|
|
152
201
|
<div className={css.tabBody}>
|
|
202
|
+
<p className={css.message}>{tt('config.mcpHint')}</p>
|
|
153
203
|
<Section title={tt('config.role')}>
|
|
154
204
|
<Field label={tt('config.role')}>
|
|
155
205
|
<select className={css.select} value={config.role} onChange={event => { setTop('role', event.target.value as RezRoleId) }}>
|
|
@@ -161,41 +211,46 @@ export function ConfigTab({ api }: { api: RezApi }) {
|
|
|
161
211
|
</Field>
|
|
162
212
|
</Section>
|
|
163
213
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
214
|
+
<McpSection
|
|
215
|
+
title={tt('config.odoo')}
|
|
216
|
+
row={config.connections.odoo}
|
|
217
|
+
secret={secrets.odoo}
|
|
218
|
+
onEnabled={enabled => { patchConnection('odoo', { enabled }) }}
|
|
219
|
+
onUrl={url => { patchConnection('odoo', { url }) }}
|
|
220
|
+
onSecret={value => { setSecrets(prev => ({ ...prev, odoo: value })) }}
|
|
221
|
+
urlLabel={tt('config.odoo.url')}
|
|
222
|
+
secretLabel={tt('config.odoo.apiKey')}
|
|
223
|
+
/>
|
|
224
|
+
<McpSection
|
|
225
|
+
title={tt('config.nextcloud')}
|
|
226
|
+
row={config.connections.nextcloud}
|
|
227
|
+
secret={secrets.nextcloud}
|
|
228
|
+
onEnabled={enabled => { patchConnection('nextcloud', { enabled }) }}
|
|
229
|
+
onUrl={url => { patchConnection('nextcloud', { url }) }}
|
|
230
|
+
onUsername={username => { patchConnection('nextcloud', { username }) }}
|
|
231
|
+
onSecret={value => { setSecrets(prev => ({ ...prev, nextcloud: value })) }}
|
|
232
|
+
urlLabel={tt('config.nextcloud.url')}
|
|
233
|
+
usernameLabel={tt('config.nextcloud.username')}
|
|
234
|
+
secretLabel={tt('config.nextcloud.appPassword')}
|
|
235
|
+
/>
|
|
236
|
+
<McpSection
|
|
237
|
+
title={tt('config.wecom')}
|
|
238
|
+
row={config.connections.wechat}
|
|
239
|
+
secret={secrets.wechat}
|
|
240
|
+
onEnabled={enabled => { patchConnection('wechat', { enabled }) }}
|
|
241
|
+
onSecret={value => { setSecrets(prev => ({ ...prev, wechat: value })) }}
|
|
242
|
+
secretLabel={tt('config.wecom.webhook')}
|
|
243
|
+
/>
|
|
244
|
+
<McpSection
|
|
245
|
+
title={tt('config.ha')}
|
|
246
|
+
row={config.connections.homeassistant}
|
|
247
|
+
secret={secrets.homeassistant}
|
|
248
|
+
onEnabled={enabled => { patchConnection('homeassistant', { enabled }) }}
|
|
249
|
+
onUrl={url => { patchConnection('homeassistant', { url }) }}
|
|
250
|
+
onSecret={value => { setSecrets(prev => ({ ...prev, homeassistant: value })) }}
|
|
251
|
+
urlLabel={tt('config.ha.url')}
|
|
252
|
+
secretLabel={tt('config.ha.token')}
|
|
253
|
+
/>
|
|
199
254
|
|
|
200
255
|
<Section title={tt('config.billing')}>
|
|
201
256
|
<Field label={tt('config.billing.input')}>
|