@xmanrui/dsh-im 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.
Files changed (133) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +143 -0
  3. package/THIRD_PARTY_NOTICES.md +15 -0
  4. package/bin/dsh-im.mjs +99 -0
  5. package/cordis.patch.yml +3 -0
  6. package/lib/client.js +6211 -0
  7. package/lib/index.js +13339 -0
  8. package/package.json +73 -0
  9. package/plugin-src/client/build.mjs +38 -0
  10. package/plugin-src/client/channel-logos.js +107 -0
  11. package/plugin-src/client/channels/dingtalk/api.js +203 -0
  12. package/plugin-src/client/channels/dingtalk/index.js +709 -0
  13. package/plugin-src/client/channels/dingtalk/styles.js +140 -0
  14. package/plugin-src/client/channels/discord/api.js +11 -0
  15. package/plugin-src/client/channels/discord/index.js +25 -0
  16. package/plugin-src/client/channels/discord/styles.js +19 -0
  17. package/plugin-src/client/channels/feishu/api.js +325 -0
  18. package/plugin-src/client/channels/feishu/index.js +986 -0
  19. package/plugin-src/client/channels/feishu/styles.js +505 -0
  20. package/plugin-src/client/channels/qq/api.js +119 -0
  21. package/plugin-src/client/channels/qq/index.js +402 -0
  22. package/plugin-src/client/channels/qq/styles.js +19 -0
  23. package/plugin-src/client/channels/shared/token-api.js +88 -0
  24. package/plugin-src/client/channels/shared/token-channel.js +277 -0
  25. package/plugin-src/client/channels/telegram/api.js +11 -0
  26. package/plugin-src/client/channels/telegram/index.js +25 -0
  27. package/plugin-src/client/channels/telegram/styles.js +19 -0
  28. package/plugin-src/client/channels/wecom/api.js +119 -0
  29. package/plugin-src/client/channels/wecom/index.js +402 -0
  30. package/plugin-src/client/channels/wecom/styles.js +19 -0
  31. package/plugin-src/client/channels/weixin/api.js +156 -0
  32. package/plugin-src/client/channels/weixin/index.js +577 -0
  33. package/plugin-src/client/channels/weixin/styles.js +120 -0
  34. package/plugin-src/client/credential-binding.js +117 -0
  35. package/plugin-src/client/index.js +192 -0
  36. package/plugin-src/client/lifecycle.js +86 -0
  37. package/plugin-src/client/styles.js +192 -0
  38. package/plugin-src/host/build.mjs +24 -0
  39. package/plugin-src/host/channels/dingtalk/connection-supervisor.mjs +130 -0
  40. package/plugin-src/host/channels/dingtalk/index.mjs +39 -0
  41. package/plugin-src/host/channels/dingtalk/production.mjs +122 -0
  42. package/plugin-src/host/channels/dingtalk/rpc.mjs +237 -0
  43. package/plugin-src/host/channels/discord/index.mjs +30 -0
  44. package/plugin-src/host/channels/discord/production.mjs +17 -0
  45. package/plugin-src/host/channels/discord/rpc.mjs +21 -0
  46. package/plugin-src/host/channels/feishu/connection-supervisor.mjs +167 -0
  47. package/plugin-src/host/channels/feishu/controller.mjs +172 -0
  48. package/plugin-src/host/channels/feishu/credential-store.mjs +84 -0
  49. package/plugin-src/host/channels/feishu/index.mjs +73 -0
  50. package/plugin-src/host/channels/feishu/production.mjs +138 -0
  51. package/plugin-src/host/channels/feishu/rpc.mjs +454 -0
  52. package/plugin-src/host/channels/qq/connection-supervisor.mjs +124 -0
  53. package/plugin-src/host/channels/qq/index.mjs +30 -0
  54. package/plugin-src/host/channels/qq/production.mjs +109 -0
  55. package/plugin-src/host/channels/qq/rpc.mjs +146 -0
  56. package/plugin-src/host/channels/shared/connection-supervisor.mjs +133 -0
  57. package/plugin-src/host/channels/shared/production.mjs +104 -0
  58. package/plugin-src/host/channels/shared/rpc.mjs +119 -0
  59. package/plugin-src/host/channels/telegram/index.mjs +30 -0
  60. package/plugin-src/host/channels/telegram/production.mjs +17 -0
  61. package/plugin-src/host/channels/telegram/rpc.mjs +21 -0
  62. package/plugin-src/host/channels/wecom/connection-supervisor.mjs +124 -0
  63. package/plugin-src/host/channels/wecom/index.mjs +36 -0
  64. package/plugin-src/host/channels/wecom/production.mjs +113 -0
  65. package/plugin-src/host/channels/wecom/rpc.mjs +151 -0
  66. package/plugin-src/host/channels/weixin/connection-supervisor.mjs +127 -0
  67. package/plugin-src/host/channels/weixin/index.mjs +39 -0
  68. package/plugin-src/host/channels/weixin/production.mjs +119 -0
  69. package/plugin-src/host/channels/weixin/rpc.mjs +178 -0
  70. package/plugin-src/host/index.mjs +44 -0
  71. package/plugin-src/host/rpc-authority.mjs +11 -0
  72. package/scripts/verify-package.mjs +97 -0
  73. package/src/channels/dingtalk/config-store.mjs +282 -0
  74. package/src/channels/dingtalk/device-auth.mjs +237 -0
  75. package/src/channels/dingtalk/dingtalk-api.mjs +579 -0
  76. package/src/channels/dingtalk/dingtalk-bridge.mjs +281 -0
  77. package/src/channels/dingtalk/dingtalk-card-stream.mjs +233 -0
  78. package/src/channels/dingtalk/dingtalk-controller.mjs +752 -0
  79. package/src/channels/dingtalk/dingtalk-runtime.mjs +358 -0
  80. package/src/channels/dingtalk/harness-client.mjs +299 -0
  81. package/src/channels/dingtalk/state-store.mjs +212 -0
  82. package/src/channels/discord/config-store.mjs +24 -0
  83. package/src/channels/discord/discord-api.mjs +153 -0
  84. package/src/channels/discord/discord-bridge.mjs +15 -0
  85. package/src/channels/discord/discord-controller.mjs +16 -0
  86. package/src/channels/discord/discord-runtime.mjs +460 -0
  87. package/src/channels/discord/harness-client.mjs +3 -0
  88. package/src/channels/discord/state-store.mjs +3 -0
  89. package/src/channels/feishu/bridge.mjs +216 -0
  90. package/src/channels/feishu/config.mjs +71 -0
  91. package/src/channels/feishu/feishu-app.mjs +51 -0
  92. package/src/channels/feishu/feishu-channel.mjs +153 -0
  93. package/src/channels/feishu/feishu-runtime.mjs +222 -0
  94. package/src/channels/feishu/harness-client.mjs +268 -0
  95. package/src/channels/feishu/message-utils.mjs +51 -0
  96. package/src/channels/feishu/multi-bot-controller.mjs +686 -0
  97. package/src/channels/feishu/plugin-config-store.mjs +204 -0
  98. package/src/channels/feishu/plugin-controller.mjs +248 -0
  99. package/src/channels/feishu/registration-manager.mjs +345 -0
  100. package/src/channels/feishu/state-store.mjs +71 -0
  101. package/src/channels/qq/config-store.mjs +171 -0
  102. package/src/channels/qq/harness-client.mjs +3 -0
  103. package/src/channels/qq/qq-bridge.mjs +177 -0
  104. package/src/channels/qq/qq-controller.mjs +452 -0
  105. package/src/channels/qq/qq-runtime.mjs +208 -0
  106. package/src/channels/qq/qr-auth.mjs +24 -0
  107. package/src/channels/qq/state-store.mjs +96 -0
  108. package/src/channels/shared/conversation-state-store.mjs +107 -0
  109. package/src/channels/shared/editable-message-stream.mjs +81 -0
  110. package/src/channels/shared/text-harness-bridge.mjs +180 -0
  111. package/src/channels/shared/token-bot-controller.mjs +298 -0
  112. package/src/channels/shared/token-config-store.mjs +180 -0
  113. package/src/channels/telegram/config-store.mjs +24 -0
  114. package/src/channels/telegram/harness-client.mjs +3 -0
  115. package/src/channels/telegram/state-store.mjs +3 -0
  116. package/src/channels/telegram/telegram-api.mjs +120 -0
  117. package/src/channels/telegram/telegram-bridge.mjs +15 -0
  118. package/src/channels/telegram/telegram-controller.mjs +16 -0
  119. package/src/channels/telegram/telegram-runtime.mjs +278 -0
  120. package/src/channels/wecom/config-store.mjs +170 -0
  121. package/src/channels/wecom/harness-client.mjs +3 -0
  122. package/src/channels/wecom/qr-auth.mjs +95 -0
  123. package/src/channels/wecom/state-store.mjs +90 -0
  124. package/src/channels/wecom/wecom-bridge.mjs +243 -0
  125. package/src/channels/wecom/wecom-controller.mjs +474 -0
  126. package/src/channels/wecom/wecom-runtime.mjs +209 -0
  127. package/src/channels/weixin/config-store.mjs +182 -0
  128. package/src/channels/weixin/harness-client.mjs +259 -0
  129. package/src/channels/weixin/state-store.mjs +112 -0
  130. package/src/channels/weixin/weixin-api.mjs +319 -0
  131. package/src/channels/weixin/weixin-bridge.mjs +173 -0
  132. package/src/channels/weixin/weixin-controller.mjs +545 -0
  133. package/src/channels/weixin/weixin-runtime.mjs +204 -0
