@xmanrui/dsh-im 0.1.0 → 0.2.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.
@@ -0,0 +1,433 @@
1
+ import * as React from 'react';
2
+
3
+ import { WhatsappLogoGlyph } from '../../channel-logos.js';
4
+ import { QrActionIcon } from '../../credential-binding.js';
5
+ import { installDingtalkStyles } from '../dingtalk/styles.js';
6
+ import {
7
+ WHATSAPP_ENDPOINTS,
8
+ formatRemaining,
9
+ normalizeProvisioning,
10
+ normalizeSnapshot,
11
+ presentError,
12
+ safeQrSource,
13
+ unwrapRpcResult,
14
+ } from './api.js';
15
+ import { installWhatsappStyles } from './styles.js';
16
+
17
+ const h = React.createElement;
18
+ const ACTIVE_STATES = new Set(['pending', 'connecting']);
19
+
20
+ const Button = React.forwardRef(function Button(
21
+ { children, kind = 'secondary', className = '', ...props },
22
+ ref,
23
+ ) {
24
+ return h('button', {
25
+ ...props,
26
+ ref,
27
+ type: 'button',
28
+ className: `ddt-button ${className}`.trim(),
29
+ 'data-kind': kind,
30
+ }, children);
31
+ });
32
+
33
+ function checkedTime(value) {
34
+ if (!value) return '尚未检查';
35
+ try {
36
+ return new Intl.DateTimeFormat('zh-CN', {
37
+ hour: '2-digit', minute: '2-digit', second: '2-digit',
38
+ }).format(new Date(value));
39
+ } catch {
40
+ return '刚刚';
41
+ }
42
+ }
43
+
44
+ function Heading({ totals, busy, onAdd, addButtonRef }) {
45
+ return h('div', { className: 'ddt-heading' },
46
+ h('div', { className: 'ddt-tools' },
47
+ h('div', { className: 'dim-bindActions' },
48
+ h(Button, {
49
+ kind: 'primary',
50
+ className: 'dim-scanButton',
51
+ onClick: onAdd,
52
+ disabled: busy,
53
+ ref: addButtonRef,
54
+ 'aria-label': '扫码接入 WhatsApp 机器人',
55
+ }, h(QrActionIcon), busy ? '正在接入' : '扫码接入机器人')),
56
+ totals.configured > 0
57
+ ? h('div', { className: 'ddt-badge dim-onlineBadge' },
58
+ h('span', null, `${totals.connected} / ${totals.configured} 在线`))
59
+ : null));
60
+ }
61
+
62
+ function LoadingView() {
63
+ return h('div', {
64
+ className: 'ddt-card ddt-loading dim-surfaceCard dim-loadingView',
65
+ 'aria-busy': 'true',
66
+ }, h('div', { className: 'ddt-spinner dim-spinner' }), '正在读取 WhatsApp 机器人状态…');
67
+ }
68
+
69
+ export function EmptyView({ busy, onStart }) {
70
+ return h('div', { className: 'ddt-card dim-surfaceCard' },
71
+ h('div', { className: 'ddt-cardBody ddt-empty dim-surfaceBody dim-emptyView' },
72
+ h('div', { className: 'dim-emptyCopy' },
73
+ h('div', { className: 'ddt-stateLabel dim-stateLabel' },
74
+ h('span', { className: 'ddt-dot dim-stateDot' }),
75
+ h('span', null, '尚未接入 WhatsApp 机器人')),
76
+ h('h3', null, '扫码绑定 WhatsApp 机器人'),
77
+ h('p', null, '使用手机 WhatsApp 扫描二维码即可接入。'),
78
+ h('div', { className: 'ddt-actions dim-viewActions' },
79
+ h(Button, { kind: 'primary', onClick: onStart, disabled: busy },
80
+ busy ? '正在生成二维码…' : '生成二维码'))),
81
+ h('div', {
82
+ className: 'ddt-brandMark dim-emptyBrand dwa-avatar',
83
+ 'aria-hidden': 'true',
84
+ }, h(WhatsappLogoGlyph, { size: 64 }))));
85
+ }
86
+
87
+ export function QrPanel({ provision, now, busy, onRefresh, onCancel }) {
88
+ const source = safeQrSource(provision.qrCodeDataUrl);
89
+ const remaining = Math.max(0, provision.expiresAt - now);
90
+ const duration = Math.max(1, provision.durationMs ?? 60_000);
91
+ const progress = Math.round(Math.min(1, remaining / duration) * 100);
92
+ return h('div', { className: 'ddt-card dim-surfaceCard' },
93
+ h('div', { className: 'ddt-cardBody ddt-qrLayout dim-surfaceBody dim-qrLayout' },
94
+ h('div', { className: 'ddt-qrColumn dim-qrColumn' },
95
+ h('div', { className: 'ddt-qrFrame dim-qrFrame' },
96
+ source ? h('img', {
97
+ src: source,
98
+ alt: '用于关联 WhatsApp 设备的一次性二维码',
99
+ }) : h('div', { className: 'ddt-qrFallback dim-qrFallback' }, '二维码正在生成…')),
100
+ h('div', { className: 'ddt-countdown dim-countdown' },
101
+ h('div', { className: 'ddt-countdownTop dim-countdownTop' },
102
+ h('span', null, '当前二维码有效时间'),
103
+ h('strong', null, formatRemaining(remaining))),
104
+ h('div', {
105
+ className: 'ddt-progress dim-progress',
106
+ style: { '--ddt-progress': `${progress}%` },
107
+ }, h('span')))),
108
+ h('div', { className: 'ddt-qrCopy dim-qrCopy' },
109
+ h('div', { className: 'ddt-stateLabel dim-stateLabel' },
110
+ h('span', { className: 'ddt-dot dim-stateDot', 'data-tone': 'warning' }),
111
+ h('span', null, '等待 WhatsApp 扫码')),
112
+ h('h3', null, '用手机 WhatsApp 扫描二维码'),
113
+ h('ol', { className: 'ddt-steps dim-steps' },
114
+ h('li', null, '打开 WhatsApp → 设置 → 已关联设备'),
115
+ h('li', null, '点击“关联设备”并扫描左侧二维码')),
116
+ h('div', { className: 'ddt-actions dim-viewActions' },
117
+ h(Button, { onClick: onRefresh, disabled: busy }, '重新生成二维码'),
118
+ h(Button, { kind: 'quiet', onClick: onCancel, disabled: busy }, '取消')))));
119
+ }
120
+
121
+ export function ProvisionView({ provision, busy, onRetry, onClose }) {
122
+ if (provision.status === 'starting' || provision.status === 'connecting') {
123
+ const starting = provision.status === 'starting';
124
+ return h('div', {
125
+ className: 'ddt-card ddt-loading dim-surfaceCard dim-specialView',
126
+ 'aria-busy': 'true',
127
+ }, h('div', { className: 'ddt-spinner dim-spinner' }),
128
+ h('h3', null, starting ? '正在生成 WhatsApp 二维码' : '已扫码,正在连接 WhatsApp'),
129
+ h('p', null, starting
130
+ ? '正在建立安全的关联设备会话。'
131
+ : '关联设备正在接入 DeepSeek Harness。'));
132
+ }
133
+ const error = provision.error ?? {
134
+ code: 'WHATSAPP_PROVISION_FAILED',
135
+ message: 'WhatsApp 没有接入完成',
136
+ };
137
+ return h('div', { className: 'ddt-card dim-surfaceCard' },
138
+ h('div', { className: 'ddt-inlineError dim-inlineError', role: 'alert' },
139
+ h('h3', null, 'WhatsApp 没有接入完成'),
140
+ h('p', null, error.message),
141
+ h('span', { className: 'ddt-errorCode' }, error.code),
142
+ h('div', { className: 'ddt-actions dim-viewActions' },
143
+ h(Button, { kind: 'primary', onClick: onRetry, disabled: busy }, '重新生成二维码'),
144
+ h(Button, { onClick: onClose, disabled: busy }, '关闭'))));
145
+ }
146
+
147
+ function RemoveConfirmation({ account, busy, onConfirm, onCancel }) {
148
+ return h('div', { className: 'ddt-confirm dim-confirm', role: 'alertdialog' },
149
+ h('strong', null, `从 DeepSeek Harness 移除“${account.bot.name}”?`),
150
+ h('p', null, '这会停止消息连接,并删除本机保存的 WhatsApp 关联设备和会话映射。'),
151
+ h('div', { className: 'ddt-actions dim-viewActions' },
152
+ h(Button, { onClick: onCancel, disabled: busy }, '保留机器人'),
153
+ h(Button, { kind: 'danger', onClick: onConfirm, disabled: busy },
154
+ busy ? '正在移除…' : '确认移除接入')));
155
+ }
156
+
157
+ export function WhatsappAccountCard({
158
+ account,
159
+ busy,
160
+ removing,
161
+ onReconnect,
162
+ onRequestRemove,
163
+ onConfirmRemove,
164
+ onCancelRemove,
165
+ }) {
166
+ const state = busy === 'reconnect' ? 'connecting' : account.state;
167
+ const tone = account.connected ? 'success' : state === 'error' ? 'error' : 'warning';
168
+ const stateLabel = account.connected ? '运行正常' : state === 'connecting' ? '正在连接' : '连接未就绪';
169
+ const summary = account.error?.message ?? (account.connected ? null : account.health.summary);
170
+ return h('article', { className: 'ddt-card dim-botCard', 'data-bot-id': account.botId },
171
+ h('div', { className: 'ddt-cardBody dim-botCardBody' },
172
+ h('div', { className: 'ddt-accountTop dim-botCardTop' },
173
+ h('div', { className: 'ddt-accountIdentity dim-botIdentity' },
174
+ h('div', {
175
+ className: 'ddt-avatar dim-botAvatar dwa-avatar',
176
+ 'aria-hidden': 'true',
177
+ }, h(WhatsappLogoGlyph, { size: 29 })),
178
+ h('div', { className: 'dim-botName' },
179
+ h('h3', null, account.bot.name), h('p', null, account.bot.idMasked))),
180
+ h('div', { className: 'ddt-health dim-botHealth' },
181
+ h('span', { className: 'ddt-dot dim-healthDot', 'data-tone': tone }),
182
+ h('span', null, stateLabel))),
183
+ h('dl', { className: 'ddt-metrics dim-botMetrics' },
184
+ h('div', { className: 'ddt-metric dim-botMetric' },
185
+ h('dt', null, '消息通道'),
186
+ h('dd', null, account.connected ? 'WhatsApp Web' : '离线')),
187
+ h('div', { className: 'ddt-metric dim-botMetric' },
188
+ h('dt', null, '最近检查'), h('dd', null, checkedTime(account.health.lastCheckedAt)))),
189
+ h('div', { className: 'ddt-accountFooter dim-cardFooter' },
190
+ summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
191
+ h('div', { className: 'ddt-actions dim-cardActions' },
192
+ h(Button, {
193
+ className: 'dim-cardAction', onClick: onReconnect, disabled: Boolean(busy),
194
+ }, busy === 'reconnect' ? '检查中…' : account.connected ? '检查连接' : '重试连接'),
195
+ h(Button, {
196
+ className: 'dim-cardAction', kind: 'danger', onClick: onRequestRemove, disabled: Boolean(busy),
197
+ }, '移除接入')))),
198
+ removing ? h(RemoveConfirmation, {
199
+ account,
200
+ busy: busy === 'delete',
201
+ onConfirm: onConfirmRemove,
202
+ onCancel: onCancelRemove,
203
+ }) : null);
204
+ }
205
+
206
+ export function WhatsappSettingsTab({ rpcCall }) {
207
+ const [model, setModel] = React.useState({
208
+ phase: 'loading', bots: [], totals: { configured: 0, connected: 0 }, error: null,
209
+ });
210
+ const [provision, setProvision] = React.useState(null);
211
+ const [busy, setBusy] = React.useState(false);
212
+ const [busyByBot, setBusyByBot] = React.useState({});
213
+ const [removeTarget, setRemoveTarget] = React.useState(null);
214
+ const [now, setNow] = React.useState(Date.now());
215
+ const mounted = React.useRef(true);
216
+ const addButtonRef = React.useRef(null);
217
+
218
+ React.useEffect(() => {
219
+ const disposeDingtalk = installDingtalkStyles();
220
+ const disposeWhatsapp = installWhatsappStyles();
221
+ mounted.current = true;
222
+ return () => {
223
+ mounted.current = false;
224
+ disposeWhatsapp();
225
+ disposeDingtalk();
226
+ };
227
+ }, []);
228
+
229
+ const invoke = React.useCallback(async (endpoint, payload = {}, signal) => {
230
+ if (typeof rpcCall !== 'function') throw new TypeError('WhatsApp 设置页缺少 RPC 连接');
231
+ return unwrapRpcResult(await rpcCall(endpoint, payload, signal));
232
+ }, [rpcCall]);
233
+
234
+ const loadStatus = React.useCallback(async ({ signal, silent = false, restore = false } = {}) => {
235
+ if (!silent && mounted.current) setModel((current) => ({ ...current, phase: 'loading', error: null }));
236
+ try {
237
+ const snapshot = normalizeSnapshot(await invoke(WHATSAPP_ENDPOINTS.status, {}, signal));
238
+ if (!mounted.current || signal?.aborted) return undefined;
239
+ setModel({ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null });
240
+ if (restore && snapshot.provisioning) setProvision({
241
+ ...snapshot.provisioning,
242
+ durationMs: Math.max(1, snapshot.provisioning.expiresAt - Date.now()),
243
+ });
244
+ return snapshot;
245
+ } catch (error) {
246
+ if (error?.name !== 'AbortError' && mounted.current && !signal?.aborted) {
247
+ setModel((current) => ({
248
+ ...current,
249
+ phase: silent ? current.phase : 'error',
250
+ error: presentError(error),
251
+ }));
252
+ }
253
+ return undefined;
254
+ }
255
+ }, [invoke]);
256
+
257
+ React.useEffect(() => {
258
+ const controller = new AbortController();
259
+ void loadStatus({ signal: controller.signal, restore: true });
260
+ return () => controller.abort();
261
+ }, [loadStatus]);
262
+
263
+ React.useEffect(() => {
264
+ if (model.phase !== 'ready') return undefined;
265
+ const controller = new AbortController();
266
+ const timer = window.setInterval(
267
+ () => void loadStatus({ signal: controller.signal, silent: true }),
268
+ 15_000,
269
+ );
270
+ return () => { controller.abort(); window.clearInterval(timer); };
271
+ }, [loadStatus, model.phase]);
272
+
273
+ React.useEffect(() => {
274
+ if (!provision || !ACTIVE_STATES.has(provision.status)) return undefined;
275
+ const timer = window.setInterval(() => mounted.current && setNow(Date.now()), 1_000);
276
+ return () => window.clearInterval(timer);
277
+ }, [provision?.attemptId, provision?.status]);
278
+
279
+ const startProvisioning = React.useCallback(async (replace = false) => {
280
+ setBusy(true);
281
+ try {
282
+ if (replace && provision?.attemptId) {
283
+ await invoke(WHATSAPP_ENDPOINTS.cancelProvisioning, { attemptId: provision.attemptId });
284
+ }
285
+ if (!mounted.current) return;
286
+ setProvision({ status: 'starting' });
287
+ const started = normalizeProvisioning(await invoke(WHATSAPP_ENDPOINTS.beginProvisioning, {}));
288
+ if (!mounted.current) return;
289
+ setNow(Date.now());
290
+ setProvision({ ...started, durationMs: Math.max(1, started.expiresAt - Date.now()) });
291
+ } catch (error) {
292
+ if (mounted.current) setProvision({ status: 'failed', error: presentError(error) });
293
+ } finally {
294
+ if (mounted.current) setBusy(false);
295
+ }
296
+ }, [invoke, provision?.attemptId]);
297
+
298
+ const closeProvision = React.useCallback(async () => {
299
+ setBusy(true);
300
+ try {
301
+ if (provision?.attemptId && ACTIVE_STATES.has(provision.status)) {
302
+ await invoke(WHATSAPP_ENDPOINTS.cancelProvisioning, { attemptId: provision.attemptId });
303
+ }
304
+ if (mounted.current) setProvision(null);
305
+ } finally {
306
+ if (mounted.current) setBusy(false);
307
+ }
308
+ }, [invoke, provision?.attemptId, provision?.status]);
309
+
310
+ React.useEffect(() => {
311
+ const attemptId = provision?.attemptId;
312
+ if (!attemptId || !ACTIVE_STATES.has(provision.status)) return undefined;
313
+ const controller = new AbortController();
314
+ let disposed = false;
315
+ let timer;
316
+ const schedule = (delay) => {
317
+ if (disposed || controller.signal.aborted) return;
318
+ timer = window.setTimeout(() => void poll(), delay);
319
+ };
320
+ const poll = async () => {
321
+ try {
322
+ const current = normalizeProvisioning(await invoke(
323
+ WHATSAPP_ENDPOINTS.pollProvisioning,
324
+ { attemptId },
325
+ controller.signal,
326
+ ));
327
+ if (disposed || controller.signal.aborted || !mounted.current) return;
328
+ if (current.status === 'connected') {
329
+ setProvision(null);
330
+ await loadStatus({ signal: controller.signal, silent: true });
331
+ return;
332
+ }
333
+ setProvision((previous) => ({
334
+ ...current,
335
+ durationMs: previous?.durationMs ?? Math.max(1, current.expiresAt - Date.now()),
336
+ }));
337
+ if (ACTIVE_STATES.has(current.status)) schedule(current.pollIntervalMs);
338
+ } catch (error) {
339
+ if (!disposed && !controller.signal.aborted && mounted.current) {
340
+ setProvision({ status: 'failed', error: presentError(error) });
341
+ }
342
+ }
343
+ };
344
+ schedule(provision.pollIntervalMs ?? 1_000);
345
+ return () => {
346
+ disposed = true;
347
+ controller.abort();
348
+ if (timer) window.clearTimeout(timer);
349
+ };
350
+ }, [invoke, loadStatus, provision?.attemptId, provision?.status]);
351
+
352
+ const botAction = React.useCallback(async (account, operation, endpoint, payload) => {
353
+ setBusyByBot((current) => ({ ...current, [account.botId]: operation }));
354
+ try {
355
+ const snapshot = normalizeSnapshot(await invoke(endpoint, payload));
356
+ if (mounted.current) {
357
+ setModel({ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null });
358
+ }
359
+ } finally {
360
+ if (mounted.current) setBusyByBot((current) => {
361
+ const next = { ...current };
362
+ delete next[account.botId];
363
+ return next;
364
+ });
365
+ }
366
+ }, [invoke]);
367
+
368
+ const botList = model.bots.length > 0
369
+ ? h('section', { className: 'dim-listSection' },
370
+ h('div', { className: 'ddt-listHeading dim-listHeading' },
371
+ h('h3', null, '已接入的 WhatsApp 机器人')),
372
+ h('ul', { className: 'ddt-list dim-botList' }, model.bots.map((account) =>
373
+ h('li', { key: account.botId }, h(WhatsappAccountCard, {
374
+ account,
375
+ busy: busyByBot[account.botId],
376
+ removing: removeTarget === account.botId,
377
+ onReconnect: () => void botAction(
378
+ account,
379
+ 'reconnect',
380
+ WHATSAPP_ENDPOINTS.reconnectBot,
381
+ { botId: account.botId },
382
+ ),
383
+ onRequestRemove: () => setRemoveTarget(account.botId),
384
+ onCancelRemove: () => setRemoveTarget(null),
385
+ onConfirmRemove: async () => {
386
+ await botAction(account, 'delete', WHATSAPP_ENDPOINTS.deleteBot, {
387
+ botId: account.botId,
388
+ confirm: true,
389
+ });
390
+ if (mounted.current) setRemoveTarget(null);
391
+ },
392
+ })))))
393
+ : null;
394
+
395
+ return h('section', {
396
+ className: 'ddt-page dwa-page dim-channelPage',
397
+ 'aria-label': 'WhatsApp 设置',
398
+ },
399
+ h(Heading, {
400
+ totals: model.totals,
401
+ busy,
402
+ onAdd: () => void startProvisioning(false),
403
+ addButtonRef,
404
+ }),
405
+ model.phase === 'loading'
406
+ ? h(LoadingView)
407
+ : model.phase === 'error'
408
+ ? h('div', { className: 'ddt-card dim-surfaceCard' },
409
+ h('div', { className: 'ddt-inlineError dim-inlineError' },
410
+ h('h3', null, '无法读取 WhatsApp 机器人状态'),
411
+ h('p', null, model.error?.message),
412
+ h(Button, { onClick: () => void loadStatus() }, '重新读取')))
413
+ : h(React.Fragment, null,
414
+ provision?.status === 'pending'
415
+ ? h(QrPanel, {
416
+ provision,
417
+ now,
418
+ busy,
419
+ onRefresh: () => void startProvisioning(true),
420
+ onCancel: () => void closeProvision(),
421
+ })
422
+ : provision
423
+ ? h(ProvisionView, {
424
+ provision,
425
+ busy,
426
+ onRetry: () => void startProvisioning(true),
427
+ onClose: () => void closeProvision(),
428
+ })
429
+ : model.bots.length === 0
430
+ ? h(EmptyView, { busy, onStart: () => void startProvisioning(false) })
431
+ : null,
432
+ botList));
433
+ }
@@ -0,0 +1,19 @@
1
+ export const WHATSAPP_STYLE_ID = 'xmanrui-dsh-im-whatsapp-settings';
2
+
3
+ const CSS = String.raw`
4
+ .dwa-page { --ddt-accent: #25d366; --ddt-accent-deep: #128c7e; --ddt-accent-wash: #eafbf0; }
5
+ .dwa-avatar { color: #fff; background: #25d366; }
6
+ .dwa-avatar svg { display: block; }
7
+ `;
8
+
9
+ export function installWhatsappStyles() {
10
+ if (typeof document === 'undefined') return () => {};
11
+ const existing = document.querySelector(`style[data-plugin-css="${WHATSAPP_STYLE_ID}"]`);
12
+ if (existing) return () => {};
13
+ const style = document.createElement('style');
14
+ style.dataset.plugin = '@xmanrui/dsh-im';
15
+ style.dataset.pluginCss = WHATSAPP_STYLE_ID;
16
+ style.textContent = CSS;
17
+ document.head.appendChild(style);
18
+ return () => style.remove();
19
+ }
@@ -8,6 +8,7 @@ import {
8
8
  TelegramLogoGlyph,
9
9
  WecomLogoGlyph,
10
10
  WeixinLogoGlyph,
11
+ WhatsappLogoGlyph,
11
12
  } from './channel-logos.js';
