dsh-connect 0.7.2 → 0.9.0

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