@rezti/dsh-rez-suite 0.1.5 → 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 +10 -3
- package/lib/client.js +209 -161
- package/lib/index.d.ts +20 -0
- package/lib/index.js +91 -12
- package/package.json +7 -7
- package/src/client/locales.ts +21 -7
- package/src/client/panel/ConfigTab.tsx +141 -86
- package/src/index.ts +33 -7
- package/src/protocol.ts +23 -0
- package/src/routes.ts +32 -4
- package/src/store.ts +37 -1
package/lib/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
+
import { REZ_CREDENTIAL_REFS, REZ_DEFAULT_CONNECTIONS, REZ_SYSTEMS, mergeConnections, secretConfigured, writeManagedCredential } from "@rezti/dsh-rez-sso";
|
|
4
5
|
import { mkdirSync, renameSync, writeFileSync } from "node:fs";
|
|
5
6
|
import { DatabaseSync } from "node:sqlite";
|
|
6
7
|
//#region ../../node_modules/.pnpm/@deepseek-ai+cosmokit@1.8.2/node_modules/@deepseek-ai/cosmokit/lib/index.js
|
|
@@ -2865,13 +2866,14 @@ const FORMAT_VERSION = 1;
|
|
|
2865
2866
|
function rezStorePath() {
|
|
2866
2867
|
return join(homedir(), ".dsh", "dsh-rez-suite.json");
|
|
2867
2868
|
}
|
|
2868
|
-
/** Defaults:
|
|
2869
|
+
/** Defaults: company MCP endpoints. Secrets live in credentials.yaml / env. */
|
|
2869
2870
|
function defaultConfig() {
|
|
2870
2871
|
return {
|
|
2871
2872
|
enabled: true,
|
|
2872
2873
|
announceToAgent: true,
|
|
2873
2874
|
role: "all",
|
|
2874
2875
|
servers: {},
|
|
2876
|
+
connections: JSON.parse(JSON.stringify(REZ_DEFAULT_CONNECTIONS)),
|
|
2875
2877
|
billing: {
|
|
2876
2878
|
inputCostPer1k: .001,
|
|
2877
2879
|
outputCostPer1k: .002,
|
|
@@ -2896,10 +2898,37 @@ function maskServer(server) {
|
|
|
2896
2898
|
function publicConfig(config) {
|
|
2897
2899
|
const servers = {};
|
|
2898
2900
|
for (const [id, server] of Object.entries(config.servers)) servers[id] = maskServer(server);
|
|
2901
|
+
const connections = mergeConnections(config.connections);
|
|
2899
2902
|
return {
|
|
2900
2903
|
enabled: config.enabled,
|
|
2901
2904
|
role: config.role,
|
|
2902
2905
|
servers,
|
|
2906
|
+
connections: {
|
|
2907
|
+
odoo: {
|
|
2908
|
+
enabled: connections.odoo.enabled,
|
|
2909
|
+
url: connections.odoo.url,
|
|
2910
|
+
secretConfigured: secretConfigured("odoo"),
|
|
2911
|
+
secretRef: REZ_CREDENTIAL_REFS.odoo
|
|
2912
|
+
},
|
|
2913
|
+
nextcloud: {
|
|
2914
|
+
enabled: connections.nextcloud.enabled,
|
|
2915
|
+
url: connections.nextcloud.url,
|
|
2916
|
+
username: connections.nextcloud.username,
|
|
2917
|
+
secretConfigured: secretConfigured("nextcloud"),
|
|
2918
|
+
secretRef: REZ_CREDENTIAL_REFS.nextcloud
|
|
2919
|
+
},
|
|
2920
|
+
wechat: {
|
|
2921
|
+
enabled: connections.wechat.enabled,
|
|
2922
|
+
secretConfigured: secretConfigured("wechat"),
|
|
2923
|
+
secretRef: REZ_CREDENTIAL_REFS.wechat
|
|
2924
|
+
},
|
|
2925
|
+
homeassistant: {
|
|
2926
|
+
enabled: connections.homeassistant.enabled,
|
|
2927
|
+
url: connections.homeassistant.url,
|
|
2928
|
+
secretConfigured: secretConfigured("homeassistant"),
|
|
2929
|
+
secretRef: REZ_CREDENTIAL_REFS.homeassistant
|
|
2930
|
+
}
|
|
2931
|
+
},
|
|
2903
2932
|
billing: { ...config.billing }
|
|
2904
2933
|
};
|
|
2905
2934
|
}
|
|
@@ -2911,6 +2940,7 @@ function mergeConfig(base, override) {
|
|
|
2911
2940
|
if (typeof value.enabled === "boolean") result.enabled = value.enabled;
|
|
2912
2941
|
if (typeof value.announceToAgent === "boolean") result.announceToAgent = value.announceToAgent;
|
|
2913
2942
|
if (value.role === "engineer" || value.role === "sales" || value.role === "operations" || value.role === "all") result.role = value.role;
|
|
2943
|
+
result.connections = mergeConnections(value.connections ?? result.connections);
|
|
2914
2944
|
if (typeof value.servers === "object" && value.servers !== null) for (const [id, raw] of Object.entries(value.servers)) {
|
|
2915
2945
|
if (typeof raw !== "object" || raw === null) continue;
|
|
2916
2946
|
const incoming = raw;
|
|
@@ -3019,8 +3049,25 @@ function mergeMaskedRecord(base, incoming) {
|
|
|
3019
3049
|
/** Apply a browser-safe public patch without overwriting masked secrets. */
|
|
3020
3050
|
function applyPublicPatch(current, patch) {
|
|
3021
3051
|
const next = JSON.parse(JSON.stringify(current));
|
|
3052
|
+
const secrets = [];
|
|
3022
3053
|
if (typeof patch.enabled === "boolean") next.enabled = patch.enabled;
|
|
3023
3054
|
if (patch.role === "engineer" || patch.role === "sales" || patch.role === "operations" || patch.role === "all") next.role = patch.role;
|
|
3055
|
+
if (typeof patch.connections === "object" && patch.connections !== null) {
|
|
3056
|
+
const incoming = patch.connections;
|
|
3057
|
+
next.connections = mergeConnections({
|
|
3058
|
+
...next.connections,
|
|
3059
|
+
...incoming
|
|
3060
|
+
});
|
|
3061
|
+
for (const system of Object.keys(REZ_CREDENTIAL_REFS)) {
|
|
3062
|
+
const row = incoming[system];
|
|
3063
|
+
if (typeof row !== "object" || row === null) continue;
|
|
3064
|
+
const secret = row.secret;
|
|
3065
|
+
if (typeof secret === "string" && secret.length > 0 && secret !== SECRET_MASK) secrets.push({
|
|
3066
|
+
system,
|
|
3067
|
+
value: secret
|
|
3068
|
+
});
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3024
3071
|
if (typeof patch.servers === "object" && patch.servers !== null) for (const [id, raw] of Object.entries(patch.servers)) {
|
|
3025
3072
|
if (typeof raw !== "object" || raw === null) continue;
|
|
3026
3073
|
const incoming = raw;
|
|
@@ -3046,11 +3093,14 @@ function applyPublicPatch(current, patch) {
|
|
|
3046
3093
|
if (typeof value.outputCostPer1k === "number") next.billing.outputCostPer1k = value.outputCostPer1k;
|
|
3047
3094
|
if (typeof value.monthlyBudget === "number") next.billing.monthlyBudget = value.monthlyBudget;
|
|
3048
3095
|
}
|
|
3049
|
-
return
|
|
3096
|
+
return {
|
|
3097
|
+
next,
|
|
3098
|
+
secrets
|
|
3099
|
+
};
|
|
3050
3100
|
}
|
|
3051
3101
|
/** Build every /api/dsh-rez-suite route (exact paths). */
|
|
3052
3102
|
function makeRoutes(deps) {
|
|
3053
|
-
const { getConfig, updateConfig, host, tokens } = deps;
|
|
3103
|
+
const { getConfig, updateConfig, host, tokens, onCredentialWritten } = deps;
|
|
3054
3104
|
const guard = (req, res, method) => {
|
|
3055
3105
|
if (!isLoopbackRequest(req)) {
|
|
3056
3106
|
writeJson(res, 403, { error: "forbidden: loopback-only" });
|
|
@@ -3077,8 +3127,11 @@ function makeRoutes(deps) {
|
|
|
3077
3127
|
writeJson(res, 400, { error: "invalid JSON body" });
|
|
3078
3128
|
return;
|
|
3079
3129
|
}
|
|
3080
|
-
const next = applyPublicPatch(getConfig(), body);
|
|
3081
|
-
|
|
3130
|
+
const { next, secrets } = applyPublicPatch(getConfig(), body);
|
|
3131
|
+
const saved = await updateConfig(next);
|
|
3132
|
+
for (const item of secrets) writeManagedCredential(REZ_CREDENTIAL_REFS[item.system], item.value);
|
|
3133
|
+
for (const system of REZ_SYSTEMS) onCredentialWritten?.(REZ_CREDENTIAL_REFS[system]);
|
|
3134
|
+
writeJson(res, 200, { config: publicConfig(saved) });
|
|
3082
3135
|
}
|
|
3083
3136
|
},
|
|
3084
3137
|
{
|
|
@@ -7537,6 +7590,22 @@ const Config = Schema.object({
|
|
|
7537
7590
|
"all"
|
|
7538
7591
|
]).default("all"),
|
|
7539
7592
|
servers: Schema.dict(serverSchema).default({}),
|
|
7593
|
+
connections: Schema.object({
|
|
7594
|
+
odoo: Schema.object({
|
|
7595
|
+
enabled: Schema.boolean().default(true),
|
|
7596
|
+
url: Schema.string().default(REZ_DEFAULT_CONNECTIONS.odoo.url)
|
|
7597
|
+
}).default(REZ_DEFAULT_CONNECTIONS.odoo),
|
|
7598
|
+
nextcloud: Schema.object({
|
|
7599
|
+
enabled: Schema.boolean().default(true),
|
|
7600
|
+
url: Schema.string().default(REZ_DEFAULT_CONNECTIONS.nextcloud.url),
|
|
7601
|
+
username: Schema.string().default(REZ_DEFAULT_CONNECTIONS.nextcloud.username)
|
|
7602
|
+
}).default(REZ_DEFAULT_CONNECTIONS.nextcloud),
|
|
7603
|
+
wechat: Schema.object({ enabled: Schema.boolean().default(true) }).default(REZ_DEFAULT_CONNECTIONS.wechat),
|
|
7604
|
+
homeassistant: Schema.object({
|
|
7605
|
+
enabled: Schema.boolean().default(true),
|
|
7606
|
+
url: Schema.string().default(REZ_DEFAULT_CONNECTIONS.homeassistant.url)
|
|
7607
|
+
}).default(REZ_DEFAULT_CONNECTIONS.homeassistant)
|
|
7608
|
+
}).default(REZ_DEFAULT_CONNECTIONS),
|
|
7540
7609
|
billing: Schema.object({
|
|
7541
7610
|
inputCostPer1k: Schema.number().default(.001),
|
|
7542
7611
|
outputCostPer1k: Schema.number().default(.002),
|
|
@@ -7572,13 +7641,19 @@ function delegatedHost() {
|
|
|
7572
7641
|
state: "disconnected",
|
|
7573
7642
|
toolCount: 0,
|
|
7574
7643
|
registeredTools: 0,
|
|
7575
|
-
lastError: "owned by @deepseek-ai/dsh-mcp-client"
|
|
7576
|
-
}));
|
|
7577
|
-
const test = async () => COMPOSED_SERVERS.map((server) => ({
|
|
7578
|
-
server,
|
|
7579
|
-
ok: false,
|
|
7580
|
-
error: "MCP is composed by dsh-mcp-client; set REZ_* credentials and check dsh logs"
|
|
7644
|
+
lastError: secretConfigured(server) ? "owned by @deepseek-ai/dsh-mcp-client" : "missing " + REZ_CREDENTIAL_REFS[server]
|
|
7581
7645
|
}));
|
|
7646
|
+
const test = async () => COMPOSED_SERVERS.map((server) => {
|
|
7647
|
+
return secretConfigured(server) ? {
|
|
7648
|
+
server,
|
|
7649
|
+
ok: true,
|
|
7650
|
+
serverInfo: REZ_CREDENTIAL_REFS[server] + " configured"
|
|
7651
|
+
} : {
|
|
7652
|
+
server,
|
|
7653
|
+
ok: false,
|
|
7654
|
+
error: "missing " + REZ_CREDENTIAL_REFS[server]
|
|
7655
|
+
};
|
|
7656
|
+
});
|
|
7582
7657
|
return {
|
|
7583
7658
|
status,
|
|
7584
7659
|
test,
|
|
@@ -7609,7 +7684,11 @@ function apply(ctx, config) {
|
|
|
7609
7684
|
return value;
|
|
7610
7685
|
},
|
|
7611
7686
|
host,
|
|
7612
|
-
tokens
|
|
7687
|
+
tokens,
|
|
7688
|
+
onCredentialWritten: (ref) => {
|
|
7689
|
+
const emit = ctx.emit;
|
|
7690
|
+
emit("credentials/updated", ref);
|
|
7691
|
+
}
|
|
7613
7692
|
});
|
|
7614
7693
|
let disposeRoutes;
|
|
7615
7694
|
let disposeTools;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rezti/dsh-rez-suite",
|
|
3
3
|
"description": "ReZ-TI employee plugin for DeepSeek Harness: one install enables company MCP (Odoo / Nextcloud / WeCom / Weixin-ClawBot / Home Assistant) and work identity.",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.6",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
|
@@ -38,13 +38,13 @@
|
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
40
40
|
"zod": "^4.4.3",
|
|
41
|
-
"@rezti/dsh-rez-
|
|
42
|
-
"@rezti/dsh-rez-
|
|
43
|
-
"@rezti/dsh-rez-
|
|
44
|
-
"@rezti/dsh-rez-
|
|
45
|
-
"@rezti/dsh-rez-
|
|
41
|
+
"@rezti/dsh-rez-nextcloud": "0.1.5",
|
|
42
|
+
"@rezti/dsh-rez-odoo": "0.1.5",
|
|
43
|
+
"@rezti/dsh-rez-sso": "0.1.5",
|
|
44
|
+
"@rezti/dsh-rez-ha-bridge": "0.1.5",
|
|
45
|
+
"@rezti/dsh-rez-wechat": "0.1.5",
|
|
46
46
|
"@rezti/dsh-rez-token-manager": "0.1.3",
|
|
47
|
-
"@rezti/dsh-rez-
|
|
47
|
+
"@rezti/dsh-rez-intent": "0.1.3"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"react": "^18.2.0",
|
package/src/client/locales.ts
CHANGED
|
@@ -27,16 +27,23 @@ export const zh = {
|
|
|
27
27
|
'config.env': '环境变量 (JSON)',
|
|
28
28
|
'config.headers': 'Headers (JSON)',
|
|
29
29
|
'config.jsonInvalid': 'JSON 格式错误:{error}',
|
|
30
|
+
'config.mcpHint': '四个公司 MCP 在这里配。密钥写入 ~/.dsh/.credentials.yaml(REZ_*)。新填的密钥会立刻拉起连接;改 URL 后如果已经连上,需要重启 dsh web。',
|
|
31
|
+
'config.secretConfigured': '已配置(留空则保持)',
|
|
32
|
+
'config.secretMissing': '未配置',
|
|
30
33
|
'config.odoo': 'Odoo',
|
|
31
|
-
'config.odoo.url': 'Odoo URL',
|
|
32
|
-
'config.odoo.apiKey': 'API
|
|
34
|
+
'config.odoo.url': 'Odoo MCP URL',
|
|
35
|
+
'config.odoo.apiKey': 'API Token',
|
|
33
36
|
'config.nextcloud': 'Nextcloud',
|
|
34
|
-
'config.nextcloud.url': 'Nextcloud
|
|
37
|
+
'config.nextcloud.url': 'Nextcloud 主机',
|
|
35
38
|
'config.nextcloud.username': '用户名',
|
|
36
39
|
'config.nextcloud.appPassword': 'App Password',
|
|
37
40
|
'config.wecom': '企业微信',
|
|
38
41
|
'config.wecom.botId': 'Bot ID',
|
|
39
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': '长期访问令牌',
|
|
40
47
|
'config.fs': '本地文件 (fs-mcp-server)',
|
|
41
48
|
'config.fs.root': '根目录',
|
|
42
49
|
'config.sqlite': 'SQLite (sqlite-mcp-server)',
|
|
@@ -122,16 +129,23 @@ export const en: Record<RezKey, string> = {
|
|
|
122
129
|
'config.env': 'Env vars (JSON)',
|
|
123
130
|
'config.headers': 'Headers (JSON)',
|
|
124
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',
|
|
125
135
|
'config.odoo': 'Odoo',
|
|
126
|
-
'config.odoo.url': 'Odoo URL',
|
|
127
|
-
'config.odoo.apiKey': 'API
|
|
136
|
+
'config.odoo.url': 'Odoo MCP URL',
|
|
137
|
+
'config.odoo.apiKey': 'API token',
|
|
128
138
|
'config.nextcloud': 'Nextcloud',
|
|
129
|
-
'config.nextcloud.url': 'Nextcloud
|
|
139
|
+
'config.nextcloud.url': 'Nextcloud host',
|
|
130
140
|
'config.nextcloud.username': 'Username',
|
|
131
|
-
'config.nextcloud.appPassword': 'App
|
|
141
|
+
'config.nextcloud.appPassword': 'App password',
|
|
132
142
|
'config.wecom': 'WeCom',
|
|
133
143
|
'config.wecom.botId': 'Bot ID',
|
|
134
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',
|
|
135
149
|
'config.fs': 'Local files (fs-mcp-server)',
|
|
136
150
|
'config.fs.root': 'Root directory',
|
|
137
151
|
'config.sqlite': 'SQLite (sqlite-mcp-server)',
|
|
@@ -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')}>
|
package/src/index.ts
CHANGED
|
@@ -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),
|
|
@@ -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
|