@zhin.js/console 1.0.51 → 1.0.53

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 (65) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +22 -0
  3. package/browser.tsconfig.json +19 -0
  4. package/client/src/components/PageHeader.tsx +26 -0
  5. package/client/src/components/ui/accordion.tsx +2 -1
  6. package/client/src/components/ui/badge.tsx +1 -3
  7. package/client/src/components/ui/scroll-area.tsx +5 -2
  8. package/client/src/components/ui/select.tsx +7 -3
  9. package/client/src/components/ui/separator.tsx +5 -2
  10. package/client/src/components/ui/tabs.tsx +4 -2
  11. package/client/src/layouts/dashboard.tsx +223 -121
  12. package/client/src/main.tsx +34 -34
  13. package/client/src/pages/bot-detail/MessageBody.tsx +110 -0
  14. package/client/src/pages/bot-detail/date-utils.ts +8 -0
  15. package/client/src/pages/bot-detail/index.tsx +798 -0
  16. package/client/src/pages/bot-detail/types.ts +92 -0
  17. package/client/src/pages/bot-detail/useBotConsole.tsx +600 -0
  18. package/client/src/pages/bots.tsx +111 -73
  19. package/client/src/pages/database/constants.ts +16 -0
  20. package/client/src/pages/database/database-page.tsx +170 -0
  21. package/client/src/pages/database/document-collection-view.tsx +155 -0
  22. package/client/src/pages/database/index.tsx +1 -0
  23. package/client/src/pages/database/json-field.tsx +11 -0
  24. package/client/src/pages/database/kv-bucket-view.tsx +169 -0
  25. package/client/src/pages/database/related-table-view.tsx +221 -0
  26. package/client/src/pages/env.tsx +38 -28
  27. package/client/src/pages/files/code-editor.tsx +85 -0
  28. package/client/src/pages/files/editor-constants.ts +9 -0
  29. package/client/src/pages/files/file-editor.tsx +133 -0
  30. package/client/src/pages/files/file-icons.tsx +25 -0
  31. package/client/src/pages/files/files-page.tsx +92 -0
  32. package/client/src/pages/files/hljs-global.d.ts +10 -0
  33. package/client/src/pages/files/index.tsx +1 -0
  34. package/client/src/pages/files/language.ts +18 -0
  35. package/client/src/pages/files/tree-node.tsx +69 -0
  36. package/client/src/pages/files/use-hljs-theme.ts +23 -0
  37. package/client/src/pages/logs.tsx +77 -22
  38. package/client/src/style.css +144 -0
  39. package/client/src/utils/parseComposerContent.ts +57 -0
  40. package/client/tailwind.config.js +1 -0
  41. package/client/tsconfig.json +3 -1
  42. package/dist/assets/index-COKXlFo2.js +124 -0
  43. package/dist/assets/style-kkLO-vsa.css +3 -0
  44. package/dist/client.js +482 -464
  45. package/dist/index.html +2 -2
  46. package/dist/style.css +1 -1
  47. package/lib/index.js +1010 -81
  48. package/lib/transform.js +16 -2
  49. package/lib/websocket.js +845 -28
  50. package/node.tsconfig.json +18 -0
  51. package/package.json +13 -15
  52. package/src/bin.ts +24 -0
  53. package/src/bot-db-models.ts +74 -0
  54. package/src/bot-hub.ts +240 -0
  55. package/src/bot-persistence.ts +270 -0
  56. package/src/build.ts +90 -0
  57. package/src/dev.ts +107 -0
  58. package/src/index.ts +337 -0
  59. package/src/transform.ts +199 -0
  60. package/src/websocket.ts +1369 -0
  61. package/client/src/pages/database.tsx +0 -708
  62. package/client/src/pages/files.tsx +0 -470
  63. package/client/src/pages/login-assist.tsx +0 -225
  64. package/dist/assets/index-DS4RbHWX.js +0 -124
  65. package/dist/assets/style-DS-m6WEr.css +0 -3
@@ -0,0 +1,92 @@
1
+ export interface BotInfo {
2
+ name: string
3
+ adapter: string
4
+ connected: boolean
5
+ status: 'online' | 'offline'
6
+ }
7
+
8
+ export interface ReqItem {
9
+ id: number
10
+ platformRequestId: string
11
+ type: string
12
+ sender: { id: string; name: string }
13
+ comment: string
14
+ channel: { id: string; type: string }
15
+ timestamp: number
16
+ canAct?: boolean
17
+ }
18
+
19
+ export interface NoticeItem {
20
+ id: number
21
+ noticeType: string
22
+ channel: { id: string; type: string }
23
+ payload: string
24
+ timestamp: number
25
+ }
26
+
27
+ export interface ReceivedMessage {
28
+ id: string
29
+ channelId: string
30
+ channelType: string
31
+ sender: { id: string; name?: string }
32
+ content: Array<{ type: string; data?: Record<string, unknown> }>
33
+ timestamp: number
34
+ }
35
+
36
+ /** 合并展示用:控制台发出的消息右对齐 */
37
+ export type ChatRow = ReceivedMessage & { outgoing?: boolean }
38
+
39
+ export interface InboxMessageRow {
40
+ id: number
41
+ platform_message_id: string
42
+ sender_id: string
43
+ sender_name: string | null
44
+ content: string
45
+ raw: string | null
46
+ created_at: number
47
+ }
48
+
49
+ export interface InboxRequestRow {
50
+ id: number
51
+ platform_request_id: string
52
+ type: string
53
+ sub_type: string | null
54
+ channel_id: string
55
+ channel_type: string
56
+ sender_id: string
57
+ sender_name: string | null
58
+ comment: string | null
59
+ created_at: number
60
+ resolved: number
61
+ resolved_at: number | null
62
+ }
63
+
64
+ export interface InboxNoticeRow {
65
+ id: number
66
+ platform_notice_id: string
67
+ type: string
68
+ sub_type: string | null
69
+ channel_id: string
70
+ channel_type: string
71
+ operator_id: string | null
72
+ operator_name: string | null
73
+ target_id: string | null
74
+ target_name: string | null
75
+ payload: string
76
+ created_at: number
77
+ }
78
+
79
+ export type SidebarSelection =
80
+ | { type: 'channel'; id: string; name: string; channelType: 'private' | 'group' | 'channel' }
81
+ | { type: 'requests' }
82
+ | { type: 'notices' }
83
+
84
+ export type MemberRow = {
85
+ user_id?: number
86
+ nickname?: string
87
+ card?: string
88
+ role?: string
89
+ id?: string
90
+ name?: string
91
+ [k: string]: unknown
92
+ }
@@ -0,0 +1,600 @@
1
+ import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react'
2
+ import { useParams } from 'react-router'
3
+ import { Hash, MessageSquare, User, Users } from 'lucide-react'
4
+ import { useWebSocket } from '@zhin.js/client'
5
+ import type {
6
+ BotInfo,
7
+ ChatRow,
8
+ InboxMessageRow,
9
+ InboxNoticeRow,
10
+ InboxRequestRow,
11
+ MemberRow,
12
+ NoticeItem,
13
+ ReceivedMessage,
14
+ ReqItem,
15
+ SidebarSelection,
16
+ } from './types'
17
+ import {
18
+ hasRenderableComposerSegments,
19
+ normalizeInboundContent,
20
+ parseComposerToSegments,
21
+ type MessageContent,
22
+ } from '../../utils/parseComposerContent'
23
+
24
+ export function useBotConsole() {
25
+ const { adapter: adapterParam, botId: botIdParam } = useParams<{
26
+ adapter: string
27
+ botId: string
28
+ }>()
29
+ const adapter = adapterParam ? decodeURIComponent(adapterParam) : ''
30
+ const botId = botIdParam ? decodeURIComponent(botIdParam) : ''
31
+ const valid = Boolean(adapter && botId)
32
+
33
+ const { sendRequest, connected } = useWebSocket()
34
+ const [info, setInfo] = useState<BotInfo | null>(null)
35
+ const [loadErr, setLoadErr] = useState<string | null>(null)
36
+
37
+ const [msgContent, setMsgContent] = useState('')
38
+ const [sending, setSending] = useState(false)
39
+
40
+ const [friends, setFriends] = useState<Array<{ user_id: number; nickname: string; remark: string }>>([])
41
+ const [groups, setGroups] = useState<Array<{ group_id: number; name: string }>>([])
42
+ const [channelList, setChannelList] = useState<Array<{ id: string; name: string }>>([])
43
+ const [listLoading, setListLoading] = useState(false)
44
+ const [listErr, setListErr] = useState<string | null>(null)
45
+
46
+ const [requests, setRequests] = useState<Map<number, ReqItem>>(new Map())
47
+ const [notices, setNotices] = useState<Map<number, NoticeItem>>(new Map())
48
+
49
+ const [selection, setSelection] = useState<SidebarSelection | null>(null)
50
+ const [showChannelList, setShowChannelList] = useState(false)
51
+ const [listSearch, setListSearch] = useState('')
52
+
53
+ const [members, setMembers] = useState<MemberRow[]>([])
54
+ const [membersLoading, setMembersLoading] = useState(false)
55
+ const [receivedMessages, setReceivedMessages] = useState<ReceivedMessage[]>([])
56
+ const [localSent, setLocalSent] = useState<
57
+ Array<{
58
+ id: string
59
+ channelId: string
60
+ channelType: string
61
+ segments: MessageContent
62
+ timestamp: number
63
+ }>
64
+ >([])
65
+ const [inboxMessages, setInboxMessages] = useState<InboxMessageRow[]>([])
66
+ const [inboxMessagesLoading, setInboxMessagesLoading] = useState(false)
67
+ const [inboxMessagesHasMore, setInboxMessagesHasMore] = useState(true)
68
+ const [inboxMessagesEnabled, setInboxMessagesEnabled] = useState(false)
69
+ const [inboxRequests, setInboxRequests] = useState<InboxRequestRow[]>([])
70
+ const [inboxRequestsLoading, setInboxRequestsLoading] = useState(false)
71
+ const [inboxRequestsOffset, setInboxRequestsOffset] = useState(0)
72
+ const [inboxRequestsEnabled, setInboxRequestsEnabled] = useState(false)
73
+ const [inboxNotices, setInboxNotices] = useState<InboxNoticeRow[]>([])
74
+ const [inboxNoticesLoading, setInboxNoticesLoading] = useState(false)
75
+ const [inboxNoticesOffset, setInboxNoticesOffset] = useState(0)
76
+ const [inboxNoticesEnabled, setInboxNoticesEnabled] = useState(false)
77
+ const [requestsTab, setRequestsTab] = useState<'pending' | 'history'>('pending')
78
+ const [noticesTab, setNoticesTab] = useState<'unread' | 'history'>('unread')
79
+
80
+ const loadInfo = useCallback(async () => {
81
+ if (!adapter || !botId || !connected) return
82
+ try {
83
+ const data = await sendRequest<BotInfo>({
84
+ type: 'bot:info',
85
+ data: { adapter, botId },
86
+ })
87
+ setInfo(data)
88
+ setLoadErr(null)
89
+ } catch (e) {
90
+ setLoadErr((e as Error).message)
91
+ }
92
+ }, [adapter, botId, connected, sendRequest])
93
+
94
+ useEffect(() => {
95
+ loadInfo()
96
+ const t = setInterval(loadInfo, 8000)
97
+ return () => clearInterval(t)
98
+ }, [loadInfo])
99
+
100
+ const loadLists = useCallback(async () => {
101
+ if (!adapter || !botId || !connected) return
102
+ setListLoading(true)
103
+ setListErr(null)
104
+ try {
105
+ if (adapter === 'icqq') {
106
+ const f = await sendRequest<{ friends: typeof friends }>({
107
+ type: 'bot:friends',
108
+ data: { adapter, botId },
109
+ }).catch(() => ({ friends: [] }))
110
+ const g = await sendRequest<{ groups: typeof groups }>({
111
+ type: 'bot:groups',
112
+ data: { adapter, botId },
113
+ }).catch(() => ({ groups: [] }))
114
+ setFriends(f.friends || [])
115
+ setGroups(g.groups || [])
116
+ setChannelList([])
117
+ } else {
118
+ setFriends([])
119
+ setGroups([])
120
+ const ch = await sendRequest<{ channels?: Array<{ id: string; name: string }> }>({
121
+ type: 'bot:channels',
122
+ data: { adapter, botId },
123
+ }).catch((): { channels?: Array<{ id: string; name: string }> } => ({}))
124
+ const list = ch.channels ?? []
125
+ setChannelList(list)
126
+ if (!list.length) setListErr('当前适配器暂不支持好友/群/频道列表')
127
+ }
128
+ } catch (e) {
129
+ setListErr((e as Error).message)
130
+ } finally {
131
+ setListLoading(false)
132
+ }
133
+ }, [adapter, botId, connected, sendRequest])
134
+
135
+ useEffect(() => {
136
+ if (connected) loadLists()
137
+ }, [connected, loadLists])
138
+
139
+ const loadRequestsFromServer = useCallback(async () => {
140
+ if (!adapter || !botId || !connected) return
141
+ try {
142
+ const { requests: rows } = await sendRequest<{ requests: ReqItem[] }>({
143
+ type: 'bot:requests',
144
+ data: { adapter, botId },
145
+ })
146
+ setRequests((prev) => {
147
+ const m = new Map(prev)
148
+ for (const r of rows || []) {
149
+ m.set(r.id, {
150
+ ...r,
151
+ canAct: false,
152
+ })
153
+ }
154
+ return m
155
+ })
156
+ } catch {
157
+ /* ignore */
158
+ }
159
+ }, [adapter, botId, connected, sendRequest])
160
+
161
+ const loadInboxMessages = useCallback(
162
+ async (beforeTs?: number) => {
163
+ if (!adapter || !botId || selection?.type !== 'channel') return
164
+ setInboxMessagesLoading(true)
165
+ const append = beforeTs != null
166
+ try {
167
+ const res = await sendRequest<{ messages: InboxMessageRow[]; inboxEnabled: boolean }>({
168
+ type: 'bot:inboxMessages',
169
+ data: {
170
+ adapter,
171
+ botId,
172
+ channelId: selection.id,
173
+ channelType: selection.channelType,
174
+ limit: 50,
175
+ ...(beforeTs != null && { beforeTs }),
176
+ },
177
+ })
178
+ setInboxMessagesEnabled(!!res.inboxEnabled)
179
+ if (!res.inboxEnabled || !res.messages?.length) {
180
+ if (!append) setInboxMessages([])
181
+ setInboxMessagesHasMore(false)
182
+ return
183
+ }
184
+ if (append) {
185
+ setInboxMessages((prev) => [...prev, ...res.messages])
186
+ } else {
187
+ setInboxMessages(res.messages)
188
+ }
189
+ setInboxMessagesHasMore(res.messages.length >= 50)
190
+ } catch {
191
+ if (!append) setInboxMessages([])
192
+ setInboxMessagesEnabled(false)
193
+ setInboxMessagesHasMore(false)
194
+ } finally {
195
+ setInboxMessagesLoading(false)
196
+ }
197
+ },
198
+ [adapter, botId, selection, sendRequest],
199
+ )
200
+
201
+ const loadInboxRequests = useCallback(
202
+ async (append: boolean) => {
203
+ if (!adapter || !botId) return
204
+ setInboxRequestsLoading(true)
205
+ try {
206
+ const offset = append ? inboxRequestsOffset : 0
207
+ const res = await sendRequest<{ requests: InboxRequestRow[]; inboxEnabled: boolean }>({
208
+ type: 'bot:inboxRequests',
209
+ data: { adapter, botId, limit: 30, offset },
210
+ })
211
+ setInboxRequestsEnabled(!!res.inboxEnabled)
212
+ if (!res.inboxEnabled || !res.requests?.length) {
213
+ if (!append) setInboxRequests([])
214
+ return
215
+ }
216
+ if (append) {
217
+ setInboxRequests((prev) => [...prev, ...res.requests])
218
+ } else {
219
+ setInboxRequests(res.requests)
220
+ }
221
+ setInboxRequestsOffset(offset + (res.requests?.length ?? 0))
222
+ } catch {
223
+ if (!append) setInboxRequests([])
224
+ setInboxRequestsEnabled(false)
225
+ } finally {
226
+ setInboxRequestsLoading(false)
227
+ }
228
+ },
229
+ [adapter, botId, inboxRequestsOffset, sendRequest],
230
+ )
231
+
232
+ const loadInboxNotices = useCallback(
233
+ async (append: boolean) => {
234
+ if (!adapter || !botId) return
235
+ setInboxNoticesLoading(true)
236
+ try {
237
+ const offset = append ? inboxNoticesOffset : 0
238
+ const res = await sendRequest<{ notices: InboxNoticeRow[]; inboxEnabled: boolean }>({
239
+ type: 'bot:inboxNotices',
240
+ data: { adapter, botId, limit: 30, offset },
241
+ })
242
+ setInboxNoticesEnabled(!!res.inboxEnabled)
243
+ if (!res.inboxEnabled || !res.notices?.length) {
244
+ if (!append) setInboxNotices([])
245
+ return
246
+ }
247
+ if (append) {
248
+ setInboxNotices((prev) => [...prev, ...res.notices])
249
+ } else {
250
+ setInboxNotices(res.notices)
251
+ }
252
+ setInboxNoticesOffset(offset + res.notices.length)
253
+ } catch {
254
+ if (!append) setInboxNotices([])
255
+ setInboxNoticesEnabled(false)
256
+ } finally {
257
+ setInboxNoticesLoading(false)
258
+ }
259
+ },
260
+ [adapter, botId, inboxNoticesOffset, sendRequest],
261
+ )
262
+
263
+ useEffect(() => {
264
+ loadRequestsFromServer()
265
+ }, [loadRequestsFromServer])
266
+
267
+ useEffect(() => {
268
+ if (selection?.type === 'channel') {
269
+ setInboxMessages([])
270
+ setInboxMessagesHasMore(true)
271
+ void loadInboxMessages()
272
+ }
273
+ }, [selection?.id, selection?.channelType, selection?.type, loadInboxMessages])
274
+
275
+ useEffect(() => {
276
+ const onPush = (ev: Event) => {
277
+ const msg = (ev as CustomEvent).detail as {
278
+ type: string
279
+ data: Record<string, unknown>
280
+ }
281
+ const d = msg.data
282
+ if (msg.type === 'bot:request') {
283
+ if (d.adapter === adapter && d.botId === botId) {
284
+ setRequests((prev) => {
285
+ const m = new Map(prev)
286
+ m.set(d.id as number, {
287
+ id: d.id as number,
288
+ platformRequestId: String(d.platformRequestId),
289
+ type: String(d.type),
290
+ sender: d.sender as ReqItem['sender'],
291
+ comment: String(d.comment ?? ''),
292
+ channel: d.channel as ReqItem['channel'],
293
+ timestamp: Number(d.timestamp),
294
+ canAct: d.canAct === true,
295
+ })
296
+ return m
297
+ })
298
+ }
299
+ } else if (msg.type === 'bot:notice') {
300
+ if (d.adapter === adapter && d.botId === botId) {
301
+ setNotices((prev) => {
302
+ const m = new Map(prev)
303
+ m.set(d.id as number, {
304
+ id: d.id as number,
305
+ noticeType: String(d.noticeType),
306
+ channel: d.channel as NoticeItem['channel'],
307
+ payload: String(d.payload ?? '{}'),
308
+ timestamp: Number(d.timestamp),
309
+ })
310
+ return m
311
+ })
312
+ }
313
+ } else if (msg.type === 'bot:message') {
314
+ if (d.adapter === adapter && d.botId === botId) {
315
+ const content = normalizeInboundContent(d.content) as ReceivedMessage['content']
316
+ setReceivedMessages((prev) => [
317
+ ...prev,
318
+ {
319
+ id: `msg-${Date.now()}-${Math.random().toString(36).slice(2)}`,
320
+ channelId: String(d.channelId ?? ''),
321
+ channelType: String(d.channelType ?? 'private'),
322
+ sender: (d.sender as ReceivedMessage['sender']) ?? { id: '', name: '' },
323
+ content,
324
+ timestamp: Number(d.timestamp ?? Date.now()),
325
+ },
326
+ ])
327
+ }
328
+ }
329
+ }
330
+ window.addEventListener('zhin-console-bot-push', onPush as EventListener)
331
+ return () => window.removeEventListener('zhin-console-bot-push', onPush as EventListener)
332
+ }, [adapter, botId])
333
+
334
+ const requestList = useMemo(() => [...requests.values()].sort((a, b) => b.timestamp - a.timestamp), [requests])
335
+ const noticeList = useMemo(() => [...notices.values()].sort((a, b) => b.timestamp - a.timestamp), [notices])
336
+
337
+ const channels = useMemo(() => {
338
+ const list: Array<{ id: string; name: string; channelType: 'private' | 'group' | 'channel' }> = []
339
+ friends.forEach((f) => {
340
+ list.push({
341
+ id: String(f.user_id),
342
+ name: f.nickname || f.remark || f.user_id.toString(),
343
+ channelType: 'private',
344
+ })
345
+ })
346
+ groups.forEach((g) => {
347
+ list.push({ id: String(g.group_id), name: g.name || String(g.group_id), channelType: 'group' })
348
+ })
349
+ channelList.forEach((c) => {
350
+ list.push({ id: c.id, name: c.name || c.id, channelType: 'channel' })
351
+ })
352
+ return list
353
+ }, [friends, groups, channelList])
354
+
355
+ const filteredChannels = useMemo(() => {
356
+ const q = listSearch.trim().toLowerCase()
357
+ if (!q) return channels
358
+ return channels.filter((ch) => ch.name.toLowerCase().includes(q) || ch.id.toLowerCase().includes(q))
359
+ }, [channels, listSearch])
360
+
361
+ const channelMessages = useMemo((): ChatRow[] => {
362
+ if (selection?.type !== 'channel') return []
363
+ const fromInbox: ChatRow[] = inboxMessages
364
+ .filter((m) => selection.id && selection.channelType)
365
+ .map((m) => {
366
+ const content = normalizeInboundContent(m.content) as ReceivedMessage['content']
367
+ return {
368
+ id: `inbox-${m.id}`,
369
+ channelId: selection.id,
370
+ channelType: selection.channelType,
371
+ sender: { id: m.sender_id, name: m.sender_name ?? undefined },
372
+ content,
373
+ timestamp: m.created_at,
374
+ outgoing: false,
375
+ }
376
+ })
377
+ const fromRealtime: ChatRow[] = receivedMessages
378
+ .filter((m) => m.channelId === selection.id && m.channelType === selection.channelType)
379
+ .map((m) => ({ ...m, outgoing: false }))
380
+ const outbound: ChatRow[] = localSent
381
+ .filter((m) => m.channelId === selection.id && m.channelType === selection.channelType)
382
+ .map((m) => ({
383
+ id: m.id,
384
+ channelId: m.channelId,
385
+ channelType: m.channelType,
386
+ sender: { id: 'self', name: '我' },
387
+ content: m.segments as ReceivedMessage['content'],
388
+ timestamp: m.timestamp,
389
+ outgoing: true,
390
+ }))
391
+ return [...fromInbox, ...fromRealtime, ...outbound].sort((a, b) => a.timestamp - b.timestamp)
392
+ }, [selection, receivedMessages, inboxMessages, localSent])
393
+
394
+ const deleteFriend = async () => {
395
+ if (selection?.type !== 'channel' || selection.channelType !== 'private') return
396
+ if (!confirm(`确定删除好友 ${selection.name}?`)) return
397
+ try {
398
+ await sendRequest({
399
+ type: 'bot:deleteFriend',
400
+ data: { adapter, botId, userId: selection.id },
401
+ })
402
+ setFriends((prev) => prev.filter((f) => String(f.user_id) !== selection.id))
403
+ setSelection(null)
404
+ loadLists()
405
+ } catch (e) {
406
+ alert((e as Error).message)
407
+ }
408
+ }
409
+
410
+ const handleSend = async () => {
411
+ const targetId = selection?.type === 'channel' ? selection.id : ''
412
+ const msgType = selection?.type === 'channel' ? selection.channelType : 'private'
413
+ const segments = parseComposerToSegments(msgContent)
414
+ if (!targetId || !hasRenderableComposerSegments(segments)) return
415
+ setSending(true)
416
+ try {
417
+ await sendRequest({
418
+ type: 'bot:sendMessage',
419
+ data: {
420
+ adapter,
421
+ botId,
422
+ id: targetId,
423
+ type: msgType,
424
+ content: segments,
425
+ },
426
+ })
427
+ setLocalSent((prev) => [
428
+ ...prev,
429
+ {
430
+ id: `local-${Date.now()}-${Math.random().toString(36).slice(2)}`,
431
+ channelId: targetId,
432
+ channelType: msgType,
433
+ segments,
434
+ timestamp: Date.now(),
435
+ },
436
+ ])
437
+ setMsgContent('')
438
+ } catch (e) {
439
+ alert((e as Error).message)
440
+ } finally {
441
+ setSending(false)
442
+ }
443
+ }
444
+
445
+ const approve = async (platformRequestId: string, approveIt: boolean) => {
446
+ try {
447
+ await sendRequest({
448
+ type: approveIt ? 'bot:requestApprove' : 'bot:requestReject',
449
+ data: { adapter, botId, requestId: platformRequestId },
450
+ })
451
+ const row = requestList.find((r) => r.platformRequestId === platformRequestId)
452
+ if (row) {
453
+ setRequests((prev) => {
454
+ const m = new Map(prev)
455
+ m.delete(row.id)
456
+ return m
457
+ })
458
+ }
459
+ } catch (e) {
460
+ alert((e as Error).message)
461
+ }
462
+ }
463
+
464
+ const dismissRequest = async (id: number) => {
465
+ try {
466
+ await sendRequest({ type: 'bot:requestConsumed', data: { id } })
467
+ setRequests((prev) => {
468
+ const m = new Map(prev)
469
+ m.delete(id)
470
+ return m
471
+ })
472
+ } catch (e) {
473
+ alert((e as Error).message)
474
+ }
475
+ }
476
+
477
+ const dismissNotice = async (id: number) => {
478
+ try {
479
+ await sendRequest({ type: 'bot:noticeConsumed', data: { id } })
480
+ setNotices((prev) => {
481
+ const m = new Map(prev)
482
+ m.delete(id)
483
+ return m
484
+ })
485
+ } catch (e) {
486
+ alert((e as Error).message)
487
+ }
488
+ }
489
+
490
+ const loadMembers = async () => {
491
+ if (selection?.type !== 'channel' || selection.channelType !== 'group' || adapter !== 'icqq') return
492
+ setMembersLoading(true)
493
+ try {
494
+ const r = await sendRequest<{ members: MemberRow[] }>({
495
+ type: 'bot:groupMembers',
496
+ data: { adapter, botId, groupId: selection.id },
497
+ })
498
+ setMembers(r.members || [])
499
+ } catch (e) {
500
+ alert((e as Error).message)
501
+ setMembers([])
502
+ } finally {
503
+ setMembersLoading(false)
504
+ }
505
+ }
506
+
507
+ const groupAction = async (
508
+ type: 'bot:groupKick' | 'bot:groupMute' | 'bot:groupAdmin',
509
+ userId: number | string,
510
+ extra?: { enable?: boolean },
511
+ ) => {
512
+ if (selection?.type !== 'channel' || selection.channelType !== 'group') return
513
+ try {
514
+ await sendRequest({
515
+ type,
516
+ data: {
517
+ adapter,
518
+ botId,
519
+ groupId: selection.id,
520
+ userId: String(userId),
521
+ ...extra,
522
+ },
523
+ })
524
+ await loadMembers()
525
+ } catch (e) {
526
+ alert((e as Error).message)
527
+ }
528
+ }
529
+
530
+ const getChannelIcon = (channelType: string): ReactNode => {
531
+ switch (channelType) {
532
+ case 'private':
533
+ return <User size={16} />
534
+ case 'group':
535
+ return <Users size={16} />
536
+ case 'channel':
537
+ return <Hash size={16} />
538
+ default:
539
+ return <MessageSquare size={16} />
540
+ }
541
+ }
542
+
543
+ const showRightPanel =
544
+ selection?.type === 'channel' && selection.channelType === 'group' && adapter === 'icqq'
545
+
546
+ return {
547
+ valid,
548
+ adapter,
549
+ botId,
550
+ connected,
551
+ info,
552
+ loadErr,
553
+ msgContent,
554
+ setMsgContent,
555
+ sending,
556
+ listLoading,
557
+ listErr,
558
+ selection,
559
+ setSelection,
560
+ showChannelList,
561
+ setShowChannelList,
562
+ listSearch,
563
+ setListSearch,
564
+ members,
565
+ membersLoading,
566
+ channelMessages,
567
+ inboxMessagesLoading,
568
+ inboxMessagesHasMore,
569
+ inboxMessagesEnabled,
570
+ loadInboxMessages,
571
+ inboxMessages,
572
+ requestList,
573
+ noticeList,
574
+ requestsTab,
575
+ setRequestsTab,
576
+ noticesTab,
577
+ setNoticesTab,
578
+ inboxRequests,
579
+ inboxRequestsLoading,
580
+ inboxRequestsEnabled,
581
+ loadInboxRequests,
582
+ inboxNotices,
583
+ inboxNoticesLoading,
584
+ inboxNoticesEnabled,
585
+ loadInboxNotices,
586
+ channels,
587
+ filteredChannels,
588
+ deleteFriend,
589
+ handleSend,
590
+ approve,
591
+ dismissRequest,
592
+ dismissNotice,
593
+ loadMembers,
594
+ groupAction,
595
+ loadLists,
596
+ loadRequestsFromServer,
597
+ getChannelIcon,
598
+ showRightPanel,
599
+ }
600
+ }