dsh-connect 0.8.0 → 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 +134 -17
- package/README.zh.md +110 -17
- package/client/client.js +510 -80
- 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 +272 -60
- 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/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 +6 -12
- package/lib/channels/feishu/adapter.d.ts.map +1 -1
- package/lib/channels/feishu/adapter.js +2 -17
- 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 +23 -2
- 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/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 +15 -5
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +201 -31
- package/lib/index.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 -2
- 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/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/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 +8 -0
- 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 +9 -1
- package/lib/settings/settings-service.d.ts.map +1 -1
- package/lib/settings/settings-service.js +74 -6
- 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/package.json +11 -9
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 ?? {})]));
|
|
@@ -152,8 +168,16 @@ function snapshotToForm(snapshot) {
|
|
|
152
168
|
channels,
|
|
153
169
|
channelDefaults: config.channelDefaults ?? {},
|
|
154
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.
|
|
155
176
|
secrets: {},
|
|
156
|
-
|
|
177
|
+
secretPresence: snapshot.secrets ?? {},
|
|
178
|
+
secretPreviews: snapshot.secretPreviews ?? {},
|
|
179
|
+
settingsStatePath: config.settingsStatePath,
|
|
180
|
+
live: snapshot.live === true
|
|
157
181
|
};
|
|
158
182
|
}
|
|
159
183
|
function stripEmpty(obj) {
|
|
@@ -186,33 +210,276 @@ function buildCredentialSaves(form) {
|
|
|
186
210
|
return out;
|
|
187
211
|
}
|
|
188
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
|
+
|
|
189
437
|
// client/settings-client.mjs
|
|
190
438
|
var name = "dsh-connect-settings";
|
|
191
439
|
var inject = ["slots", "connection", "locale"];
|
|
192
440
|
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
|
-
};
|
|
441
|
+
var locale = LOCALES;
|
|
197
442
|
var h = React.createElement;
|
|
443
|
+
var ALL_CHANNELS = Object.keys(CHANNEL_SECRET_FIELDS);
|
|
198
444
|
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
|
|
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}
|
|
202
448
|
.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-
|
|
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}
|
|
212
475
|
.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-
|
|
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))}
|
|
216
483
|
.dsh-connect-settings .ds-btn:disabled{opacity:.55;cursor:default}
|
|
217
484
|
.dsh-connect-settings .ds-status{font-size:12px;color:var(--ds-muted)}
|
|
218
485
|
`;
|
|
@@ -224,48 +491,174 @@ function injectStyles() {
|
|
|
224
491
|
el.textContent = STYLE;
|
|
225
492
|
document.head.appendChild(el);
|
|
226
493
|
}
|
|
227
|
-
function renderConfigField(field, value, onChange) {
|
|
228
|
-
const label = field.label ?? field.key;
|
|
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;
|
|
229
498
|
if (field.kind === "boolean") {
|
|
230
|
-
|
|
499
|
+
control = h(
|
|
231
500
|
"label",
|
|
232
|
-
{ className: "ds-
|
|
501
|
+
{ className: "ds-control ds-check-field" },
|
|
233
502
|
h("input", { className: "ds-check", type: "checkbox", checked: !!value, onChange: (e) => onChange(e.target.checked) }),
|
|
234
503
|
" " + label
|
|
235
504
|
);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
return h(
|
|
505
|
+
} else if (field.kind === "select") {
|
|
506
|
+
control = h(
|
|
239
507
|
"label",
|
|
240
|
-
{ className: "ds-
|
|
508
|
+
{ className: "ds-control" },
|
|
241
509
|
label + " ",
|
|
242
510
|
h(
|
|
243
511
|
"select",
|
|
244
512
|
{ 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))
|
|
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)))
|
|
247
515
|
)
|
|
248
516
|
);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
return h(
|
|
517
|
+
} else if (field.kind === "number") {
|
|
518
|
+
control = h(
|
|
252
519
|
"label",
|
|
253
|
-
{ className: "ds-
|
|
520
|
+
{ className: "ds-control" },
|
|
254
521
|
label + " ",
|
|
255
522
|
h("input", { className: "ds-input", type: "number", value: value ?? "", onChange: (e) => onChange(e.target.value) })
|
|
256
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
|
+
);
|
|
257
531
|
}
|
|
258
532
|
return h(
|
|
259
|
-
"
|
|
533
|
+
"div",
|
|
260
534
|
{ className: "ds-field", key: `cfg-${field.key}` },
|
|
261
|
-
|
|
262
|
-
h("
|
|
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
|
|
263
654
|
);
|
|
264
655
|
}
|
|
265
656
|
function ConnectSettingsTab({ rpcCall, t }) {
|
|
266
657
|
const [form, setForm] = React.useState(null);
|
|
267
658
|
const [status, setStatus] = React.useState("loading");
|
|
268
659
|
const [creds, setCreds] = React.useState({});
|
|
660
|
+
const [openOverride, setOpenOverride] = React.useState(null);
|
|
661
|
+
const [advOverride, setAdvOverride] = React.useState(null);
|
|
269
662
|
const rpc = (endpoint, payload) => rpcCall(endpoint, payload);
|
|
270
663
|
React.useEffect(() => {
|
|
271
664
|
let alive = true;
|
|
@@ -283,14 +676,19 @@ function ConnectSettingsTab({ rpcCall, t }) {
|
|
|
283
676
|
if (!form) return;
|
|
284
677
|
setStatus("saving");
|
|
285
678
|
try {
|
|
286
|
-
await saveSettings(rpc, buildConfigSave(form));
|
|
287
|
-
for (const c of buildCredentialSaves(form)) await saveCredentials(rpc, c.channel, c.values);
|
|
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 ?? {});
|
|
288
683
|
setStatus("saved");
|
|
289
684
|
} catch {
|
|
290
685
|
setStatus("error");
|
|
291
686
|
}
|
|
292
687
|
};
|
|
293
|
-
const setChannels = (ch, on) =>
|
|
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
|
+
};
|
|
294
692
|
const setField = (ch, field, value) => setForm((f) => ({ ...f, secrets: { ...f.secrets, [ch]: { ...f.secrets[ch] ?? {}, [field]: value } } }));
|
|
295
693
|
const setChannelConfig = (ch, key, raw) => setForm((f) => {
|
|
296
694
|
const descriptor = (CHANNEL_CONFIG_FIELDS[ch] ?? []).find((x) => x.key === key);
|
|
@@ -309,6 +707,14 @@ function ConnectSettingsTab({ rpcCall, t }) {
|
|
|
309
707
|
return { ...f, channelDefaults: defaults };
|
|
310
708
|
});
|
|
311
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
|
+
};
|
|
312
718
|
return h(
|
|
313
719
|
"div",
|
|
314
720
|
{ className: "dsh-connect-settings" },
|
|
@@ -316,53 +722,77 @@ function ConnectSettingsTab({ rpcCall, t }) {
|
|
|
316
722
|
"section",
|
|
317
723
|
{ className: "ds-card" },
|
|
318
724
|
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
|
-
|
|
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
|
+
}))
|
|
345
760
|
),
|
|
346
761
|
h(
|
|
347
762
|
"section",
|
|
348
763
|
{ className: "ds-card" },
|
|
349
764
|
h("h4", { className: "ds-card-title" }, t("defaults")),
|
|
765
|
+
h("p", { className: "ds-note" }, t("defaultsHint")),
|
|
350
766
|
h(
|
|
351
767
|
"div",
|
|
352
768
|
{ className: "ds-fields" },
|
|
353
|
-
...CHANNEL_DEFAULT_FIELDS.map((field) => renderConfigField(field, form.channelDefaults?.[field.key], (raw) => setDefault(field.key, raw))),
|
|
354
|
-
|
|
355
|
-
|
|
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",
|
|
356
776
|
{ className: "ds-field" },
|
|
357
|
-
|
|
358
|
-
|
|
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"))
|
|
359
784
|
)
|
|
360
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.
|
|
361
789
|
h(
|
|
362
790
|
"div",
|
|
363
|
-
{ className: "ds-
|
|
364
|
-
h("button", { className: "ds-btn", onClick: onSave, disabled: status === "saving" }, t("save")),
|
|
365
|
-
|
|
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))
|
|
366
796
|
)
|
|
367
797
|
)
|
|
368
798
|
);
|