12
13
  import { DINGTALK_RPC_CHANNEL } from './channels/dingtalk/api.js';
13
14
  import { DingtalkSettingsTab } from './channels/dingtalk/index.js';
@@ -29,6 +30,9 @@ import { installWecomStyles } from './channels/wecom/styles.js';
29
30
  import { WeixinSettingsTab } from './channels/weixin/index.js';
30
31
  import { WEIXIN_RPC_CHANNEL } from './channels/weixin/api.js';
31
32
  import { installWeixinStyles } from './channels/weixin/styles.js';
33
+ import { WHATSAPP_RPC_CHANNEL } from './channels/whatsapp/api.js';
34
+ import { WhatsappSettingsTab } from './channels/whatsapp/index.js';
35
+ import { installWhatsappStyles } from './channels/whatsapp/styles.js';
32
36
  import { installImStyles } from './styles.js';
33
37
 
34
38
  const h = React.createElement;
@@ -44,6 +48,7 @@ const CHANNELS = Object.freeze([
44
48
  { id: 'qq', label: 'QQ' },
45
49
  { id: 'telegram', label: 'Telegram' },
46
50
  { id: 'discord', label: 'Discord' },
51
+ { id: 'whatsapp', label: 'WhatsApp' },
47
52
  ]);
48
53
 
49
54
  function WeixinLogo() {
@@ -79,6 +84,11 @@ function DiscordLogo() {
79
84
  h(DiscordLogoGlyph));
80
85
  }
81
86
 
87
+ function WhatsappLogo() {
88
+ return h('span', { className: 'dim-logo dim-logoWhatsapp', 'aria-hidden': 'true' },
89
+ h(WhatsappLogoGlyph));
90
+ }
91
+
82
92
  function ChannelLogo({ channel }) {
83
93
  if (channel === 'weixin') return h(WeixinLogo);
84
94
  if (channel === 'feishu') return h(FeishuLogo);
@@ -86,7 +96,8 @@ function ChannelLogo({ channel }) {
86
96
  if (channel === 'wecom') return h(WecomLogo);
87
97
  if (channel === 'qq') return h(QqLogo);
88
98
  if (channel === 'telegram') return h(TelegramLogo);
89
- return h(DiscordLogo);
99
+ if (channel === 'discord') return h(DiscordLogo);
100
+ return h(WhatsappLogo);
90
101
  }
91
102
 
92
103
  export function IMSettingsTab({
@@ -97,6 +108,7 @@ export function IMSettingsTab({
97
108
  telegramRpcCall,
98
109
  wecomRpcCall,
99
110
  weixinRpcCall,
111
+ whatsappRpcCall,
100
112
  }) {
101
113
  const [selected, setSelected] = React.useState('weixin');
102
114
  const active = CHANNELS.find((channel) => channel.id === selected) ?? CHANNELS[0];
@@ -138,7 +150,9 @@ export function IMSettingsTab({
138
150
  ? h(QqSettingsTab, { rpcCall: qqRpcCall })
139
151
  : active.id === 'telegram'
140
152
  ? h(TelegramSettingsTab, { rpcCall: telegramRpcCall })
141
- : h(DiscordSettingsTab, { rpcCall: discordRpcCall })),
153
+ : active.id === 'discord'
154
+ ? h(DiscordSettingsTab, { rpcCall: discordRpcCall })
155
+ : h(WhatsappSettingsTab, { rpcCall: whatsappRpcCall })),
142
156
  ),
143
157
  );
144
158
  }
@@ -152,6 +166,7 @@ export function apply(ctx) {
152
166
  installQqStyles(),
153
167
  installTelegramStyles(),
154
168
  installDiscordStyles(),
169
+ installWhatsappStyles(),
155
170
  installImStyles(),
156
171
  ];
157
172
  return () => {
@@ -173,6 +188,8 @@ export function apply(ctx) {
173
188
  ctx.connection.rpc.call(TELEGRAM_RPC_CHANNEL, endpoint, payload, signal);
174
189
  const discordRpcCall = (endpoint, payload, signal) =>
175
190
  ctx.connection.rpc.call(DISCORD_RPC_CHANNEL, endpoint, payload, signal);
191
+ const whatsappRpcCall = (endpoint, payload, signal) =>
192
+ ctx.connection.rpc.call(WHATSAPP_RPC_CHANNEL, endpoint, payload, signal);
176
193
 
177
194
  ctx.slots.inject('settings.plugins.tab', () => ctx.slots.register({
178
195
  name: 'settings.plugins.tab',
@@ -187,6 +204,7 @@ export function apply(ctx) {
187
204
  telegramRpcCall,
188
205
  wecomRpcCall,
189
206
  weixinRpcCall,
207
+ whatsappRpcCall,
190
208
  }),
191
209
  }, IMSettingsTab));
192
210
  }
@@ -37,11 +37,13 @@ const CSS = String.raw`
37
37
  .dim-logoTelegram svg { width: 21px; height: 21px; }
38
38
  .dim-logoDiscord { color: white; background: #5865f2; }
39
39
  .dim-logoDiscord svg { width: 21px; height: 21px; }
40
+ .dim-logoWhatsapp { color: white; background: #25d366; }
41
+ .dim-logoWhatsapp svg { width: 21px; height: 21px; }
40
42
  .dim-channelCopy { min-width: 0; display: block; }
41
43
  .dim-channelCopy strong { overflow: hidden; color: inherit; font-size: 14px; line-height: 20px; font-weight: 680; text-overflow: ellipsis; white-space: nowrap; }
42
44
  .dim-divider { width: 1px; min-height: 520px; background: var(--dsw-alias-line-divider, #eef0f3); }
43
45
  .dim-panel { min-width: 0; container-type: inline-size; }
44
- .dim-panel .bxf-page, .dim-panel .dxw-page, .dim-panel .ddt-page, .dim-panel .dqq-page, .dim-panel .dwecom-page { width: 100%; max-width: none; padding: 0 0 24px; }
46
+ .dim-panel .bxf-page, .dim-panel .dxw-page, .dim-panel .ddt-page, .dim-panel .dqq-page, .dim-panel .dwecom-page, .dim-panel .dwa-page { width: 100%; max-width: none; padding: 0 0 24px; }
45
47
  .dim-panel .bxf-heading, .dim-panel .dxw-heading, .dim-panel .ddt-heading { justify-content: flex-end; }
46
48
  .dim-panel .bxf-headingTools, .dim-panel .dxw-tools, .dim-panel .ddt-tools { width: 100%; justify-content: space-between; }
47
49
  .dim-panel .dim-bindActions { min-width: 0; display: flex; align-items: center; flex-wrap: nowrap; gap: 8px; }
@@ -7,6 +7,14 @@ import { build } from 'esbuild';
7
7
  const sourceDirectory = dirname(fileURLToPath(import.meta.url));
8
8
  const packageRoot = resolve(sourceDirectory, '../..');
9
9
  const outputPath = resolve(packageRoot, 'lib/index.js');
10
+ const externalRuntimePackages = [
11
+ '@tencent-connect/qqbot-connector',
12
+ '@tencent-connect/qqbot-nodejs',
13
+ '@wecom/aibot-node-sdk',
14
+ 'dingtalk-stream',
15
+ 'qrcode',
16
+ ];
17
+ const external = externalRuntimePackages.flatMap((name) => [name, `${name}/*`]);
10
18
 
11
19
  await mkdir(dirname(outputPath), { recursive: true });
12
20
  await build({
@@ -15,10 +23,22 @@ await build({
15
23
  format: 'esm',
16
24
  platform: 'node',
17
25
  target: ['node22'],
18
- packages: 'external',
26
+ mainFields: ['module', 'main'],
27
+ external,
19
28
  outfile: outputPath,
29
+ banner: {
30
+ js: [
31
+ "import { createRequire as __dshCreateRequire } from 'node:module';",
32
+ "import { dirname as __dshDirname } from 'node:path';",
33
+ "import { fileURLToPath as __dshFileURLToPath } from 'node:url';",
34
+ 'const require = __dshCreateRequire(import.meta.url);',
35
+ 'const __filename = __dshFileURLToPath(import.meta.url);',
36
+ 'const __dirname = __dshDirname(__filename);',
37
+ ].join('\n'),
38
+ },
20
39
  sourcemap: false,
21
- legalComments: 'none',
40
+ minify: true,
41
+ legalComments: 'eof',
22
42
  });
23
43
 
24
44
  console.log(`Wrote ${outputPath}`);
@@ -0,0 +1,35 @@
1
+ import { createProductionController } from './production.mjs';
2
+ import { installWhatsappRpc } from './rpc.mjs';
3
+
4
+ export const name = 'dsh-im-whatsapp-host';
5
+ export const inject = ['connection', 'webServer'];
6
+
7
+ export async function apply(ctx, config = {}) {
8
+ if (config?.controller) {
9
+ return installWhatsappRpc(ctx, config.controller, config.rpcOptions, config.rpcAuthority);
10
+ }
11
+ const production = await createProductionController(ctx, config, config.internals ?? {});
12
+ const disposeRpc = installWhatsappRpc(
13
+ ctx,
14
+ production.controller,
15
+ config.rpcOptions,
16
+ config.rpcAuthority,
17
+ );
18
+ ctx.effect(() => async () => production.close(), 'dsh-im: close WhatsApp Web connections');
19
+ return disposeRpc;
20
+ }
21
+
22
+ export function createWhatsappHostPlugin(config) {
23
+ return Object.freeze({ name, inject, apply: (ctx) => apply(ctx, config) });
24
+ }
25
+
26
+ export { createProductionController } from './production.mjs';
27
+ export {
28
+ WHATSAPP_ENDPOINTS,
29
+ WHATSAPP_RPC_CHANNEL,
30
+ WHATSAPP_RPC_ENDPOINTS,
31
+ createWhatsappRpcHandler,
32
+ installWhatsappRpc,
33
+ } from './rpc.mjs';
34
+ export { WhatsappController } from '../../../../src/channels/whatsapp/whatsapp-controller.mjs';
35
+ export { WhatsappRuntime } from '../../../../src/channels/whatsapp/whatsapp-runtime.mjs';