dsh-connect 0.7.2 → 0.8.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 (114) hide show
  1. package/README.md +68 -32
  2. package/README.zh.md +67 -31
  3. package/client/client.js +387 -0
  4. package/client/client.js.map +7 -0
  5. package/client/settings-client.mjs +177 -0
  6. package/examples/minimal.config.json +16 -0
  7. package/lib/channels/dingtalk/adapter.d.ts +59 -0
  8. package/lib/channels/dingtalk/adapter.d.ts.map +1 -0
  9. package/lib/channels/dingtalk/adapter.js +123 -0
  10. package/lib/channels/dingtalk/adapter.js.map +1 -0
  11. package/lib/channels/dingtalk/i18n.d.ts +14 -0
  12. package/lib/channels/dingtalk/i18n.d.ts.map +1 -0
  13. package/lib/channels/dingtalk/i18n.js +16 -0
  14. package/lib/channels/dingtalk/i18n.js.map +1 -0
  15. package/lib/channels/dingtalk/index.d.ts +127 -0
  16. package/lib/channels/dingtalk/index.d.ts.map +1 -0
  17. package/lib/channels/dingtalk/index.js +144 -0
  18. package/lib/channels/dingtalk/index.js.map +1 -0
  19. package/lib/channels/dingtalk/message.d.ts +49 -0
  20. package/lib/channels/dingtalk/message.d.ts.map +1 -0
  21. package/lib/channels/dingtalk/message.js +43 -0
  22. package/lib/channels/dingtalk/message.js.map +1 -0
  23. package/lib/channels/dingtalk/stomp.d.ts +36 -0
  24. package/lib/channels/dingtalk/stomp.d.ts.map +1 -0
  25. package/lib/channels/dingtalk/stomp.js +107 -0
  26. package/lib/channels/dingtalk/stomp.js.map +1 -0
  27. package/lib/channels/dingtalk/stream.d.ts +53 -0
  28. package/lib/channels/dingtalk/stream.d.ts.map +1 -0
  29. package/lib/channels/dingtalk/stream.js +199 -0
  30. package/lib/channels/dingtalk/stream.js.map +1 -0
  31. package/lib/channels/dingtalk/webhook.d.ts +84 -0
  32. package/lib/channels/dingtalk/webhook.d.ts.map +1 -0
  33. package/lib/channels/dingtalk/webhook.js +143 -0
  34. package/lib/channels/dingtalk/webhook.js.map +1 -0
  35. package/lib/channels/feishu/adapter.d.ts +139 -0
  36. package/lib/channels/feishu/adapter.d.ts.map +1 -0
  37. package/lib/channels/feishu/adapter.js +677 -0
  38. package/lib/channels/feishu/adapter.js.map +1 -0
  39. package/lib/channels/feishu/i18n.d.ts +28 -0
  40. package/lib/channels/feishu/i18n.d.ts.map +1 -0
  41. package/lib/channels/feishu/i18n.js +40 -0
  42. package/lib/channels/feishu/i18n.js.map +1 -0
  43. package/lib/channels/feishu/index.d.ts +61 -0
  44. package/lib/channels/feishu/index.d.ts.map +1 -0
  45. package/lib/channels/feishu/index.js +77 -0
  46. package/lib/channels/feishu/index.js.map +1 -0
  47. package/lib/channels/feishu/onboard.d.ts +16 -0
  48. package/lib/channels/feishu/onboard.d.ts.map +1 -0
  49. package/lib/channels/feishu/onboard.js +86 -0
  50. package/lib/channels/feishu/onboard.js.map +1 -0
  51. package/lib/channels/telegram/adapter.d.ts +70 -0
  52. package/lib/channels/telegram/adapter.d.ts.map +1 -0
  53. package/lib/channels/telegram/adapter.js +454 -0
  54. package/lib/channels/telegram/adapter.js.map +1 -0
  55. package/lib/channels/telegram/client.d.ts +115 -0
  56. package/lib/channels/telegram/client.d.ts.map +1 -0
  57. package/lib/channels/telegram/client.js +198 -0
  58. package/lib/channels/telegram/client.js.map +1 -0
  59. package/lib/channels/telegram/i18n.d.ts +14 -0
  60. package/lib/channels/telegram/i18n.d.ts.map +1 -0
  61. package/lib/channels/telegram/i18n.js +18 -0
  62. package/lib/channels/telegram/i18n.js.map +1 -0
  63. package/lib/channels/telegram/index.d.ts +46 -0
  64. package/lib/channels/telegram/index.d.ts.map +1 -0
  65. package/lib/channels/telegram/index.js +54 -0
  66. package/lib/channels/telegram/index.js.map +1 -0
  67. package/lib/channels/web/adapter.d.ts +103 -0
  68. package/lib/channels/web/adapter.d.ts.map +1 -0
  69. package/lib/channels/web/adapter.js +161 -0
  70. package/lib/channels/web/adapter.js.map +1 -0
  71. package/lib/channels/web/index.d.ts +47 -0
  72. package/lib/channels/web/index.d.ts.map +1 -0
  73. package/lib/channels/web/index.js +59 -0
  74. package/lib/channels/web/index.js.map +1 -0
  75. package/lib/index.d.ts +68 -11
  76. package/lib/index.d.ts.map +1 -1
  77. package/lib/index.js +141 -8
  78. package/lib/index.js.map +1 -1
  79. package/lib/runner.d.ts +1 -2
  80. package/lib/runner.d.ts.map +1 -1
  81. package/lib/runner.js.map +1 -1
  82. package/lib/service.d.ts +1 -1
  83. package/lib/service.d.ts.map +1 -1
  84. package/lib/service.js +1 -1
  85. package/lib/service.js.map +1 -1
  86. package/lib/settings/channels.d.ts +51 -0
  87. package/lib/settings/channels.d.ts.map +1 -0
  88. package/lib/settings/channels.js +101 -0
  89. package/lib/settings/channels.js.map +1 -0
  90. package/lib/settings/credential-store.d.ts +39 -0
  91. package/lib/settings/credential-store.d.ts.map +1 -0
  92. package/lib/settings/credential-store.js +68 -0
  93. package/lib/settings/credential-store.js.map +1 -0
  94. package/lib/settings/index.d.ts +14 -0
  95. package/lib/settings/index.d.ts.map +1 -0
  96. package/lib/settings/index.js +14 -0
  97. package/lib/settings/index.js.map +1 -0
  98. package/lib/settings/rpc-client.d.ts +36 -0
  99. package/lib/settings/rpc-client.d.ts.map +1 -0
  100. package/lib/settings/rpc-client.js +45 -0
  101. package/lib/settings/rpc-client.js.map +1 -0
  102. package/lib/settings/settings-model.d.ts +107 -0
  103. package/lib/settings/settings-model.d.ts.map +1 -0
  104. package/lib/settings/settings-model.js +110 -0
  105. package/lib/settings/settings-model.js.map +1 -0
  106. package/lib/settings/settings-rpc.d.ts +62 -0
  107. package/lib/settings/settings-rpc.d.ts.map +1 -0
  108. package/lib/settings/settings-rpc.js +90 -0
  109. package/lib/settings/settings-rpc.js.map +1 -0
  110. package/lib/settings/settings-service.d.ts +27 -0
  111. package/lib/settings/settings-service.d.ts.map +1 -0
  112. package/lib/settings/settings-service.js +103 -0
  113. package/lib/settings/settings-service.js.map +1 -0
  114. package/package.json +48 -6
