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