@rezti/dsh-rez-suite 0.1.0
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/LICENSE +13 -0
- package/README.md +9 -0
- package/README.zh.md +11 -0
- package/cordis.patch.yml +68 -0
- package/lib/client.d.ts +7479 -0
- package/lib/client.js +1190 -0
- package/lib/index.d.ts +1726 -0
- package/lib/index.js +7659 -0
- package/lib/style.css +309 -0
- package/package.json +103 -0
- package/servers/fs-mcp-server.mjs +71 -0
- package/servers/sqlite-mcp-server.mjs +50 -0
- package/src/client/api.ts +75 -0
- package/src/client/css-modules.d.ts +4 -0
- package/src/client/index.ts +45 -0
- package/src/client/locales.ts +176 -0
- package/src/client/mount.tsx +97 -0
- package/src/client/panel/AuditTab.tsx +87 -0
- package/src/client/panel/ConfigTab.tsx +223 -0
- package/src/client/panel/RezPanel.tsx +50 -0
- package/src/client/panel/StatusTab.tsx +66 -0
- package/src/client/panel/controller.ts +42 -0
- package/src/client/panel/helpers.ts +21 -0
- package/src/client/panel/panel.module.css +316 -0
- package/src/client/sidebar-entry.ts +80 -0
- package/src/index.ts +166 -0
- package/src/mcp-host.ts +418 -0
- package/src/presets.ts +125 -0
- package/src/protocol.ts +133 -0
- package/src/routes.ts +191 -0
- package/src/store.ts +152 -0
- package/src/token-manager.ts +252 -0
- package/src/tools.ts +167 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status tab: live MCP server states and registered-tool counts.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useEffect, useState } from 'react'
|
|
6
|
+
import type { RezApi } from '../api.ts'
|
|
7
|
+
import type { RezStatusResponse } from '../../protocol.ts'
|
|
8
|
+
import { errorMessage, tt } from './helpers.ts'
|
|
9
|
+
import css from './panel.module.css'
|
|
10
|
+
|
|
11
|
+
export function StatusTab({ api }: { api: RezApi }) {
|
|
12
|
+
const [status, setStatus] = useState<RezStatusResponse | null>(null)
|
|
13
|
+
const [error, setError] = useState('')
|
|
14
|
+
const [loading, setLoading] = useState(true)
|
|
15
|
+
|
|
16
|
+
const load = async (): Promise<void> => {
|
|
17
|
+
setLoading(true)
|
|
18
|
+
setError('')
|
|
19
|
+
try {
|
|
20
|
+
setStatus(await api.status())
|
|
21
|
+
} catch (err) {
|
|
22
|
+
setError(errorMessage(err))
|
|
23
|
+
} finally {
|
|
24
|
+
setLoading(false)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
useEffect(() => { void load() }, [])
|
|
29
|
+
|
|
30
|
+
if (loading) return <div className={css.message}>{tt('common.loading')}</div>
|
|
31
|
+
if (status === null) return <div className={css.error}>{error}</div>
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className={css.tabBody}>
|
|
35
|
+
<div className={css.toolbar}>
|
|
36
|
+
<span className={css.message}>{tt('status.total', { role: status.role, count: status.totalRegisteredTools })}</span>
|
|
37
|
+
<button type="button" className={css.button} onClick={() => { void load() }}>{tt('common.refresh')}</button>
|
|
38
|
+
</div>
|
|
39
|
+
{error !== '' && <div className={css.error}>{error}</div>}
|
|
40
|
+
<div className={css.tableWrap}>
|
|
41
|
+
<table className={css.table}>
|
|
42
|
+
<thead>
|
|
43
|
+
<tr>
|
|
44
|
+
<th>{tt('status.col.server')}</th>
|
|
45
|
+
<th>{tt('status.col.state')}</th>
|
|
46
|
+
<th>{tt('status.col.tools')}</th>
|
|
47
|
+
<th>{tt('status.col.registered')}</th>
|
|
48
|
+
<th>{tt('status.col.error')}</th>
|
|
49
|
+
</tr>
|
|
50
|
+
</thead>
|
|
51
|
+
<tbody>
|
|
52
|
+
{status.servers.map(server => (
|
|
53
|
+
<tr key={server.server}>
|
|
54
|
+
<td>{server.server}</td>
|
|
55
|
+
<td><span className={css.badge + ' ' + (server.state === 'connected' ? css.badgeOk : css.badgeFail)}>{server.state}</span></td>
|
|
56
|
+
<td>{server.toolCount}</td>
|
|
57
|
+
<td>{server.registeredTools}</td>
|
|
58
|
+
<td>{server.lastError ?? ''}</td>
|
|
59
|
+
</tr>
|
|
60
|
+
))}
|
|
61
|
+
</tbody>
|
|
62
|
+
</table>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rez panel controller: single owner of open/closed state (browser session).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface PanelControllerSnapshot {
|
|
6
|
+
panelOpen: boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class PanelController {
|
|
10
|
+
private panelOpen = false
|
|
11
|
+
private listeners = new Set<() => void>()
|
|
12
|
+
|
|
13
|
+
getSnapshot(): PanelControllerSnapshot {
|
|
14
|
+
return { panelOpen: this.panelOpen }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
subscribe(fn: () => void): () => void {
|
|
18
|
+
this.listeners.add(fn)
|
|
19
|
+
return () => { this.listeners.delete(fn) }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
open(): void {
|
|
23
|
+
if (this.panelOpen) return
|
|
24
|
+
this.panelOpen = true
|
|
25
|
+
this.notify()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
close(): void {
|
|
29
|
+
if (!this.panelOpen) return
|
|
30
|
+
this.panelOpen = false
|
|
31
|
+
this.notify()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
toggle(): void {
|
|
35
|
+
if (this.panelOpen) this.close()
|
|
36
|
+
else this.open()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private notify(): void {
|
|
40
|
+
for (const fn of [...this.listeners]) fn()
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared panel helpers: active-language dictionary pick and error extraction.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { en, t, zh, type RezKey } from '../locales.ts'
|
|
6
|
+
|
|
7
|
+
export type TranslateValues = Record<string, string | number>
|
|
8
|
+
|
|
9
|
+
export function dictionary(): Record<string, string> {
|
|
10
|
+
const lang = typeof document !== 'undefined' ? document.documentElement.lang : 'zh'
|
|
11
|
+
return lang.toLowerCase().startsWith('en') ? { ...en } : { ...zh }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function tt(key: RezKey, values?: TranslateValues): string {
|
|
15
|
+
return t(dictionary(), key, values)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function errorMessage(error: unknown): string {
|
|
19
|
+
if (error instanceof Error) return error.message
|
|
20
|
+
return String(error)
|
|
21
|
+
}
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-rez-suite panel styles. Scoped by plugin data attributes, colors ride
|
|
3
|
+
* dsh --dsw-* tokens. The center-column takeover rules are attribute-scoped
|
|
4
|
+
* and must stay in this stylesheet (imported by mount.tsx).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
[data-pane='conversation'] {
|
|
8
|
+
position: relative;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
[data-dsh-rez-view] {
|
|
12
|
+
position: absolute;
|
|
13
|
+
inset: 0;
|
|
14
|
+
display: none;
|
|
15
|
+
z-index: 60;
|
|
16
|
+
background: var(--dsw-alias-bg-base);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
html[data-dsh-rez-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-active]) [data-dsh-rez-view] {
|
|
20
|
+
display: block;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
html[data-dsh-rez-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-active]) [data-pane='conversation'] > :not([data-dsh-rez-view]),
|
|
24
|
+
html[data-dsh-rez-active]:not([data-dsh-taskboard-active]):not([data-dsh-ssh-active]) [class*='centerCol'] > :not([data-dsh-rez-view]) {
|
|
25
|
+
display: none !important;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.entry {
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
gap: 8px;
|
|
32
|
+
width: 100%;
|
|
33
|
+
height: 32px;
|
|
34
|
+
padding: 0 12px;
|
|
35
|
+
background: transparent;
|
|
36
|
+
border: none;
|
|
37
|
+
border-radius: 8px;
|
|
38
|
+
color: var(--dsw-alias-label-secondary);
|
|
39
|
+
cursor: pointer;
|
|
40
|
+
font-size: 13px;
|
|
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;
|
|
51
|
+
align-items: center;
|
|
52
|
+
justify-content: center;
|
|
53
|
+
flex: none;
|
|
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;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.view {
|
|
72
|
+
overflow: hidden;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.panel {
|
|
76
|
+
display: flex;
|
|
77
|
+
flex-direction: column;
|
|
78
|
+
height: 100%;
|
|
79
|
+
min-width: 0;
|
|
80
|
+
min-height: 0;
|
|
81
|
+
padding: 14px 16px 16px;
|
|
82
|
+
gap: 10px;
|
|
83
|
+
background: var(--dsw-alias-bg-base);
|
|
84
|
+
color: var(--dsw-alias-label-primary);
|
|
85
|
+
font-family: var(--dsw-font-family);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.panelHeader {
|
|
89
|
+
display: flex;
|
|
90
|
+
align-items: center;
|
|
91
|
+
gap: 10px;
|
|
92
|
+
flex: none;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.panelTitle {
|
|
96
|
+
margin: 0;
|
|
97
|
+
flex: 1;
|
|
98
|
+
font-size: 16px;
|
|
99
|
+
font-weight: 700;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.iconButton {
|
|
103
|
+
border: none;
|
|
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;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.iconButton:hover {
|
|
113
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
114
|
+
color: var(--dsw-alias-label-primary);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.tabBar {
|
|
118
|
+
display: flex;
|
|
119
|
+
gap: 2px;
|
|
120
|
+
flex: none;
|
|
121
|
+
border-bottom: 1px solid var(--dsw-alias-border-l1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.tab {
|
|
125
|
+
padding: 7px 14px;
|
|
126
|
+
font-size: 13px;
|
|
127
|
+
color: var(--dsw-alias-label-secondary);
|
|
128
|
+
background: transparent;
|
|
129
|
+
border: none;
|
|
130
|
+
border-bottom: 2px solid transparent;
|
|
131
|
+
border-radius: 6px 6px 0 0;
|
|
132
|
+
cursor: pointer;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.tab:hover {
|
|
136
|
+
color: var(--dsw-alias-label-primary);
|
|
137
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.tab[data-active] {
|
|
141
|
+
color: var(--dsw-alias-label-primary);
|
|
142
|
+
font-weight: 600;
|
|
143
|
+
border-bottom-color: var(--dsw-alias-state-business-primary);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.panelContent {
|
|
147
|
+
flex: 1;
|
|
148
|
+
min-height: 0;
|
|
149
|
+
display: flex;
|
|
150
|
+
flex-direction: column;
|
|
151
|
+
overflow: hidden;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.tabBody {
|
|
155
|
+
display: flex;
|
|
156
|
+
flex-direction: column;
|
|
157
|
+
gap: 12px;
|
|
158
|
+
flex: 1;
|
|
159
|
+
min-height: 0;
|
|
160
|
+
overflow-y: auto;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.section {
|
|
164
|
+
display: flex;
|
|
165
|
+
flex-direction: column;
|
|
166
|
+
gap: 8px;
|
|
167
|
+
padding: 12px;
|
|
168
|
+
border: 1px solid var(--dsw-alias-border-l1);
|
|
169
|
+
border-radius: 10px;
|
|
170
|
+
background: var(--dsw-alias-bg-layer-1, var(--dsw-alias-bg-base));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.sectionTitle {
|
|
174
|
+
margin: 0;
|
|
175
|
+
font-size: 13px;
|
|
176
|
+
font-weight: 600;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.field {
|
|
180
|
+
display: grid;
|
|
181
|
+
grid-template-columns: 140px 1fr;
|
|
182
|
+
gap: 8px;
|
|
183
|
+
align-items: center;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.label {
|
|
187
|
+
font-size: 12.5px;
|
|
188
|
+
color: var(--dsw-alias-label-secondary);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.input, .select {
|
|
192
|
+
padding: 6px 8px;
|
|
193
|
+
font-size: 13px;
|
|
194
|
+
color: var(--dsw-alias-label-primary);
|
|
195
|
+
background: var(--dsw-specific-input-major);
|
|
196
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
197
|
+
border-radius: 8px;
|
|
198
|
+
outline: none;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.checkbox {
|
|
202
|
+
justify-self: start;
|
|
203
|
+
width: 15px;
|
|
204
|
+
height: 15px;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.toolbar {
|
|
208
|
+
display: flex;
|
|
209
|
+
align-items: center;
|
|
210
|
+
gap: 8px;
|
|
211
|
+
flex: none;
|
|
212
|
+
flex-wrap: wrap;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.button {
|
|
216
|
+
padding: 7px 12px;
|
|
217
|
+
font-size: 13px;
|
|
218
|
+
border-radius: 8px;
|
|
219
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
220
|
+
background: var(--dsw-specific-input-major);
|
|
221
|
+
color: var(--dsw-alias-label-primary);
|
|
222
|
+
cursor: pointer;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.button:hover {
|
|
226
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.primaryButton {
|
|
230
|
+
background: var(--dsw-alias-state-business-primary);
|
|
231
|
+
border-color: var(--dsw-alias-state-business-primary);
|
|
232
|
+
color: #fff;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.dangerButton {
|
|
236
|
+
background: var(--dsw-alias-state-danger, #c62828);
|
|
237
|
+
border-color: var(--dsw-alias-state-danger, #c62828);
|
|
238
|
+
color: #fff;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.message {
|
|
242
|
+
font-size: 12.5px;
|
|
243
|
+
color: var(--dsw-alias-label-secondary);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.error {
|
|
247
|
+
color: var(--dsw-alias-state-danger, #c62828);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.tableWrap {
|
|
251
|
+
flex: 1;
|
|
252
|
+
min-height: 0;
|
|
253
|
+
overflow: auto;
|
|
254
|
+
border: 1px solid var(--dsw-alias-border-l1);
|
|
255
|
+
border-radius: 10px;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.table {
|
|
259
|
+
width: 100%;
|
|
260
|
+
border-collapse: collapse;
|
|
261
|
+
font-size: 12.5px;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.table th {
|
|
265
|
+
position: sticky;
|
|
266
|
+
top: 0;
|
|
267
|
+
z-index: 1;
|
|
268
|
+
padding: 8px 10px;
|
|
269
|
+
text-align: left;
|
|
270
|
+
background: var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-layer-1));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.table td {
|
|
274
|
+
padding: 8px 10px;
|
|
275
|
+
border-top: 1px solid var(--dsw-alias-border-l1);
|
|
276
|
+
vertical-align: top;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.badge {
|
|
280
|
+
display: inline-block;
|
|
281
|
+
padding: 2px 8px;
|
|
282
|
+
border-radius: 999px;
|
|
283
|
+
font-size: 11px;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.badgeOk {
|
|
287
|
+
background: rgba(34, 154, 85, 0.14);
|
|
288
|
+
color: #229a55;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.badgeFail {
|
|
292
|
+
background: rgba(198, 40, 40, 0.14);
|
|
293
|
+
color: #c62828;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.cards {
|
|
297
|
+
display: grid;
|
|
298
|
+
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
|
299
|
+
gap: 8px;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.card {
|
|
303
|
+
padding: 10px;
|
|
304
|
+
border: 1px solid var(--dsw-alias-border-l1);
|
|
305
|
+
border-radius: 10px;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.cardLabel {
|
|
309
|
+
font-size: 11px;
|
|
310
|
+
color: var(--dsw-alias-label-tertiary);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.cardValue {
|
|
314
|
+
font-size: 15px;
|
|
315
|
+
font-weight: 600;
|
|
316
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sidebar entry injection (DOM-level extension, task-board/ssh precedent).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { PanelController } from './panel/controller.ts'
|
|
6
|
+
import { tt } from './panel/helpers.ts'
|
|
7
|
+
import css from './panel/panel.module.css'
|
|
8
|
+
|
|
9
|
+
export const ENTRY_SELECTOR = '[data-dsh-rez-entry]'
|
|
10
|
+
|
|
11
|
+
const ICON = '<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2.5" y="2.5" width="11" height="11" rx="2"/><path d="M5 8l2 2 4-4"/></svg>'
|
|
12
|
+
|
|
13
|
+
function sidebarRoot(): HTMLElement | undefined {
|
|
14
|
+
const column = document.querySelector<HTMLElement>('[data-pane="sidebar"], [class*="sidebarCol"]')
|
|
15
|
+
if (column === null) return undefined
|
|
16
|
+
const logoOwner = column.querySelector<HTMLElement>('[class*="logoRow"]')?.parentElement
|
|
17
|
+
return logoOwner ?? (column.firstElementChild as HTMLElement | undefined)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function newSessionButton(root: HTMLElement): HTMLButtonElement | undefined {
|
|
21
|
+
const nested = root.querySelector<HTMLButtonElement>('button[class*="newSession"]')
|
|
22
|
+
if (nested !== null) return nested
|
|
23
|
+
for (const child of root.children) {
|
|
24
|
+
if (child.tagName === 'BUTTON') return child as HTMLButtonElement
|
|
25
|
+
}
|
|
26
|
+
return undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function createEntry(controller: PanelController): HTMLButtonElement {
|
|
30
|
+
const entry = document.createElement('button')
|
|
31
|
+
entry.type = 'button'
|
|
32
|
+
entry.dataset.dshRezEntry = ''
|
|
33
|
+
entry.className = css.entry
|
|
34
|
+
entry.setAttribute('aria-label', tt('entry.label'))
|
|
35
|
+
entry.setAttribute('title', tt('entry.tooltip'))
|
|
36
|
+
entry.innerHTML = '<span class="' + css.entryIcon + '">' + ICON + '</span><span class="' + css.entryLabel + '">' + tt('entry.label') + '</span>'
|
|
37
|
+
entry.addEventListener('click', () => { controller.toggle() })
|
|
38
|
+
return entry
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function placeEntry(root: HTMLElement, entry: HTMLButtonElement): boolean {
|
|
42
|
+
const button = newSessionButton(root)
|
|
43
|
+
if (button === undefined) return false
|
|
44
|
+
if (entry.parentElement !== root) {
|
|
45
|
+
const row = button.closest('[class*="logoRow"]')
|
|
46
|
+
const base = (row !== null && row.parentElement === root) ? row : button
|
|
47
|
+
const family = Array.from(root.children).filter(
|
|
48
|
+
(el): el is HTMLElement => el instanceof HTMLElement && el.matches('[data-dsh-taskboard-entry], [data-dsh-ssh-entry], [data-dsh-rez-entry]'),
|
|
49
|
+
)
|
|
50
|
+
const anchor = family.length > 0 ? family[family.length - 1].nextElementSibling : base.nextElementSibling
|
|
51
|
+
root.insertBefore(entry, anchor)
|
|
52
|
+
}
|
|
53
|
+
return true
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function mountSidebarEntry(controller: PanelController): () => void {
|
|
57
|
+
const entry = createEntry(controller)
|
|
58
|
+
let root: HTMLElement | undefined
|
|
59
|
+
let placed = false
|
|
60
|
+
|
|
61
|
+
const place = (): void => {
|
|
62
|
+
const next = sidebarRoot()
|
|
63
|
+
if (next === undefined) return
|
|
64
|
+
if (root !== undefined && root !== next) {
|
|
65
|
+
if (entry.parentElement === root) entry.remove()
|
|
66
|
+
placed = false
|
|
67
|
+
}
|
|
68
|
+
root = next
|
|
69
|
+
if (!placed) placed = placeEntry(root, entry)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const observer = new MutationObserver(place)
|
|
73
|
+
observer.observe(document.body, { childList: true, subtree: true })
|
|
74
|
+
place()
|
|
75
|
+
|
|
76
|
+
return () => {
|
|
77
|
+
observer.disconnect()
|
|
78
|
+
entry.remove()
|
|
79
|
+
}
|
|
80
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-rez-suite — employee-facing host half.
|
|
3
|
+
*
|
|
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 Rez panel,
|
|
6
|
+
* loopback settings API, and a short system-prompt announcement. Do not
|
|
7
|
+
* start a second MCP host here.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
11
|
+
import { homedir } from 'node:os'
|
|
12
|
+
import { join } from 'node:path'
|
|
13
|
+
import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings'
|
|
14
|
+
import z from '@deepseek-ai/schemastery'
|
|
15
|
+
import type {} from '@deepseek-ai/dsh-host-webserver'
|
|
16
|
+
import type {} from '@deepseek-ai/dsh-system-prompt'
|
|
17
|
+
import type {} from '@deepseek-ai/dsh-tools'
|
|
18
|
+
import type { McpHost } from './mcp-host.ts'
|
|
19
|
+
import { makeRoutes } from './routes.ts'
|
|
20
|
+
import { defaultConfig, mergeConfig, saveConfig } from './store.ts'
|
|
21
|
+
import { TokenManager } from './token-manager.ts'
|
|
22
|
+
import { rezConfigTool, rezStatusTool } from './tools.ts'
|
|
23
|
+
import type { RezConfig, RezServerStatus, RezTestResult } from './protocol.ts'
|
|
24
|
+
|
|
25
|
+
export const name = 'rez-suite'
|
|
26
|
+
|
|
27
|
+
export const inject = ['webServer', 'tools', 'systemPrompt']
|
|
28
|
+
|
|
29
|
+
export const REZ_SETTINGS_NAMESPACE = settingsNamespace('dsh-rez-suite')
|
|
30
|
+
|
|
31
|
+
const serverSchema = z.object({
|
|
32
|
+
enabled: z.boolean().default(false),
|
|
33
|
+
transport: z.union(['stdio', 'streamable-http'] as const).default('stdio'),
|
|
34
|
+
command: z.string().required(false),
|
|
35
|
+
args: z.array(z.string()).required(false),
|
|
36
|
+
env: z.dict(z.string()).required(false),
|
|
37
|
+
cwd: z.string().required(false),
|
|
38
|
+
url: z.string().required(false),
|
|
39
|
+
headers: z.dict(z.string()).required(false),
|
|
40
|
+
toolCallTimeoutMs: z.number().required(false),
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
export const Config: z<RezConfig> = z.object({
|
|
44
|
+
enabled: z.boolean().default(true),
|
|
45
|
+
announceToAgent: z.boolean().default(true),
|
|
46
|
+
role: z.union(['engineer', 'sales', 'operations', 'all'] as const).default('all'),
|
|
47
|
+
servers: z.dict(serverSchema).default({}),
|
|
48
|
+
billing: z.object({
|
|
49
|
+
inputCostPer1k: z.number().default(0.001),
|
|
50
|
+
outputCostPer1k: z.number().default(0.002),
|
|
51
|
+
monthlyBudget: z.number().default(0),
|
|
52
|
+
}).default({ inputCostPer1k: 0.001, outputCostPer1k: 0.002, monthlyBudget: 0 }),
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const SECTION_ORDER = 150
|
|
56
|
+
|
|
57
|
+
const COMPOSED_SERVERS = ['odoo', 'nextcloud', 'wechat', 'homeassistant'] as const
|
|
58
|
+
|
|
59
|
+
export const REZ_GUIDANCE = [
|
|
60
|
+
'本机已安装 ReZ-TI 套件(一次安装):工作身份由 dsh-rez-sso 提供,MCP 由官方 dsh-mcp-client 接入 Odoo / Nextcloud / 企业微信 / Home Assistant。',
|
|
61
|
+
'工具名 mcp__odoo__*、mcp__nextcloud__*、mcp__wechat__*、mcp__homeassistant__*。',
|
|
62
|
+
'密钥走官方 dsh-credentials / 环境变量:REZ_ODOO_TOKEN、REZ_NEXTCLOUD_PASSWORD、REZ_WECHAT_WEBHOOK、REZ_HA_TOKEN。',
|
|
63
|
+
'意图用 rez_set_intent(erp/files/chat/home/auto)。审计用 rez_audit。',
|
|
64
|
+
'涉及资金、对外发送、设备控制、生产写入时先征得用户批准。',
|
|
65
|
+
].join('')
|
|
66
|
+
|
|
67
|
+
function normalize(input: Partial<RezConfig> | undefined): RezConfig {
|
|
68
|
+
return mergeConfig(defaultConfig(), input ?? {})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function delegatedHost(): McpHost {
|
|
72
|
+
const status = (): RezServerStatus[] => COMPOSED_SERVERS.map(server => ({
|
|
73
|
+
server,
|
|
74
|
+
enabled: true,
|
|
75
|
+
state: 'disconnected',
|
|
76
|
+
toolCount: 0,
|
|
77
|
+
registeredTools: 0,
|
|
78
|
+
lastError: 'owned by @deepseek-ai/dsh-mcp-client',
|
|
79
|
+
}))
|
|
80
|
+
const test = async (): Promise<RezTestResult[]> => COMPOSED_SERVERS.map(server => ({
|
|
81
|
+
server,
|
|
82
|
+
ok: false,
|
|
83
|
+
error: 'MCP is composed by dsh-mcp-client; set REZ_* credentials and check dsh logs',
|
|
84
|
+
}))
|
|
85
|
+
return {
|
|
86
|
+
status,
|
|
87
|
+
test,
|
|
88
|
+
sync: async () => undefined,
|
|
89
|
+
dispose: async () => undefined,
|
|
90
|
+
} as unknown as McpHost
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function apply(ctx: Context, config?: RezConfig): void {
|
|
94
|
+
let current: () => RezConfig = () => normalize(config)
|
|
95
|
+
|
|
96
|
+
const tokens = new TokenManager(joinTokenDb())
|
|
97
|
+
const host = delegatedHost()
|
|
98
|
+
ctx.effect(() => () => { tokens.close() }, 'dsh-rez-suite: ledger view')
|
|
99
|
+
|
|
100
|
+
const tools = [
|
|
101
|
+
rezStatusTool(host, () => current()),
|
|
102
|
+
rezConfigTool(() => current()),
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
const routes = makeRoutes({
|
|
106
|
+
getConfig: () => current(),
|
|
107
|
+
updateConfig: async (next) => {
|
|
108
|
+
const value = normalize(next as Partial<RezConfig>)
|
|
109
|
+
saveConfig(value)
|
|
110
|
+
current = () => value
|
|
111
|
+
try {
|
|
112
|
+
await ctx.settings?.update(REZ_SETTINGS_NAMESPACE, value)
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.warn('[dsh-rez-suite] settings update failed, using local store:', error)
|
|
115
|
+
}
|
|
116
|
+
return value
|
|
117
|
+
},
|
|
118
|
+
host,
|
|
119
|
+
tokens,
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
let disposeRoutes: (() => void) | undefined
|
|
123
|
+
let disposeTools: (() => void) | undefined
|
|
124
|
+
let disposeSection: (() => void) | undefined
|
|
125
|
+
|
|
126
|
+
const sync = (): void => {
|
|
127
|
+
if (disposeSection !== undefined) { disposeSection(); disposeSection = undefined }
|
|
128
|
+
if (disposeRoutes !== undefined) { disposeRoutes(); disposeRoutes = undefined }
|
|
129
|
+
if (disposeTools !== undefined) { disposeTools(); disposeTools = undefined }
|
|
130
|
+
|
|
131
|
+
const value = current()
|
|
132
|
+
if (!value.enabled) return
|
|
133
|
+
|
|
134
|
+
if (value.announceToAgent) {
|
|
135
|
+
disposeSection = ctx.systemPrompt.section({
|
|
136
|
+
name: 'plugin:dsh-rez-suite',
|
|
137
|
+
order: SECTION_ORDER,
|
|
138
|
+
text: REZ_GUIDANCE,
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
disposeRoutes = ctx.effect(() => {
|
|
143
|
+
const disposers = routes.map(route => ctx.webServer.register(route))
|
|
144
|
+
return () => { for (const dispose of disposers) dispose() }
|
|
145
|
+
}, 'dsh-rez-suite: routes')
|
|
146
|
+
|
|
147
|
+
disposeTools = ctx.effect(() => {
|
|
148
|
+
const disposers = tools.map(tool => ctx.tools.register(tool))
|
|
149
|
+
return () => { for (const dispose of disposers) dispose() }
|
|
150
|
+
}, 'dsh-rez-suite: tools')
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
installSettingsSection(ctx, REZ_SETTINGS_NAMESPACE, Config, normalize(config), {
|
|
154
|
+
setSource: (source) => {
|
|
155
|
+
current = source
|
|
156
|
+
sync()
|
|
157
|
+
},
|
|
158
|
+
onChange: sync,
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
sync()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function joinTokenDb(): string {
|
|
165
|
+
return join(homedir(), '.dsh', 'dsh-rez-token-manager.sqlite')
|
|
166
|
+
}
|