@@ -0,0 +1,387 @@
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
+
54
+ // lib/settings/rpc-client.js
55
+ var RpcError = class extends Error {
56
+ constructor(code, message) {
57
+ super(message);
58
+ __publicField(this, "code");
59
+ this.name = "RpcError";
60
+ this.code = code;
61
+ }
62
+ };
63
+ function callRpc(rpcCall, endpoint, payload, signal) {
64
+ return rpcCall(endpoint, payload, signal).then((res) => {
65
+ if (res.ok)
66
+ return res.value;
67
+ const code = res.error?.code ?? "settings-failed";
68
+ throw new RpcError(code, res.error?.message ?? code);
69
+ });
70
+ }
71
+ function loadSettings(rpcCall) {
72
+ return callRpc(rpcCall, "settings.get", {});
73
+ }
74
+ function saveSettings(rpcCall, config) {
75
+ return callRpc(rpcCall, "settings.save", config);
76
+ }
77
+ function saveCredentials(rpcCall, channel, values) {
78
+ return callRpc(rpcCall, "credentials.save", { channel, values });
79
+ }
80
+
81
+ // 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
+ var CHANNEL_SECRET_KEYS = Object.freeze({
89
+ feishu: { appId: "DSH_CONNECT_FEISHU_APP_ID", appSecret: "DSH_CONNECT_FEISHU_APP_SECRET" },
90
+ telegram: { botToken: "DSH_CONNECT_TELEGRAM_BOT_TOKEN" },
91
+ dingtalk: {
92
+ webhookUrl: "DSH_CONNECT_DINGTALK_WEBHOOK_URL",
93
+ secret: "DSH_CONNECT_DINGTALK_SECRET",
94
+ clientId: "DSH_CONNECT_DINGTALK_CLIENT_ID",
95
+ clientSecret: "DSH_CONNECT_DINGTALK_CLIENT_SECRET"
96
+ },
97
+ web: {}
98
+ });
99
+
100
+ // lib/settings/settings-model.js
101
+ var CHANNEL_SECRET_FIELDS = Object.fromEntries(Object.entries(CHANNEL_SECRET_KEYS).map(([ch, map]) => [ch, Object.keys(map ?? {})]));
102
+ var CHANNEL_CONFIG_FIELDS = {
103
+ feishu: [
104
+ { key: "transport", kind: "select", options: ["websocket", "webhook"], label: "transport" },
105
+ { key: "requireMention", kind: "boolean", label: "requireMention" },
106
+ { key: "dmMode", kind: "select", options: ["open", "allowlist", "pair", "disabled"], label: "dmMode" },
107
+ { key: "language", kind: "select", options: ["zh", "en"], label: "language" },
108
+ { key: "webhookPort", kind: "number", label: "webhookPort" },
109
+ { key: "webhookPath", kind: "text", label: "webhookPath" }
110
+ ],
111
+ telegram: [
112
+ { key: "requireMention", kind: "boolean", label: "requireMention" },
113
+ { key: "language", kind: "select", options: ["zh", "en"], label: "language" },
114
+ { key: "pollingTimeoutSeconds", kind: "number", label: "pollingTimeoutSeconds" },
115
+ { key: "baseUrl", kind: "text", label: "baseUrl" }
116
+ ],
117
+ dingtalk: [
118
+ { key: "language", kind: "select", options: ["zh", "en"], label: "language" },
119
+ { key: "defaultAt", kind: "text", label: "defaultAt" }
120
+ ],
121
+ web: [
122
+ { key: "pollIntervalMs", kind: "number", label: "pollIntervalMs" }
123
+ ]
124
+ };
125
+ var CHANNEL_DEFAULT_FIELDS = [
126
+ { key: "language", kind: "select", options: ["zh", "en"], label: "language" },
127
+ { key: "notifyLevel", kind: "select", options: ["full", "important", "result"], label: "notifyLevel" }
128
+ ];
129
+ function coerceConfigValue(kind, raw) {
130
+ if (raw === void 0 || raw === null || raw === "")
131
+ return void 0;
132
+ switch (kind) {
133
+ case "number": {
134
+ const n = Number(raw);
135
+ return Number.isFinite(n) ? n : void 0;
136
+ }
137
+ case "boolean":
138
+ return typeof raw === "boolean" ? raw : raw === true || raw === "true";
139
+ case "select":
140
+ return String(raw);
141
+ default:
142
+ return String(raw);
143
+ }
144
+ }
145
+ function snapshotToForm(snapshot) {
146
+ const config = snapshot.config ?? {};
147
+ const channels = snapshot.enabled ?? [];
148
+ const channelConfigs = {};
149
+ for (const ch of channels)
150
+ channelConfigs[ch] = config[ch] ?? {};
151
+ return {
152
+ channels,
153
+ channelDefaults: config.channelDefaults ?? {},
154
+ channelConfigs,
155
+ secrets: {},
156
+ settingsStatePath: config.settingsStatePath
157
+ };
158
+ }
159
+ function stripEmpty(obj) {
160
+ const out = {};
161
+ for (const [k, v] of Object.entries(obj))
162
+ if (v !== void 0 && v !== null)
163
+ out[k] = v;
164
+ return out;
165
+ }
166
+ function buildConfigSave(form) {
167
+ const config = { channels: form.channels };
168
+ const defaults = stripEmpty(form.channelDefaults ?? {});
169
+ if (Object.keys(defaults).length > 0)
170
+ config.channelDefaults = defaults;
171
+ for (const [ch, cfg] of Object.entries(form.channelConfigs ?? {})) {
172
+ const clean = stripEmpty(cfg ?? {});
173
+ if (Object.keys(clean).length > 0)
174
+ config[ch] = clean;
175
+ }
176
+ if (form.settingsStatePath)
177
+ config.settingsStatePath = form.settingsStatePath;
178
+ return config;
179
+ }
180
+ function buildCredentialSaves(form) {
181
+ const out = [];
182
+ for (const [ch, values] of Object.entries(form.secrets ?? {})) {
183
+ if (values && Object.keys(values).length > 0)
184
+ out.push({ channel: ch, values });
185
+ }
186
+ return out;
187
+ }
188
+
189
+ // client/settings-client.mjs
190
+ var name = "dsh-connect-settings";
191
+ var inject = ["slots", "connection", "locale"];
192
+ 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
+ };
197
+ var h = React.createElement;
198
+ var STYLE = `
199
+ .dsh-connect-settings{--ds-bg:#ffffff;--ds-bg-sub:#f6f7f9;--ds-text:#1f2329;--ds-muted:#646a73;--ds-border:#e2e4e8;--ds-border-2:#c8cbd0;--ds-accent:#3b82f6;--ds-accent-fg:#ffffff;--ds-focus:rgba(59,130,246,.22);display:flex;flex-direction:column;gap:14px;min-width:280px;max-width:760px;color:var(--ds-text);font:13px/1.6 -apple-system,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;padding:4px 2px}
200
+ @media(prefers-color-scheme:dark){.dsh-connect-settings{--ds-bg:#1a1b1e;--ds-bg-sub:#232428;--ds-text:#e8eaed;--ds-muted:#9aa0a6;--ds-border:#3b3d42;--ds-border-2:#5c5f66;--ds-accent:#5b9dff;--ds-accent-fg:#0f1115;--ds-focus:rgba(91,157,255,.25)}}
201
+ .dsh-connect-settings .ds-card{border:1px solid var(--ds-border);border-radius:10px;background:var(--ds-bg);padding:12px 14px;display:flex;flex-direction:column;gap:10px}
202
+ .dsh-connect-settings .ds-card-title{margin:0;font-size:13px;font-weight:600}
203
+ .dsh-connect-settings .ds-channel{border:1px dashed var(--ds-border-2);border-radius:8px;padding:8px 12px;margin:0;background:var(--ds-bg-sub)}
204
+ .dsh-connect-settings .ds-legend{padding:0 6px}
205
+ .dsh-connect-settings .ds-legend label{display:flex;align-items:center;gap:6px;font-size:12px;font-weight:600}
206
+ .dsh-connect-settings .ds-badge{margin-left:4px;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)}
207
+ .dsh-connect-settings .ds-fields{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:10px 14px;margin-top:8px}
208
+ .dsh-connect-settings .ds-field{display:flex;flex-direction:column;gap:4px;font-size:12px;color:var(--ds-muted)}
209
+ .dsh-connect-settings .ds-field.ds-check-field{flex-direction:row;align-items:center;gap:6px}
210
+ .dsh-connect-settings .ds-check{width:16px;height:16px;accent-color:var(--ds-accent)}
211
+ .dsh-connect-settings .ds-input{height:30px;width:100%;padding:0 9px;border:1px solid var(--ds-border-2);border-radius:6px;background:var(--ds-bg);color:var(--ds-text);font:inherit}
212
+ .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-focus)}
214
+ .dsh-connect-settings .ds-actions{display:flex;align-items:center;gap:12px;margin-top:2px}
215
+ .dsh-connect-settings .ds-btn{height:32px;padding:0 18px;border:none;border-radius:6px;background:var(--ds-accent);color:var(--ds-accent-fg);font:inherit;font-weight:500;cursor:pointer}
216
+ .dsh-connect-settings .ds-btn:disabled{opacity:.55;cursor:default}
217
+ .dsh-connect-settings .ds-status{font-size:12px;color:var(--ds-muted)}
218
+ `;
219
+ function injectStyles() {
220
+ if (typeof document === "undefined") return;
221
+ if (document.getElementById("dsh-connect-settings-style")) return;
222
+ const el = document.createElement("style");
223
+ el.id = "dsh-connect-settings-style";
224
+ el.textContent = STYLE;
225
+ document.head.appendChild(el);
226
+ }
227
+ function renderConfigField(field, value, onChange) {
228
+ const label = field.label ?? field.key;
229
+ if (field.kind === "boolean") {
230
+ return h(
231
+ "label",
232
+ { className: "ds-field ds-check-field", key: `cfg-${field.key}` },
233
+ h("input", { className: "ds-check", type: "checkbox", checked: !!value, onChange: (e) => onChange(e.target.checked) }),
234
+ " " + label
235
+ );
236
+ }
237
+ if (field.kind === "select") {
238
+ return h(
239
+ "label",
240
+ { className: "ds-field", key: `cfg-${field.key}` },
241
+ label + " ",
242
+ h(
243
+ "select",
244
+ { className: "ds-input", value: value ?? "", onChange: (e) => onChange(e.target.value) },
245
+ h("option", { value: "" }, "(default)"),
246
+ ...(field.options ?? []).map((opt) => h("option", { key: opt, value: opt }, opt))
247
+ )
248
+ );
249
+ }
250
+ if (field.kind === "number") {
251
+ return h(
252
+ "label",
253
+ { className: "ds-field", key: `cfg-${field.key}` },
254
+ label + " ",
255
+ h("input", { className: "ds-input", type: "number", value: value ?? "", onChange: (e) => onChange(e.target.value) })
256
+ );
257
+ }
258
+ return h(
259
+ "label",
260
+ { className: "ds-field", key: `cfg-${field.key}` },
261
+ label + " ",
262
+ h("input", { className: "ds-input", type: "text", value: value ?? "", onChange: (e) => onChange(e.target.value) })
263
+ );
264
+ }
265
+ function ConnectSettingsTab({ rpcCall, t }) {
266
+ const [form, setForm] = React.useState(null);
267
+ const [status, setStatus] = React.useState("loading");
268
+ const [creds, setCreds] = React.useState({});
269
+ const rpc = (endpoint, payload) => rpcCall(endpoint, payload);
270
+ React.useEffect(() => {
271
+ let alive = true;
272
+ loadSettings(rpc).then((snap) => {
273
+ if (!alive) return;
274
+ setForm(snapshotToForm(snap));
275
+ setCreds(snap.credentials ?? {});
276
+ setStatus("idle");
277
+ }).catch(() => alive && setStatus("error"));
278
+ return () => {
279
+ alive = false;
280
+ };
281
+ }, [rpcCall]);
282
+ const onSave = async () => {
283
+ if (!form) return;
284
+ setStatus("saving");
285
+ try {
286
+ await saveSettings(rpc, buildConfigSave(form));
287
+ for (const c of buildCredentialSaves(form)) await saveCredentials(rpc, c.channel, c.values);
288
+ setStatus("saved");
289
+ } catch {
290
+ setStatus("error");
291
+ }
292
+ };
293
+ const setChannels = (ch, on) => setForm((f) => ({ ...f, channels: on ? [...f.channels, ch] : f.channels.filter((x) => x !== ch) }));
294
+ const setField = (ch, field, value) => setForm((f) => ({ ...f, secrets: { ...f.secrets, [ch]: { ...f.secrets[ch] ?? {}, [field]: value } } }));
295
+ const setChannelConfig = (ch, key, raw) => setForm((f) => {
296
+ const descriptor = (CHANNEL_CONFIG_FIELDS[ch] ?? []).find((x) => x.key === key);
297
+ const value = coerceConfigValue(descriptor?.kind ?? "text", raw);
298
+ const cfg = { ...f.channelConfigs[ch] ?? {} };
299
+ if (value === void 0) delete cfg[key];
300
+ else cfg[key] = value;
301
+ return { ...f, channelConfigs: { ...f.channelConfigs, [ch]: cfg } };
302
+ });
303
+ const setDefault = (key, raw) => setForm((f) => {
304
+ const descriptor = CHANNEL_DEFAULT_FIELDS.find((x) => x.key === key);
305
+ const value = coerceConfigValue(descriptor?.kind ?? "text", raw);
306
+ const defaults = { ...f.channelDefaults ?? {} };
307
+ if (value === void 0) delete defaults[key];
308
+ else defaults[key] = value;
309
+ return { ...f, channelDefaults: defaults };
310
+ });
311
+ if (!form) return h("div", { className: "dsh-connect-settings" }, t("loading"));
312
+ return h(
313
+ "div",
314
+ { className: "dsh-connect-settings" },
315
+ h(
316
+ "section",
317
+ { className: "ds-card" },
318
+ h("h4", { className: "ds-card-title" }, t("channels")),
319
+ ...Object.keys(CHANNEL_SECRET_FIELDS).map((ch) => h(
320
+ "fieldset",
321
+ { className: "ds-channel", key: ch },
322
+ h(
323
+ "legend",
324
+ { className: "ds-legend" },
325
+ h(
326
+ "label",
327
+ null,
328
+ h("input", { className: "ds-check", type: "checkbox", checked: form.channels.includes(ch), onChange: (e) => setChannels(ch, e.target.checked) }),
329
+ h("span", null, ch),
330
+ h("span", { className: "ds-badge" }, creds[ch] ? t("reachable") : t("unreachable"))
331
+ )
332
+ ),
333
+ h(
334
+ "div",
335
+ { className: "ds-fields" },
336
+ ...CHANNEL_SECRET_FIELDS[ch].map((field) => h(
337
+ "label",
338
+ { className: "ds-field", key: `sec-${field}` },
339
+ field,
340
+ h("input", { className: "ds-input", type: "password", placeholder: field, value: form.secrets?.[ch]?.[field] ?? "", onChange: (e) => setField(ch, field, e.target.value) })
341
+ )),
342
+ ...(CHANNEL_CONFIG_FIELDS[ch] ?? []).map((field) => renderConfigField(field, form.channelConfigs?.[ch]?.[field.key], (raw) => setChannelConfig(ch, field.key, raw)))
343
+ )
344
+ ))
345
+ ),
346
+ h(
347
+ "section",
348
+ { className: "ds-card" },
349
+ h("h4", { className: "ds-card-title" }, t("defaults")),
350
+ h(
351
+ "div",
352
+ { className: "ds-fields" },
353
+ ...CHANNEL_DEFAULT_FIELDS.map((field) => renderConfigField(field, form.channelDefaults?.[field.key], (raw) => setDefault(field.key, raw))),
354
+ h(
355
+ "label",
356
+ { className: "ds-field" },
357
+ t("statePath"),
358
+ h("input", { className: "ds-input", value: form.settingsStatePath ?? "", onChange: (e) => setForm((f) => ({ ...f, settingsStatePath: e.target.value })) })
359
+ )
360
+ ),
361
+ h(
362
+ "div",
363
+ { className: "ds-actions" },
364
+ h("button", { className: "ds-btn", onClick: onSave, disabled: status === "saving" }, t("save")),
365
+ h("span", { className: "ds-status" }, status === "saved" ? t("saved") : status === "error" ? t("error") : status)
366
+ )
367
+ )
368
+ );
369
+ }
370
+ function apply(ctx) {
371
+ injectStyles();
372
+ ctx.effect(() => ctx.locale.register(NS, locale), "dsh-connect: locale");
373
+ const t = ctx.locale.bind(NS);
374
+ const rpcCall = (endpoint, payload, signal) => ctx.connection.rpc.call(SETTINGS_RPC_CHANNEL, endpoint, payload, signal);
375
+ ctx.effect(() => ctx.slots.inject("settings.section", () => ctx.slots.register({
376
+ name: "settings.section",
377
+ id: "dsh-connect",
378
+ order: 20,
379
+ label: () => t("title"),
380
+ locale: NS,
381
+ inject: () => ({ rpcCall, t })
382
+ }, ConnectSettingsTab)), "dsh-connect: settings.section");
383
+ }
384
+ return module.exports;
385
+ }
386
+ });
387
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["settings-client.mjs", "../src/settings/settings-rpc.ts", "../src/settings/rpc-client.ts", "../src/settings/credential-store.ts", "../src/settings/settings-model.ts"],
4
+ "sourcesContent": ["/**\n * dsh-connect client settings plugin.\n *\n * Registers a \"settings.section\" in the DeepSeek Harness web UI, mirroring\n * xmanrui/dsh-im (plugin-src/client/index.js). The pane talks to the host over\n * the `/dsh-connect` channel via `ctx.connection.rpc.call`. All the data logic\n * (snapshot<->form mapping, save-payload building) lives in the tested\n * `settings-model.js` / `rpc-client.js` helpers; this component is a thin\n * renderer. Build the lib first (`pnpm --filter dsh-connect build`), then\n * bundle this in a `dsh web` shell (Vite).\n */\nimport * as React from 'react';\n\nimport { SETTINGS_RPC_CHANNEL } from '../lib/settings/settings-rpc.js';\nimport { loadSettings, saveSettings, saveCredentials } from '../lib/settings/rpc-client.js';\nimport { snapshotToForm, buildConfigSave, buildCredentialSaves, CHANNEL_SECRET_FIELDS, CHANNEL_CONFIG_FIELDS, CHANNEL_DEFAULT_FIELDS, coerceConfigValue } from '../lib/settings/settings-model.js';\n\nexport const name = 'dsh-connect-settings';\nexport const inject = ['slots', 'connection', 'locale'];\nexport const NS = 'dsh-connect';\n\nexport const locale = {\n 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' },\n 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' },\n};\n\nconst h = React.createElement;\n\n// Self-contained scoped stylesheet for the pane (injected once). The host's own\n// settings panes use per-feature CSS modules we can't import into this separate\n// bundle, so we ship our own consistent, theme-aware styles instead. Everything\n// is scoped under `.dsh-connect-settings` so nothing leaks into the shell.\nconst STYLE = `\n.dsh-connect-settings{--ds-bg:#ffffff;--ds-bg-sub:#f6f7f9;--ds-text:#1f2329;--ds-muted:#646a73;--ds-border:#e2e4e8;--ds-border-2:#c8cbd0;--ds-accent:#3b82f6;--ds-accent-fg:#ffffff;--ds-focus:rgba(59,130,246,.22);display:flex;flex-direction:column;gap:14px;min-width:280px;max-width:760px;color:var(--ds-text);font:13px/1.6 -apple-system,\"Segoe UI\",\"PingFang SC\",\"Microsoft YaHei\",sans-serif;padding:4px 2px}\n@media(prefers-color-scheme:dark){.dsh-connect-settings{--ds-bg:#1a1b1e;--ds-bg-sub:#232428;--ds-text:#e8eaed;--ds-muted:#9aa0a6;--ds-border:#3b3d42;--ds-border-2:#5c5f66;--ds-accent:#5b9dff;--ds-accent-fg:#0f1115;--ds-focus:rgba(91,157,255,.25)}}\n.dsh-connect-settings .ds-card{border:1px solid var(--ds-border);border-radius:10px;background:var(--ds-bg);padding:12px 14px;display:flex;flex-direction:column;gap:10px}\n.dsh-connect-settings .ds-card-title{margin:0;font-size:13px;font-weight:600}\n.dsh-connect-settings .ds-channel{border:1px dashed var(--ds-border-2);border-radius:8px;padding:8px 12px;margin:0;background:var(--ds-bg-sub)}\n.dsh-connect-settings .ds-legend{padding:0 6px}\n.dsh-connect-settings .ds-legend label{display:flex;align-items:center;gap:6px;font-size:12px;font-weight:600}\n.dsh-connect-settings .ds-badge{margin-left:4px;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)}\n.dsh-connect-settings .ds-fields{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:10px 14px;margin-top:8px}\n.dsh-connect-settings .ds-field{display:flex;flex-direction:column;gap:4px;font-size:12px;color:var(--ds-muted)}\n.dsh-connect-settings .ds-field.ds-check-field{flex-direction:row;align-items:center;gap:6px}\n.dsh-connect-settings .ds-check{width:16px;height:16px;accent-color:var(--ds-accent)}\n.dsh-connect-settings .ds-input{height:30px;width:100%;padding:0 9px;border:1px solid var(--ds-border-2);border-radius:6px;background:var(--ds-bg);color:var(--ds-text);font:inherit}\n.dsh-connect-settings select.ds-input{cursor:pointer}\n.dsh-connect-settings .ds-input:focus{outline:none;border-color:var(--ds-accent);box-shadow:0 0 0 2px var(--ds-focus)}\n.dsh-connect-settings .ds-actions{display:flex;align-items:center;gap:12px;margin-top:2px}\n.dsh-connect-settings .ds-btn{height:32px;padding:0 18px;border:none;border-radius:6px;background:var(--ds-accent);color:var(--ds-accent-fg);font:inherit;font-weight:500;cursor:pointer}\n.dsh-connect-settings .ds-btn:disabled{opacity:.55;cursor:default}\n.dsh-connect-settings .ds-status{font-size:12px;color:var(--ds-muted)}\n`;\n\nfunction injectStyles() {\n if (typeof document === 'undefined') return;\n if (document.getElementById('dsh-connect-settings-style')) return;\n const el = document.createElement('style');\n el.id = 'dsh-connect-settings-style';\n el.textContent = STYLE;\n document.head.appendChild(el);\n}\n\n// Render a single config field control per its `kind` (text / number / boolean / select).\nfunction renderConfigField(field, value, onChange) {\n const label = field.label ?? field.key;\n if (field.kind === 'boolean') {\n return h('label', { className: 'ds-field ds-check-field', key: `cfg-${field.key}` },\n h('input', { className: 'ds-check', type: 'checkbox', checked: !!value, onChange: (e) => onChange(e.target.checked) }),\n ' ' + label);\n }\n if (field.kind === 'select') {\n return h('label', { className: 'ds-field', key: `cfg-${field.key}` },\n label + ' ',\n h('select', { className: 'ds-input', value: value ?? '', onChange: (e) => onChange(e.target.value) },\n h('option', { value: '' }, '(default)'),\n ...(field.options ?? []).map((opt) => h('option', { key: opt, value: opt }, opt))));\n }\n if (field.kind === 'number') {\n return h('label', { className: 'ds-field', key: `cfg-${field.key}` },\n label + ' ',\n h('input', { className: 'ds-input', type: 'number', value: value ?? '', onChange: (e) => onChange(e.target.value) }));\n }\n return h('label', { className: 'ds-field', key: `cfg-${field.key}` },\n label + ' ',\n h('input', { className: 'ds-input', type: 'text', value: value ?? '', onChange: (e) => onChange(e.target.value) }));\n}\n\n// Thin React renderer over the tested settings-model helpers.\nexport function ConnectSettingsTab({ rpcCall, t }) {\n const [form, setForm] = React.useState(null);\n const [status, setStatus] = React.useState('loading');\n const [creds, setCreds] = React.useState({});\n const rpc = (endpoint, payload) => rpcCall(endpoint, payload);\n\n React.useEffect(() => {\n let alive = true;\n loadSettings(rpc).then((snap) => {\n if (!alive) return;\n setForm(snapshotToForm(snap));\n setCreds(snap.credentials ?? {});\n setStatus('idle');\n }).catch(() => alive && setStatus('error'));\n return () => { alive = false; };\n }, [rpcCall]);\n\n const onSave = async () => {\n if (!form) return;\n setStatus('saving');\n try {\n await saveSettings(rpc, buildConfigSave(form));\n for (const c of buildCredentialSaves(form)) await saveCredentials(rpc, c.channel, c.values);\n setStatus('saved');\n } catch { setStatus('error'); }\n };\n\n const setChannels = (ch, on) => setForm((f) => ({ ...f, channels: on ? [...f.channels, ch] : f.channels.filter((x) => x !== ch) }));\n const setField = (ch, field, value) => setForm((f) => ({ ...f, secrets: { ...f.secrets, [ch]: { ...(f.secrets[ch] ?? {}), [field]: value } } }));\n const setChannelConfig = (ch, key, raw) => setForm((f) => {\n const descriptor = (CHANNEL_CONFIG_FIELDS[ch] ?? []).find((x) => x.key === key);\n const value = coerceConfigValue(descriptor?.kind ?? 'text', raw);\n const cfg = { ...(f.channelConfigs[ch] ?? {}) };\n if (value === undefined) delete cfg[key]; else cfg[key] = value;\n return { ...f, channelConfigs: { ...f.channelConfigs, [ch]: cfg } };\n });\n const setDefault = (key, raw) => setForm((f) => {\n const descriptor = CHANNEL_DEFAULT_FIELDS.find((x) => x.key === key);\n const value = coerceConfigValue(descriptor?.kind ?? 'text', raw);\n const defaults = { ...(f.channelDefaults ?? {}) };\n if (value === undefined) delete defaults[key]; else defaults[key] = value;\n return { ...f, channelDefaults: defaults };\n });\n\n if (!form) return h('div', { className: 'dsh-connect-settings' }, t('loading'));\n return h('div', { className: 'dsh-connect-settings' },\n h('section', { className: 'ds-card' },\n h('h4', { className: 'ds-card-title' }, t('channels')),\n // One card per built-in channel: enable toggle + cred badge + secret + config fields.\n ...Object.keys(CHANNEL_SECRET_FIELDS).map((ch) => h('fieldset', { className: 'ds-channel', key: ch },\n h('legend', { className: 'ds-legend' },\n h('label', null,\n h('input', { className: 'ds-check', type: 'checkbox', checked: form.channels.includes(ch), onChange: (e) => setChannels(ch, e.target.checked) }),\n h('span', null, ch),\n h('span', { className: 'ds-badge' }, creds[ch] ? t('reachable') : t('unreachable'))),\n ),\n h('div', { className: 'ds-fields' },\n ...CHANNEL_SECRET_FIELDS[ch].map((field) => h('label', { className: 'ds-field', key: `sec-${field}` },\n field,\n h('input', { className: 'ds-input', type: 'password', placeholder: field, value: form.secrets?.[ch]?.[field] ?? '', onChange: (e) => setField(ch, field, e.target.value) }))),\n ...(CHANNEL_CONFIG_FIELDS[ch] ?? []).map((field) => renderConfigField(field, form.channelConfigs?.[ch]?.[field.key], (raw) => setChannelConfig(ch, field.key, raw))),\n ),\n )),\n ),\n h('section', { className: 'ds-card' },\n h('h4', { className: 'ds-card-title' }, t('defaults')),\n h('div', { className: 'ds-fields' },\n ...CHANNEL_DEFAULT_FIELDS.map((field) => renderConfigField(field, form.channelDefaults?.[field.key], (raw) => setDefault(field.key, raw))),\n h('label', { className: 'ds-field' }, t('statePath'),\n h('input', { className: 'ds-input', value: form.settingsStatePath ?? '', onChange: (e) => setForm((f) => ({ ...f, settingsStatePath: e.target.value })) })),\n ),\n h('div', { className: 'ds-actions' },\n h('button', { className: 'ds-btn', onClick: onSave, disabled: status === 'saving' }, t('save')),\n h('span', { className: 'ds-status' }, status === 'saved' ? t('saved') : status === 'error' ? t('error') : status),\n ),\n ),\n );\n}\n\nexport function apply(ctx) {\n injectStyles();\n ctx.effect(() => ctx.locale.register(NS, locale), 'dsh-connect: locale');\n const t = ctx.locale.bind(NS);\n const rpcCall = (endpoint, payload, signal) => ctx.connection.rpc.call(SETTINGS_RPC_CHANNEL, endpoint, payload, signal);\n ctx.effect(() => ctx.slots.inject('settings.section', () => ctx.slots.register({\n name: 'settings.section', id: 'dsh-connect', order: 20, label: () => t('title'), locale: NS, inject: () => ({ rpcCall, t }),\n }, ConnectSettingsTab)), 'dsh-connect: settings.section');\n}\n", "/**\n * Host-side RPC facade for the dsh-connect web settings pane.\n *\n * Mirrors dsh-im's host RPC pattern: a plugin registers a single channel on\n * `ctx.connection.rpc.handle(...)` and a handler dispatches dotted endpoints,\n * returning `{ ok: true, value }` on success or `{ ok: false, error }` on\n * failure (see dsh-im `plugin-src/host/update-rpc.mjs`).\n *\n * Endpoints:\n * - `settings.get` \u2014 current non-secret config + enabled channels + credential presence.\n * - `settings.save` \u2014 persist non-secret config (payload must be non-empty).\n * - `credentials.save` \u2014 persist secrets to the DSH credential store (payload\n * `{ channel, values }`, values are {configKey: secret}).\n * - `settings.status` \u2014 fresh status snapshot.\n *\n * @module dsh-connect/settings/settings-rpc\n */\n\n/** RPC channel the settings pane calls. */\nexport const SETTINGS_RPC_CHANNEL = \"/dsh-connect\";\n\n/** Endpoints the settings pane can invoke on the channel. */\nexport const SETTINGS_ENDPOINTS = Object.freeze([\"settings.get\", \"settings.save\", \"credentials.save\", \"settings.status\"]);\n\n/** Error codes considered safe to surface verbatim to the browser. */\nconst PUBLIC_ERRORS = new Set([\n \"settings-failed\", \"bad-request\", \"cancelled\", \"invalid-payload\", \"unknown-profile\",\n \"unsupported-runtime\", \"not-configured\", \"credential-missing\", \"credential-invalid\",\n \"channel-disabled\", \"save-failed\", \"state-unavailable\", \"disposed\", \"interrupted\",\n \"unsupported\", \"invalid-channel\", \"invalid-credentials\",\n]);\n\n/** A settings pane `settings.get`/status snapshot. */\nexport interface SettingsSnapshot {\n /** The current plugin config (non-secret fields). */\n config: Record<string, unknown>;\n /** The channel names currently enabled. */\n enabled: string[];\n /** Per-channel credential presence (true = stored & set). */\n credentials: Record<string, boolean>;\n}\n\n/** The service backing the RPC; supplied by the web-settings integration. */\nexport interface SettingsService {\n get(): Promise<SettingsSnapshot>;\n save(config: Record<string, unknown>): Promise<SettingsSnapshot>;\n /** Persist channel secrets ({configKey: secret}) to the credential store. */\n saveCredentials?(channel: string, values: Record<string, string>): Promise<SettingsSnapshot>;\n status(): Promise<SettingsSnapshot>;\n}\n\nfunction validPayload(endpoint: string, payload: unknown): boolean {\n if (payload === null || typeof payload !== \"object\" || Array.isArray(payload)) return false;\n const keys = Object.keys(payload);\n if (endpoint === \"settings.save\") return keys.length > 0;\n if (endpoint === \"credentials.save\") {\n return keys.length > 0 && typeof (payload as any).channel === \"string\"\n && (payload as any).values !== null && typeof (payload as any).values === \"object\"\n && !Array.isArray((payload as any).values);\n }\n // The reads carry no payload.\n return keys.length === 0;\n}\n\n/** Build the channel handler for a settings service. */\nexport function createSettingsRpcHandler(service: SettingsService) {\n return async (endpoint: string, payload: unknown, signal?: { aborted?: boolean }) => {\n if (!SETTINGS_ENDPOINTS.includes(endpoint as never) || !validPayload(endpoint, payload)) {\n return { ok: false, error: { code: \"bad-request\", message: \"Invalid settings request.\" } };\n }\n if (signal?.aborted) return { ok: false, error: { code: \"cancelled\", message: \"Request cancelled.\" } };\n try {\n const record = (payload ?? {}) as Record<string, unknown>;\n const value = endpoint === \"settings.get\"\n ? await service.get()\n : endpoint === \"settings.save\"\n ? await service.save(record)\n : endpoint === \"credentials.save\"\n ? await saveCredentialsP(service, record)\n : await service.status();\n return { ok: true, value };\n } catch (error) {\n const code = PUBLIC_ERRORS.has((error as any)?.code) ? (error as any).code : \"settings-failed\";\n return { ok: false, error: { code, message: code } };\n }\n };\n}\n\nasync function saveCredentialsP(service: SettingsService, record: Record<string, unknown>): Promise<SettingsSnapshot> {\n if (typeof service.saveCredentials !== \"function\") {\n const err = new Error(\"unsupported\") as any;\n err.code = \"unsupported\";\n throw err;\n }\n const channel = record.channel as string;\n const values = (record.values ?? {}) as Record<string, string>;\n return service.saveCredentials(channel, values);\n}\n\n/**\n * Register the settings RPC channel, mirroring dsh-im's `installUpdateRpc`.\n * No-ops (returns a no-op disposer) when the host connection is unavailable,\n * so the plugin stays safe to load in any context.\n */\nexport function installSettingsRpc(ctx: unknown, options: { service: SettingsService }) {\n const conn = (ctx as any)?.connection;\n const handle = conn?.rpc?.handle;\n if (typeof handle !== \"function\") return () => {};\n return handle(SETTINGS_RPC_CHANNEL, createSettingsRpcHandler(options.service), { authority: \"loopback\" }) as () => void;\n}", "/**\n * Client-side RPC helper for the dsh-connect web settings pane.\n *\n * The frontend talks to the host over the `/dsh-connect` channel; the channel\n * returns `{ ok, value }`/`{ ok:false, error }` envelopes (see `settings-rpc.js`).\n * This helper unwraps the envelope and throws a typed error on failure, so the\n * UI can catch it and render a message. Pure + dependency-free for easy testing.\n *\n * @module dsh-connect/settings/rpc-client\n */\n\nimport type { SettingsSnapshot } from \"./settings-rpc.js\";\n\n/** The `ctx.connection.rpc.call` signature the settings pane uses. */\nexport type RpcCall = (\n endpoint: string,\n payload?: unknown,\n signal?: AbortSignal,\n) => Promise<{ ok: boolean; value?: unknown; error?: { code: string; message: string } }>;\n\n/** A typed RPC failure, surfaced to the settings UI. */\nexport class RpcError extends Error {\n readonly code: string;\n constructor(code: string, message: string) {\n super(message);\n this.name = \"RpcError\";\n this.code = code;\n }\n}\n\n/** Invoke one endpoint and unwrap the envelope (throws `RpcError` on failure). */\nexport function callRpc<T = unknown>(\n rpcCall: RpcCall,\n endpoint: string,\n payload?: unknown,\n signal?: AbortSignal,\n): Promise<T> {\n return rpcCall(endpoint, payload, signal).then((res) => {\n if (res.ok) return res.value as T;\n const code = res.error?.code ?? \"settings-failed\";\n throw new RpcError(code, res.error?.message ?? code);\n });\n}\n\n/** Load the current settings snapshot via `settings.get`. */\nexport function loadSettings(rpcCall: RpcCall): Promise<SettingsSnapshot> {\n return callRpc<SettingsSnapshot>(rpcCall, \"settings.get\", {});\n}\n\n/** Persist the settings via `settings.save`. */\nexport function saveSettings(rpcCall: RpcCall, config: Record<string, unknown>): Promise<SettingsSnapshot> {\n return callRpc<SettingsSnapshot>(rpcCall, \"settings.save\", config);\n}\n\n/** Persist channel secrets to the credential store via `credentials.save`. */\nexport function saveCredentials(\n rpcCall: RpcCall,\n channel: string,\n values: Record<string, string>,\n): Promise<SettingsSnapshot> {\n return callRpc<SettingsSnapshot>(rpcCall, \"credentials.save\", { channel, values });\n}\n\n/** Ask the host for a fresh status snapshot via `settings.status`. */\nexport function loadStatus(rpcCall: RpcCall): Promise<SettingsSnapshot> {\n return callRpc<SettingsSnapshot>(rpcCall, \"settings.status\", {});\n}", "/**\n * Host-side credential store for dsh-connect channels. Adapts the DSH\n * `credentials` provider (`resolve/describe/set/unset`, env-style refs) into a\n * per-channel interface, so secrets live in the DSH credential store instead of\n * the plugin config (mirroring dsh-im's `credential-store.mjs`). The web\n * settings pane only ever sees `configured` booleans, never secret values.\n *\n * @module dsh-connect/settings/credential-store\n */\nimport { CHANNELS, type ChannelName } from \"./channels.js\";\n\n/** The seam the DSH host provides; `describe` reports source metadata. */\nexport interface CredentialsProvider {\n resolve(ref: string): Promise<string | null | undefined>;\n describe(ref: string): Promise<{ configured: boolean; [k: string]: unknown }>;\n set(ref: string, value: string): Promise<void>;\n unset(ref: string): Promise<void>;\n}\n\n/** Credential references owned by each channel (alphabetical, stable). */\nexport const CREDENTIAL_REFS: Record<ChannelName, readonly string[]> = Object.freeze({\n feishu: [\"DSH_CONNECT_FEISHU_APP_ID\", \"DSH_CONNECT_FEISHU_APP_SECRET\"],\n telegram: [\"DSH_CONNECT_TELEGRAM_BOT_TOKEN\"],\n dingtalk: [\"DSH_CONNECT_DINGTALK_WEBHOOK_URL\", \"DSH_CONNECT_DINGTALK_SECRET\"],\n web: [],\n}) as Record<ChannelName, readonly string[]>;\n\n/** Maps each channel's secret *config key* to its credential ref, so the\n * settings service and activation can enrich a channel config from the store. */\nexport const CHANNEL_SECRET_KEYS: Record<ChannelName, Record<string, string>> = Object.freeze({\n feishu: { appId: \"DSH_CONNECT_FEISHU_APP_ID\", appSecret: \"DSH_CONNECT_FEISHU_APP_SECRET\" },\n telegram: { botToken: \"DSH_CONNECT_TELEGRAM_BOT_TOKEN\" },\n dingtalk: {\n webhookUrl: \"DSH_CONNECT_DINGTALK_WEBHOOK_URL\",\n secret: \"DSH_CONNECT_DINGTALK_SECRET\",\n clientId: \"DSH_CONNECT_DINGTALK_CLIENT_ID\",\n clientSecret: \"DSH_CONNECT_DINGTALK_CLIENT_SECRET\",\n },\n web: {},\n});\n\n/** A channel credential store bound to a DSH provider. */\nexport interface CredentialStore {\n /** Whether every ref for the channel is configured (secrets present). */\n configured(channel: ChannelName): Promise<boolean>;\n /** Persist a channel secret under its ref. */\n save(channel: ChannelName, values: Record<string, string>): Promise<void>;\n /** Read the channel's stored secrets as { configKey: value }. */\n get(channel: ChannelName): Promise<Record<string, string>>;\n /** Remove every ref for the channel. */\n clear(channel: ChannelName): Promise<void>;\n}\n\nfunction assertProvider(provider: CredentialsProvider): void {\n for (const method of [\"resolve\", \"describe\", \"set\", \"unset\"] as const) {\n if (typeof provider[method] !== \"function\") {\n throw new TypeError(`A DSH credential provider with ${method}() is required`);\n }\n }\n}\n\n/** Build a per-channel credential store over a DSH `credentials` provider. */\nexport function createCredentialStore(provider: CredentialsProvider): CredentialStore {\n assertProvider(provider);\n const refs = (channel: ChannelName) => CREDENTIAL_REFS[channel] ?? [];\n return Object.freeze({\n async configured(channel: ChannelName) {\n const list = refs(channel);\n if (list.length === 0) return false;\n const described = await Promise.all(list.map((ref) => provider.describe(ref)));\n return described.every((d) => d.configured === true);\n },\n async save(channel: ChannelName, values: Record<string, string>) {\n for (const ref of refs(channel)) {\n const value = values[ref];\n if (value !== undefined) await provider.set(ref, value);\n }\n },\n async get(channel: ChannelName) {\n const map = CHANNEL_SECRET_KEYS[channel] ?? {};\n const entries = await Promise.all(\n Object.entries(map).map(async ([configKey, ref]) => {\n const value = await provider.resolve(ref);\n return [configKey, value] as const;\n }),\n );\n const out: Record<string, string> = {};\n for (const [configKey, value] of entries) {\n if (typeof value === \"string\" && value.length > 0) out[configKey] = value;\n }\n return out;\n },\n async clear(channel: ChannelName) {\n const outcomes = await Promise.allSettled(refs(channel).map((ref) => provider.unset(ref)));\n if (outcomes.some((o) => o.status === \"rejected\")) {\n throw new Error(`Unable to clear ${channel} credentials.`);\n }\n },\n });\n}", "/**\n * Client-side settings model: pure, dependency-free helpers that map between the\n * host snapshot and the edit form, and build the save payloads. Kept separate from\n * the React component so the client settings logic is unit-testable anywhere.\n * The host-only secret-ref mapping lives on the server; the client only carries\n * field names.\n *\n * @module dsh-connect/settings/settings-model\n */\nimport { CHANNEL_SECRET_KEYS } from \"./credential-store.js\";\nimport type { ChannelName } from \"./channels.js\";\nimport type { SettingsSnapshot } from \"./settings-rpc.js\";\n\n/** Secret config-keys per channel, for rendering secret inputs. */\nexport const CHANNEL_SECRET_FIELDS = Object.fromEntries(\n Object.entries(CHANNEL_SECRET_KEYS).map(([ch, map]) => [ch, Object.keys(map ?? {})]),\n) as Record<ChannelName, string[]>;\n\n/**\n * A non-secret, user-editable per-channel config field. `kind` drives how the\n * pane renders the control and how `coerceConfigValue` normalizes the raw input\n * (text \u2192 string, number \u2192 finite number, boolean \u2192 false on empty, select \u2192\n * one of `options`).\n */\nexport interface ConfigField {\n key: string;\n kind: 'text' | 'number' | 'boolean' | 'select';\n options?: string[];\n /** Human label; defaults to the key if absent. */\n label?: string;\n}\n\n/**\n * Non-secret per-channel fields the pane exposes, mirroring the keys used by\n * each channel adapter config (see `examples/profile-cordis.patch.yml`). Order\n * is the render order; keep secrets separate (`CHANNEL_SECRET_FIELDS`).\n */\nexport const CHANNEL_CONFIG_FIELDS = {\n feishu: [\n { key: 'transport', kind: 'select', options: ['websocket', 'webhook'], label: 'transport' },\n { key: 'requireMention', kind: 'boolean', label: 'requireMention' },\n { key: 'dmMode', kind: 'select', options: ['open', 'allowlist', 'pair', 'disabled'], label: 'dmMode' },\n { key: 'language', kind: 'select', options: ['zh', 'en'], label: 'language' },\n { key: 'webhookPort', kind: 'number', label: 'webhookPort' },\n { key: 'webhookPath', kind: 'text', label: 'webhookPath' },\n ],\n telegram: [\n { key: 'requireMention', kind: 'boolean', label: 'requireMention' },\n { key: 'language', kind: 'select', options: ['zh', 'en'], label: 'language' },\n { key: 'pollingTimeoutSeconds', kind: 'number', label: 'pollingTimeoutSeconds' },\n { key: 'baseUrl', kind: 'text', label: 'baseUrl' },\n ],\n dingtalk: [\n { key: 'language', kind: 'select', options: ['zh', 'en'], label: 'language' },\n { key: 'defaultAt', kind: 'text', label: 'defaultAt' },\n ],\n web: [\n { key: 'pollIntervalMs', kind: 'number', label: 'pollIntervalMs' },\n ],\n} as const satisfies Record<ChannelName, readonly ConfigField[]>;\n\n/** Channel-agnostic keys applied to every channel that doesn't set its own. */\nexport const CHANNEL_DEFAULT_FIELDS: readonly ConfigField[] = [\n { key: 'language', kind: 'select', options: ['zh', 'en'], label: 'language' },\n { key: 'notifyLevel', kind: 'select', options: ['full', 'important', 'result'], label: 'notifyLevel' },\n];\n\n/** Normalize a raw form input for a field `kind` (empty \u2192 undefined = not saved). */\nexport function coerceConfigValue(kind: ConfigField['kind'], raw: unknown): unknown {\n if (raw === undefined || raw === null || raw === '') return undefined;\n switch (kind) {\n case 'number': {\n const n = Number(raw);\n return Number.isFinite(n) ? n : undefined;\n }\n case 'boolean':\n return typeof raw === 'boolean' ? raw : raw === true || raw === 'true';\n case 'select':\n return String(raw);\n default:\n return String(raw);\n }\n}\n\n/** The edit form the pane works on. */\nexport interface SettingsForm {\n channels: ChannelName[];\n channelDefaults: Record<string, unknown>;\n channelConfigs: Record<string, Record<string, unknown>>;\n secrets: Record<string, Record<string, string>>;\n settingsStatePath?: string;\n}\n\n/** Build the initial form from a snapshot. */\nexport function snapshotToForm(snapshot: SettingsSnapshot): SettingsForm {\n const config = snapshot.config ?? {};\n const channels = (snapshot.enabled ?? []) as ChannelName[];\n const channelConfigs: Record<string, Record<string, unknown>> = {};\n for (const ch of channels) channelConfigs[ch] = (config[ch] ?? {}) as Record<string, unknown>;\n return {\n channels,\n channelDefaults: (config.channelDefaults ?? {}) as Record<string, unknown>,\n channelConfigs,\n secrets: {},\n settingsStatePath: config.settingsStatePath as string | undefined,\n };\n}\n\n/** Drop keys whose value is `undefined`/`null` so cleared inputs aren't persisted. */\nfunction stripEmpty(obj: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(obj)) if (v !== undefined && v !== null) out[k] = v;\n return out;\n}\n\n/** Build the non-secret config to persist via `settings.save`. */\nexport function buildConfigSave(form: SettingsForm): Record<string, unknown> {\n const config: Record<string, unknown> = { channels: form.channels };\n const defaults = stripEmpty(form.channelDefaults ?? {});\n if (Object.keys(defaults).length > 0) config.channelDefaults = defaults;\n for (const [ch, cfg] of Object.entries(form.channelConfigs ?? {})) {\n const clean = stripEmpty(cfg ?? {});\n if (Object.keys(clean).length > 0) config[ch] = clean;\n }\n if (form.settingsStatePath) config.settingsStatePath = form.settingsStatePath;\n return config;\n}\n\n/** Build the `credentials.save` payloads for every channel with secret values. */\nexport function buildCredentialSaves(form: SettingsForm): { channel: string; values: Record<string, string> }[] {\n const out: { channel: string; values: Record<string, string> }[] = [];\n for (const [ch, values] of Object.entries(form.secrets ?? {})) {\n if (values && Object.keys(values).length > 0) out.push({ channel: ch, values });\n }\n return out;\n}"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }