@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
|
@@ -1,117 +1,89 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Settings-native styles for the ReZ card/tab. Colors ride dsh --dsw-* tokens
|
|
3
|
+
* so the form matches 设置 → 插件. The stylesheet is inlined into client.js
|
|
4
|
+
* at wrap time — dsh only loads the JS module, not lib/style.css.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
.shell {
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-direction: column;
|
|
10
|
+
gap: 12px;
|
|
11
|
+
min-width: 0;
|
|
9
12
|
}
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
.card {
|
|
15
|
+
list-style: none;
|
|
16
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
17
|
+
border-radius: 12px;
|
|
18
|
+
background: var(--dsw-alias-bg-layer-3);
|
|
19
|
+
transition: border-color 0.16s, background 0.16s;
|
|
17
20
|
}
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
.card:hover {
|
|
23
|
+
border-color: var(--dsw-alias-label-dimmed);
|
|
21
24
|
}
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
.cardOpen {
|
|
27
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
28
|
+
border-color: var(--dsw-alias-label-dimmed);
|
|
26
29
|
}
|
|
27
30
|
|
|
28
|
-
.
|
|
29
|
-
display: flex;
|
|
30
|
-
align-items: center;
|
|
31
|
-
gap: 8px;
|
|
31
|
+
.header {
|
|
32
32
|
width: 100%;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
background:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
appearance: none;
|
|
34
|
+
border: 0;
|
|
35
|
+
background: none;
|
|
36
|
+
font: inherit;
|
|
37
|
+
color: inherit;
|
|
38
|
+
text-align: left;
|
|
39
39
|
cursor: pointer;
|
|
40
|
-
|
|
41
|
-
white-space: nowrap;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
.entry:hover {
|
|
45
|
-
background: var(--dsw-specific-sidebar-nav-item-hover);
|
|
46
|
-
color: var(--dsw-alias-label-primary);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
.entryIcon {
|
|
50
|
-
display: inline-flex;
|
|
40
|
+
display: flex;
|
|
51
41
|
align-items: center;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
.entryLabel {
|
|
57
|
-
overflow: hidden;
|
|
58
|
-
text-overflow: ellipsis;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
[data-dsh-frame][data-sidebar-collapsed] .entry {
|
|
62
|
-
justify-content: center;
|
|
63
|
-
padding: 0;
|
|
64
|
-
width: 100%;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
[data-dsh-frame][data-sidebar-collapsed] .entryLabel {
|
|
68
|
-
display: none;
|
|
42
|
+
gap: 12px;
|
|
43
|
+
padding: 14px 16px;
|
|
44
|
+
border-radius: 12px;
|
|
69
45
|
}
|
|
70
46
|
|
|
71
|
-
.
|
|
72
|
-
|
|
47
|
+
.header:focus-visible {
|
|
48
|
+
outline: 2px solid var(--dsw-alias-brand-primary);
|
|
49
|
+
outline-offset: -2px;
|
|
73
50
|
}
|
|
74
51
|
|
|
75
|
-
.
|
|
52
|
+
.headText {
|
|
53
|
+
flex: 1;
|
|
54
|
+
min-width: 0;
|
|
76
55
|
display: flex;
|
|
77
56
|
flex-direction: column;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
57
|
+
gap: 4px;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.name {
|
|
61
|
+
font-size: 15px;
|
|
62
|
+
font-weight: 600;
|
|
63
|
+
line-height: 1.4;
|
|
84
64
|
color: var(--dsw-alias-label-primary);
|
|
85
|
-
font-family: var(--dsw-font-family);
|
|
86
65
|
}
|
|
87
66
|
|
|
88
|
-
.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
flex: none;
|
|
67
|
+
.description {
|
|
68
|
+
font-size: 13px;
|
|
69
|
+
line-height: 1.5;
|
|
70
|
+
color: var(--dsw-alias-label-tertiary);
|
|
93
71
|
}
|
|
94
72
|
|
|
95
|
-
.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
font-weight: 700;
|
|
73
|
+
.chevron {
|
|
74
|
+
flex: none;
|
|
75
|
+
color: var(--dsw-alias-label-tertiary);
|
|
76
|
+
transition: transform 0.16s;
|
|
100
77
|
}
|
|
101
78
|
|
|
102
|
-
.
|
|
103
|
-
|
|
104
|
-
background: transparent;
|
|
105
|
-
color: var(--dsw-alias-label-secondary);
|
|
106
|
-
cursor: pointer;
|
|
107
|
-
font-size: 14px;
|
|
108
|
-
padding: 4px 8px;
|
|
109
|
-
border-radius: 6px;
|
|
79
|
+
.chevronOpen {
|
|
80
|
+
transform: rotate(180deg);
|
|
110
81
|
}
|
|
111
82
|
|
|
112
|
-
.
|
|
113
|
-
|
|
114
|
-
|
|
83
|
+
.body {
|
|
84
|
+
border-top: 1px solid var(--dsw-alias-border-l2);
|
|
85
|
+
margin: 0 16px 12px;
|
|
86
|
+
padding-top: 8px;
|
|
115
87
|
}
|
|
116
88
|
|
|
117
89
|
.tabBar {
|
|
@@ -143,21 +115,12 @@ html[data-dsh-rez-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-act
|
|
|
143
115
|
border-bottom-color: var(--dsw-alias-state-business-primary);
|
|
144
116
|
}
|
|
145
117
|
|
|
146
|
-
.panelContent {
|
|
147
|
-
flex: 1;
|
|
148
|
-
min-height: 0;
|
|
149
|
-
display: flex;
|
|
150
|
-
flex-direction: column;
|
|
151
|
-
overflow: hidden;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
118
|
.tabBody {
|
|
155
119
|
display: flex;
|
|
156
120
|
flex-direction: column;
|
|
157
121
|
gap: 12px;
|
|
158
|
-
|
|
159
|
-
min-
|
|
160
|
-
overflow-y: auto;
|
|
122
|
+
padding: 12px 0 4px;
|
|
123
|
+
min-width: 0;
|
|
161
124
|
}
|
|
162
125
|
|
|
163
126
|
.section {
|
|
@@ -174,13 +137,14 @@ html[data-dsh-rez-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-act
|
|
|
174
137
|
margin: 0;
|
|
175
138
|
font-size: 13px;
|
|
176
139
|
font-weight: 600;
|
|
140
|
+
color: var(--dsw-alias-label-primary);
|
|
177
141
|
}
|
|
178
142
|
|
|
179
143
|
.field {
|
|
180
|
-
display:
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
144
|
+
display: flex;
|
|
145
|
+
flex-direction: column;
|
|
146
|
+
align-items: stretch;
|
|
147
|
+
gap: 4px;
|
|
184
148
|
}
|
|
185
149
|
|
|
186
150
|
.label {
|
|
@@ -188,11 +152,12 @@ html[data-dsh-rez-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-act
|
|
|
188
152
|
color: var(--dsw-alias-label-secondary);
|
|
189
153
|
}
|
|
190
154
|
|
|
191
|
-
.input,
|
|
155
|
+
.input,
|
|
156
|
+
.select {
|
|
192
157
|
padding: 6px 8px;
|
|
193
158
|
font-size: 13px;
|
|
194
159
|
color: var(--dsw-alias-label-primary);
|
|
195
|
-
background: var(--dsw-specific-input-major);
|
|
160
|
+
background: var(--dsw-specific-input-major, var(--dsw-alias-bg-layer-3));
|
|
196
161
|
border: 1px solid var(--dsw-alias-border-l2);
|
|
197
162
|
border-radius: 8px;
|
|
198
163
|
outline: none;
|
|
@@ -217,28 +182,24 @@ html[data-dsh-rez-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-act
|
|
|
217
182
|
font-size: 13px;
|
|
218
183
|
border-radius: 8px;
|
|
219
184
|
border: 1px solid var(--dsw-alias-border-l2);
|
|
220
|
-
background: var(--dsw-specific-input-major);
|
|
185
|
+
background: var(--dsw-specific-input-major, var(--dsw-alias-bg-layer-3));
|
|
221
186
|
color: var(--dsw-alias-label-primary);
|
|
222
187
|
cursor: pointer;
|
|
223
188
|
}
|
|
224
189
|
|
|
225
|
-
.button:hover {
|
|
190
|
+
.button:hover:not(:disabled) {
|
|
226
191
|
background: var(--dsw-alias-interactive-bg-hover);
|
|
227
192
|
}
|
|
228
193
|
|
|
229
|
-
.
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
height: 240px;
|
|
233
|
-
margin: 8px 0;
|
|
234
|
-
background: #fff;
|
|
235
|
-
border-radius: 8px;
|
|
194
|
+
.button:disabled {
|
|
195
|
+
opacity: 0.4;
|
|
196
|
+
cursor: default;
|
|
236
197
|
}
|
|
237
198
|
|
|
238
199
|
.primaryButton {
|
|
239
|
-
background: var(--dsw-alias-
|
|
240
|
-
border-color: var(--dsw-alias-
|
|
241
|
-
color:
|
|
200
|
+
background: var(--dsw-alias-label-primary);
|
|
201
|
+
border-color: var(--dsw-alias-label-primary);
|
|
202
|
+
color: var(--dsw-alias-bg-layer-3);
|
|
242
203
|
}
|
|
243
204
|
|
|
244
205
|
.dangerButton {
|
|
@@ -247,8 +208,19 @@ html[data-dsh-rez-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-act
|
|
|
247
208
|
color: #fff;
|
|
248
209
|
}
|
|
249
210
|
|
|
211
|
+
.qr {
|
|
212
|
+
display: block;
|
|
213
|
+
width: 200px;
|
|
214
|
+
height: 200px;
|
|
215
|
+
margin: 8px 0;
|
|
216
|
+
background: #fff;
|
|
217
|
+
border-radius: 8px;
|
|
218
|
+
}
|
|
219
|
+
|
|
250
220
|
.message {
|
|
221
|
+
margin: 0;
|
|
251
222
|
font-size: 12.5px;
|
|
223
|
+
line-height: 1.5;
|
|
252
224
|
color: var(--dsw-alias-label-secondary);
|
|
253
225
|
}
|
|
254
226
|
|
|
@@ -257,8 +229,6 @@ html[data-dsh-rez-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-act
|
|
|
257
229
|
}
|
|
258
230
|
|
|
259
231
|
.tableWrap {
|
|
260
|
-
flex: 1;
|
|
261
|
-
min-height: 0;
|
|
262
232
|
overflow: auto;
|
|
263
233
|
border: 1px solid var(--dsw-alias-border-l1);
|
|
264
234
|
border-radius: 10px;
|
|
@@ -304,11 +274,11 @@ html[data-dsh-rez-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-act
|
|
|
304
274
|
|
|
305
275
|
.cards {
|
|
306
276
|
display: grid;
|
|
307
|
-
grid-template-columns: repeat(auto-fill, minmax(
|
|
277
|
+
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
|
308
278
|
gap: 8px;
|
|
309
279
|
}
|
|
310
280
|
|
|
311
|
-
.
|
|
281
|
+
.stat {
|
|
312
282
|
padding: 10px;
|
|
313
283
|
border: 1px solid var(--dsw-alias-border-l1);
|
|
314
284
|
border-radius: 10px;
|
|
@@ -322,4 +292,5 @@ html[data-dsh-rez-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-act
|
|
|
322
292
|
.cardValue {
|
|
323
293
|
font-size: 15px;
|
|
324
294
|
font-weight: 600;
|
|
295
|
+
color: var(--dsw-alias-label-primary);
|
|
325
296
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ReZ settings pages: one accordion card in 设置 → 插件 → 插件配置,
|
|
3
|
+
* plus a dedicated Plugins tab for the same content.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useMemo, useState } from 'react'
|
|
7
|
+
import { RezApi } from './api.ts'
|
|
8
|
+
import { tt } from './panel/helpers.ts'
|
|
9
|
+
import { AuditTab } from './panel/AuditTab.tsx'
|
|
10
|
+
import { ConfigTab } from './panel/ConfigTab.tsx'
|
|
11
|
+
import { StatusTab } from './panel/StatusTab.tsx'
|
|
12
|
+
import { WeixinTab } from './panel/WeixinTab.tsx'
|
|
13
|
+
import css from './panel/panel.module.css'
|
|
14
|
+
|
|
15
|
+
export type RezTab = 'config' | 'weixin' | 'status' | 'audit'
|
|
16
|
+
|
|
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() {
|
|
25
|
+
const api = useMemo(() => new RezApi(), [])
|
|
26
|
+
const [activeTab, setActiveTab] = useState<RezTab>('config')
|
|
27
|
+
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
|
+
))}
|
|
43
|
+
</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>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function RezPluginCard() {
|
|
53
|
+
const [open, setOpen] = useState(false)
|
|
54
|
+
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>
|
|
67
|
+
)
|
|
68
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* dsh-rez-suite — employee-facing host half.
|
|
3
3
|
*
|
|
4
4
|
* MCP connections are owned by official @deepseek-ai/dsh-mcp-client rows in
|
|
5
|
-
* this package's cordis.patch.yml. This plugin only mounts the
|
|
6
|
-
*
|
|
7
|
-
* start a second MCP host here.
|
|
5
|
+
* this package's cordis.patch.yml. This plugin only mounts the loopback
|
|
6
|
+
* settings API, a Settings → Plugins card, and a short system-prompt
|
|
7
|
+
* announcement. Do not start a second MCP host here.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { Context } from '@deepseek-ai/cordis'
|
|
@@ -15,6 +15,7 @@ import z from '@deepseek-ai/schemastery'
|
|
|
15
15
|
import type {} from '@deepseek-ai/dsh-host-webserver'
|
|
16
16
|
import type {} from '@deepseek-ai/dsh-system-prompt'
|
|
17
17
|
import type {} from '@deepseek-ai/dsh-tools'
|
|
18
|
+
import { REZ_CREDENTIAL_REFS, REZ_DEFAULT_CONNECTIONS, secretConfigured } from '@rezti/dsh-rez-sso'
|
|
18
19
|
import type { McpHost } from './mcp-host.ts'
|
|
19
20
|
import { makeRoutes } from './routes.ts'
|
|
20
21
|
import { defaultConfig, mergeConfig, saveConfig } from './store.ts'
|
|
@@ -45,6 +46,24 @@ export const Config: z<RezConfig> = z.object({
|
|
|
45
46
|
announceToAgent: z.boolean().default(true),
|
|
46
47
|
role: z.union(['engineer', 'sales', 'operations', 'all'] as const).default('all'),
|
|
47
48
|
servers: z.dict(serverSchema).default({}),
|
|
49
|
+
connections: z.object({
|
|
50
|
+
odoo: z.object({
|
|
51
|
+
enabled: z.boolean().default(true),
|
|
52
|
+
url: z.string().default(REZ_DEFAULT_CONNECTIONS.odoo.url),
|
|
53
|
+
}).default(REZ_DEFAULT_CONNECTIONS.odoo),
|
|
54
|
+
nextcloud: z.object({
|
|
55
|
+
enabled: z.boolean().default(true),
|
|
56
|
+
url: z.string().default(REZ_DEFAULT_CONNECTIONS.nextcloud.url),
|
|
57
|
+
username: z.string().default(REZ_DEFAULT_CONNECTIONS.nextcloud.username),
|
|
58
|
+
}).default(REZ_DEFAULT_CONNECTIONS.nextcloud),
|
|
59
|
+
wechat: z.object({
|
|
60
|
+
enabled: z.boolean().default(true),
|
|
61
|
+
}).default(REZ_DEFAULT_CONNECTIONS.wechat),
|
|
62
|
+
homeassistant: z.object({
|
|
63
|
+
enabled: z.boolean().default(true),
|
|
64
|
+
url: z.string().default(REZ_DEFAULT_CONNECTIONS.homeassistant.url),
|
|
65
|
+
}).default(REZ_DEFAULT_CONNECTIONS.homeassistant),
|
|
66
|
+
}).default(REZ_DEFAULT_CONNECTIONS),
|
|
48
67
|
billing: z.object({
|
|
49
68
|
inputCostPer1k: z.number().default(0.001),
|
|
50
69
|
outputCostPer1k: z.number().default(0.002),
|
|
@@ -60,7 +79,7 @@ export const REZ_GUIDANCE = [
|
|
|
60
79
|
'本机已安装 ReZ-TI 套件(一次安装):工作身份由 dsh-rez-sso 提供,MCP 由官方 dsh-mcp-client 接入 Odoo / Nextcloud / 企业微信 / Home Assistant。',
|
|
61
80
|
'工具名 mcp__odoo__*、mcp__nextcloud__*、mcp__wechat__*、mcp__homeassistant__*。',
|
|
62
81
|
'密钥:REZ_ODOO_TOKEN、REZ_NEXTCLOUD_PASSWORD、REZ_WECHAT_WEBHOOK、REZ_HA_TOKEN。',
|
|
63
|
-
'个人微信(QClaw/ClawBot):打开
|
|
82
|
+
'个人微信(QClaw/ClawBot):打开 设置 → 插件 → ReZ-TI → 微信 扫码,然后直接在微信里说话,不必再跑命令、也不要调用微信 MCP 工具。',
|
|
64
83
|
'意图用 rez_set_intent(erp/files/chat/home/auto)。审计用 rez_audit。',
|
|
65
84
|
'涉及资金、对外发送、设备控制、生产写入时先征得用户批准。',
|
|
66
85
|
].join('')
|
|
@@ -73,16 +92,19 @@ function delegatedHost(): McpHost {
|
|
|
73
92
|
const status = (): RezServerStatus[] => COMPOSED_SERVERS.map(server => ({
|
|
74
93
|
server,
|
|
75
94
|
enabled: true,
|
|
76
|
-
state: 'disconnected',
|
|
95
|
+
state: 'disconnected' as const,
|
|
77
96
|
toolCount: 0,
|
|
78
97
|
registeredTools: 0,
|
|
79
|
-
lastError:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
server,
|
|
83
|
-
ok: false,
|
|
84
|
-
error: 'MCP is composed by dsh-mcp-client; set REZ_* credentials and check dsh logs',
|
|
98
|
+
lastError: secretConfigured(server)
|
|
99
|
+
? 'owned by @deepseek-ai/dsh-mcp-client'
|
|
100
|
+
: 'missing ' + REZ_CREDENTIAL_REFS[server],
|
|
85
101
|
}))
|
|
102
|
+
const test = async (): Promise<RezTestResult[]> => COMPOSED_SERVERS.map(server => {
|
|
103
|
+
const ok = secretConfigured(server)
|
|
104
|
+
return ok
|
|
105
|
+
? { server, ok: true, serverInfo: REZ_CREDENTIAL_REFS[server] + ' configured' }
|
|
106
|
+
: { server, ok: false, error: 'missing ' + REZ_CREDENTIAL_REFS[server] }
|
|
107
|
+
})
|
|
86
108
|
return {
|
|
87
109
|
status,
|
|
88
110
|
test,
|
|
@@ -119,6 +141,10 @@ export function apply(ctx: Context, config?: RezConfig): void {
|
|
|
119
141
|
},
|
|
120
142
|
host,
|
|
121
143
|
tokens,
|
|
144
|
+
onCredentialWritten: (ref) => {
|
|
145
|
+
const emit = ctx.emit as unknown as (event: string, value: string) => void
|
|
146
|
+
emit('credentials/updated', ref)
|
|
147
|
+
},
|
|
122
148
|
})
|
|
123
149
|
|
|
124
150
|
let disposeRoutes: (() => void) | undefined
|
package/src/protocol.ts
CHANGED
|
@@ -35,12 +35,29 @@ export interface RezMcpServerSpec {
|
|
|
35
35
|
toolCallTimeoutMs?: number
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/** Company MCP endpoints persisted under ~/.dsh/dsh-rez-suite.json. Secrets are not stored here. */
|
|
39
|
+
export interface RezConnections {
|
|
40
|
+
odoo: { enabled: boolean; url: string }
|
|
41
|
+
nextcloud: { enabled: boolean; url: string; username: string }
|
|
42
|
+
wechat: { enabled: boolean }
|
|
43
|
+
homeassistant: { enabled: boolean; url: string }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface RezPublicConnection {
|
|
47
|
+
enabled: boolean
|
|
48
|
+
url?: string
|
|
49
|
+
username?: string
|
|
50
|
+
secretConfigured: boolean
|
|
51
|
+
secretRef: string
|
|
52
|
+
}
|
|
53
|
+
|
|
38
54
|
/** Full plugin configuration persisted under ~/.dsh/dsh-rez-suite.json. */
|
|
39
55
|
export interface RezConfig {
|
|
40
56
|
enabled: boolean
|
|
41
57
|
announceToAgent: boolean
|
|
42
58
|
role: RezRoleId
|
|
43
59
|
servers: Record<string, RezMcpServerSpec>
|
|
60
|
+
connections: RezConnections
|
|
44
61
|
billing: {
|
|
45
62
|
inputCostPer1k: number
|
|
46
63
|
outputCostPer1k: number
|
|
@@ -56,6 +73,12 @@ export interface RezPublicConfig {
|
|
|
56
73
|
enabled: boolean
|
|
57
74
|
role: RezRoleId
|
|
58
75
|
servers: Record<string, RezPublicServer>
|
|
76
|
+
connections: {
|
|
77
|
+
odoo: RezPublicConnection
|
|
78
|
+
nextcloud: RezPublicConnection
|
|
79
|
+
wechat: RezPublicConnection
|
|
80
|
+
homeassistant: RezPublicConnection
|
|
81
|
+
}
|
|
59
82
|
billing: { inputCostPer1k: number; outputCostPer1k: number; monthlyBudget: number }
|
|
60
83
|
}
|
|
61
84
|
|
package/src/routes.ts
CHANGED
|
@@ -6,6 +6,13 @@
|
|
|
6
6
|
|
|
7
7
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
8
8
|
import type { WebRoute } from '@deepseek-ai/dsh-host-webserver'
|
|
9
|
+
import {
|
|
10
|
+
REZ_CREDENTIAL_REFS,
|
|
11
|
+
REZ_SYSTEMS,
|
|
12
|
+
mergeConnections,
|
|
13
|
+
writeManagedCredential,
|
|
14
|
+
type RezSystem,
|
|
15
|
+
} from '@rezti/dsh-rez-sso'
|
|
9
16
|
import type { RezConfig, RezMcpServerSpec, RezRoleId } from './protocol.ts'
|
|
10
17
|
import { REZ_API } from './protocol.ts'
|
|
11
18
|
import type { McpHost } from './mcp-host.ts'
|
|
@@ -74,11 +81,25 @@ function mergeMaskedRecord(base: Record<string, string> | undefined, incoming: u
|
|
|
74
81
|
}
|
|
75
82
|
|
|
76
83
|
/** Apply a browser-safe public patch without overwriting masked secrets. */
|
|
77
|
-
function applyPublicPatch(current: RezConfig, patch: Record<string, unknown>): RezConfig {
|
|
84
|
+
function applyPublicPatch(current: RezConfig, patch: Record<string, unknown>): { next: RezConfig; secrets: Array<{ system: RezSystem; value: string }> } {
|
|
78
85
|
const next: RezConfig = JSON.parse(JSON.stringify(current)) as RezConfig
|
|
86
|
+
const secrets: Array<{ system: RezSystem; value: string }> = []
|
|
79
87
|
if (typeof patch.enabled === 'boolean') next.enabled = patch.enabled
|
|
80
88
|
if (patch.role === 'engineer' || patch.role === 'sales' || patch.role === 'operations' || patch.role === 'all') next.role = patch.role as RezRoleId
|
|
81
89
|
|
|
90
|
+
if (typeof patch.connections === 'object' && patch.connections !== null) {
|
|
91
|
+
const incoming = patch.connections as Record<string, unknown>
|
|
92
|
+
next.connections = mergeConnections({ ...next.connections, ...incoming })
|
|
93
|
+
for (const system of Object.keys(REZ_CREDENTIAL_REFS) as RezSystem[]) {
|
|
94
|
+
const row = incoming[system]
|
|
95
|
+
if (typeof row !== 'object' || row === null) continue
|
|
96
|
+
const secret = (row as { secret?: unknown }).secret
|
|
97
|
+
if (typeof secret === 'string' && secret.length > 0 && secret !== SECRET_MASK) {
|
|
98
|
+
secrets.push({ system, value: secret })
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
82
103
|
if (typeof patch.servers === 'object' && patch.servers !== null) {
|
|
83
104
|
for (const [id, raw] of Object.entries(patch.servers as Record<string, unknown>)) {
|
|
84
105
|
if (typeof raw !== 'object' || raw === null) continue
|
|
@@ -104,7 +125,7 @@ function applyPublicPatch(current: RezConfig, patch: Record<string, unknown>): R
|
|
|
104
125
|
if (typeof value.outputCostPer1k === 'number') next.billing.outputCostPer1k = value.outputCostPer1k
|
|
105
126
|
if (typeof value.monthlyBudget === 'number') next.billing.monthlyBudget = value.monthlyBudget
|
|
106
127
|
}
|
|
107
|
-
return next
|
|
128
|
+
return { next, secrets }
|
|
108
129
|
}
|
|
109
130
|
|
|
110
131
|
export interface RezRoutesDeps {
|
|
@@ -113,11 +134,12 @@ export interface RezRoutesDeps {
|
|
|
113
134
|
updateConfig: (next: RezConfig) => Promise<RezConfig>
|
|
114
135
|
host: McpHost
|
|
115
136
|
tokens: TokenManager
|
|
137
|
+
onCredentialWritten?: (ref: string) => void
|
|
116
138
|
}
|
|
117
139
|
|
|
118
140
|
/** Build every /api/dsh-rez-suite route (exact paths). */
|
|
119
141
|
export function makeRoutes(deps: RezRoutesDeps): WebRoute[] {
|
|
120
|
-
const { getConfig, updateConfig, host, tokens } = deps
|
|
142
|
+
const { getConfig, updateConfig, host, tokens, onCredentialWritten } = deps
|
|
121
143
|
|
|
122
144
|
const guard = (req: IncomingMessage, res: ServerResponse, method: string): boolean => {
|
|
123
145
|
if (!isLoopbackRequest(req)) {
|
|
@@ -146,8 +168,14 @@ export function makeRoutes(deps: RezRoutesDeps): WebRoute[] {
|
|
|
146
168
|
writeJson(res, 400, { error: 'invalid JSON body' })
|
|
147
169
|
return
|
|
148
170
|
}
|
|
149
|
-
const next = applyPublicPatch(getConfig(), body)
|
|
171
|
+
const { next, secrets } = applyPublicPatch(getConfig(), body)
|
|
150
172
|
const saved = await updateConfig(next)
|
|
173
|
+
for (const item of secrets) {
|
|
174
|
+
writeManagedCredential(REZ_CREDENTIAL_REFS[item.system], item.value)
|
|
175
|
+
}
|
|
176
|
+
for (const system of REZ_SYSTEMS) {
|
|
177
|
+
onCredentialWritten?.(REZ_CREDENTIAL_REFS[system])
|
|
178
|
+
}
|
|
151
179
|
writeJson(res, 200, { config: publicConfig(saved) })
|
|
152
180
|
},
|
|
153
181
|
},
|
package/src/store.ts
CHANGED
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
|
|
9
9
|
import { homedir } from 'node:os'
|
|
10
10
|
import { dirname, join, resolve } from 'node:path'
|
|
11
|
+
import {
|
|
12
|
+
REZ_CREDENTIAL_REFS,
|
|
13
|
+
REZ_DEFAULT_CONNECTIONS,
|
|
14
|
+
mergeConnections,
|
|
15
|
+
secretConfigured,
|
|
16
|
+
} from '@rezti/dsh-rez-sso'
|
|
11
17
|
import type { RezConfig, RezMcpServerSpec, RezPublicConfig } from './protocol.ts'
|
|
12
18
|
|
|
13
19
|
/** File format version. */
|
|
@@ -36,13 +42,14 @@ export function expandHome(path: string): string {
|
|
|
36
42
|
return resolve(path)
|
|
37
43
|
}
|
|
38
44
|
|
|
39
|
-
/** Defaults:
|
|
45
|
+
/** Defaults: company MCP endpoints. Secrets live in credentials.yaml / env. */
|
|
40
46
|
export function defaultConfig(): RezConfig {
|
|
41
47
|
return {
|
|
42
48
|
enabled: true,
|
|
43
49
|
announceToAgent: true,
|
|
44
50
|
role: 'all',
|
|
45
51
|
servers: {},
|
|
52
|
+
connections: JSON.parse(JSON.stringify(REZ_DEFAULT_CONNECTIONS)) as RezConfig['connections'],
|
|
46
53
|
billing: {
|
|
47
54
|
inputCostPer1k: 0.001,
|
|
48
55
|
outputCostPer1k: 0.002,
|
|
@@ -74,10 +81,37 @@ export function publicConfig(config: RezConfig): RezPublicConfig {
|
|
|
74
81
|
for (const [id, server] of Object.entries(config.servers)) {
|
|
75
82
|
servers[id] = maskServer(server)
|
|
76
83
|
}
|
|
84
|
+
const connections = mergeConnections(config.connections)
|
|
77
85
|
return {
|
|
78
86
|
enabled: config.enabled,
|
|
79
87
|
role: config.role,
|
|
80
88
|
servers,
|
|
89
|
+
connections: {
|
|
90
|
+
odoo: {
|
|
91
|
+
enabled: connections.odoo.enabled,
|
|
92
|
+
url: connections.odoo.url,
|
|
93
|
+
secretConfigured: secretConfigured('odoo'),
|
|
94
|
+
secretRef: REZ_CREDENTIAL_REFS.odoo,
|
|
95
|
+
},
|
|
96
|
+
nextcloud: {
|
|
97
|
+
enabled: connections.nextcloud.enabled,
|
|
98
|
+
url: connections.nextcloud.url,
|
|
99
|
+
username: connections.nextcloud.username,
|
|
100
|
+
secretConfigured: secretConfigured('nextcloud'),
|
|
101
|
+
secretRef: REZ_CREDENTIAL_REFS.nextcloud,
|
|
102
|
+
},
|
|
103
|
+
wechat: {
|
|
104
|
+
enabled: connections.wechat.enabled,
|
|
105
|
+
secretConfigured: secretConfigured('wechat'),
|
|
106
|
+
secretRef: REZ_CREDENTIAL_REFS.wechat,
|
|
107
|
+
},
|
|
108
|
+
homeassistant: {
|
|
109
|
+
enabled: connections.homeassistant.enabled,
|
|
110
|
+
url: connections.homeassistant.url,
|
|
111
|
+
secretConfigured: secretConfigured('homeassistant'),
|
|
112
|
+
secretRef: REZ_CREDENTIAL_REFS.homeassistant,
|
|
113
|
+
},
|
|
114
|
+
},
|
|
81
115
|
billing: { ...config.billing },
|
|
82
116
|
}
|
|
83
117
|
}
|
|
@@ -96,6 +130,8 @@ export function mergeConfig(base: RezConfig, override: unknown): RezConfig {
|
|
|
96
130
|
if (typeof value.announceToAgent === 'boolean') result.announceToAgent = value.announceToAgent
|
|
97
131
|
if (value.role === 'engineer' || value.role === 'sales' || value.role === 'operations' || value.role === 'all') result.role = value.role
|
|
98
132
|
|
|
133
|
+
result.connections = mergeConnections(value.connections ?? result.connections)
|
|
134
|
+
|
|
99
135
|
if (typeof value.servers === 'object' && value.servers !== null) {
|
|
100
136
|
for (const [id, raw] of Object.entries(value.servers as Record<string, unknown>)) {
|
|
101
137
|
if (typeof raw !== 'object' || raw === null) continue
|