dsh-connect 0.7.2 → 0.9.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/README.i18n.yaml +2 -2
- package/README.md +196 -43
- package/README.zh.md +171 -42
- package/client/client.js +817 -0
- package/client/client.js.map +7 -0
- package/client/locale.mjs +227 -0
- package/client/panel-state.mjs +54 -0
- package/client/settings-client.mjs +389 -0
- package/docs/images/settings-advanced-en.png +0 -0
- package/docs/images/settings-advanced-zh.png +0 -0
- package/docs/images/settings-defaults-en.png +0 -0
- package/docs/images/settings-defaults-zh.png +0 -0
- package/docs/images/settings-overview-en.png +0 -0
- package/docs/images/settings-overview-zh.png +0 -0
- package/examples/minimal.config.json +16 -0
- package/lib/binding.d.ts.map +1 -1
- package/lib/binding.js +2 -2
- package/lib/binding.js.map +1 -1
- package/lib/channels/dingtalk/adapter.d.ts +59 -0
- package/lib/channels/dingtalk/adapter.d.ts.map +1 -0
- package/lib/channels/dingtalk/adapter.js +123 -0
- package/lib/channels/dingtalk/adapter.js.map +1 -0
- package/lib/channels/dingtalk/i18n.d.ts +14 -0
- package/lib/channels/dingtalk/i18n.d.ts.map +1 -0
- package/lib/channels/dingtalk/i18n.js +16 -0
- package/lib/channels/dingtalk/i18n.js.map +1 -0
- package/lib/channels/dingtalk/index.d.ts +127 -0
- package/lib/channels/dingtalk/index.d.ts.map +1 -0
- package/lib/channels/dingtalk/index.js +144 -0
- package/lib/channels/dingtalk/index.js.map +1 -0
- package/lib/channels/dingtalk/message.d.ts +49 -0
- package/lib/channels/dingtalk/message.d.ts.map +1 -0
- package/lib/channels/dingtalk/message.js +50 -0
- package/lib/channels/dingtalk/message.js.map +1 -0
- package/lib/channels/dingtalk/stomp.d.ts +36 -0
- package/lib/channels/dingtalk/stomp.d.ts.map +1 -0
- package/lib/channels/dingtalk/stomp.js +107 -0
- package/lib/channels/dingtalk/stomp.js.map +1 -0
- package/lib/channels/dingtalk/stream.d.ts +53 -0
- package/lib/channels/dingtalk/stream.d.ts.map +1 -0
- package/lib/channels/dingtalk/stream.js +199 -0
- package/lib/channels/dingtalk/stream.js.map +1 -0
- package/lib/channels/dingtalk/webhook.d.ts +84 -0
- package/lib/channels/dingtalk/webhook.d.ts.map +1 -0
- package/lib/channels/dingtalk/webhook.js +143 -0
- package/lib/channels/dingtalk/webhook.js.map +1 -0
- package/lib/channels/feishu/adapter.d.ts +133 -0
- package/lib/channels/feishu/adapter.d.ts.map +1 -0
- package/lib/channels/feishu/adapter.js +662 -0
- package/lib/channels/feishu/adapter.js.map +1 -0
- package/lib/channels/feishu/i18n.d.ts +30 -0
- package/lib/channels/feishu/i18n.d.ts.map +1 -0
- package/lib/channels/feishu/i18n.js +42 -0
- package/lib/channels/feishu/i18n.js.map +1 -0
- package/lib/channels/feishu/index.d.ts +82 -0
- package/lib/channels/feishu/index.d.ts.map +1 -0
- package/lib/channels/feishu/index.js +103 -0
- package/lib/channels/feishu/index.js.map +1 -0
- package/lib/channels/feishu/onboard.d.ts +16 -0
- package/lib/channels/feishu/onboard.d.ts.map +1 -0
- package/lib/channels/feishu/onboard.js +86 -0
- package/lib/channels/feishu/onboard.js.map +1 -0
- package/lib/channels/telegram/adapter.d.ts +70 -0
- package/lib/channels/telegram/adapter.d.ts.map +1 -0
- package/lib/channels/telegram/adapter.js +460 -0
- package/lib/channels/telegram/adapter.js.map +1 -0
- package/lib/channels/telegram/client.d.ts +115 -0
- package/lib/channels/telegram/client.d.ts.map +1 -0
- package/lib/channels/telegram/client.js +198 -0
- package/lib/channels/telegram/client.js.map +1 -0
- package/lib/channels/telegram/i18n.d.ts +14 -0
- package/lib/channels/telegram/i18n.d.ts.map +1 -0
- package/lib/channels/telegram/i18n.js +18 -0
- package/lib/channels/telegram/i18n.js.map +1 -0
- package/lib/channels/telegram/index.d.ts +46 -0
- package/lib/channels/telegram/index.d.ts.map +1 -0
- package/lib/channels/telegram/index.js +54 -0
- package/lib/channels/telegram/index.js.map +1 -0
- package/lib/channels/web/adapter.d.ts +103 -0
- package/lib/channels/web/adapter.d.ts.map +1 -0
- package/lib/channels/web/adapter.js +161 -0
- package/lib/channels/web/adapter.js.map +1 -0
- package/lib/channels/web/index.d.ts +47 -0
- package/lib/channels/web/index.d.ts.map +1 -0
- package/lib/channels/web/index.js +59 -0
- package/lib/channels/web/index.js.map +1 -0
- package/lib/chat-key.d.ts +29 -0
- package/lib/chat-key.d.ts.map +1 -0
- package/lib/chat-key.js +38 -0
- package/lib/chat-key.js.map +1 -0
- package/lib/index.d.ts +78 -11
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +312 -9
- package/lib/index.js.map +1 -1
- package/lib/runner.d.ts +55 -2
- package/lib/runner.d.ts.map +1 -1
- package/lib/runner.js +158 -23
- package/lib/runner.js.map +1 -1
- package/lib/scheduler.d.ts.map +1 -1
- package/lib/scheduler.js +2 -2
- package/lib/scheduler.js.map +1 -1
- package/lib/service.d.ts +17 -1
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +47 -3
- package/lib/service.js.map +1 -1
- package/lib/settings/channel-runtime.d.ts +75 -0
- package/lib/settings/channel-runtime.d.ts.map +1 -0
- package/lib/settings/channel-runtime.js +165 -0
- package/lib/settings/channel-runtime.js.map +1 -0
- package/lib/settings/channels.d.ts +62 -0
- package/lib/settings/channels.d.ts.map +1 -0
- package/lib/settings/channels.js +132 -0
- package/lib/settings/channels.js.map +1 -0
- package/lib/settings/credential-store.d.ts +85 -0
- package/lib/settings/credential-store.d.ts.map +1 -0
- package/lib/settings/credential-store.js +142 -0
- package/lib/settings/credential-store.js.map +1 -0
- package/lib/settings/index.d.ts +14 -0
- package/lib/settings/index.d.ts.map +1 -0
- package/lib/settings/index.js +14 -0
- package/lib/settings/index.js.map +1 -0
- package/lib/settings/namespace.d.ts +175 -0
- package/lib/settings/namespace.d.ts.map +1 -0
- package/lib/settings/namespace.js +200 -0
- package/lib/settings/namespace.js.map +1 -0
- package/lib/settings/rpc-client.d.ts +36 -0
- package/lib/settings/rpc-client.d.ts.map +1 -0
- package/lib/settings/rpc-client.js +45 -0
- package/lib/settings/rpc-client.js.map +1 -0
- package/lib/settings/secret-disclosure.d.ts +50 -0
- package/lib/settings/secret-disclosure.d.ts.map +1 -0
- package/lib/settings/secret-disclosure.js +132 -0
- package/lib/settings/secret-disclosure.js.map +1 -0
- package/lib/settings/settings-model.d.ts +124 -0
- package/lib/settings/settings-model.d.ts.map +1 -0
- package/lib/settings/settings-model.js +118 -0
- package/lib/settings/settings-model.js.map +1 -0
- package/lib/settings/settings-rpc.d.ts +130 -0
- package/lib/settings/settings-rpc.d.ts.map +1 -0
- package/lib/settings/settings-rpc.js +233 -0
- package/lib/settings/settings-rpc.js.map +1 -0
- package/lib/settings/settings-service.d.ts +35 -0
- package/lib/settings/settings-service.d.ts.map +1 -0
- package/lib/settings/settings-service.js +171 -0
- package/lib/settings/settings-service.js.map +1 -0
- package/lib/state-dir.d.ts +28 -0
- package/lib/state-dir.d.ts.map +1 -0
- package/lib/state-dir.js +33 -0
- package/lib/state-dir.js.map +1 -0
- package/lib/stream.d.ts.map +1 -1
- package/lib/stream.js +3 -2
- package/lib/stream.js.map +1 -1
- package/package.json +56 -12
package/client/client.js
ADDED
|
@@ -0,0 +1,817 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "dsh-connect",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
"use strict";
|
|
7
|
+
var __create = Object.create;
|
|
8
|
+
var __defProp = Object.defineProperty;
|
|
9
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
10
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
11
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
12
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
13
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
14
|
+
var __export = (target, all) => {
|
|
15
|
+
for (var name2 in all)
|
|
16
|
+
__defProp(target, name2, { get: all[name2], enumerable: true });
|
|
17
|
+
};
|
|
18
|
+
var __copyProps = (to, from, except, desc) => {
|
|
19
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
20
|
+
for (let key of __getOwnPropNames(from))
|
|
21
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
22
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
23
|
+
}
|
|
24
|
+
return to;
|
|
25
|
+
};
|
|
26
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
27
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
28
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
29
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
30
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
31
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
32
|
+
mod
|
|
33
|
+
));
|
|
34
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
35
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
36
|
+
|
|
37
|
+
// client/settings-client.mjs
|
|
38
|
+
var settings_client_exports = {};
|
|
39
|
+
__export(settings_client_exports, {
|
|
40
|
+
ConnectSettingsTab: () => ConnectSettingsTab,
|
|
41
|
+
NS: () => NS,
|
|
42
|
+
apply: () => apply,
|
|
43
|
+
inject: () => inject,
|
|
44
|
+
locale: () => locale,
|
|
45
|
+
name: () => name
|
|
46
|
+
});
|
|
47
|
+
module.exports = __toCommonJS(settings_client_exports);
|
|
48
|
+
var React = __toESM(require("react"), 1);
|
|
49
|
+
|
|
50
|
+
// lib/settings/settings-rpc.js
|
|
51
|
+
var SETTINGS_RPC_CHANNEL = "/dsh-connect";
|
|
52
|
+
var SETTINGS_ENDPOINTS = Object.freeze(["settings.get", "settings.save", "credentials.save", "settings.status"]);
|
|
53
|
+
var MAX_BODY_BYTES = 1024 * 1024;
|
|
54
|
+
|
|
55
|
+
// lib/settings/rpc-client.js
|
|
56
|
+
var RpcError = class extends Error {
|
|
57
|
+
constructor(code, message) {
|
|
58
|
+
super(message);
|
|
59
|
+
__publicField(this, "code");
|
|
60
|
+
this.name = "RpcError";
|
|
61
|
+
this.code = code;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
function callRpc(rpcCall, endpoint, payload, signal) {
|
|
65
|
+
return rpcCall(endpoint, payload, signal).then((res) => {
|
|
66
|
+
if (res.ok)
|
|
67
|
+
return res.value;
|
|
68
|
+
const code = res.error?.code ?? "settings-failed";
|
|
69
|
+
throw new RpcError(code, res.error?.message ?? code);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
function loadSettings(rpcCall) {
|
|
73
|
+
return callRpc(rpcCall, "settings.get", {});
|
|
74
|
+
}
|
|
75
|
+
function saveSettings(rpcCall, config) {
|
|
76
|
+
return callRpc(rpcCall, "settings.save", config);
|
|
77
|
+
}
|
|
78
|
+
function saveCredentials(rpcCall, channel, values) {
|
|
79
|
+
return callRpc(rpcCall, "credentials.save", { channel, values });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// lib/settings/channels.js
|
|
83
|
+
var CHANNELS = ["feishu", "telegram", "dingtalk", "web"];
|
|
84
|
+
|
|
85
|
+
// lib/settings/credential-store.js
|
|
86
|
+
var CHANNEL_SECRET_KEYS = Object.freeze({
|
|
87
|
+
feishu: { appId: "DSH_CONNECT_FEISHU_APP_ID", appSecret: "DSH_CONNECT_FEISHU_APP_SECRET" },
|
|
88
|
+
telegram: { botToken: "DSH_CONNECT_TELEGRAM_BOT_TOKEN" },
|
|
89
|
+
dingtalk: {
|
|
90
|
+
webhookUrl: "DSH_CONNECT_DINGTALK_WEBHOOK_URL",
|
|
91
|
+
secret: "DSH_CONNECT_DINGTALK_SECRET",
|
|
92
|
+
clientId: "DSH_CONNECT_DINGTALK_CLIENT_ID",
|
|
93
|
+
clientSecret: "DSH_CONNECT_DINGTALK_CLIENT_SECRET"
|
|
94
|
+
},
|
|
95
|
+
web: {}
|
|
96
|
+
});
|
|
97
|
+
var CREDENTIAL_GROUPS = Object.freeze({
|
|
98
|
+
feishu: [["DSH_CONNECT_FEISHU_APP_ID", "DSH_CONNECT_FEISHU_APP_SECRET"]],
|
|
99
|
+
telegram: [["DSH_CONNECT_TELEGRAM_BOT_TOKEN"]],
|
|
100
|
+
dingtalk: [
|
|
101
|
+
["DSH_CONNECT_DINGTALK_WEBHOOK_URL", "DSH_CONNECT_DINGTALK_SECRET"],
|
|
102
|
+
["DSH_CONNECT_DINGTALK_CLIENT_ID", "DSH_CONNECT_DINGTALK_CLIENT_SECRET"]
|
|
103
|
+
],
|
|
104
|
+
web: []
|
|
105
|
+
});
|
|
106
|
+
var CREDENTIAL_REFS = Object.freeze(Object.fromEntries(CHANNELS.map((channel) => [channel, groupRefs(channel)])));
|
|
107
|
+
function groupRefs(channel) {
|
|
108
|
+
const seen = /* @__PURE__ */ new Set();
|
|
109
|
+
for (const group of CREDENTIAL_GROUPS[channel] ?? []) {
|
|
110
|
+
for (const ref of group)
|
|
111
|
+
seen.add(ref);
|
|
112
|
+
}
|
|
113
|
+
return Object.freeze([...seen]);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// lib/settings/settings-model.js
|
|
117
|
+
var CHANNEL_SECRET_FIELDS = Object.fromEntries(Object.entries(CHANNEL_SECRET_KEYS).map(([ch, map]) => [ch, Object.keys(map ?? {})]));
|
|
118
|
+
var CHANNEL_CONFIG_FIELDS = {
|
|
119
|
+
feishu: [
|
|
120
|
+
{ key: "transport", kind: "select", options: ["websocket", "webhook"], label: "transport" },
|
|
121
|
+
{ key: "requireMention", kind: "boolean", label: "requireMention" },
|
|
122
|
+
{ key: "dmMode", kind: "select", options: ["open", "allowlist", "pair", "disabled"], label: "dmMode" },
|
|
123
|
+
{ key: "language", kind: "select", options: ["zh", "en"], label: "language" },
|
|
124
|
+
{ key: "webhookPort", kind: "number", label: "webhookPort" },
|
|
125
|
+
{ key: "webhookPath", kind: "text", label: "webhookPath" }
|
|
126
|
+
],
|
|
127
|
+
telegram: [
|
|
128
|
+
{ key: "requireMention", kind: "boolean", label: "requireMention" },
|
|
129
|
+
{ key: "language", kind: "select", options: ["zh", "en"], label: "language" },
|
|
130
|
+
{ key: "pollingTimeoutSeconds", kind: "number", label: "pollingTimeoutSeconds" },
|
|
131
|
+
{ key: "baseUrl", kind: "text", label: "baseUrl" }
|
|
132
|
+
],
|
|
133
|
+
dingtalk: [
|
|
134
|
+
{ key: "language", kind: "select", options: ["zh", "en"], label: "language" },
|
|
135
|
+
{ key: "defaultAt", kind: "text", label: "defaultAt" }
|
|
136
|
+
],
|
|
137
|
+
web: [
|
|
138
|
+
{ key: "pollIntervalMs", kind: "number", label: "pollIntervalMs" }
|
|
139
|
+
]
|
|
140
|
+
};
|
|
141
|
+
var CHANNEL_DEFAULT_FIELDS = [
|
|
142
|
+
{ key: "language", kind: "select", options: ["zh", "en"], label: "language" },
|
|
143
|
+
{ key: "notifyLevel", kind: "select", options: ["full", "important", "result"], label: "notifyLevel" }
|
|
144
|
+
];
|
|
145
|
+
function coerceConfigValue(kind, raw) {
|
|
146
|
+
if (raw === void 0 || raw === null || raw === "")
|
|
147
|
+
return void 0;
|
|
148
|
+
switch (kind) {
|
|
149
|
+
case "number": {
|
|
150
|
+
const n = Number(raw);
|
|
151
|
+
return Number.isFinite(n) ? n : void 0;
|
|
152
|
+
}
|
|
153
|
+
case "boolean":
|
|
154
|
+
return typeof raw === "boolean" ? raw : raw === true || raw === "true";
|
|
155
|
+
case "select":
|
|
156
|
+
return String(raw);
|
|
157
|
+
default:
|
|
158
|
+
return String(raw);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function snapshotToForm(snapshot) {
|
|
162
|
+
const config = snapshot.config ?? {};
|
|
163
|
+
const channels = snapshot.enabled ?? [];
|
|
164
|
+
const channelConfigs = {};
|
|
165
|
+
for (const ch of channels)
|
|
166
|
+
channelConfigs[ch] = config[ch] ?? {};
|
|
167
|
+
return {
|
|
168
|
+
channels,
|
|
169
|
+
channelDefaults: config.channelDefaults ?? {},
|
|
170
|
+
channelConfigs,
|
|
171
|
+
// Deliberately empty even though `secretPreviews` carries something: an
|
|
172
|
+
// input is a place to *type a new* secret, and prefilling it with the mask
|
|
173
|
+
// would either overwrite the stored secret with its own preview on save, or
|
|
174
|
+
// train the user to think a masked value is the real one. The preview is
|
|
175
|
+
// rendered beside the input instead.
|
|
176
|
+
secrets: {},
|
|
177
|
+
secretPresence: snapshot.secrets ?? {},
|
|
178
|
+
secretPreviews: snapshot.secretPreviews ?? {},
|
|
179
|
+
settingsStatePath: config.settingsStatePath,
|
|
180
|
+
live: snapshot.live === true
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function stripEmpty(obj) {
|
|
184
|
+
const out = {};
|
|
185
|
+
for (const [k, v] of Object.entries(obj))
|
|
186
|
+
if (v !== void 0 && v !== null)
|
|
187
|
+
out[k] = v;
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
function buildConfigSave(form) {
|
|
191
|
+
const config = { channels: form.channels };
|
|
192
|
+
const defaults = stripEmpty(form.channelDefaults ?? {});
|
|
193
|
+
if (Object.keys(defaults).length > 0)
|
|
194
|
+
config.channelDefaults = defaults;
|
|
195
|
+
for (const [ch, cfg] of Object.entries(form.channelConfigs ?? {})) {
|
|
196
|
+
const clean = stripEmpty(cfg ?? {});
|
|
197
|
+
if (Object.keys(clean).length > 0)
|
|
198
|
+
config[ch] = clean;
|
|
199
|
+
}
|
|
200
|
+
if (form.settingsStatePath)
|
|
201
|
+
config.settingsStatePath = form.settingsStatePath;
|
|
202
|
+
return config;
|
|
203
|
+
}
|
|
204
|
+
function buildCredentialSaves(form) {
|
|
205
|
+
const out = [];
|
|
206
|
+
for (const [ch, values] of Object.entries(form.secrets ?? {})) {
|
|
207
|
+
if (values && Object.keys(values).length > 0)
|
|
208
|
+
out.push({ channel: ch, values });
|
|
209
|
+
}
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// lib/settings/secret-disclosure.js
|
|
214
|
+
var SECRET_DISCLOSURE = Object.freeze({
|
|
215
|
+
appId: "full",
|
|
216
|
+
clientId: "full",
|
|
217
|
+
appSecret: "mask",
|
|
218
|
+
clientSecret: "mask",
|
|
219
|
+
botToken: "mask",
|
|
220
|
+
secret: "mask",
|
|
221
|
+
webhookUrl: "url"
|
|
222
|
+
});
|
|
223
|
+
function disclosureOf(configKey) {
|
|
224
|
+
return SECRET_DISCLOSURE[configKey] ?? "mask";
|
|
225
|
+
}
|
|
226
|
+
function isMaskedSecret(configKey) {
|
|
227
|
+
return disclosureOf(configKey) !== "full";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// client/locale.mjs
|
|
231
|
+
var LOCALES = {
|
|
232
|
+
zh: {
|
|
233
|
+
title: "dsh-connect",
|
|
234
|
+
channels: "\u6E20\u9053",
|
|
235
|
+
defaults: "\u516C\u5171\u9ED8\u8BA4",
|
|
236
|
+
defaultsHint: "\u672A\u5355\u72EC\u914D\u7F6E\u7684\u6E20\u9053\u6CBF\u7528\u8FD9\u91CC\u7684\u53D6\u503C\uFF1B\u7559\u7A7A\u8868\u793A\u6CBF\u7528\u5404\u6E20\u9053\u81EA\u8EAB\u7684\u9ED8\u8BA4\u503C\u3002",
|
|
237
|
+
save: "\u4FDD\u5B58",
|
|
238
|
+
saved: "\u5DF2\u4FDD\u5B58",
|
|
239
|
+
error: "\u4FDD\u5B58\u5931\u8D25",
|
|
240
|
+
loading: "\u52A0\u8F7D\u4E2D\u2026",
|
|
241
|
+
statePath: "\u8BBE\u7F6E\u6587\u4EF6",
|
|
242
|
+
statePathHint: "\u6CA1\u6709 settings.yaml \u547D\u540D\u7A7A\u95F4\u65F6\uFF0C\u914D\u7F6E\u56DE\u9000\u5B58\u653E\u5728\u8FD9\u4E2A JSON \u6587\u4EF6\u91CC\u3002",
|
|
243
|
+
livePlane: "\u914D\u7F6E\u5B58\u653E\u5728 settings.yaml\uFF0C\u4FDD\u5B58\u540E\u7ACB\u5373\u751F\u6548\u3002",
|
|
244
|
+
filePlane: "\u914D\u7F6E\u5B58\u653E\u5728\u672C\u5730\u8BBE\u7F6E\u6587\u4EF6\uFF0C\u91CD\u542F dsh \u540E\u751F\u6548\u3002",
|
|
245
|
+
reachable: "\u5DF2\u914D\u7F6E\u51ED\u636E",
|
|
246
|
+
unreachable: "\u672A\u914D\u7F6E\u51ED\u636E",
|
|
247
|
+
configured: "\u5DF2\u914D\u7F6E\uFF0C\u91CD\u65B0\u586B\u5199\u53EF\u8986\u76D6",
|
|
248
|
+
current: "\u5F53\u524D\u503C\uFF1A",
|
|
249
|
+
notConfigured: "\u672A\u914D\u7F6E",
|
|
250
|
+
previewNote: "\u4E3A\u4FBF\u4E8E\u786E\u8BA4\uFF0C\u6B64\u5904\u53EA\u663E\u793A\u8131\u654F\u540E\u7684\u90E8\u5206\u5B57\u7B26\uFF1B\u8F93\u5165\u6846\u7559\u7A7A\u8868\u793A\u4E0D\u4FEE\u6539\u3002",
|
|
251
|
+
secrets: "\u51ED\u636E\u4E0E\u5BC6\u94A5",
|
|
252
|
+
expand: "\u5C55\u5F00",
|
|
253
|
+
collapse: "\u6536\u8D77",
|
|
254
|
+
advanced: "\u9AD8\u7EA7\u9009\u9879",
|
|
255
|
+
tabsAria: "\u6E20\u9053\u5207\u6362",
|
|
256
|
+
"status.loading": "\u52A0\u8F7D\u4E2D\u2026",
|
|
257
|
+
"status.idle": "\u5C31\u7EEA",
|
|
258
|
+
"status.saving": "\u4FDD\u5B58\u4E2D\u2026",
|
|
259
|
+
"status.saved": "\u5DF2\u4FDD\u5B58",
|
|
260
|
+
"status.error": "\u4FDD\u5B58\u5931\u8D25",
|
|
261
|
+
"channel.feishu": "\u98DE\u4E66 / Lark",
|
|
262
|
+
"channel.feishu.hint": "\u5728\u98DE\u4E66\u5F00\u653E\u5E73\u53F0\u521B\u5EFA\u81EA\u5EFA\u5E94\u7528\uFF0C\u7528\u957F\u8FDE\u63A5\u63A5\u6536\u6D88\u606F\u3002",
|
|
263
|
+
"channel.telegram": "Telegram",
|
|
264
|
+
"channel.telegram.hint": "\u901A\u8FC7 Telegram Bot API \u957F\u8F6E\u8BE2\u63A5\u6536\u6D88\u606F\uFF0C\u673A\u5668\u4EBA\u5411 @BotFather \u7533\u8BF7\u3002",
|
|
265
|
+
"channel.dingtalk": "\u9489\u9489",
|
|
266
|
+
"channel.dingtalk.hint": "\u652F\u6301 Webhook \u63A8\u9001\u4E0E Stream \u53CC\u5411\u4E24\u79CD\u6A21\u5F0F\uFF0C\u586B\u5176\u4E2D\u4E00\u7EC4\u5373\u53EF\u3002",
|
|
267
|
+
"channel.web": "\u7F51\u9875",
|
|
268
|
+
"channel.web.hint": "\u76F4\u63A5\u5728 DSH \u7F51\u9875\u754C\u9762\u91CC\u5BF9\u8BDD\uFF0C\u4E0D\u9700\u8981\u4EFB\u4F55\u51ED\u636E\u3002",
|
|
269
|
+
"f.transport": "\u63A5\u5165\u65B9\u5F0F",
|
|
270
|
+
"f.transport.hint": "\u957F\u8FDE\u63A5\u65E0\u9700\u516C\u7F51\u5730\u5740\uFF0C\u63A8\u8350\uFF1B\u56DE\u8C03\u5730\u5740\u9700\u8981 DSH \u6240\u5728\u673A\u5668\u53EF\u4ECE\u516C\u7F51\u8BBF\u95EE\u3002",
|
|
271
|
+
"f.requireMention": "\u4EC5\u54CD\u5E94 @ \u63D0\u53CA",
|
|
272
|
+
"f.requireMention.hint": "\u5F00\u542F\u540E\u7FA4\u804A\u4E2D\u53EA\u6709 @ \u673A\u5668\u4EBA\u624D\u5904\u7406\uFF1B\u5355\u804A\u4E0D\u53D7\u5F71\u54CD\u3002",
|
|
273
|
+
"f.dmMode": "\u5355\u804A\u7B56\u7565",
|
|
274
|
+
"f.dmMode.hint": "\u63A7\u5236\u54EA\u4E9B\u4EBA\u53EF\u4EE5\u76F4\u63A5\u79C1\u804A\u673A\u5668\u4EBA\u3002",
|
|
275
|
+
"f.language": "\u56DE\u590D\u8BED\u8A00",
|
|
276
|
+
"f.language.hint": "\u673A\u5668\u4EBA\u56DE\u590D\u7528\u6237\u65F6\u4F7F\u7528\u7684\u8BED\u8A00\u3002",
|
|
277
|
+
"f.webhookPort": "\u56DE\u8C03\u7AEF\u53E3",
|
|
278
|
+
"f.webhookPort.hint": "\u4EC5\u300C\u56DE\u8C03\u5730\u5740\u300D\u63A5\u5165\u65B9\u5F0F\u4F7F\u7528\uFF0C\u9ED8\u8BA4 3000\u3002",
|
|
279
|
+
"f.webhookPath": "\u56DE\u8C03\u8DEF\u5F84",
|
|
280
|
+
"f.webhookPath.hint": "\u4EC5\u300C\u56DE\u8C03\u5730\u5740\u300D\u63A5\u5165\u65B9\u5F0F\u4F7F\u7528\uFF0C\u4F8B\u5982 /feishu/events\u3002",
|
|
281
|
+
"f.pollingTimeoutSeconds": "\u8F6E\u8BE2\u8D85\u65F6\uFF08\u79D2\uFF09",
|
|
282
|
+
"f.pollingTimeoutSeconds.hint": "\u5355\u6B21\u957F\u8F6E\u8BE2\u7B49\u5F85\u79D2\u6570\uFF0C\u9ED8\u8BA4 30\u3002",
|
|
283
|
+
"f.baseUrl": "\u63A5\u53E3\u5730\u5740",
|
|
284
|
+
"f.baseUrl.hint": "Telegram Bot API \u5730\u5740\uFF0C\u53EA\u6709\u5728\u81EA\u5EFA\u53CD\u5411\u4EE3\u7406\u65F6\u624D\u9700\u8981\u4FEE\u6539\u3002",
|
|
285
|
+
"f.defaultAt": "\u9ED8\u8BA4 @ \u6210\u5458",
|
|
286
|
+
"f.defaultAt.hint": "\u7FA4\u6D88\u606F\u9ED8\u8BA4 @ \u7684\u6210\u5458\uFF0C\u591A\u4E2A\u7528\u9017\u53F7\u5206\u9694\u3002",
|
|
287
|
+
"f.pollIntervalMs": "\u8F6E\u8BE2\u95F4\u9694\uFF08\u6BEB\u79D2\uFF09",
|
|
288
|
+
"f.pollIntervalMs.hint": "\u7F51\u9875\u6E20\u9053\u68C0\u67E5\u65B0\u6D88\u606F\u7684\u95F4\u9694\uFF0C\u9ED8\u8BA4 1000\u3002",
|
|
289
|
+
"f.notifyLevel": "\u901A\u77E5\u7EA7\u522B",
|
|
290
|
+
"f.notifyLevel.hint": "\u63A7\u5236\u673A\u5668\u4EBA\u628A\u591A\u5C11\u8FC7\u7A0B\u4FE1\u606F\u53D1\u5230\u804A\u5929\u91CC\u3002",
|
|
291
|
+
"s.feishu.appId": "App ID",
|
|
292
|
+
"s.feishu.appId.hint": "\u5F00\u653E\u5E73\u53F0\u300C\u51ED\u8BC1\u4E0E\u57FA\u7840\u4FE1\u606F\u300D\u4E2D\u7684 App ID\uFF0C\u975E\u673A\u5BC6\uFF0C\u5B8C\u6574\u663E\u793A\u3002",
|
|
293
|
+
"s.feishu.appSecret": "App Secret",
|
|
294
|
+
"s.feishu.appSecret.hint": "\u4E0E App ID \u914D\u5BF9\u7684\u5E94\u7528\u5BC6\u94A5\uFF0C\u5C5E\u4E8E\u673A\u5BC6\uFF0C\u4EC5\u663E\u793A\u9996\u5C3E\u5404 4 \u4F4D\u3002",
|
|
295
|
+
"s.telegram.botToken": "Bot Token",
|
|
296
|
+
"s.telegram.botToken.hint": "@BotFather \u751F\u6210\u7684\u673A\u5668\u4EBA\u4EE4\u724C\uFF0C\u5F62\u5982 123456:ABC\u2026\uFF0C\u5C5E\u4E8E\u673A\u5BC6\u3002",
|
|
297
|
+
"s.dingtalk.webhookUrl": "Webhook \u5730\u5740",
|
|
298
|
+
"s.dingtalk.webhookUrl.hint": "\u7FA4\u673A\u5668\u4EBA\u7684\u5B8C\u6574 Webhook \u5730\u5740\uFF1B\u9884\u89C8\u4FDD\u7559\u57DF\u540D\u4E0E\u8DEF\u5F84\uFF0C\u53EA\u906E\u853D access_token\u3002",
|
|
299
|
+
"s.dingtalk.secret": "\u52A0\u7B7E\u5BC6\u94A5",
|
|
300
|
+
"s.dingtalk.secret.hint": "\u7FA4\u673A\u5668\u4EBA\u5F00\u542F\u300C\u52A0\u7B7E\u300D\u5B89\u5168\u8BBE\u7F6E\u540E\u624D\u6709\uFF0C\u5C5E\u4E8E\u673A\u5BC6\u3002",
|
|
301
|
+
"s.dingtalk.clientId": "Client ID",
|
|
302
|
+
"s.dingtalk.clientId.hint": "Stream \u6A21\u5F0F\u5E94\u7528\u7684 AppKey\uFF0C\u975E\u673A\u5BC6\uFF0C\u5B8C\u6574\u663E\u793A\u3002",
|
|
303
|
+
"s.dingtalk.clientSecret": "Client Secret",
|
|
304
|
+
"s.dingtalk.clientSecret.hint": "Stream \u6A21\u5F0F\u5E94\u7528\u7684 AppSecret\uFF0C\u5C5E\u4E8E\u673A\u5BC6\uFF0C\u4EC5\u663E\u793A\u9996\u5C3E\u5404 4 \u4F4D\u3002",
|
|
305
|
+
"o.default": "\uFF08\u6CBF\u7528\u9ED8\u8BA4\uFF09",
|
|
306
|
+
"o.websocket": "\u957F\u8FDE\u63A5\uFF08\u63A8\u8350\uFF09",
|
|
307
|
+
"o.webhook": "\u56DE\u8C03\u5730\u5740",
|
|
308
|
+
"o.open": "\u6240\u6709\u4EBA\u90FD\u53EF\u4EE5",
|
|
309
|
+
"o.allowlist": "\u4EC5\u540D\u5355\u5185\u7684\u4EBA",
|
|
310
|
+
"o.pair": "\u9700\u8981\u5148\u914D\u5BF9",
|
|
311
|
+
"o.disabled": "\u5173\u95ED\u5355\u804A",
|
|
312
|
+
"o.zh": "\u4E2D\u6587",
|
|
313
|
+
"o.en": "\u82F1\u6587",
|
|
314
|
+
"o.full": "\u5168\u90E8\u8FC7\u7A0B",
|
|
315
|
+
"o.important": "\u5173\u952E\u8282\u70B9",
|
|
316
|
+
"o.result": "\u4EC5\u6700\u7EC8\u7ED3\u679C"
|
|
317
|
+
},
|
|
318
|
+
en: {
|
|
319
|
+
title: "dsh-connect",
|
|
320
|
+
channels: "Channels",
|
|
321
|
+
defaults: "Defaults",
|
|
322
|
+
defaultsHint: "Channels without their own value fall back to these; leave a field empty to use each channel\u2019s built-in default.",
|
|
323
|
+
save: "Save",
|
|
324
|
+
saved: "Saved",
|
|
325
|
+
error: "Save failed",
|
|
326
|
+
loading: "Loading\u2026",
|
|
327
|
+
statePath: "Settings file",
|
|
328
|
+
statePathHint: "Where the config falls back to when no settings.yaml namespace is live.",
|
|
329
|
+
livePlane: "Stored in settings.yaml \u2014 a save takes effect immediately.",
|
|
330
|
+
filePlane: "Stored in a local settings file \u2014 a save applies after dsh restarts.",
|
|
331
|
+
reachable: "Credentials set",
|
|
332
|
+
unreachable: "Credentials missing",
|
|
333
|
+
configured: "Configured \u2014 type to replace",
|
|
334
|
+
current: "Current value: ",
|
|
335
|
+
notConfigured: "Not configured",
|
|
336
|
+
previewNote: "Shown masked, so you can confirm the stored value without exposing it. Leave the input empty to keep it.",
|
|
337
|
+
secrets: "Credentials & secrets",
|
|
338
|
+
expand: "Expand",
|
|
339
|
+
collapse: "Collapse",
|
|
340
|
+
advanced: "Advanced",
|
|
341
|
+
tabsAria: "Channel switcher",
|
|
342
|
+
"status.loading": "Loading\u2026",
|
|
343
|
+
"status.idle": "Ready",
|
|
344
|
+
"status.saving": "Saving\u2026",
|
|
345
|
+
"status.saved": "Saved",
|
|
346
|
+
"status.error": "Save failed",
|
|
347
|
+
"channel.feishu": "Feishu / Lark",
|
|
348
|
+
"channel.feishu.hint": "Create a custom app on the Feishu open platform and receive messages over a long connection.",
|
|
349
|
+
"channel.telegram": "Telegram",
|
|
350
|
+
"channel.telegram.hint": "Receive messages by long-polling the Telegram Bot API; get a bot from @BotFather.",
|
|
351
|
+
"channel.dingtalk": "DingTalk",
|
|
352
|
+
"channel.dingtalk.hint": "Supports both webhook push and stream mode \u2014 fill in either set.",
|
|
353
|
+
"channel.web": "Web",
|
|
354
|
+
"channel.web.hint": "Chat straight from the DSH web UI; needs no credentials at all.",
|
|
355
|
+
"f.transport": "Transport",
|
|
356
|
+
"f.transport.hint": "A long connection needs no public address and is recommended; a callback URL requires this machine to be reachable from the internet.",
|
|
357
|
+
"f.requireMention": "Only when @-mentioned",
|
|
358
|
+
"f.requireMention.hint": "In group chats, handle a message only if the bot was mentioned. Direct messages are unaffected.",
|
|
359
|
+
"f.dmMode": "Direct messages",
|
|
360
|
+
"f.dmMode.hint": "Who is allowed to message the bot directly.",
|
|
361
|
+
"f.language": "Reply language",
|
|
362
|
+
"f.language.hint": "The language the bot replies to users in.",
|
|
363
|
+
"f.webhookPort": "Callback port",
|
|
364
|
+
"f.webhookPort.hint": "Used only by the callback-URL transport; defaults to 3000.",
|
|
365
|
+
"f.webhookPath": "Callback path",
|
|
366
|
+
"f.webhookPath.hint": "Used only by the callback-URL transport, e.g. /feishu/events.",
|
|
367
|
+
"f.pollingTimeoutSeconds": "Poll timeout (s)",
|
|
368
|
+
"f.pollingTimeoutSeconds.hint": "How long one long poll waits; defaults to 30.",
|
|
369
|
+
"f.baseUrl": "API base URL",
|
|
370
|
+
"f.baseUrl.hint": "The Telegram Bot API endpoint \u2014 only change it if you run your own proxy.",
|
|
371
|
+
"f.defaultAt": "Default @-list",
|
|
372
|
+
"f.defaultAt.hint": "Members to @ by default on group messages, comma-separated.",
|
|
373
|
+
"f.pollIntervalMs": "Poll interval (ms)",
|
|
374
|
+
"f.pollIntervalMs.hint": "How often the web channel checks for new messages; defaults to 1000.",
|
|
375
|
+
"f.notifyLevel": "Notify level",
|
|
376
|
+
"f.notifyLevel.hint": "How much of the working process the bot posts into the chat.",
|
|
377
|
+
"s.feishu.appId": "App ID",
|
|
378
|
+
"s.feishu.appId.hint": "The App ID from the open platform\u2019s credentials page. Not a secret \u2014 shown in full.",
|
|
379
|
+
"s.feishu.appSecret": "App Secret",
|
|
380
|
+
"s.feishu.appSecret.hint": "The app secret paired with the App ID. Confidential \u2014 only the first and last 4 characters are shown.",
|
|
381
|
+
"s.telegram.botToken": "Bot Token",
|
|
382
|
+
"s.telegram.botToken.hint": "The bot token from @BotFather, like 123456:ABC\u2026 \u2014 confidential.",
|
|
383
|
+
"s.dingtalk.webhookUrl": "Webhook URL",
|
|
384
|
+
"s.dingtalk.webhookUrl.hint": "The group robot\u2019s full webhook URL; the preview keeps the host and path and masks only the access_token.",
|
|
385
|
+
"s.dingtalk.secret": "Signing secret",
|
|
386
|
+
"s.dingtalk.secret.hint": "Only present once the group robot has the \u201Csign\u201D security setting enabled \u2014 confidential.",
|
|
387
|
+
"s.dingtalk.clientId": "Client ID",
|
|
388
|
+
"s.dingtalk.clientId.hint": "The AppKey of the stream-mode app. Not a secret \u2014 shown in full.",
|
|
389
|
+
"s.dingtalk.clientSecret": "Client Secret",
|
|
390
|
+
"s.dingtalk.clientSecret.hint": "The AppSecret of the stream-mode app. Confidential \u2014 only the first and last 4 characters are shown.",
|
|
391
|
+
"o.default": "(use default)",
|
|
392
|
+
"o.websocket": "Long connection (recommended)",
|
|
393
|
+
"o.webhook": "Callback URL",
|
|
394
|
+
"o.open": "Anyone",
|
|
395
|
+
"o.allowlist": "Allowlisted users only",
|
|
396
|
+
"o.pair": "Pairing required",
|
|
397
|
+
"o.disabled": "Direct messages off",
|
|
398
|
+
"o.zh": "Chinese",
|
|
399
|
+
"o.en": "English",
|
|
400
|
+
"o.full": "Everything",
|
|
401
|
+
"o.important": "Key milestones",
|
|
402
|
+
"o.result": "Final result only"
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
var LOCALE_KEYS = Object.keys(LOCALES.zh);
|
|
406
|
+
function hasLocale(key) {
|
|
407
|
+
return Object.prototype.hasOwnProperty.call(LOCALES.zh, key) && Object.prototype.hasOwnProperty.call(LOCALES.en, key);
|
|
408
|
+
}
|
|
409
|
+
function tr(t, key, fallback) {
|
|
410
|
+
return hasLocale(key) ? t(key) : fallback;
|
|
411
|
+
}
|
|
412
|
+
function optionalText(t, key) {
|
|
413
|
+
return hasLocale(key) ? t(key) : void 0;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// client/panel-state.mjs
|
|
417
|
+
var ADVANCED_KEYS = {
|
|
418
|
+
feishu: ["webhookPort", "webhookPath"],
|
|
419
|
+
telegram: ["pollingTimeoutSeconds", "baseUrl"],
|
|
420
|
+
dingtalk: ["defaultAt"],
|
|
421
|
+
web: ["pollIntervalMs"]
|
|
422
|
+
};
|
|
423
|
+
function isAdvanced(channel, key) {
|
|
424
|
+
return (ADVANCED_KEYS[channel] ?? []).includes(key);
|
|
425
|
+
}
|
|
426
|
+
function initialOpenChannels(enabled, all) {
|
|
427
|
+
const open = (enabled ?? []).filter((ch) => all.includes(ch));
|
|
428
|
+
return new Set(open.length > 0 ? open : all.slice(0, 1));
|
|
429
|
+
}
|
|
430
|
+
function toggleInSet(set, key) {
|
|
431
|
+
const next = new Set(set);
|
|
432
|
+
if (next.has(key)) next.delete(key);
|
|
433
|
+
else next.add(key);
|
|
434
|
+
return next;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// client/settings-client.mjs
|
|
438
|
+
var name = "dsh-connect-settings";
|
|
439
|
+
var inject = ["slots", "connection", "locale"];
|
|
440
|
+
var NS = "dsh-connect";
|
|
441
|
+
var locale = LOCALES;
|
|
442
|
+
var h = React.createElement;
|
|
443
|
+
var ALL_CHANNELS = Object.keys(CHANNEL_SECRET_FIELDS);
|
|
444
|
+
var STYLE = `
|
|
445
|
+
.dsh-connect-settings,.dsh-connect-settings *,.dsh-connect-settings *::before,.dsh-connect-settings *::after{box-sizing:border-box}
|
|
446
|
+
.dsh-connect-settings{--ds-bg:var(--dsw-alias-bg-layer-3,#ffffff);--ds-bg-sub:var(--dsw-alias-bg-layer-1,#f6f7f9);--ds-text:var(--dsw-alias-label-primary,#1f2329);--ds-muted:var(--dsw-alias-label-tertiary,#646a73);--ds-border:var(--dsw-alias-border-l2,#e2e4e8);--ds-border-2:var(--dsw-alias-border-l3,#c8cbd0);--ds-accent:var(--dsw-alias-state-business-primary,#3b82f6);--ds-hover:var(--dsw-alias-interactive-bg-hover,#2631480f);display:flex;flex-direction:column;gap:12px;max-width:760px;color:var(--ds-text);font:13px/1.6 -apple-system,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif}
|
|
447
|
+
.dsh-connect-settings .ds-card{display:flex;flex-direction:column;gap:10px;border:1px solid var(--ds-border);border-radius:10px;background:var(--ds-bg);padding:12px 14px}
|
|
448
|
+
.dsh-connect-settings .ds-card-title{margin:0;font-size:13px;font-weight:600}
|
|
449
|
+
.dsh-connect-settings .ds-note{margin:0;font-size:11px;line-height:1.5;color:var(--ds-muted)}
|
|
450
|
+
.dsh-connect-settings .ds-tabs{display:flex;flex-wrap:wrap;gap:6px}
|
|
451
|
+
.dsh-connect-settings .ds-tab{display:inline-flex;align-items:center;gap:6px;height:28px;padding:0 10px;border:1px solid var(--ds-border);border-radius:14px;background:transparent;color:var(--ds-text);font:inherit;font-size:12px;cursor:pointer}
|
|
452
|
+
.dsh-connect-settings .ds-tab:hover{background:var(--ds-hover)}
|
|
453
|
+
.dsh-connect-settings .ds-tab[aria-expanded=true]{border-color:var(--ds-accent);color:var(--ds-accent)}
|
|
454
|
+
.dsh-connect-settings .ds-dot{width:6px;height:6px;border-radius:50%;background:var(--ds-border-2);flex:none}
|
|
455
|
+
.dsh-connect-settings .ds-dot[data-on="1"]{background:var(--ds-accent)}
|
|
456
|
+
.dsh-connect-settings .ds-channel{border:1px solid var(--ds-border);border-radius:8px;background:var(--ds-bg-sub)}
|
|
457
|
+
.dsh-connect-settings .ds-channel-head{display:flex;align-items:center;gap:8px;padding:5px 10px}
|
|
458
|
+
.dsh-connect-settings .ds-enable{display:flex;align-items:center;flex:none}
|
|
459
|
+
.dsh-connect-settings .ds-channel-toggle{flex:1;min-width:0;display:flex;align-items:center;gap:8px;padding:5px 6px;border:0;border-radius:6px;background:transparent;color:inherit;font:inherit;font-size:12px;font-weight:600;text-align:left;cursor:pointer}
|
|
460
|
+
.dsh-connect-settings .ds-channel-toggle:hover{background:var(--ds-hover);color:var(--ds-accent)}
|
|
461
|
+
.dsh-connect-settings .ds-channel-name{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
462
|
+
.dsh-connect-settings .ds-channel-body{display:flex;flex-direction:column;gap:8px;padding:0 10px 10px}
|
|
463
|
+
.dsh-connect-settings .ds-chevron{width:7px;height:7px;margin:-3px 4px 0 auto;border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;transform:rotate(45deg);transition:transform .15s ease;flex:none}
|
|
464
|
+
.dsh-connect-settings [aria-expanded=true]>.ds-chevron,.dsh-connect-settings [aria-expanded=true] .ds-chevron{transform:rotate(-135deg);margin-top:2px}
|
|
465
|
+
.dsh-connect-settings .ds-badge{flex:none;font-size:11px;font-weight:500;padding:1px 8px;border-radius:99px;background:var(--ds-bg);color:var(--ds-muted);border:1px solid var(--ds-border-2)}
|
|
466
|
+
.dsh-connect-settings .ds-fields{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:10px 14px}
|
|
467
|
+
.dsh-connect-settings .ds-field{display:flex;flex-direction:column;gap:4px;min-width:0}
|
|
468
|
+
.dsh-connect-settings .ds-control{display:flex;flex-direction:column;gap:4px;font-size:12px;color:var(--ds-muted)}
|
|
469
|
+
.dsh-connect-settings .ds-control.ds-check-field{flex-direction:row;align-items:center;gap:6px}
|
|
470
|
+
.dsh-connect-settings .ds-hint{margin:0;font-size:11px;line-height:1.5;color:var(--ds-muted)}
|
|
471
|
+
.dsh-connect-settings .ds-preview{margin:0;font-size:11px;line-height:1.5;color:var(--ds-muted);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;word-break:break-all}
|
|
472
|
+
.dsh-connect-settings .ds-preview b{font-weight:500;font-family:inherit;opacity:.75}
|
|
473
|
+
.dsh-connect-settings .ds-check{flex:none;width:16px;height:16px;accent-color:var(--ds-accent)}
|
|
474
|
+
.dsh-connect-settings .ds-input{height:30px;width:100%;min-width:0;padding:0 9px;border:1px solid var(--ds-border-2);border-radius:6px;background:var(--ds-bg);color:var(--ds-text);font:inherit}
|
|
475
|
+
.dsh-connect-settings select.ds-input{cursor:pointer}
|
|
476
|
+
.dsh-connect-settings .ds-input:focus{outline:none;border-color:var(--ds-accent);box-shadow:0 0 0 2px color-mix(in srgb,var(--ds-accent) 25%,transparent)}
|
|
477
|
+
.dsh-connect-settings .ds-adv{display:flex;flex-direction:column;gap:8px}
|
|
478
|
+
.dsh-connect-settings .ds-advanced-toggle{align-self:flex-start;display:inline-flex;align-items:center;gap:6px;padding:3px 10px;border:1px dashed var(--ds-border-2);border-radius:99px;background:transparent;color:var(--ds-muted);font:inherit;font-size:11px;cursor:pointer}
|
|
479
|
+
.dsh-connect-settings .ds-advanced-toggle:hover{background:var(--ds-hover);color:var(--ds-text)}
|
|
480
|
+
.dsh-connect-settings .ds-footer{position:sticky;bottom:0;z-index:1;display:flex;align-items:center;gap:12px;margin:0 -14px -12px;padding:10px 14px;border-top:1px solid var(--ds-border);border-radius:0 0 9px 9px;background:var(--ds-bg)}
|
|
481
|
+
.dsh-connect-settings .ds-btn{height:32px;padding:0 18px;border:0;border-radius:6px;background:var(--dsw-alias-button-primary-fill,var(--ds-accent));color:var(--dsw-alias-label-primary-foreground,#ffffff);font:inherit;font-weight:500;cursor:pointer}
|
|
482
|
+
.dsh-connect-settings .ds-btn:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover,var(--ds-accent))}
|
|
483
|
+
.dsh-connect-settings .ds-btn:disabled{opacity:.55;cursor:default}
|
|
484
|
+
.dsh-connect-settings .ds-status{font-size:12px;color:var(--ds-muted)}
|
|
485
|
+
`;
|
|
486
|
+
function injectStyles() {
|
|
487
|
+
if (typeof document === "undefined") return;
|
|
488
|
+
if (document.getElementById("dsh-connect-settings-style")) return;
|
|
489
|
+
const el = document.createElement("style");
|
|
490
|
+
el.id = "dsh-connect-settings-style";
|
|
491
|
+
el.textContent = STYLE;
|
|
492
|
+
document.head.appendChild(el);
|
|
493
|
+
}
|
|
494
|
+
function renderConfigField(field, value, onChange, t) {
|
|
495
|
+
const label = tr(t, `f.${field.key}`, field.label ?? field.key);
|
|
496
|
+
const hint = optionalText(t, `f.${field.key}.hint`);
|
|
497
|
+
let control;
|
|
498
|
+
if (field.kind === "boolean") {
|
|
499
|
+
control = h(
|
|
500
|
+
"label",
|
|
501
|
+
{ className: "ds-control ds-check-field" },
|
|
502
|
+
h("input", { className: "ds-check", type: "checkbox", checked: !!value, onChange: (e) => onChange(e.target.checked) }),
|
|
503
|
+
" " + label
|
|
504
|
+
);
|
|
505
|
+
} else if (field.kind === "select") {
|
|
506
|
+
control = h(
|
|
507
|
+
"label",
|
|
508
|
+
{ className: "ds-control" },
|
|
509
|
+
label + " ",
|
|
510
|
+
h(
|
|
511
|
+
"select",
|
|
512
|
+
{ className: "ds-input", value: value ?? "", onChange: (e) => onChange(e.target.value) },
|
|
513
|
+
h("option", { value: "" }, tr(t, "o.default", label)),
|
|
514
|
+
...(field.options ?? []).map((opt) => h("option", { key: opt, value: opt }, tr(t, `o.${opt}`, opt)))
|
|
515
|
+
)
|
|
516
|
+
);
|
|
517
|
+
} else if (field.kind === "number") {
|
|
518
|
+
control = h(
|
|
519
|
+
"label",
|
|
520
|
+
{ className: "ds-control" },
|
|
521
|
+
label + " ",
|
|
522
|
+
h("input", { className: "ds-input", type: "number", value: value ?? "", onChange: (e) => onChange(e.target.value) })
|
|
523
|
+
);
|
|
524
|
+
} else {
|
|
525
|
+
control = h(
|
|
526
|
+
"label",
|
|
527
|
+
{ className: "ds-control" },
|
|
528
|
+
label + " ",
|
|
529
|
+
h("input", { className: "ds-input", type: "text", value: value ?? "", onChange: (e) => onChange(e.target.value) })
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
return h(
|
|
533
|
+
"div",
|
|
534
|
+
{ className: "ds-field", key: `cfg-${field.key}` },
|
|
535
|
+
control,
|
|
536
|
+
hint ? h("p", { className: "ds-hint" }, hint) : null
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
function renderSecretField(ch, field, form, onChange, t) {
|
|
540
|
+
const preview = form.secretPreviews?.[ch]?.[field] ?? "";
|
|
541
|
+
const hint = optionalText(t, `s.${ch}.${field}.hint`);
|
|
542
|
+
return h(
|
|
543
|
+
"div",
|
|
544
|
+
{ className: "ds-field", key: `sec-${ch}-${field}` },
|
|
545
|
+
h(
|
|
546
|
+
"label",
|
|
547
|
+
{ className: "ds-control" },
|
|
548
|
+
tr(t, `s.${ch}.${field}`, field),
|
|
549
|
+
h("input", {
|
|
550
|
+
className: "ds-input",
|
|
551
|
+
// Confidential keys stay masked while typing; identifiers (appId,
|
|
552
|
+
// clientId) don't, so a typo is visible before it is ever saved.
|
|
553
|
+
type: isMaskedSecret(field) ? "password" : "text",
|
|
554
|
+
autoComplete: "off",
|
|
555
|
+
placeholder: preview ? t("configured") : t("notConfigured"),
|
|
556
|
+
value: form.secrets?.[ch]?.[field] ?? "",
|
|
557
|
+
onChange: (e) => onChange(e.target.value)
|
|
558
|
+
})
|
|
559
|
+
),
|
|
560
|
+
h(
|
|
561
|
+
"p",
|
|
562
|
+
{ className: "ds-preview" },
|
|
563
|
+
h("b", null, t("current")),
|
|
564
|
+
preview === "" ? t("notConfigured") : preview
|
|
565
|
+
),
|
|
566
|
+
hint ? h("p", { className: "ds-hint" }, hint) : null
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
function renderChannel(ch, ctx) {
|
|
570
|
+
const { form, creds, t, open, advOverride, setChannels, setField, setChannelConfig, toggleOpen, toggleAdvanced } = ctx;
|
|
571
|
+
const name2 = tr(t, `channel.${ch}`, ch);
|
|
572
|
+
const channelHint = optionalText(t, `channel.${ch}.hint`);
|
|
573
|
+
const isOpen = open.has(ch);
|
|
574
|
+
const advOpen = advOverride?.has(ch) ?? false;
|
|
575
|
+
const configFields = CHANNEL_CONFIG_FIELDS[ch] ?? [];
|
|
576
|
+
const common = configFields.filter((f) => !isAdvanced(ch, f.key));
|
|
577
|
+
const advanced = configFields.filter((f) => isAdvanced(ch, f.key));
|
|
578
|
+
const configField = (field) => renderConfigField(
|
|
579
|
+
field,
|
|
580
|
+
form.channelConfigs?.[ch]?.[field.key],
|
|
581
|
+
(raw) => setChannelConfig(ch, field.key, raw),
|
|
582
|
+
t
|
|
583
|
+
);
|
|
584
|
+
return h(
|
|
585
|
+
"div",
|
|
586
|
+
{ className: "ds-channel", key: ch, id: `ds-ch-${ch}` },
|
|
587
|
+
// The checkbox sits beside the header button, not inside it: a button may not
|
|
588
|
+
// contain another interactive element, and browsers disagree about what to do
|
|
589
|
+
// when one does.
|
|
590
|
+
h(
|
|
591
|
+
"div",
|
|
592
|
+
{ className: "ds-channel-head" },
|
|
593
|
+
h(
|
|
594
|
+
"label",
|
|
595
|
+
{ className: "ds-enable" },
|
|
596
|
+
h("input", {
|
|
597
|
+
className: "ds-check",
|
|
598
|
+
type: "checkbox",
|
|
599
|
+
"aria-label": name2,
|
|
600
|
+
checked: form.channels.includes(ch),
|
|
601
|
+
onChange: (e) => setChannels(ch, e.target.checked)
|
|
602
|
+
})
|
|
603
|
+
),
|
|
604
|
+
h(
|
|
605
|
+
"button",
|
|
606
|
+
{
|
|
607
|
+
type: "button",
|
|
608
|
+
className: "ds-channel-toggle",
|
|
609
|
+
"aria-expanded": isOpen,
|
|
610
|
+
// Only while the body exists: pointing at an id that is not in the
|
|
611
|
+
// document is worse than not pointing at all.
|
|
612
|
+
"aria-controls": isOpen ? `ds-ch-${ch}-body` : void 0,
|
|
613
|
+
"aria-label": `${name2} \u2014 ${isOpen ? t("collapse") : t("expand")}`,
|
|
614
|
+
onClick: () => toggleOpen(ch)
|
|
615
|
+
},
|
|
616
|
+
h("span", { className: "ds-channel-name" }, name2),
|
|
617
|
+
h("span", { className: "ds-badge" }, creds[ch] ? t("reachable") : t("unreachable")),
|
|
618
|
+
// Drawn in CSS, never a text node: the bundle test reads rendered text as
|
|
619
|
+
// user-visible copy, and a `▾` here would be collected as a stray string.
|
|
620
|
+
h("span", { className: "ds-chevron" })
|
|
621
|
+
)
|
|
622
|
+
),
|
|
623
|
+
// Collapsed means *not rendered*, not `display:none`. The inputs are
|
|
624
|
+
// controlled from the parent's `form.secrets`, so folding a card cannot lose
|
|
625
|
+
// an unsaved secret — keeping it mounted would buy nothing.
|
|
626
|
+
isOpen ? h(
|
|
627
|
+
"div",
|
|
628
|
+
{ className: "ds-channel-body", id: `ds-ch-${ch}-body` },
|
|
629
|
+
channelHint ? h("p", { className: "ds-note" }, channelHint) : null,
|
|
630
|
+
h(
|
|
631
|
+
"div",
|
|
632
|
+
{ className: "ds-fields" },
|
|
633
|
+
...CHANNEL_SECRET_FIELDS[ch].map((field) => renderSecretField(ch, field, form, (value) => setField(ch, field, value), t)),
|
|
634
|
+
...common.map(configField)
|
|
635
|
+
),
|
|
636
|
+
advanced.length === 0 ? null : h(
|
|
637
|
+
"div",
|
|
638
|
+
{ className: "ds-adv" },
|
|
639
|
+
h(
|
|
640
|
+
"button",
|
|
641
|
+
{
|
|
642
|
+
type: "button",
|
|
643
|
+
className: "ds-advanced-toggle",
|
|
644
|
+
"aria-expanded": advOpen,
|
|
645
|
+
"aria-controls": advOpen ? `ds-ch-${ch}-adv` : void 0,
|
|
646
|
+
onClick: () => toggleAdvanced(ch)
|
|
647
|
+
},
|
|
648
|
+
h("span", null, t("advanced")),
|
|
649
|
+
h("span", { className: "ds-chevron" })
|
|
650
|
+
),
|
|
651
|
+
advOpen ? h("div", { className: "ds-fields", id: `ds-ch-${ch}-adv` }, ...advanced.map(configField)) : null
|
|
652
|
+
)
|
|
653
|
+
) : null
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
function ConnectSettingsTab({ rpcCall, t }) {
|
|
657
|
+
const [form, setForm] = React.useState(null);
|
|
658
|
+
const [status, setStatus] = React.useState("loading");
|
|
659
|
+
const [creds, setCreds] = React.useState({});
|
|
660
|
+
const [openOverride, setOpenOverride] = React.useState(null);
|
|
661
|
+
const [advOverride, setAdvOverride] = React.useState(null);
|
|
662
|
+
const rpc = (endpoint, payload) => rpcCall(endpoint, payload);
|
|
663
|
+
React.useEffect(() => {
|
|
664
|
+
let alive = true;
|
|
665
|
+
loadSettings(rpc).then((snap) => {
|
|
666
|
+
if (!alive) return;
|
|
667
|
+
setForm(snapshotToForm(snap));
|
|
668
|
+
setCreds(snap.credentials ?? {});
|
|
669
|
+
setStatus("idle");
|
|
670
|
+
}).catch(() => alive && setStatus("error"));
|
|
671
|
+
return () => {
|
|
672
|
+
alive = false;
|
|
673
|
+
};
|
|
674
|
+
}, [rpcCall]);
|
|
675
|
+
const onSave = async () => {
|
|
676
|
+
if (!form) return;
|
|
677
|
+
setStatus("saving");
|
|
678
|
+
try {
|
|
679
|
+
let snap = await saveSettings(rpc, buildConfigSave(form));
|
|
680
|
+
for (const c of buildCredentialSaves(form)) snap = await saveCredentials(rpc, c.channel, c.values);
|
|
681
|
+
setForm(snapshotToForm(snap));
|
|
682
|
+
setCreds(snap.credentials ?? {});
|
|
683
|
+
setStatus("saved");
|
|
684
|
+
} catch {
|
|
685
|
+
setStatus("error");
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
const setChannels = (ch, on) => {
|
|
689
|
+
setForm((f) => ({ ...f, channels: on ? [...f.channels, ch] : f.channels.filter((x) => x !== ch) }));
|
|
690
|
+
if (on && !open.has(ch)) setOpenOverride(new Set(open).add(ch));
|
|
691
|
+
};
|
|
692
|
+
const setField = (ch, field, value) => setForm((f) => ({ ...f, secrets: { ...f.secrets, [ch]: { ...f.secrets[ch] ?? {}, [field]: value } } }));
|
|
693
|
+
const setChannelConfig = (ch, key, raw) => setForm((f) => {
|
|
694
|
+
const descriptor = (CHANNEL_CONFIG_FIELDS[ch] ?? []).find((x) => x.key === key);
|
|
695
|
+
const value = coerceConfigValue(descriptor?.kind ?? "text", raw);
|
|
696
|
+
const cfg = { ...f.channelConfigs[ch] ?? {} };
|
|
697
|
+
if (value === void 0) delete cfg[key];
|
|
698
|
+
else cfg[key] = value;
|
|
699
|
+
return { ...f, channelConfigs: { ...f.channelConfigs, [ch]: cfg } };
|
|
700
|
+
});
|
|
701
|
+
const setDefault = (key, raw) => setForm((f) => {
|
|
702
|
+
const descriptor = CHANNEL_DEFAULT_FIELDS.find((x) => x.key === key);
|
|
703
|
+
const value = coerceConfigValue(descriptor?.kind ?? "text", raw);
|
|
704
|
+
const defaults = { ...f.channelDefaults ?? {} };
|
|
705
|
+
if (value === void 0) delete defaults[key];
|
|
706
|
+
else defaults[key] = value;
|
|
707
|
+
return { ...f, channelDefaults: defaults };
|
|
708
|
+
});
|
|
709
|
+
if (!form) return h("div", { className: "dsh-connect-settings" }, t("loading"));
|
|
710
|
+
const open = openOverride ?? initialOpenChannels(form.channels, ALL_CHANNELS);
|
|
711
|
+
const toggleOpen = (ch) => setOpenOverride(toggleInSet(open, ch));
|
|
712
|
+
const toggleAdvanced = (ch) => setAdvOverride(toggleInSet(advOverride ?? /* @__PURE__ */ new Set(), ch));
|
|
713
|
+
const focusChannel = (ch) => {
|
|
714
|
+
if (!open.has(ch)) setOpenOverride(new Set(open).add(ch));
|
|
715
|
+
if (typeof document === "undefined" || !document.getElementById) return;
|
|
716
|
+
document.getElementById(`ds-ch-${ch}`)?.scrollIntoView?.({ block: "nearest" });
|
|
717
|
+
};
|
|
718
|
+
return h(
|
|
719
|
+
"div",
|
|
720
|
+
{ className: "dsh-connect-settings" },
|
|
721
|
+
h(
|
|
722
|
+
"section",
|
|
723
|
+
{ className: "ds-card" },
|
|
724
|
+
h("h4", { className: "ds-card-title" }, t("channels")),
|
|
725
|
+
// Explains the masking before the user meets a truncated value and wonders
|
|
726
|
+
// whether their stored secret is corrupt.
|
|
727
|
+
h("p", { className: "ds-note" }, t("previewNote")),
|
|
728
|
+
// A tab strip, but not `role=tablist`: several channels can be open at
|
|
729
|
+
// once, so there is no single "selected" tab to report. These are buttons
|
|
730
|
+
// that open and jump to a channel, and `aria-expanded` says so honestly.
|
|
731
|
+
h(
|
|
732
|
+
"nav",
|
|
733
|
+
{ className: "ds-tabs", "aria-label": t("tabsAria") },
|
|
734
|
+
...ALL_CHANNELS.map((ch) => h(
|
|
735
|
+
"button",
|
|
736
|
+
{
|
|
737
|
+
key: `tab-${ch}`,
|
|
738
|
+
type: "button",
|
|
739
|
+
className: "ds-tab",
|
|
740
|
+
"aria-expanded": open.has(ch),
|
|
741
|
+
"aria-controls": open.has(ch) ? `ds-ch-${ch}-body` : void 0,
|
|
742
|
+
onClick: () => focusChannel(ch)
|
|
743
|
+
},
|
|
744
|
+
h("span", { className: "ds-dot", "data-on": form.channels.includes(ch) ? "1" : "0" }),
|
|
745
|
+
tr(t, `channel.${ch}`, ch)
|
|
746
|
+
))
|
|
747
|
+
),
|
|
748
|
+
...ALL_CHANNELS.map((ch) => renderChannel(ch, {
|
|
749
|
+
form,
|
|
750
|
+
creds,
|
|
751
|
+
t,
|
|
752
|
+
open,
|
|
753
|
+
advOverride,
|
|
754
|
+
setChannels,
|
|
755
|
+
setField,
|
|
756
|
+
setChannelConfig,
|
|
757
|
+
toggleOpen,
|
|
758
|
+
toggleAdvanced
|
|
759
|
+
}))
|
|
760
|
+
),
|
|
761
|
+
h(
|
|
762
|
+
"section",
|
|
763
|
+
{ className: "ds-card" },
|
|
764
|
+
h("h4", { className: "ds-card-title" }, t("defaults")),
|
|
765
|
+
h("p", { className: "ds-note" }, t("defaultsHint")),
|
|
766
|
+
h(
|
|
767
|
+
"div",
|
|
768
|
+
{ className: "ds-fields" },
|
|
769
|
+
...CHANNEL_DEFAULT_FIELDS.map((field) => renderConfigField(field, form.channelDefaults?.[field.key], (raw) => setDefault(field.key, raw), t)),
|
|
770
|
+
// Only meaningful on the fallback plane: it names the file the pane
|
|
771
|
+
// persists to. With the namespace live that path is not consulted (and
|
|
772
|
+
// is not part of the section), so showing an editable field for it would
|
|
773
|
+
// silently swallow edits.
|
|
774
|
+
form.live ? null : h(
|
|
775
|
+
"div",
|
|
776
|
+
{ className: "ds-field" },
|
|
777
|
+
h(
|
|
778
|
+
"label",
|
|
779
|
+
{ className: "ds-control" },
|
|
780
|
+
t("statePath"),
|
|
781
|
+
h("input", { className: "ds-input", value: form.settingsStatePath ?? "", onChange: (e) => setForm((f) => ({ ...f, settingsStatePath: e.target.value })) })
|
|
782
|
+
),
|
|
783
|
+
h("p", { className: "ds-hint" }, t("statePathHint"))
|
|
784
|
+
)
|
|
785
|
+
),
|
|
786
|
+
h("div", { className: "ds-status" }, form.live ? t("livePlane") : t("filePlane")),
|
|
787
|
+
// Sticky to the bottom of the host's scrolling region, so Save stays in
|
|
788
|
+
// reach however far the channel list has been unfolded.
|
|
789
|
+
h(
|
|
790
|
+
"div",
|
|
791
|
+
{ className: "ds-footer" },
|
|
792
|
+
h("button", { className: "ds-btn", type: "button", onClick: onSave, disabled: status === "saving" }, t("save")),
|
|
793
|
+
// Rendering `status` directly leaks the raw state ids (`idle`, `saving`)
|
|
794
|
+
// into the UI; every state has a locale entry instead.
|
|
795
|
+
h("span", { className: "ds-status" }, tr(t, `status.${status}`, status))
|
|
796
|
+
)
|
|
797
|
+
)
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
function apply(ctx) {
|
|
801
|
+
injectStyles();
|
|
802
|
+
ctx.effect(() => ctx.locale.register(NS, locale), "dsh-connect: locale");
|
|
803
|
+
const t = ctx.locale.bind(NS);
|
|
804
|
+
const rpcCall = (endpoint, payload, signal) => ctx.connection.rpc.call(SETTINGS_RPC_CHANNEL, endpoint, payload, signal);
|
|
805
|
+
ctx.effect(() => ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
806
|
+
name: "settings.section",
|
|
807
|
+
id: "dsh-connect",
|
|
808
|
+
order: 20,
|
|
809
|
+
label: () => t("title"),
|
|
810
|
+
locale: NS,
|
|
811
|
+
inject: () => ({ rpcCall, t })
|
|
812
|
+
}, ConnectSettingsTab)), "dsh-connect: settings.section");
|
|
813
|
+
}
|
|
814
|
+
return module.exports;
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
//# sourceMappingURL=client.js.map
|