@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/LICENSE +21 -0
- package/README.md +183 -0
- package/README.zh-CN.md +207 -0
- package/cordis.patch.yml +24 -0
- package/lib/client.js +1233 -0
- package/lib/index-2mMnMMFx.d.ts +304 -0
- package/lib/index.js +3623 -0
- package/package.json +103 -0
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
|