@lijian-ui/dsh-im-gateway 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js ADDED
@@ -0,0 +1,1233 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@dsh/im-gateway",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react = require("react");
8
+ //#region src/client/ImChannelModal.ts
9
+ /**
10
+ * Add / edit IM channel modal (browser half).
11
+ *
12
+ * Ported from pi-desk-top/src/renderer/im/ImChannelModal.tsx, adapted to the
13
+ * multi-instance model: the bundle's `im-gateway` namespace holds
14
+ * `channels: ImChannelInstance[]`, and the SAME channel type may appear many
15
+ * times (multi-bot support). The modal either adds a brand-new instance
16
+ * (type picker + name + credentials; a unique `<type>-<rand>` id is minted)
17
+ * or edits an existing one (type locked, values prefilled).
18
+ *
19
+ * Saving hands the COMPLETE new channels array to the parent section via
20
+ * `onSave` (host `mergeLayers` replaces arrays wholesale, so the caller must
21
+ * write the full array — see docs/plugin-development.md §10).
22
+ *
23
+ * Browser-half constraints: no JSX (React.createElement), no CSS modules
24
+ * (inline styles), no lucide icons (inline SVG), platform modules only.
25
+ */
26
+ /** Display label per channel type. */
27
+ const TYPE_LABELS = {
28
+ dingtalk: "钉钉",
29
+ qq: "QQ",
30
+ weixin: "个人微信"
31
+ };
32
+ /** Display order of the type groups on the page. */
33
+ const TYPE_ORDER = [
34
+ "dingtalk",
35
+ "qq",
36
+ "weixin"
37
+ ];
38
+ /**
39
+ * Credential fields per channel type. qq / weixin are QR-scan bound: their
40
+ * fields are HIDDEN from the form (the QR flow fills them), but the keys are
41
+ * still needed to initialize/save values. dingtalk stays a manual form.
42
+ */
43
+ const TYPE_FIELDS = {
44
+ dingtalk: [{
45
+ key: "clientId",
46
+ label: "AppKey (clientId)"
47
+ }, {
48
+ key: "clientSecret",
49
+ label: "AppSecret (clientSecret)",
50
+ secret: true
51
+ }],
52
+ qq: [],
53
+ weixin: []
54
+ };
55
+ /** Every credential key per type — used to init/persist QR-hidden values. */
56
+ const TYPE_CREDENTIAL_KEYS = {
57
+ dingtalk: ["clientId", "clientSecret"],
58
+ qq: [
59
+ "appId",
60
+ "clientSecret",
61
+ "botAppId"
62
+ ],
63
+ weixin: [
64
+ "token",
65
+ "botId",
66
+ "baseUrl",
67
+ "cdnBaseUrl"
68
+ ]
69
+ };
70
+ const overlay = {
71
+ position: "fixed",
72
+ inset: 0,
73
+ zIndex: 300,
74
+ display: "flex",
75
+ alignItems: "center",
76
+ justifyContent: "center",
77
+ background: "rgba(0, 0, 0, 0.5)",
78
+ padding: 24
79
+ };
80
+ const modal = {
81
+ display: "flex",
82
+ flexDirection: "column",
83
+ width: 520,
84
+ maxWidth: "100%",
85
+ maxHeight: "90vh",
86
+ background: "var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-layer-1, #fff))",
87
+ border: "1px solid var(--dsw-alias-border-l2, #ddd)",
88
+ borderRadius: 14,
89
+ overflow: "hidden",
90
+ boxShadow: "0 20px 48px rgba(0,0,0,0.36), 0 4px 12px rgba(0,0,0,0.2)"
91
+ };
92
+ const modalHeader = {
93
+ display: "flex",
94
+ alignItems: "center",
95
+ gap: 8,
96
+ padding: "14px 18px",
97
+ borderBottom: "1px solid var(--dsw-alias-border-l1, #eee)",
98
+ flexShrink: 0
99
+ };
100
+ const modalTitle = {
101
+ flex: 1,
102
+ fontSize: 16,
103
+ fontWeight: 500,
104
+ color: "var(--dsw-alias-label-primary, inherit)",
105
+ margin: 0
106
+ };
107
+ const closeBtn = {
108
+ display: "inline-flex",
109
+ alignItems: "center",
110
+ justifyContent: "center",
111
+ width: 28,
112
+ height: 28,
113
+ border: "none",
114
+ background: "transparent",
115
+ color: "var(--dsw-alias-label-tertiary, #888)",
116
+ cursor: "pointer",
117
+ borderRadius: 6,
118
+ fontSize: 16,
119
+ lineHeight: 1
120
+ };
121
+ const modalBody = {
122
+ flex: 1,
123
+ overflowY: "auto",
124
+ padding: "18px 18px 14px",
125
+ display: "flex",
126
+ flexDirection: "column",
127
+ gap: 14
128
+ };
129
+ const field = {
130
+ display: "flex",
131
+ flexDirection: "column",
132
+ gap: 6
133
+ };
134
+ const fieldLabel = {
135
+ fontSize: 12,
136
+ color: "var(--dsw-alias-label-secondary, #555)",
137
+ fontWeight: 500
138
+ };
139
+ const required = {
140
+ color: "var(--dsw-alias-state-error-primary, #e24b4a)",
141
+ marginLeft: 2
142
+ };
143
+ const input = {
144
+ height: 40,
145
+ padding: "0 12px",
146
+ background: "var(--dsw-alias-bg-layer-1, transparent)",
147
+ border: "1px solid var(--dsw-alias-border-l2, #ccc)",
148
+ borderRadius: 8,
149
+ color: "var(--dsw-alias-label-primary, inherit)",
150
+ fontSize: 13,
151
+ outline: "none",
152
+ boxSizing: "border-box"
153
+ };
154
+ const selectInput = {
155
+ ...input,
156
+ cursor: "pointer"
157
+ };
158
+ const errorStyle$1 = {
159
+ fontSize: 12,
160
+ color: "var(--dsw-alias-state-error-primary, #e24b4a)",
161
+ padding: "8px 12px",
162
+ background: "rgba(226, 75, 74, 0.08)",
163
+ borderRadius: 6
164
+ };
165
+ const footer = {
166
+ display: "flex",
167
+ alignItems: "center",
168
+ justifyContent: "flex-end",
169
+ gap: 8,
170
+ padding: "14px 18px",
171
+ borderTop: "1px solid var(--dsw-alias-border-l1, #eee)",
172
+ flexShrink: 0
173
+ };
174
+ const btnGhost = {
175
+ height: 36,
176
+ padding: "0 16px",
177
+ background: "transparent",
178
+ color: "var(--dsw-alias-label-secondary, #555)",
179
+ border: "1px solid var(--dsw-alias-border-l2, #ccc)",
180
+ borderRadius: 8,
181
+ cursor: "pointer",
182
+ fontSize: 13
183
+ };
184
+ const btnPrimary = {
185
+ display: "inline-flex",
186
+ alignItems: "center",
187
+ gap: 6,
188
+ height: 36,
189
+ padding: "0 18px",
190
+ background: "var(--dsw-alias-state-business-primary, #185FA5)",
191
+ color: "#fff",
192
+ border: "none",
193
+ borderRadius: 8,
194
+ cursor: "pointer",
195
+ fontSize: 13,
196
+ fontWeight: 500
197
+ };
198
+ const qrBox = {
199
+ display: "flex",
200
+ flexDirection: "column",
201
+ alignItems: "center",
202
+ gap: 10,
203
+ padding: "16px",
204
+ background: "var(--dsw-alias-bg-layer-1, rgba(0,0,0,0.02))",
205
+ border: "1px dashed var(--dsw-alias-border-l3, #ccc)",
206
+ borderRadius: 10
207
+ };
208
+ const qrImg = {
209
+ width: 200,
210
+ height: 200,
211
+ borderRadius: 8,
212
+ objectFit: "contain",
213
+ background: "#fff"
214
+ };
215
+ const qrHint = {
216
+ margin: 0,
217
+ fontSize: 12,
218
+ color: "var(--dsw-alias-label-secondary, #555)",
219
+ textAlign: "center",
220
+ lineHeight: 1.5,
221
+ wordBreak: "break-all"
222
+ };
223
+ const qrRow = {
224
+ display: "flex",
225
+ alignItems: "center",
226
+ gap: 8,
227
+ width: "100%",
228
+ justifyContent: "center"
229
+ };
230
+ const qrLink = {
231
+ fontSize: 12,
232
+ color: "var(--dsw-alias-state-business-primary, #185FA5)",
233
+ textDecoration: "none",
234
+ cursor: "pointer",
235
+ maxWidth: "100%",
236
+ overflow: "hidden",
237
+ textOverflow: "ellipsis",
238
+ whiteSpace: "nowrap"
239
+ };
240
+ /** Mint a unique instance id: `<type>-<rand>` (never contains ':'). */
241
+ function mintInstanceId(type, existing) {
242
+ for (let i = 0; i < 50; i++) {
243
+ const id = `${type}-${Math.random().toString(36).slice(2, 6)}`;
244
+ if (!existing.some((inst) => inst.id === id)) return id;
245
+ }
246
+ return `${type}-${Date.now().toString(36)}`;
247
+ }
248
+ /**
249
+ * The add/edit channel modal.
250
+ * @param props - edit target, current instances, close/save actions.
251
+ */
252
+ function ImChannelModal(props) {
253
+ const { editId, instances, configApi, onClose, onSave } = props;
254
+ const editing = editId !== void 0 ? instances.find((inst) => inst.id === editId) : void 0;
255
+ const initialType = editing?.type ?? "dingtalk";
256
+ const [type, setType] = (0, react.useState)(initialType);
257
+ const [name, setName] = (0, react.useState)(editing?.name ?? "");
258
+ const [values, setValues] = (0, react.useState)(() => {
259
+ const pre = editing?.config ?? {};
260
+ const out = {};
261
+ for (const key of TYPE_CREDENTIAL_KEYS[initialType] ?? []) {
262
+ const v = pre[key];
263
+ if (typeof v === "string" || typeof v === "number") out[key] = String(v);
264
+ else out[key] = "";
265
+ }
266
+ return out;
267
+ });
268
+ const [saving, setSaving] = (0, react.useState)(false);
269
+ const [error, setError] = (0, react.useState)("");
270
+ const [qr, setQr] = (0, react.useState)(null);
271
+ const [qrPolling, setQrPolling] = (0, react.useState)(false);
272
+ const [verifyCode, setVerifyCode] = (0, react.useState)("");
273
+ const pollTimer = (0, react.useRef)(null);
274
+ (0, react.useEffect)(() => {
275
+ if (!qrPolling || !qr?.loginId || !configApi) return;
276
+ const loginId = qr.loginId;
277
+ const tick = async () => {
278
+ const res = await configApi.getQrLoginStatus(loginId);
279
+ if (!res.ok || !res.status) return;
280
+ setQr(res.status);
281
+ const s = res.status.status;
282
+ if (s === "confirmed" || s === "error" || s === "canceled" || s === "expired") setQrPolling(false);
283
+ };
284
+ tick();
285
+ pollTimer.current = setInterval(() => void tick(), 1500);
286
+ return () => {
287
+ if (pollTimer.current) clearInterval(pollTimer.current);
288
+ pollTimer.current = null;
289
+ };
290
+ }, [
291
+ qrPolling,
292
+ qr?.loginId,
293
+ configApi
294
+ ]);
295
+ (0, react.useEffect)(() => {
296
+ if (!qr || qr.status !== "confirmed" || !qr.credentials) return;
297
+ setValues((prev) => {
298
+ const next = { ...prev };
299
+ if (type === "qq" && qr.credentials) {
300
+ if (qr.credentials.appId) next.appId = qr.credentials.appId;
301
+ if (qr.credentials.appSecret) next.clientSecret = qr.credentials.appSecret;
302
+ }
303
+ if (type === "weixin" && qr.credentials) {
304
+ if (qr.credentials.token) next.token = qr.credentials.token;
305
+ if (qr.credentials.botId) next.botId = qr.credentials.botId;
306
+ if (qr.credentials.baseUrl) next.baseUrl = qr.credentials.baseUrl;
307
+ }
308
+ return next;
309
+ });
310
+ }, [qr, type]);
311
+ const handleStartQrLogin = async () => {
312
+ if (!configApi) return;
313
+ setError("");
314
+ setVerifyCode("");
315
+ const res = await configApi.startQrLogin(type);
316
+ if (!res.ok || !res.status) {
317
+ setError(res.message ?? "扫码登录启动失败");
318
+ return;
319
+ }
320
+ setQr(res.status);
321
+ setQrPolling(true);
322
+ };
323
+ const handleSubmitVerifyCode = async () => {
324
+ if (!qr?.loginId || !verifyCode.trim() || !configApi) return;
325
+ await configApi.submitQrVerifyCode(qr.loginId, verifyCode.trim());
326
+ setVerifyCode("");
327
+ };
328
+ const handleCancelQrLogin = async () => {
329
+ if (!qr?.loginId || !configApi) return;
330
+ await configApi.cancelQrLogin(qr.loginId);
331
+ setQr(null);
332
+ setQrPolling(false);
333
+ };
334
+ const fields = TYPE_FIELDS[type] ?? [];
335
+ const credentialKeys = TYPE_CREDENTIAL_KEYS[type] ?? [];
336
+ const hasCredentials = (0, react.useMemo)(() => {
337
+ if (type === "qq") return (values["appId"] ?? "").trim() !== "" && (values["clientSecret"] ?? "").trim() !== "";
338
+ if (type === "weixin") return (values["token"] ?? "").trim() !== "" && (values["botId"] ?? "").trim() !== "";
339
+ return false;
340
+ }, [values, type]);
341
+ const canSave = (0, react.useMemo)(() => {
342
+ if (type === "qq") return (values["appId"] ?? "").trim() !== "" && (values["clientSecret"] ?? "").trim() !== "";
343
+ if (type === "weixin") return (values["token"] ?? "").trim() !== "" && (values["botId"] ?? "").trim() !== "";
344
+ return fields.filter((f) => !f.secret).map((f) => f.key).every((k) => (values[k] ?? "").trim() !== "");
345
+ }, [
346
+ fields,
347
+ values,
348
+ type
349
+ ]);
350
+ const handleTypeChange = (next) => {
351
+ setType(next);
352
+ const out = {};
353
+ for (const key of TYPE_CREDENTIAL_KEYS[next] ?? []) out[key] = "";
354
+ setValues(out);
355
+ setError("");
356
+ setQr(null);
357
+ setQrPolling(false);
358
+ setVerifyCode("");
359
+ };
360
+ const handleEdit = (key, text) => {
361
+ setValues((prev) => ({
362
+ ...prev,
363
+ [key]: text
364
+ }));
365
+ setError("");
366
+ };
367
+ const handleSave = async () => {
368
+ if (!canSave || !type) return;
369
+ setSaving(true);
370
+ setError("");
371
+ const config = {};
372
+ for (const key of credentialKeys) {
373
+ const raw = (values[key] ?? "").trim();
374
+ if ((TYPE_FIELDS[type]?.find((f) => f.key === key))?.type === "number") config[key] = raw === "" ? "" : Number(raw);
375
+ else config[key] = raw;
376
+ }
377
+ const next = instances.map((inst) => ({ ...inst }));
378
+ let instanceId;
379
+ if (editing) {
380
+ instanceId = editing.id;
381
+ const target = next.find((inst) => inst.id === instanceId);
382
+ if (target) {
383
+ target.name = name;
384
+ target.config = config;
385
+ }
386
+ } else {
387
+ instanceId = mintInstanceId(type, instances);
388
+ next.push({
389
+ id: instanceId,
390
+ type,
391
+ name,
392
+ enabled: true,
393
+ config
394
+ });
395
+ }
396
+ try {
397
+ await onSave({
398
+ channels: next,
399
+ instanceId
400
+ });
401
+ onClose();
402
+ } catch (err) {
403
+ setError(err instanceof Error ? err.message : String(err));
404
+ } finally {
405
+ setSaving(false);
406
+ }
407
+ };
408
+ const typeOptions = Object.keys(TYPE_LABELS).filter((k) => editId === void 0 || k === editing?.type);
409
+ return (0, react.createElement)("div", {
410
+ style: overlay,
411
+ onMouseDown: (e) => {
412
+ if (e.target === e.currentTarget) onClose();
413
+ }
414
+ }, (0, react.createElement)("div", {
415
+ style: modal,
416
+ onClick: (e) => e.stopPropagation()
417
+ }, (0, react.createElement)("div", { style: modalHeader }, (0, react.createElement)("h3", { style: modalTitle }, editId ? `编辑通道 · ${TYPE_LABELS[editing?.type ?? ""] ?? ""}` : "添加通道"), (0, react.createElement)("button", {
418
+ style: closeBtn,
419
+ onClick: onClose,
420
+ title: "关闭",
421
+ "aria-label": "关闭"
422
+ }, (0, react.createElement)("svg", {
423
+ width: 16,
424
+ height: 16,
425
+ viewBox: "0 0 16 16",
426
+ fill: "none",
427
+ stroke: "currentColor",
428
+ strokeWidth: 1.5,
429
+ strokeLinecap: "round"
430
+ }, (0, react.createElement)("path", { d: "M3 3l10 10M13 3L3 13" })))), (0, react.createElement)("div", { style: modalBody }, (0, react.createElement)("label", { style: field }, (0, react.createElement)("span", { style: fieldLabel }, "渠道类型", (0, react.createElement)("span", { style: required }, "*")), (0, react.createElement)("select", {
431
+ style: selectInput,
432
+ value: type,
433
+ disabled: editId !== void 0,
434
+ onChange: (e) => handleTypeChange(e.target.value)
435
+ }, typeOptions.map((ct) => (0, react.createElement)("option", {
436
+ key: ct,
437
+ value: ct
438
+ }, TYPE_LABELS[ct] ?? ct)))), (0, react.createElement)("label", { style: field }, (0, react.createElement)("span", { style: fieldLabel }, "通道名称"), (0, react.createElement)("input", {
439
+ style: input,
440
+ type: "text",
441
+ value: name,
442
+ placeholder: `如:${TYPE_LABELS[type] ?? type}主机器人`,
443
+ onChange: (e) => {
444
+ setName(e.target.value);
445
+ setError("");
446
+ }
447
+ })), fields.map((f) => (0, react.createElement)("label", {
448
+ key: f.key,
449
+ style: field
450
+ }, (0, react.createElement)("span", { style: fieldLabel }, f.label, f.secret ? null : (0, react.createElement)("span", { style: required }, "*")), (0, react.createElement)("input", {
451
+ style: input,
452
+ type: f.secret ? "password" : f.type === "number" ? "number" : "text",
453
+ value: values[f.key] ?? "",
454
+ placeholder: f.secret ? "••••••••" : "",
455
+ onChange: (e) => handleEdit(f.key, e.target.value)
456
+ }))), (type === "qq" || type === "weixin") && configApi ? (0, react.createElement)("div", { style: qrBox }, qr && qr.status === "confirmed" ? (0, react.createElement)("p", { style: qrHint }, "✅ 扫码登录成功,凭据已自动填入下方字段,点击「保存」生效。") : qr && qr.qrcodeUrl ? [
457
+ (0, react.createElement)("img", {
458
+ key: "img",
459
+ src: qr.qrcodeUrl,
460
+ style: qrImg,
461
+ alt: "登录二维码"
462
+ }),
463
+ (0, react.createElement)("p", {
464
+ key: "hint",
465
+ style: qrHint
466
+ }, qr.message || (type === "qq" ? "请用手机 QQ 扫码绑定机器人" : "请用手机微信扫码") + ";若图片无法显示,可点击下方链接扫码"),
467
+ (0, react.createElement)("div", {
468
+ key: "row",
469
+ style: qrRow
470
+ }, qr.qrcode ? (0, react.createElement)("a", {
471
+ style: qrLink,
472
+ href: qr.qrcode,
473
+ target: "_blank",
474
+ rel: "noreferrer"
475
+ }, qr.qrcode.slice(0, 60)) : null, (0, react.createElement)("button", {
476
+ style: btnGhost,
477
+ onClick: () => void handleCancelQrLogin()
478
+ }, "取消")),
479
+ qr.verifyCodeNeeded || qr.status === "need_verifycode" ? (0, react.createElement)("div", {
480
+ key: "verify",
481
+ style: {
482
+ ...qrRow,
483
+ marginTop: 4
484
+ }
485
+ }, (0, react.createElement)("input", {
486
+ style: {
487
+ ...input,
488
+ width: 140,
489
+ height: 32
490
+ },
491
+ type: "text",
492
+ value: verifyCode,
493
+ placeholder: "输入配对码",
494
+ onChange: (e) => setVerifyCode(e.target.value)
495
+ }), (0, react.createElement)("button", {
496
+ style: btnPrimary,
497
+ onClick: () => void handleSubmitVerifyCode(),
498
+ disabled: !verifyCode.trim()
499
+ }, "确认")) : null
500
+ ] : [(0, react.createElement)("p", {
501
+ key: "hint",
502
+ style: qrHint
503
+ }, qr?.message || (hasCredentials ? type === "qq" ? "已绑定机器人凭据(AppID 已自动填入)。如需更换,重新扫码即可。" : "已绑定微信凭据。如需更换,重新扫码即可。" : type === "qq" ? "尚未配置机器人凭据?扫码绑定,免去在 QQ 开放平台手动创建" : "尚未绑定微信?扫码连接个人微信账号")), (0, react.createElement)("button", {
504
+ key: "start",
505
+ style: btnGhost,
506
+ onClick: () => void handleStartQrLogin(),
507
+ disabled: qrPolling
508
+ }, qrPolling ? "获取二维码中…" : `扫码登录(${TYPE_LABELS[type] ?? type})`)]) : null, error ? (0, react.createElement)("div", { style: errorStyle$1 }, error) : null), (0, react.createElement)("div", { style: footer }, (0, react.createElement)("button", {
509
+ style: btnGhost,
510
+ onClick: onClose
511
+ }, "取消"), (0, react.createElement)("button", {
512
+ style: btnPrimary,
513
+ onClick: () => void handleSave(),
514
+ disabled: saving || !canSave
515
+ }, saving ? "保存中…" : "保存"))));
516
+ }
517
+ //#endregion
518
+ //#region src/client/ImChannelsSection.ts
519
+ /**
520
+ * IM Channels settings section (browser half).
521
+ *
522
+ * Renders the whole IM bundle config on one page: header with an "Add channel"
523
+ * button (top-right), channel instances GROUPED BY TYPE (钉钉 / QQ / 个人微信 —
524
+ * each type may have MULTIPLE bot instances), empty state, and the add/edit
525
+ * modal. Data lives in the `im-gateway` config as `channels: ImChannelInstance[]`
526
+ * and is read/written through the host config route (see config-api.ts) — the
527
+ * official `api.settings.*` refuses third-party namespaces.
528
+ *
529
+ * Browser-half constraints: no JSX (React.createElement), no CSS modules
530
+ * (inline styles), no lucide icons (inline SVG), platform modules only.
531
+ */
532
+ const page = {
533
+ display: "flex",
534
+ flexDirection: "column",
535
+ minWidth: 0,
536
+ width: "100%"
537
+ };
538
+ const header = {
539
+ display: "flex",
540
+ alignItems: "center",
541
+ justifyContent: "space-between",
542
+ padding: "14px 0",
543
+ flexShrink: 0,
544
+ borderBottom: "1px solid var(--dsw-alias-border-l2, #eee)"
545
+ };
546
+ const titleRow = {
547
+ display: "flex",
548
+ alignItems: "center",
549
+ gap: 10,
550
+ minWidth: 0
551
+ };
552
+ const title = {
553
+ fontSize: 17,
554
+ fontWeight: 500,
555
+ color: "var(--dsw-alias-label-primary, inherit)",
556
+ margin: 0,
557
+ whiteSpace: "nowrap"
558
+ };
559
+ const countBadge = {
560
+ display: "inline-flex",
561
+ alignItems: "center",
562
+ justifyContent: "center",
563
+ minWidth: 22,
564
+ height: 22,
565
+ padding: "0 7px",
566
+ borderRadius: 999,
567
+ background: "var(--dsw-alias-bg-layer-3, #eee)",
568
+ color: "var(--dsw-alias-label-secondary, #555)",
569
+ fontSize: 11,
570
+ fontWeight: 500
571
+ };
572
+ const headerRight = {
573
+ display: "flex",
574
+ alignItems: "center",
575
+ gap: 8
576
+ };
577
+ const addBtn = {
578
+ display: "inline-flex",
579
+ alignItems: "center",
580
+ gap: 6,
581
+ height: 32,
582
+ padding: "0 14px",
583
+ border: "none",
584
+ borderRadius: 8,
585
+ background: "var(--dsw-alias-state-business-primary, #185FA5)",
586
+ color: "#fff",
587
+ fontSize: 13,
588
+ fontWeight: 500,
589
+ cursor: "pointer"
590
+ };
591
+ const body = {
592
+ flex: 1,
593
+ overflowY: "auto",
594
+ padding: "16px 0 24px",
595
+ display: "flex",
596
+ flexDirection: "column",
597
+ gap: 16,
598
+ maxWidth: 760,
599
+ width: "100%"
600
+ };
601
+ const errorStyle = {
602
+ fontSize: 12,
603
+ lineHeight: 1.5,
604
+ color: "var(--dsw-alias-state-error-primary, #e24b4a)",
605
+ padding: "10px 14px",
606
+ background: "rgba(226,75,74,0.08)",
607
+ border: "1px solid rgba(226,75,74,0.35)",
608
+ borderRadius: 8,
609
+ whiteSpace: "pre-wrap"
610
+ };
611
+ const empty = {
612
+ display: "flex",
613
+ flexDirection: "column",
614
+ alignItems: "center",
615
+ justifyContent: "center",
616
+ gap: 6,
617
+ padding: "32px 24px",
618
+ border: "1px dashed var(--dsw-alias-border-l3, #ccc)",
619
+ borderRadius: 10,
620
+ background: "var(--dsw-alias-bg-layer-1, transparent)",
621
+ textAlign: "center"
622
+ };
623
+ const emptyText = {
624
+ margin: 0,
625
+ fontSize: 13,
626
+ color: "var(--dsw-alias-label-secondary, #555)"
627
+ };
628
+ const groupHead = {
629
+ display: "flex",
630
+ alignItems: "center",
631
+ gap: 8
632
+ };
633
+ const groupTitle = {
634
+ fontSize: 13,
635
+ fontWeight: 600,
636
+ color: "var(--dsw-alias-label-primary, inherit)",
637
+ margin: 0
638
+ };
639
+ const groupCount = {
640
+ display: "inline-flex",
641
+ alignItems: "center",
642
+ justifyContent: "center",
643
+ minWidth: 18,
644
+ height: 18,
645
+ padding: "0 6px",
646
+ borderRadius: 999,
647
+ background: "var(--dsw-alias-bg-layer-3, #eee)",
648
+ color: "var(--dsw-alias-label-tertiary, #888)",
649
+ fontSize: 10,
650
+ fontWeight: 500
651
+ };
652
+ const channelList = {
653
+ display: "flex",
654
+ flexDirection: "column",
655
+ gap: 10
656
+ };
657
+ const channelCard = {
658
+ display: "flex",
659
+ alignItems: "center",
660
+ gap: 14,
661
+ padding: "14px 16px",
662
+ background: "var(--dsw-alias-bg-layer-2, transparent)",
663
+ border: "1px solid var(--dsw-alias-border-l2, #e0e0e0)",
664
+ borderRadius: 10
665
+ };
666
+ const channelIcon = {
667
+ display: "inline-flex",
668
+ alignItems: "center",
669
+ justifyContent: "center",
670
+ width: 32,
671
+ height: 32,
672
+ borderRadius: 8,
673
+ background: "var(--dsw-alias-bg-layer-3, #eee)",
674
+ color: "var(--dsw-alias-label-secondary, #555)",
675
+ flexShrink: 0
676
+ };
677
+ const channelInfo = {
678
+ flex: 1,
679
+ minWidth: 0,
680
+ display: "flex",
681
+ flexDirection: "column",
682
+ gap: 4
683
+ };
684
+ const channelNameRow = {
685
+ display: "flex",
686
+ alignItems: "center",
687
+ gap: 8,
688
+ minWidth: 0
689
+ };
690
+ const channelName = {
691
+ fontSize: 13,
692
+ fontWeight: 500,
693
+ color: "var(--dsw-alias-label-primary, inherit)",
694
+ overflow: "hidden",
695
+ textOverflow: "ellipsis",
696
+ whiteSpace: "nowrap"
697
+ };
698
+ const statusDot = {
699
+ width: 8,
700
+ height: 8,
701
+ borderRadius: "50%",
702
+ flexShrink: 0
703
+ };
704
+ /** Color for a channel runtime status dot. */
705
+ function statusColor(s) {
706
+ if (s?.status === "online") return "var(--dsw-alias-state-success-primary, #1D9E75)";
707
+ if (s?.status === "error") return "var(--dsw-alias-state-error-primary, #e24b4a)";
708
+ return "var(--dsw-alias-label-tertiary, #bdbdbd)";
709
+ }
710
+ const badge = {
711
+ display: "inline-flex",
712
+ alignItems: "center",
713
+ fontSize: 10,
714
+ lineHeight: 1,
715
+ padding: "3px 9px",
716
+ borderRadius: 999,
717
+ border: "1px solid transparent",
718
+ flexShrink: 0,
719
+ fontWeight: 500
720
+ };
721
+ const badgeOff = {
722
+ ...badge,
723
+ color: "var(--dsw-alias-state-error-primary, #e24b4a)",
724
+ borderColor: "rgba(226,75,74,0.35)",
725
+ background: "rgba(226,75,74,0.08)"
726
+ };
727
+ const badgeOn = {
728
+ ...badge,
729
+ color: "var(--dsw-alias-state-success-primary, #1D9E75)",
730
+ borderColor: "rgba(29,158,117,0.35)",
731
+ background: "rgba(29,158,117,0.08)"
732
+ };
733
+ const channelSub = {
734
+ fontSize: 11,
735
+ color: "var(--dsw-alias-label-tertiary, #888)",
736
+ overflow: "hidden",
737
+ textOverflow: "ellipsis",
738
+ whiteSpace: "nowrap"
739
+ };
740
+ const channelActions = {
741
+ display: "flex",
742
+ alignItems: "center",
743
+ gap: 4,
744
+ flexShrink: 0
745
+ };
746
+ const iconBtn = {
747
+ display: "inline-flex",
748
+ alignItems: "center",
749
+ justifyContent: "center",
750
+ width: 30,
751
+ height: 30,
752
+ border: "none",
753
+ borderRadius: 6,
754
+ background: "transparent",
755
+ color: "var(--dsw-alias-label-tertiary, #888)",
756
+ cursor: "pointer"
757
+ };
758
+ const toggleBtn = {
759
+ position: "relative",
760
+ width: 36,
761
+ height: 21,
762
+ border: "none",
763
+ borderRadius: 999,
764
+ background: "var(--dsw-alias-border-l2, #ccc)",
765
+ cursor: "pointer",
766
+ padding: 0,
767
+ flexShrink: 0
768
+ };
769
+ const toggleOn = {
770
+ ...toggleBtn,
771
+ background: "var(--dsw-alias-state-business-primary, #185FA5)"
772
+ };
773
+ const toggleKnob = {
774
+ position: "absolute",
775
+ left: 2,
776
+ top: 2,
777
+ width: 17,
778
+ height: 17,
779
+ borderRadius: "50%",
780
+ background: "#fff",
781
+ boxShadow: "0 1px 2px rgba(0,0,0,0.25)",
782
+ transition: "left 0.15s ease"
783
+ };
784
+ const toggleKnobOn = {
785
+ ...toggleKnob,
786
+ left: 17
787
+ };
788
+ /**
789
+ * Official brand glyphs per channel type (24×24 fill paths — QQ/WeChat from
790
+ * Simple Icons, DingTalk from MingCute, WeCom from TDesign; CC0/Apache/MIT).
791
+ * Unknown types fall back to the DingTalk glyph.
792
+ */
793
+ const BRAND_PATHS = {
794
+ qq: "M21.395 15.035a40 40 0 0 0-.803-2.264l-1.079-2.695c.001-.032.014-.562.014-.836C19.526 4.632 17.351 0 12 0S4.474 4.632 4.474 9.241c0 .274.013.804.014.836l-1.08 2.695a39 39 0 0 0-.802 2.264c-1.021 3.283-.69 4.643-.438 4.673c.54.065 2.103-2.472 2.103-2.472c0 1.469.756 3.387 2.394 4.771c-.612.188-1.363.479-1.845.835c-.434.32-.379.646-.301.778c.343.578 5.883.369 7.482.189c1.6.18 7.14.389 7.483-.189c.078-.132.132-.458-.301-.778c-.483-.356-1.233-.646-1.846-.836c1.637-1.384 2.393-3.302 2.393-4.771c0 0 1.563 2.537 2.103 2.472c.251-.03.581-1.39-.438-4.673",
795
+ weixin: "M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.48c-.019.07-.048.141-.048.213c0 .163.13.295.29.295a.33.33 0 0 0 .167-.054l1.903-1.114a.86.86 0 0 1 .717-.098a10.2 10.2 0 0 0 2.837.403c.276 0 .543-.027.811-.05c-.857-2.578.157-4.972 1.932-6.446c1.703-1.415 3.882-1.98 5.853-1.838c-.576-3.583-4.196-6.348-8.596-6.348M5.785 5.991c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178A1.17 1.17 0 0 1 4.623 7.17c0-.651.52-1.18 1.162-1.18zm5.813 0c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178a1.17 1.17 0 0 1-1.162-1.178c0-.651.52-1.18 1.162-1.18m5.34 2.867c-1.797-.052-3.746.512-5.28 1.786c-1.72 1.428-2.687 3.72-1.78 6.22c.942 2.453 3.666 4.229 6.884 4.229c.826 0 1.622-.12 2.361-.336a.72.72 0 0 1 .598.082l1.584.926a.3.3 0 0 0 .14.047c.134 0 .24-.111.24-.247c0-.06-.023-.12-.038-.177l-.327-1.233a.6.6 0 0 1-.023-.156a.49.49 0 0 1 .201-.398C23.024 18.48 24 16.82 24 14.98c0-3.21-2.931-5.837-6.656-6.088V8.89c-.135-.01-.27-.027-.407-.03zm-2.53 3.274c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983a.976.976 0 0 1-.969-.983c0-.542.434-.982.97-.982zm4.844 0c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983a.976.976 0 0 1-.969-.983c0-.542.434-.982.969-.982",
796
+ dingtalk: "M6.802 2.02a1 1 0 0 1 .849.22l9.751 8.359a2 2 0 0 1 .235 2.799l-1.06 1.272l.87.436a1 1 0 0 1 .134 1.708l-7 5a1 1 0 0 1-1.539-1.101l1.21-4.034c-2.363-.9-3.747-3.055-4.233-5.483A1 1 0 0 1 7.01 10c-.474-.703-.86-1.42-1.134-2.149c-.649-1.73-.658-3.523.23-5.298a1 1 0 0 1 .696-.533",
797
+ wecom: "m17.326 8.158l-.003-.007a6.6 6.6 0 0 0-1.178-1.674c-1.266-1.307-3.067-2.19-5.102-2.417a9.3 9.3 0 0 0-2.124 0h-.001c-2.061.228-3.882 1.107-5.14 2.405a6.7 6.7 0 0 0-1.194 1.682A5.7 5.7 0 0 0 2 10.657c0 1.106.332 2.218.988 3.201l.006.01c.391.594 1.092 1.39 1.637 1.83l.983.793l-.208.875l.527-.267l.708-.358l.761.225c.467.137.955.227 1.517.29h.005q.515.06 1.026.059c.355 0 .724-.02 1.095-.06a9 9 0 0 0 1.346-.258c.095.7.43 1.337.932 1.81c-.658.208-1.352.358-2.061.436c-.442.048-.883.072-1.312.072q-.627 0-1.253-.072a10.7 10.7 0 0 1-1.861-.36l-2.84 1.438s-.29.131-.44.131c-.418 0-.702-.285-.702-.704c0-.252.067-.598.128-.84l.394-1.653c-.728-.586-1.563-1.544-2.052-2.287A7.76 7.76 0 0 1 0 10.658a7.7 7.7 0 0 1 .787-3.39a8.7 8.7 0 0 1 1.551-2.19c1.61-1.665 3.878-2.73 6.359-3.006a11.3 11.3 0 0 1 2.565 0c2.47.275 4.712 1.353 6.323 3.017a8.6 8.6 0 0 1 1.539 2.192c.466.945.769 1.937.769 2.978a3.06 3.06 0 0 0-2-.005c-.001-.644-.189-1.329-.564-2.09zm4.125 6.977l-.024-.024l-.024-.018l-.024-.018l-.096-.095a4.24 4.24 0 0 1-1.169-2.192q0-.038-.006-.075l-.006-.056l-.035-.144a1.3 1.3 0 0 0-.358-.61a1.386 1.386 0 0 0-1.957 0a1.4 1.4 0 0 0 0 1.963c.191.191.418.311.668.371c.024.012.06.012.084.012q.019 0 .041.006q.023.005.042.006a4.24 4.24 0 0 1 2.231 1.186c.048.048.096.095.131.143a.323.323 0 0 0 .466 0a.35.35 0 0 0 .036-.455m-1.05 4.37l-.025.025c-.119.096-.31.096-.453-.036a.326.326 0 0 1 0-.467c.047-.036.094-.083.141-.13l.002-.002a4.27 4.27 0 0 0 1.187-2.28q.005-.024.006-.043c0-.024 0-.06.012-.084a1.386 1.386 0 0 1 2.326-.67a1.4 1.4 0 0 1 0 1.964c-.167.18-.382.299-.608.359l-.143.036l-.057.005q-.035.006-.075.007a4.2 4.2 0 0 0-2.183 1.173l-.095.096q-.009.01-.018.024t-.018.024m-4.392-1.053l.024.024l.024.018q.015.009.024.018l.096.096a4.25 4.25 0 0 1 1.169 2.19q0 .04.006.076q.005.03.006.057l.035.143c.06.228.18.443.358.611c.537.539 1.42.539 1.957 0a1.4 1.4 0 0 0 0-1.964a1.4 1.4 0 0 0-.668-.371c-.024-.012-.06-.012-.084-.012q-.018 0-.041-.006l-.042-.006a4.25 4.25 0 0 1-2.231-1.185a1.4 1.4 0 0 1-.131-.144a.323.323 0 0 0-.466 0a.325.325 0 0 0-.036.455m1.039-4.358l.024-.024a.32.32 0 0 1 .453.035a.326.326 0 0 1 0 .467c-.047.036-.094.083-.141.13l-.002.002a4.27 4.27 0 0 0-1.187 2.281l-.006.042c0 .024 0 .06-.012.084a1.386 1.386 0 0 1-2.326.67a1.4 1.4 0 0 1 0-1.963c.166-.18.381-.3.608-.36l.143-.035q.026 0 .056-.006q.037-.005.075-.006a4.2 4.2 0 0 0 2.183-1.174l.096-.095l.018-.025z"
798
+ };
799
+ /** Official brand accent colors (fallback to currentColor when unset). */
800
+ const BRAND_COLORS = {
801
+ qq: "#12B7F5",
802
+ weixin: "#07C160",
803
+ dingtalk: "#0089FF",
804
+ wecom: "#0082EF"
805
+ };
806
+ /** Channel-type icon as inline SVG (official brand glyphs). */
807
+ function ChannelGlyph({ type }) {
808
+ const d = BRAND_PATHS[type] ?? BRAND_PATHS.dingtalk;
809
+ const color = BRAND_COLORS[type];
810
+ return (0, react.createElement)("svg", {
811
+ width: 20,
812
+ height: 20,
813
+ viewBox: "0 0 24 24",
814
+ fill: color ?? "currentColor",
815
+ "aria-hidden": true
816
+ }, (0, react.createElement)("path", { d }));
817
+ }
818
+ /**
819
+ * Render the IM channels settings page.
820
+ * @param props - injected config api.
821
+ */
822
+ function ImChannelsSection(props) {
823
+ const { configApi } = props;
824
+ const [channels, setChannels] = (0, react.useState)([]);
825
+ const [revision, setRevision] = (0, react.useState)(void 0);
826
+ const [loading, setLoading] = (0, react.useState)(true);
827
+ const [modalOpen, setModalOpen] = (0, react.useState)(false);
828
+ const [editId, setEditId] = (0, react.useState)(void 0);
829
+ const [pendingDelete, setPendingDelete] = (0, react.useState)(void 0);
830
+ const [error, setError] = (0, react.useState)("");
831
+ const [statuses, setStatuses] = (0, react.useState)({});
832
+ (0, react.useEffect)(() => {
833
+ let current = true;
834
+ configApi.load().then((result) => {
835
+ if (!current) return;
836
+ if (!result.ok) setError(result.message ?? "加载配置失败");
837
+ else {
838
+ setChannels(Array.isArray(result.channels) ? result.channels : []);
839
+ setRevision(result.revision);
840
+ }
841
+ setLoading(false);
842
+ });
843
+ return () => {
844
+ current = false;
845
+ };
846
+ }, [configApi]);
847
+ (0, react.useEffect)(() => {
848
+ let current = true;
849
+ const tick = async () => {
850
+ const result = await configApi.statuses();
851
+ if (current && result.ok && result.statuses) setStatuses(result.statuses);
852
+ };
853
+ tick();
854
+ const id = setInterval(tick, 5e3);
855
+ return () => {
856
+ current = false;
857
+ clearInterval(id);
858
+ };
859
+ }, [configApi]);
860
+ /** Load fresh state after a write. */
861
+ const refresh = async () => {
862
+ const result = await configApi.load();
863
+ if (!result.ok) {
864
+ setError(result.message ?? "刷新配置失败");
865
+ return;
866
+ }
867
+ setChannels(Array.isArray(result.channels) ? result.channels : []);
868
+ setRevision(result.revision);
869
+ setError("");
870
+ };
871
+ /** Wholesale-replace the channels array through the host route. */
872
+ const writeChannels = async (next) => {
873
+ setError("");
874
+ const result = await configApi.save(next, revision);
875
+ if (!result.ok) {
876
+ setError(result.message ?? `保存失败(${result.code ?? "unknown"})`);
877
+ throw new Error(result.message ?? "save failed");
878
+ }
879
+ await refresh();
880
+ };
881
+ const handleSave = async (draft) => {
882
+ await writeChannels(draft.channels);
883
+ setModalOpen(false);
884
+ setEditId(void 0);
885
+ };
886
+ const handleToggle = async (id, current) => {
887
+ const next = channels.map((inst) => inst.id === id ? {
888
+ ...inst,
889
+ enabled: !current
890
+ } : { ...inst });
891
+ await writeChannels(next);
892
+ };
893
+ const handleDelete = async () => {
894
+ if (!pendingDelete) return;
895
+ const id = pendingDelete;
896
+ setPendingDelete(void 0);
897
+ const next = channels.filter((inst) => inst.id !== id);
898
+ await writeChannels(next);
899
+ };
900
+ const groups = TYPE_ORDER.map((type) => ({
901
+ type,
902
+ label: TYPE_LABELS[type] ?? type,
903
+ items: channels.filter((inst) => inst.type === type)
904
+ }));
905
+ return (0, react.createElement)("div", { style: page }, (0, react.createElement)("div", { style: header }, (0, react.createElement)("div", { style: titleRow }, (0, react.createElement)("h2", { style: title }, "IM 通道"), channels.length > 0 ? (0, react.createElement)("span", { style: countBadge }, String(channels.length)) : null), (0, react.createElement)("div", { style: headerRight }, (0, react.createElement)("button", {
906
+ style: addBtn,
907
+ onClick: () => {
908
+ setEditId(void 0);
909
+ setModalOpen(true);
910
+ }
911
+ }, (0, react.createElement)("svg", {
912
+ width: 16,
913
+ height: 16,
914
+ viewBox: "0 0 16 16",
915
+ fill: "none",
916
+ stroke: "currentColor",
917
+ strokeWidth: 1.5,
918
+ strokeLinecap: "round"
919
+ }, (0, react.createElement)("path", { d: "M8 3v10M3 8h10" })), (0, react.createElement)("span", null, "添加通道")))), (0, react.createElement)("div", { style: body }, error ? (0, react.createElement)("div", { style: errorStyle }, error) : null, loading ? (0, react.createElement)("div", { style: empty }, (0, react.createElement)("p", { style: emptyText }, "加载中…")) : channels.length === 0 ? (0, react.createElement)("div", { style: empty }, (0, react.createElement)("p", { style: emptyText }, "暂无通道,点击右上角「添加通道」开始接入")) : (0, react.createElement)("div", { style: {
920
+ display: "flex",
921
+ flexDirection: "column",
922
+ gap: 16
923
+ } }, groups.filter((g) => g.items.length > 0).map((group) => (0, react.createElement)("div", {
924
+ key: group.type,
925
+ style: group
926
+ }, (0, react.createElement)("div", { style: groupHead }, (0, react.createElement)("span", { style: channelIcon }, (0, react.createElement)(ChannelGlyph, { type: group.type })), (0, react.createElement)("span", { style: groupTitle }, group.label), (0, react.createElement)("span", { style: groupCount }, String(group.items.length))), (0, react.createElement)("div", { style: channelList }, group.items.map((inst) => (0, react.createElement)("div", {
927
+ key: inst.id,
928
+ style: channelCard
929
+ }, (0, react.createElement)("span", { style: channelIcon }, (0, react.createElement)(ChannelGlyph, { type: inst.type })), (0, react.createElement)("div", { style: channelInfo }, (0, react.createElement)("div", { style: channelNameRow }, (0, react.createElement)("span", {
930
+ style: {
931
+ ...statusDot,
932
+ background: statusColor(statuses[inst.id])
933
+ },
934
+ title: statuses[inst.id]?.status ?? "unknown"
935
+ }), (0, react.createElement)("span", { style: channelName }, inst.name || (TYPE_LABELS[inst.type] ?? inst.type)), (0, react.createElement)("span", { style: inst.enabled ? badgeOn : badgeOff }, inst.enabled ? "已连接" : "未启用")), (0, react.createElement)("span", { style: channelSub }, inst.id)), (0, react.createElement)("span", { style: channelActions }, (0, react.createElement)("button", {
936
+ type: "button",
937
+ style: inst.enabled ? toggleOn : toggleBtn,
938
+ title: inst.enabled ? "停用" : "启用",
939
+ onClick: () => void handleToggle(inst.id, inst.enabled)
940
+ }, (0, react.createElement)("span", { style: inst.enabled ? toggleKnobOn : toggleKnob })), (0, react.createElement)("button", {
941
+ type: "button",
942
+ style: iconBtn,
943
+ title: "编辑",
944
+ onClick: () => {
945
+ setEditId(inst.id);
946
+ setModalOpen(true);
947
+ }
948
+ }, (0, react.createElement)("svg", {
949
+ width: 14,
950
+ height: 14,
951
+ viewBox: "0 0 16 16",
952
+ fill: "none",
953
+ stroke: "currentColor",
954
+ strokeWidth: 1.3,
955
+ strokeLinecap: "round",
956
+ strokeLinejoin: "round"
957
+ }, (0, react.createElement)("path", { d: "M11.5 2.5l2 2L6 12l-3 1 1-3 7.5-7.5z" }))), (0, react.createElement)("button", {
958
+ type: "button",
959
+ style: iconBtn,
960
+ title: "删除",
961
+ onClick: () => setPendingDelete(inst.id)
962
+ }, (0, react.createElement)("svg", {
963
+ width: 14,
964
+ height: 14,
965
+ viewBox: "0 0 16 16",
966
+ fill: "none",
967
+ stroke: "currentColor",
968
+ strokeWidth: 1.3,
969
+ strokeLinecap: "round",
970
+ strokeLinejoin: "round"
971
+ }, (0, react.createElement)("path", { d: "M3 5h10M6.5 5V3.5h3V5M5 5l.5 7.5h5L11 5M6.5 7.5v3M9.5 7.5v3" }))))))))))), modalOpen ? (0, react.createElement)(ImChannelModal, {
972
+ editId,
973
+ instances: channels,
974
+ configApi,
975
+ onClose: () => {
976
+ setModalOpen(false);
977
+ setEditId(void 0);
978
+ },
979
+ onSave: handleSave
980
+ }) : null, pendingDelete !== void 0 ? (0, react.createElement)("div", {
981
+ style: {
982
+ position: "fixed",
983
+ inset: 0,
984
+ zIndex: 300,
985
+ display: "flex",
986
+ alignItems: "center",
987
+ justifyContent: "center",
988
+ background: "rgba(0,0,0,0.5)",
989
+ padding: 24
990
+ },
991
+ onMouseDown: (e) => {
992
+ if (e.target === e.currentTarget) setPendingDelete(void 0);
993
+ }
994
+ }, (0, react.createElement)("div", {
995
+ style: {
996
+ display: "flex",
997
+ flexDirection: "column",
998
+ gap: 12,
999
+ width: 380,
1000
+ maxWidth: "100%",
1001
+ background: "var(--dsw-alias-bg-layer-2, #fff)",
1002
+ border: "1px solid var(--dsw-alias-border-l2, #ddd)",
1003
+ borderRadius: 14,
1004
+ padding: "18px",
1005
+ boxShadow: "0 20px 48px rgba(0,0,0,0.36)"
1006
+ },
1007
+ onClick: (e) => e.stopPropagation()
1008
+ }, (0, react.createElement)("div", { style: {
1009
+ fontSize: 15,
1010
+ fontWeight: 500
1011
+ } }, "删除通道"), (0, react.createElement)("p", { style: {
1012
+ fontSize: 13,
1013
+ color: "var(--dsw-alias-label-secondary, #555)",
1014
+ margin: 0
1015
+ } }, "确定删除该通道?其配置将被移除。"), (0, react.createElement)("div", { style: {
1016
+ display: "flex",
1017
+ justifyContent: "flex-end",
1018
+ gap: 8
1019
+ } }, (0, react.createElement)("button", {
1020
+ style: {
1021
+ height: 32,
1022
+ padding: "0 14px",
1023
+ background: "transparent",
1024
+ color: "inherit",
1025
+ border: "1px solid var(--dsw-alias-border-l2, #ccc)",
1026
+ borderRadius: 8,
1027
+ cursor: "pointer",
1028
+ fontSize: 13
1029
+ },
1030
+ onClick: () => setPendingDelete(void 0)
1031
+ }, "取消"), (0, react.createElement)("button", {
1032
+ style: {
1033
+ height: 32,
1034
+ padding: "0 14px",
1035
+ background: "var(--dsw-alias-state-error-primary, #e24b4a)",
1036
+ color: "#fff",
1037
+ border: "none",
1038
+ borderRadius: 8,
1039
+ cursor: "pointer",
1040
+ fontSize: 13
1041
+ },
1042
+ onClick: () => void handleDelete()
1043
+ }, "删除")))) : null);
1044
+ }
1045
+ //#endregion
1046
+ //#region src/client/config-api.ts
1047
+ const PACKAGE = "@lijian-ui/dsh-im-gateway";
1048
+ const SERVICE = "imGatewayRemote";
1049
+ /** Identity codec — the host manifest owns strict validation. */
1050
+ const identity = (value) => value;
1051
+ const codec = (symbol) => ({
1052
+ mode: "strict",
1053
+ typeSymbol: symbol,
1054
+ schema: { parse: identity }
1055
+ });
1056
+ /** Wire descriptors mirroring the host contribution (src/remote.ts). */
1057
+ const CONTRIBUTION = {
1058
+ package: PACKAGE,
1059
+ descriptors: [
1060
+ {
1061
+ id: `${PACKAGE}#${SERVICE}/getConfig`,
1062
+ service: SERVICE,
1063
+ namespace: SERVICE,
1064
+ method: "getConfig",
1065
+ invocation: { kind: "direct" },
1066
+ parameters: [],
1067
+ result: codec(`${PACKAGE}#ImGatewayConfigView`)
1068
+ },
1069
+ {
1070
+ id: `${PACKAGE}#${SERVICE}/saveConfig`,
1071
+ service: SERVICE,
1072
+ namespace: SERVICE,
1073
+ method: "saveConfig",
1074
+ invocation: { kind: "direct" },
1075
+ parameters: [{
1076
+ name: "channels",
1077
+ wire: "channels",
1078
+ source: "json",
1079
+ codec: codec(`${PACKAGE}#ChannelsArray`)
1080
+ }, {
1081
+ name: "expectedRevision",
1082
+ wire: "expectedRevision",
1083
+ source: "json",
1084
+ acceptsUndefined: true,
1085
+ codec: codec(`${PACKAGE}#Revision`)
1086
+ }],
1087
+ result: codec(`${PACKAGE}#ImGatewayConfigView`)
1088
+ },
1089
+ {
1090
+ id: `${PACKAGE}#${SERVICE}/getChannelStatuses`,
1091
+ service: SERVICE,
1092
+ namespace: SERVICE,
1093
+ method: "getChannelStatuses",
1094
+ invocation: { kind: "direct" },
1095
+ parameters: [],
1096
+ result: codec(`${PACKAGE}#ChannelStatuses`)
1097
+ },
1098
+ {
1099
+ id: `${PACKAGE}#${SERVICE}/startQrLogin`,
1100
+ service: SERVICE,
1101
+ namespace: SERVICE,
1102
+ method: "startQrLogin",
1103
+ invocation: { kind: "direct" },
1104
+ parameters: [{
1105
+ name: "channelType",
1106
+ wire: "channelType",
1107
+ source: "json",
1108
+ codec: codec(`${PACKAGE}#ChannelType`)
1109
+ }],
1110
+ result: codec(`${PACKAGE}#QrLoginStatus`)
1111
+ },
1112
+ {
1113
+ id: `${PACKAGE}#${SERVICE}/getQrLoginStatus`,
1114
+ service: SERVICE,
1115
+ namespace: SERVICE,
1116
+ method: "getQrLoginStatus",
1117
+ invocation: { kind: "direct" },
1118
+ parameters: [{
1119
+ name: "loginId",
1120
+ wire: "loginId",
1121
+ source: "json",
1122
+ codec: codec(`${PACKAGE}#LoginId`)
1123
+ }],
1124
+ result: codec(`${PACKAGE}#QrLoginStatus`)
1125
+ },
1126
+ {
1127
+ id: `${PACKAGE}#${SERVICE}/submitQrVerifyCode`,
1128
+ service: SERVICE,
1129
+ namespace: SERVICE,
1130
+ method: "submitQrVerifyCode",
1131
+ invocation: { kind: "direct" },
1132
+ parameters: [{
1133
+ name: "loginId",
1134
+ wire: "loginId",
1135
+ source: "json",
1136
+ codec: codec(`${PACKAGE}#LoginId`)
1137
+ }, {
1138
+ name: "code",
1139
+ wire: "code",
1140
+ source: "json",
1141
+ codec: codec(`${PACKAGE}#VerifyCode`)
1142
+ }],
1143
+ result: codec(`${PACKAGE}#Bool`)
1144
+ },
1145
+ {
1146
+ id: `${PACKAGE}#${SERVICE}/cancelQrLogin`,
1147
+ service: SERVICE,
1148
+ namespace: SERVICE,
1149
+ method: "cancelQrLogin",
1150
+ invocation: { kind: "direct" },
1151
+ parameters: [{
1152
+ name: "loginId",
1153
+ wire: "loginId",
1154
+ source: "json",
1155
+ codec: codec(`${PACKAGE}#LoginId`)
1156
+ }],
1157
+ result: codec(`${PACKAGE}#Bool`)
1158
+ }
1159
+ ]
1160
+ };
1161
+ /**
1162
+ * Create the config api bound to this browser plugin's context.
1163
+ * Mounts the remote contribution once; every call awaits the mount, then
1164
+ * invokes the namespace service and unwraps the `{ok, value/error}` envelope.
1165
+ */
1166
+ function createConfigApi(ctx) {
1167
+ const mount = ctx.remote.$mount(CONTRIBUTION);
1168
+ const call = async (method, ...args) => {
1169
+ try {
1170
+ await mount;
1171
+ const result = await ctx.get(`remote.${SERVICE}`)[method](...args);
1172
+ if (!result.ok) return {
1173
+ ok: false,
1174
+ code: result.error?.code ?? "remote-error",
1175
+ message: result.error?.message ?? `${SERVICE}.${method} failed`
1176
+ };
1177
+ return {
1178
+ ok: true,
1179
+ ...result.value
1180
+ };
1181
+ } catch (err) {
1182
+ return {
1183
+ ok: false,
1184
+ code: "remote-error",
1185
+ message: err instanceof Error ? err.message : String(err)
1186
+ };
1187
+ }
1188
+ };
1189
+ return {
1190
+ load: () => call("getConfig"),
1191
+ save: (channels, expectedRevision) => call("saveConfig", channels, expectedRevision),
1192
+ statuses: () => call("getChannelStatuses"),
1193
+ startQrLogin: (channelType) => call("startQrLogin", channelType),
1194
+ getQrLoginStatus: (loginId) => call("getQrLoginStatus", loginId),
1195
+ submitQrVerifyCode: (loginId, code) => call("submitQrVerifyCode", loginId, code),
1196
+ cancelQrLogin: (loginId) => call("cancelQrLogin", loginId)
1197
+ };
1198
+ }
1199
+ //#endregion
1200
+ //#region src/client/index.ts
1201
+ /** Section identity in the Settings left-nav rail. */
1202
+ const SECTION_ID = "im-channels";
1203
+ /** Sits below Models (order 10) in the settings nav. */
1204
+ const SECTION_ORDER = 20;
1205
+ /** Services this browser plugin requires (cordis fiber inject). */
1206
+ const inject = [
1207
+ "slots",
1208
+ "locale",
1209
+ "connection",
1210
+ "remote"
1211
+ ];
1212
+ /**
1213
+ * Mount the IM channels settings page.
1214
+ * @param ctx - the browser plugin context (guarded facade).
1215
+ */
1216
+ function apply(ctx) {
1217
+ const configApi = createConfigApi(ctx);
1218
+ ctx.slots.inject("settings.section", () => ctx.slots.register({
1219
+ name: "settings.section",
1220
+ id: SECTION_ID,
1221
+ order: SECTION_ORDER,
1222
+ label: () => "IM 通道",
1223
+ inject: () => ({ configApi })
1224
+ }, ImChannelsSection));
1225
+ }
1226
+ //#endregion
1227
+ exports.apply = apply;
1228
+ exports.inject = inject;
1229
+ return module.exports;
1230
+ }
1231
+ });
1232
+
1233
+ //# sourceMappingURL=client.js.map