@@ -0,0 +1,277 @@
1
+ import * as React from 'react';
2
+
3
+ import { CredentialActionIcon, CredentialBindingPanel } from '../../credential-binding.js';
4
+ import { installDingtalkStyles } from '../dingtalk/styles.js';
5
+
6
+ const h = React.createElement;
7
+
8
+ const Button = React.forwardRef(function Button(
9
+ { children, kind = 'secondary', className = '', ...props },
10
+ ref,
11
+ ) {
12
+ return h('button', {
13
+ ...props,
14
+ ref,
15
+ type: 'button',
16
+ className: `ddt-button ${className}`.trim(),
17
+ 'data-kind': kind,
18
+ }, children);
19
+ });
20
+
21
+ function checkedTime(value) {
22
+ if (!value) return '尚未检查';
23
+ try {
24
+ return new Intl.DateTimeFormat('zh-CN', {
25
+ hour: '2-digit', minute: '2-digit', second: '2-digit',
26
+ }).format(new Date(value));
27
+ } catch {
28
+ return '刚刚';
29
+ }
30
+ }
31
+
32
+ export function createTokenChannelSettings(definition) {
33
+ const {
34
+ channel,
35
+ endpoints,
36
+ api,
37
+ LogoGlyph,
38
+ installStyles,
39
+ pageClass,
40
+ avatarClass,
41
+ connectionLabel,
42
+ tokenPlaceholder,
43
+ emptyTitle,
44
+ emptyDescription,
45
+ platformLabel,
46
+ } = definition;
47
+
48
+ function AccountCard({ account, busy, removing, onReconnect, onRequestRemove, onConfirmRemove, onCancelRemove }) {
49
+ const state = busy === 'reconnect' ? 'connecting' : account.state;
50
+ const tone = account.connected ? 'success' : state === 'error' ? 'error' : 'warning';
51
+ const stateLabel = account.connected ? '运行正常' : state === 'connecting' ? '正在连接' : '连接未就绪';
52
+ const summary = account.error?.message ?? (account.connected ? null : account.health.summary);
53
+ const identity = account.bot.username ? `@${account.bot.username}` : account.bot.idMasked;
54
+ return h('article', { className: 'ddt-card dim-botCard', 'data-bot-id': account.botId },
55
+ h('div', { className: 'ddt-cardBody dim-botCardBody' },
56
+ h('div', { className: 'ddt-accountTop dim-botCardTop' },
57
+ h('div', { className: 'ddt-accountIdentity dim-botIdentity' },
58
+ h('div', { className: `ddt-avatar dim-botAvatar ${avatarClass}`, 'aria-hidden': 'true' },
59
+ h(LogoGlyph, { size: 29 })),
60
+ h('div', { className: 'dim-botName' },
61
+ h('h3', null, account.bot.name), h('p', null, identity))),
62
+ h('div', { className: 'ddt-health dim-botHealth' },
63
+ h('span', { className: 'ddt-dot dim-healthDot', 'data-tone': tone }),
64
+ h('span', null, stateLabel))),
65
+ h('dl', { className: 'ddt-metrics dim-botMetrics' },
66
+ h('div', { className: 'ddt-metric dim-botMetric' },
67
+ h('dt', null, '消息通道'), h('dd', null, account.connected ? connectionLabel : '离线')),
68
+ h('div', { className: 'ddt-metric dim-botMetric' },
69
+ h('dt', null, '最近检查'), h('dd', null, checkedTime(account.health.lastCheckedAt)))),
70
+ h('div', { className: 'ddt-accountFooter dim-cardFooter' },
71
+ summary ? h('div', { className: 'ddt-summary dim-cardSummary' }, summary) : null,
72
+ h('div', { className: 'ddt-actions dim-cardActions' },
73
+ h(Button, {
74
+ className: 'dim-cardAction',
75
+ onClick: onReconnect,
76
+ disabled: Boolean(busy),
77
+ }, busy === 'reconnect' ? '检查中…' : account.connected ? '检查连接' : '重试连接'),
78
+ h(Button, {
79
+ className: 'dim-cardAction',
80
+ kind: 'danger',
81
+ onClick: onRequestRemove,
82
+ disabled: Boolean(busy),
83
+ }, '移除接入')))),
84
+ removing ? h('div', { className: 'ddt-confirm dim-confirm', role: 'alertdialog' },
85
+ h('strong', null, `从 DeepSeek Harness 移除“${account.bot.name}”?`),
86
+ h('p', null, `这会停止消息连接,并删除本机保存的 Bot Token、机器人配置及会话映射。${platformLabel}中的机器人不会被自动删除。`),
87
+ h('div', { className: 'ddt-actions dim-viewActions' },
88
+ h(Button, { onClick: onCancelRemove, disabled: Boolean(busy) }, '保留机器人'),
89
+ h(Button, { kind: 'danger', onClick: onConfirmRemove, disabled: Boolean(busy) },
90
+ busy === 'delete' ? '正在移除…' : '确认移除接入'))) : null);
91
+ }
92
+
93
+ function SettingsTab({ rpcCall }) {
94
+ const [model, setModel] = React.useState({
95
+ phase: 'loading', bots: [], totals: { configured: 0, connected: 0 }, error: null,
96
+ });
97
+ const [credentialOpen, setCredentialOpen] = React.useState(false);
98
+ const [credentialError, setCredentialError] = React.useState(null);
99
+ const [busy, setBusy] = React.useState(false);
100
+ const [busyByBot, setBusyByBot] = React.useState({});
101
+ const [removeTarget, setRemoveTarget] = React.useState(null);
102
+ const mounted = React.useRef(true);
103
+
104
+ React.useEffect(() => {
105
+ const disposeDingtalk = installDingtalkStyles();
106
+ const disposeChannel = installStyles();
107
+ mounted.current = true;
108
+ return () => {
109
+ mounted.current = false;
110
+ disposeChannel();
111
+ disposeDingtalk();
112
+ };
113
+ }, []);
114
+
115
+ const invoke = React.useCallback(async (endpoint, payload = {}, signal) => {
116
+ if (typeof rpcCall !== 'function') throw new TypeError(`${channel} 设置页缺少 RPC 连接`);
117
+ return api.unwrapRpcResult(await rpcCall(endpoint, payload, signal));
118
+ }, [rpcCall]);
119
+
120
+ const loadStatus = React.useCallback(async ({ signal, silent = false } = {}) => {
121
+ if (!silent && mounted.current) setModel((current) => ({ ...current, phase: 'loading', error: null }));
122
+ try {
123
+ const snapshot = api.normalizeSnapshot(await invoke(endpoints.status, {}, signal));
124
+ if (!mounted.current || signal?.aborted) return;
125
+ setModel({ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null });
126
+ } catch (error) {
127
+ if (error?.name !== 'AbortError' && mounted.current && !signal?.aborted) {
128
+ setModel((current) => ({
129
+ ...current,
130
+ phase: silent ? current.phase : 'error',
131
+ error: api.presentError(error),
132
+ }));
133
+ }
134
+ }
135
+ }, [invoke]);
136
+
137
+ React.useEffect(() => {
138
+ const controller = new AbortController();
139
+ void loadStatus({ signal: controller.signal });
140
+ return () => controller.abort();
141
+ }, [loadStatus]);
142
+
143
+ React.useEffect(() => {
144
+ if (model.phase !== 'ready') return undefined;
145
+ const controller = new AbortController();
146
+ const timer = window.setInterval(
147
+ () => void loadStatus({ signal: controller.signal, silent: true }),
148
+ 15_000,
149
+ );
150
+ return () => {
151
+ controller.abort();
152
+ window.clearInterval(timer);
153
+ };
154
+ }, [loadStatus, model.phase]);
155
+
156
+ const bindCredentials = React.useCallback(async ({ secret }) => {
157
+ setBusy(true);
158
+ setCredentialError(null);
159
+ try {
160
+ const snapshot = api.normalizeSnapshot(await invoke(endpoints.bindCredentials, { token: secret }));
161
+ if (!mounted.current) return;
162
+ setModel({ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null });
163
+ setCredentialOpen(false);
164
+ } catch (error) {
165
+ if (mounted.current) setCredentialError(api.presentError(error));
166
+ } finally {
167
+ if (mounted.current) setBusy(false);
168
+ }
169
+ }, [invoke]);
170
+
171
+ const botAction = React.useCallback(async (account, operation, endpoint, payload) => {
172
+ setBusyByBot((current) => ({ ...current, [account.botId]: operation }));
173
+ try {
174
+ const snapshot = api.normalizeSnapshot(await invoke(endpoint, payload));
175
+ if (mounted.current) {
176
+ setModel({ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null });
177
+ }
178
+ } finally {
179
+ if (mounted.current) setBusyByBot((current) => {
180
+ const next = { ...current };
181
+ delete next[account.botId];
182
+ return next;
183
+ });
184
+ }
185
+ }, [invoke]);
186
+
187
+ const botList = model.bots.length > 0
188
+ ? h('section', { className: 'dim-listSection' },
189
+ h('div', { className: 'ddt-listHeading dim-listHeading' },
190
+ h('h3', null, `已接入的 ${channel} 机器人`)),
191
+ h('ul', { className: 'ddt-list dim-botList' }, model.bots.map((account) =>
192
+ h('li', { key: account.botId }, h(AccountCard, {
193
+ account,
194
+ busy: busyByBot[account.botId],
195
+ removing: removeTarget === account.botId,
196
+ onReconnect: () => void botAction(
197
+ account,
198
+ 'reconnect',
199
+ endpoints.reconnectBot,
200
+ { botId: account.botId },
201
+ ),
202
+ onRequestRemove: () => setRemoveTarget(account.botId),
203
+ onCancelRemove: () => setRemoveTarget(null),
204
+ onConfirmRemove: async () => {
205
+ await botAction(account, 'delete', endpoints.deleteBot, {
206
+ botId: account.botId,
207
+ confirm: true,
208
+ });
209
+ if (mounted.current) setRemoveTarget(null);
210
+ },
211
+ })))))
212
+ : null;
213
+
214
+ return h('section', {
215
+ className: `ddt-page ${pageClass} dim-channelPage`,
216
+ 'aria-label': `${channel} 设置`,
217
+ },
218
+ h('div', { className: 'ddt-heading' },
219
+ h('div', { className: 'ddt-tools' },
220
+ h('div', { className: 'dim-bindActions' },
221
+ h(Button, {
222
+ kind: 'credential',
223
+ className: 'dim-credentialButton',
224
+ onClick: () => { setCredentialOpen((value) => !value); setCredentialError(null); },
225
+ disabled: busy,
226
+ 'aria-pressed': credentialOpen,
227
+ 'aria-label': `使用 Bot Token 接入 ${channel} 机器人`,
228
+ }, h(CredentialActionIcon), credentialOpen ? '收起凭据' : '手动接入')),
229
+ model.totals.configured > 0
230
+ ? h('div', { className: 'ddt-badge dim-onlineBadge' },
231
+ h('span', null, `${model.totals.connected} / ${model.totals.configured} 在线`))
232
+ : null)),
233
+ model.phase === 'loading'
234
+ ? h('div', {
235
+ className: 'ddt-card ddt-loading dim-surfaceCard dim-loadingView',
236
+ 'aria-busy': 'true',
237
+ }, h('div', { className: 'ddt-spinner dim-spinner' }), `正在读取 ${channel} 机器人状态…`)
238
+ : model.phase === 'error'
239
+ ? h('div', { className: 'ddt-card dim-surfaceCard' },
240
+ h('div', { className: 'ddt-inlineError dim-inlineError' },
241
+ h('h3', null, `无法读取 ${channel} 机器人状态`),
242
+ h('p', null, model.error?.message),
243
+ h(Button, { onClick: () => void loadStatus() }, '重新读取')))
244
+ : h(React.Fragment, null,
245
+ credentialOpen ? h(CredentialBindingPanel, {
246
+ channel,
247
+ secretLabel: 'Bot Token',
248
+ secretPlaceholder: tokenPlaceholder,
249
+ busy,
250
+ error: credentialError,
251
+ onSubmit: bindCredentials,
252
+ onCancel: () => { setCredentialOpen(false); setCredentialError(null); },
253
+ }) : null,
254
+ model.bots.length === 0 && !credentialOpen
255
+ ? h('div', { className: 'ddt-card dim-surfaceCard' },
256
+ h('div', { className: 'ddt-cardBody ddt-empty dim-surfaceBody dim-emptyView' },
257
+ h('div', { className: 'dim-emptyCopy' },
258
+ h('div', { className: 'ddt-stateLabel dim-stateLabel' },
259
+ h('span', { className: 'ddt-dot dim-stateDot' }),
260
+ h('span', null, `尚未接入 ${channel} 机器人`)),
261
+ h('h3', null, emptyTitle),
262
+ h('p', null, emptyDescription),
263
+ h('div', { className: 'ddt-actions dim-viewActions' },
264
+ h(Button, {
265
+ kind: 'primary',
266
+ onClick: () => setCredentialOpen(true),
267
+ }, '填写 Bot Token'))),
268
+ h('div', {
269
+ className: `ddt-brandMark dim-emptyBrand ${avatarClass}`,
270
+ 'aria-hidden': 'true',
271
+ }, h(LogoGlyph, { size: 64 }))))
272
+ : null,
273
+ botList));
274
+ }
275
+
276
+ return { SettingsTab, AccountCard };
277
+ }
@@ -0,0 +1,11 @@
1
+ import { TOKEN_BOT_ENDPOINTS, createTokenChannelApi } from '../shared/token-api.js';
2
+
3
+ export const TELEGRAM_RPC_CHANNEL = '/telegram';
4
+ export const TELEGRAM_ENDPOINTS = TOKEN_BOT_ENDPOINTS;
5
+
6
+ const api = createTokenChannelApi('Telegram', ' Bot API 长轮询');
7
+
8
+ export const unwrapRpcResult = api.unwrapRpcResult;
9
+ export const normalizeSnapshot = api.normalizeSnapshot;
10
+ export const presentError = api.presentError;
11
+ export { api as telegramClientApi };
@@ -0,0 +1,25 @@
1
+ import { TelegramLogoGlyph } from '../../channel-logos.js';
2
+ import { createTokenChannelSettings } from '../shared/token-channel.js';
3
+ import {
4
+ TELEGRAM_ENDPOINTS,
5
+ telegramClientApi,
6
+ } from './api.js';
7
+ import { installTelegramStyles } from './styles.js';
8
+
9
+ const channel = createTokenChannelSettings({
10
+ channel: 'Telegram',
11
+ endpoints: TELEGRAM_ENDPOINTS,
12
+ api: telegramClientApi,
13
+ LogoGlyph: TelegramLogoGlyph,
14
+ installStyles: installTelegramStyles,
15
+ pageClass: 'dtg-page',
16
+ avatarClass: 'dtg-avatar',
17
+ connectionLabel: 'Bot API 长轮询',
18
+ tokenPlaceholder: '填写 @BotFather 生成的 Bot Token',
19
+ emptyTitle: '接入 Telegram 机器人',
20
+ emptyDescription: '先通过 @BotFather 获取 Bot Token,再在这里完成接入。',
21
+ platformLabel: 'Telegram',
22
+ });
23
+
24
+ export const TelegramSettingsTab = channel.SettingsTab;
25
+ export const TelegramAccountCard = channel.AccountCard;
@@ -0,0 +1,19 @@
1
+ export const TELEGRAM_STYLE_ID = 'xmanrui-dsh-im-telegram-settings';
2
+
3
+ const CSS = String.raw`
4
+ .dtg-page { --ddt-accent: #229ed9; --ddt-accent-deep: #1687bd; --ddt-accent-wash: #eaf7fd; }
5
+ .dtg-avatar { color: #fff; background: #229ed9; }
6
+ .dtg-avatar svg { display: block; }
7
+ `;
8
+
9
+ export function installTelegramStyles() {
10
+ if (typeof document === 'undefined') return () => {};
11
+ const existing = document.querySelector(`style[data-plugin-css="${TELEGRAM_STYLE_ID}"]`);
12
+ if (existing) return () => {};
13
+ const style = document.createElement('style');
14
+ style.dataset.plugin = '@xmanrui/dsh-im';
15
+ style.dataset.pluginCss = TELEGRAM_STYLE_ID;
16
+ style.textContent = CSS;
17
+ document.head.appendChild(style);
18
+ return () => style.remove();
19
+ }
@@ -0,0 +1,119 @@
1
+ export const WECOM_RPC_CHANNEL = '/wecom';
2
+
3
+ export const WECOM_ENDPOINTS = Object.freeze({
4
+ status: 'connection.status',
5
+ beginProvisioning: 'provision.begin',
6
+ pollProvisioning: 'provision.poll',
7
+ cancelProvisioning: 'provision.cancel',
8
+ bindCredentials: 'bot.bind-credentials',
9
+ reconnectBot: 'bot.reconnect',
10
+ deleteBot: 'bot.delete',
11
+ });
12
+
13
+ const PROVISION_STATES = new Set(['starting', 'pending', 'refreshing', 'connecting', 'connected', 'failed', 'cancelled']);
14
+ const ACCOUNT_STATES = new Set(['connected', 'connecting', 'offline', 'error']);
15
+ const QR_DATA_URL = /^data:image\/(?:png|webp);base64,[a-z\d+/]+={0,2}$/i;
16
+
17
+ function isRecord(value) {
18
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
19
+ }
20
+
21
+ function text(value, fallback, max = 240) {
22
+ return typeof value === 'string' && value.trim() ? value.trim().slice(0, max) : fallback;
23
+ }
24
+
25
+ function id(value) {
26
+ const result = text(value, '', 128);
27
+ return /^[a-z\d_-]+$/i.test(result) ? result : undefined;
28
+ }
29
+
30
+ function timestamp(value) {
31
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
32
+ const parsed = typeof value === 'string' ? Date.parse(value) : Number.NaN;
33
+ return Number.isNaN(parsed) ? undefined : parsed;
34
+ }
35
+
36
+ export function unwrapRpcResult(result) {
37
+ if (!isRecord(result) || typeof result.ok !== 'boolean') throw new Error('企业微信服务返回了无法识别的响应');
38
+ if (!result.ok) {
39
+ const error = new Error(text(result.error?.message, '企业微信操作失败'));
40
+ error.code = text(result.error?.code, 'WECOM_RPC_ERROR', 80);
41
+ throw error;
42
+ }
43
+ return result.value;
44
+ }
45
+
46
+ export function safeQrSource(value) {
47
+ return typeof value === 'string' && value.length <= 2 * 1024 * 1024 && QR_DATA_URL.test(value)
48
+ ? value : undefined;
49
+ }
50
+
51
+ export function normalizeProvisioning(value, now = Date.now()) {
52
+ const source = isRecord(value?.provisioning) ? value.provisioning : value;
53
+ if (!isRecord(source)) throw new Error('企业微信服务没有返回扫码绑定进度');
54
+ const attemptId = id(source.attemptId);
55
+ if (!attemptId) throw new Error('企业微信扫码服务没有返回有效的绑定任务');
56
+ const reported = text(source.status, 'failed', 32);
57
+ const result = {
58
+ attemptId,
59
+ status: PROVISION_STATES.has(reported) ? reported : 'failed',
60
+ expiresAt: timestamp(source.expiresAt) ?? now + 5 * 60_000,
61
+ pollIntervalMs: Math.min(10_000, Math.max(500, Number(source.pollIntervalMs) || 1_000)),
62
+ qrRevision: Number.isSafeInteger(source.qrRevision) ? source.qrRevision : 0,
63
+ };
64
+ const qrCodeDataUrl = safeQrSource(source.qrCodeDataUrl);
65
+ if (qrCodeDataUrl) result.qrCodeDataUrl = qrCodeDataUrl;
66
+ if (id(source.botId)) result.botId = id(source.botId);
67
+ if (isRecord(source.error)) result.error = {
68
+ code: text(source.error.code, 'WECOM_PROVISION_FAILED', 80),
69
+ message: text(source.error.message, '企业微信机器人没有接入完成'),
70
+ };
71
+ return result;
72
+ }
73
+
74
+ function normalizeBot(value) {
75
+ if (!isRecord(value) || !id(value.botId)) return undefined;
76
+ const connected = value.connected === true;
77
+ const state = ACCOUNT_STATES.has(value.state) ? value.state : 'offline';
78
+ return {
79
+ botId: id(value.botId),
80
+ connected,
81
+ state: connected ? 'connected' : state,
82
+ bot: {
83
+ name: text(value.bot?.name, '企业微信机器人', 100),
84
+ appIdMasked: text(value.bot?.appIdMasked, '应用标识已安全保存', 140),
85
+ },
86
+ health: {
87
+ summary: text(value.health?.summary, connected ? '企业微信 WebSocket 长连接运行正常' : '企业微信连接尚未就绪'),
88
+ lastCheckedAt: timestamp(value.health?.lastCheckedAt),
89
+ },
90
+ error: isRecord(value.error) ? {
91
+ code: text(value.error.code, 'WECOM_ACCOUNT_ERROR', 80),
92
+ message: text(value.error.message, '企业微信连接尚未就绪'),
93
+ } : null,
94
+ };
95
+ }
96
+
97
+ export function normalizeSnapshot(value) {
98
+ const source = isRecord(value?.snapshot) ? value.snapshot : value;
99
+ if (!isRecord(source) || !Array.isArray(source.bots)) throw new Error('企业微信服务没有返回有效的机器人列表');
100
+ const bots = source.bots.map(normalizeBot).filter(Boolean);
101
+ return {
102
+ revision: Number.isSafeInteger(source.revision) ? source.revision : 0,
103
+ bots,
104
+ totals: { configured: bots.length, connected: bots.filter((bot) => bot.connected).length },
105
+ provisioning: source.provisioning ? normalizeProvisioning(source.provisioning) : null,
106
+ };
107
+ }
108
+
109
+ export function presentError(error) {
110
+ return {
111
+ code: text(error?.code, 'WECOM_ERROR', 80),
112
+ message: text(error?.message, '企业微信操作失败,请稍后重试'),
113
+ };
114
+ }
115
+
116
+ export function formatRemaining(milliseconds) {
117
+ const seconds = Math.max(0, Math.ceil(Number(milliseconds) / 1_000) || 0);
118
+ return `${String(Math.floor(seconds / 60)).padStart(2, '0')}:${String(seconds % 60).padStart(2, '0')}`;
119
+ }