@zhin.js/console 1.0.51 → 1.0.52
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/CHANGELOG.md +13 -0
- package/README.md +22 -0
- package/browser.tsconfig.json +19 -0
- package/client/src/components/PageHeader.tsx +26 -0
- package/client/src/components/ui/accordion.tsx +2 -1
- package/client/src/components/ui/badge.tsx +1 -3
- package/client/src/components/ui/scroll-area.tsx +5 -2
- package/client/src/components/ui/select.tsx +7 -3
- package/client/src/components/ui/separator.tsx +5 -2
- package/client/src/components/ui/tabs.tsx +4 -2
- package/client/src/layouts/dashboard.tsx +223 -121
- package/client/src/main.tsx +34 -34
- package/client/src/pages/bot-detail/MessageBody.tsx +110 -0
- package/client/src/pages/bot-detail/date-utils.ts +8 -0
- package/client/src/pages/bot-detail/index.tsx +798 -0
- package/client/src/pages/bot-detail/types.ts +92 -0
- package/client/src/pages/bot-detail/useBotConsole.tsx +600 -0
- package/client/src/pages/bots.tsx +111 -73
- package/client/src/pages/database/constants.ts +16 -0
- package/client/src/pages/database/database-page.tsx +170 -0
- package/client/src/pages/database/document-collection-view.tsx +155 -0
- package/client/src/pages/database/index.tsx +1 -0
- package/client/src/pages/database/json-field.tsx +11 -0
- package/client/src/pages/database/kv-bucket-view.tsx +169 -0
- package/client/src/pages/database/related-table-view.tsx +221 -0
- package/client/src/pages/env.tsx +38 -28
- package/client/src/pages/files/code-editor.tsx +85 -0
- package/client/src/pages/files/editor-constants.ts +9 -0
- package/client/src/pages/files/file-editor.tsx +133 -0
- package/client/src/pages/files/file-icons.tsx +25 -0
- package/client/src/pages/files/files-page.tsx +92 -0
- package/client/src/pages/files/hljs-global.d.ts +10 -0
- package/client/src/pages/files/index.tsx +1 -0
- package/client/src/pages/files/language.ts +18 -0
- package/client/src/pages/files/tree-node.tsx +69 -0
- package/client/src/pages/files/use-hljs-theme.ts +23 -0
- package/client/src/pages/logs.tsx +77 -22
- package/client/src/style.css +144 -0
- package/client/src/utils/parseComposerContent.ts +57 -0
- package/client/tailwind.config.js +1 -0
- package/client/tsconfig.json +3 -1
- package/dist/assets/index-COKXlFo2.js +124 -0
- package/dist/assets/style-kkLO-vsa.css +3 -0
- package/dist/client.js +482 -464
- package/dist/index.html +2 -2
- package/dist/style.css +1 -1
- package/lib/index.js +1010 -81
- package/lib/transform.js +16 -2
- package/lib/websocket.js +845 -28
- package/node.tsconfig.json +18 -0
- package/package.json +13 -15
- package/src/bin.ts +24 -0
- package/src/bot-db-models.ts +74 -0
- package/src/bot-hub.ts +240 -0
- package/src/bot-persistence.ts +270 -0
- package/src/build.ts +90 -0
- package/src/dev.ts +107 -0
- package/src/index.ts +337 -0
- package/src/transform.ts +199 -0
- package/src/websocket.ts +1369 -0
- package/client/src/pages/database.tsx +0 -708
- package/client/src/pages/files.tsx +0 -470
- package/client/src/pages/login-assist.tsx +0 -225
- package/dist/assets/index-DS4RbHWX.js +0 -124
- package/dist/assets/style-DS-m6WEr.css +0 -3
|
@@ -0,0 +1,798 @@
|
|
|
1
|
+
import { Fragment, useRef, useState } from 'react'
|
|
2
|
+
import { Link } from 'react-router'
|
|
3
|
+
import {
|
|
4
|
+
ArrowLeft,
|
|
5
|
+
Bell,
|
|
6
|
+
Bot,
|
|
7
|
+
Check,
|
|
8
|
+
Image,
|
|
9
|
+
Loader2,
|
|
10
|
+
MessageSquare,
|
|
11
|
+
MoreHorizontal,
|
|
12
|
+
Music,
|
|
13
|
+
Send,
|
|
14
|
+
UserMinus,
|
|
15
|
+
UserPlus,
|
|
16
|
+
Video,
|
|
17
|
+
Wifi,
|
|
18
|
+
WifiOff,
|
|
19
|
+
} from 'lucide-react'
|
|
20
|
+
import { cn } from '@zhin.js/client'
|
|
21
|
+
import { Button } from '../../components/ui/button'
|
|
22
|
+
import { Input } from '../../components/ui/input'
|
|
23
|
+
import { Textarea } from '../../components/ui/textarea'
|
|
24
|
+
import { Badge } from '../../components/ui/badge'
|
|
25
|
+
import { Alert, AlertDescription } from '../../components/ui/alert'
|
|
26
|
+
import { Tabs, TabsList, TabsTrigger, TabsContent } from '../../components/ui/tabs'
|
|
27
|
+
import { MessageBody } from './MessageBody'
|
|
28
|
+
import { useBotConsole } from './useBotConsole'
|
|
29
|
+
import { hasRenderableComposerSegments, parseComposerToSegments } from '../../utils/parseComposerContent'
|
|
30
|
+
import { dayKey, dayLabel } from './date-utils'
|
|
31
|
+
|
|
32
|
+
export default function BotDetailPage() {
|
|
33
|
+
const ctx = useBotConsole()
|
|
34
|
+
|
|
35
|
+
if (!ctx.valid) {
|
|
36
|
+
return (
|
|
37
|
+
<div className="p-4">
|
|
38
|
+
<Alert>
|
|
39
|
+
<AlertDescription>参数无效</AlertDescription>
|
|
40
|
+
</Alert>
|
|
41
|
+
</div>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const {
|
|
46
|
+
adapter,
|
|
47
|
+
botId,
|
|
48
|
+
connected,
|
|
49
|
+
info,
|
|
50
|
+
loadErr,
|
|
51
|
+
msgContent,
|
|
52
|
+
setMsgContent,
|
|
53
|
+
sending,
|
|
54
|
+
listLoading,
|
|
55
|
+
listErr,
|
|
56
|
+
selection,
|
|
57
|
+
setSelection,
|
|
58
|
+
showChannelList,
|
|
59
|
+
setShowChannelList,
|
|
60
|
+
listSearch,
|
|
61
|
+
setListSearch,
|
|
62
|
+
members,
|
|
63
|
+
membersLoading,
|
|
64
|
+
channelMessages,
|
|
65
|
+
inboxMessagesLoading,
|
|
66
|
+
inboxMessagesHasMore,
|
|
67
|
+
inboxMessagesEnabled,
|
|
68
|
+
loadInboxMessages,
|
|
69
|
+
inboxMessages,
|
|
70
|
+
requestList,
|
|
71
|
+
noticeList,
|
|
72
|
+
requestsTab,
|
|
73
|
+
setRequestsTab,
|
|
74
|
+
noticesTab,
|
|
75
|
+
setNoticesTab,
|
|
76
|
+
inboxRequests,
|
|
77
|
+
inboxRequestsLoading,
|
|
78
|
+
inboxRequestsEnabled,
|
|
79
|
+
loadInboxRequests,
|
|
80
|
+
inboxNotices,
|
|
81
|
+
inboxNoticesLoading,
|
|
82
|
+
inboxNoticesEnabled,
|
|
83
|
+
loadInboxNotices,
|
|
84
|
+
filteredChannels,
|
|
85
|
+
deleteFriend,
|
|
86
|
+
handleSend,
|
|
87
|
+
approve,
|
|
88
|
+
dismissRequest,
|
|
89
|
+
dismissNotice,
|
|
90
|
+
loadMembers,
|
|
91
|
+
groupAction,
|
|
92
|
+
loadLists,
|
|
93
|
+
loadRequestsFromServer,
|
|
94
|
+
getChannelIcon,
|
|
95
|
+
showRightPanel,
|
|
96
|
+
} = ctx
|
|
97
|
+
|
|
98
|
+
const [mediaPanel, setMediaPanel] = useState<null | 'image' | 'video' | 'audio'>(null)
|
|
99
|
+
const [mediaUrl, setMediaUrl] = useState('')
|
|
100
|
+
const imageFileRef = useRef<HTMLInputElement>(null)
|
|
101
|
+
|
|
102
|
+
const appendComposerToken = (token: string) => {
|
|
103
|
+
setMsgContent((c) => {
|
|
104
|
+
if (!c) return token
|
|
105
|
+
const needsSpace = !/\s$/.test(c) && !/^[\s[,]/.test(token)
|
|
106
|
+
return c + (needsSpace ? ' ' : '') + token
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const commitMediaUrl = () => {
|
|
111
|
+
const u = mediaUrl.trim()
|
|
112
|
+
if (!u || !mediaPanel) return
|
|
113
|
+
const tag =
|
|
114
|
+
mediaPanel === 'image' ? `[image:${u}]` : mediaPanel === 'video' ? `[video:${u}]` : `[audio:${u}]`
|
|
115
|
+
appendComposerToken(tag)
|
|
116
|
+
setMediaUrl('')
|
|
117
|
+
setMediaPanel(null)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const onPickImageFile = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
121
|
+
const f = e.target.files?.[0]
|
|
122
|
+
e.target.value = ''
|
|
123
|
+
if (!f?.type.startsWith('image/')) return
|
|
124
|
+
const r = new FileReader()
|
|
125
|
+
r.onload = () => {
|
|
126
|
+
const dataUrl = String(r.result || '')
|
|
127
|
+
if (dataUrl) appendComposerToken(`[image:${dataUrl}]`)
|
|
128
|
+
}
|
|
129
|
+
r.readAsDataURL(f)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const canSend = connected && !sending && hasRenderableComposerSegments(parseComposerToSegments(msgContent))
|
|
133
|
+
|
|
134
|
+
let lastDay = ''
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<div className="sandbox-container im-layout">
|
|
138
|
+
<button
|
|
139
|
+
type="button"
|
|
140
|
+
className="mobile-channel-toggle md:hidden"
|
|
141
|
+
onClick={() => setShowChannelList(!showChannelList)}
|
|
142
|
+
>
|
|
143
|
+
<MessageSquare size={20} /> 会话列表
|
|
144
|
+
</button>
|
|
145
|
+
|
|
146
|
+
<div className={cn('channel-sidebar', showChannelList && 'show')}>
|
|
147
|
+
<div className="p-3 border-b border-border/60">
|
|
148
|
+
<div className="flex items-center gap-2">
|
|
149
|
+
<Button variant="ghost" size="icon" className="h-8 w-8 shrink-0" asChild>
|
|
150
|
+
<Link to="/bots">
|
|
151
|
+
<ArrowLeft className="h-4 w-4" />
|
|
152
|
+
</Link>
|
|
153
|
+
</Button>
|
|
154
|
+
<div className="min-w-0 flex-1">
|
|
155
|
+
<div className="flex items-center gap-2">
|
|
156
|
+
<Bot className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
157
|
+
<span className="font-semibold truncate text-sm">{info?.name || botId}</span>
|
|
158
|
+
</div>
|
|
159
|
+
<div className="flex items-center gap-1.5 mt-0.5 flex-wrap">
|
|
160
|
+
<Badge variant="outline" className="text-[10px] px-1 font-normal">
|
|
161
|
+
{adapter}
|
|
162
|
+
</Badge>
|
|
163
|
+
<span
|
|
164
|
+
className={cn(
|
|
165
|
+
'inline-flex items-center gap-0.5 text-[10px] px-1.5 py-0 rounded-full border',
|
|
166
|
+
connected
|
|
167
|
+
? 'bg-emerald-100 text-emerald-800 border-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400'
|
|
168
|
+
: 'bg-muted text-muted-foreground',
|
|
169
|
+
)}
|
|
170
|
+
>
|
|
171
|
+
{connected ? <Wifi size={10} /> : <WifiOff size={10} />}
|
|
172
|
+
{connected ? '已连接' : '未连接'}
|
|
173
|
+
</span>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
{loadErr && (
|
|
180
|
+
<div className="px-3 py-2">
|
|
181
|
+
<p className="text-xs text-destructive">{loadErr}</p>
|
|
182
|
+
</div>
|
|
183
|
+
)}
|
|
184
|
+
|
|
185
|
+
<div className="px-2 pt-2 pb-1">
|
|
186
|
+
<Input
|
|
187
|
+
value={listSearch}
|
|
188
|
+
onChange={(e) => setListSearch(e.target.value)}
|
|
189
|
+
placeholder="搜索会话…"
|
|
190
|
+
className="h-9 text-sm bg-background/80"
|
|
191
|
+
/>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<div className="flex-1 overflow-y-auto p-2 space-y-0.5 min-h-0">
|
|
195
|
+
{listLoading && (
|
|
196
|
+
<div className="flex items-center justify-center py-6">
|
|
197
|
+
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
|
|
198
|
+
</div>
|
|
199
|
+
)}
|
|
200
|
+
{!listLoading && listErr && <p className="text-xs text-muted-foreground px-2 py-1">{listErr}</p>}
|
|
201
|
+
{filteredChannels.length === 0 && !listLoading && !listErr && (
|
|
202
|
+
<p className="text-xs text-muted-foreground px-2 py-4 text-center">
|
|
203
|
+
{listSearch.trim() ? `无匹配「${listSearch.trim()}」` : '暂无会话'}
|
|
204
|
+
</p>
|
|
205
|
+
)}
|
|
206
|
+
{filteredChannels.map((ch) => {
|
|
207
|
+
const isActive = selection?.type === 'channel' && selection.id === ch.id
|
|
208
|
+
return (
|
|
209
|
+
<div
|
|
210
|
+
key={`${ch.channelType}-${ch.id}`}
|
|
211
|
+
role="button"
|
|
212
|
+
tabIndex={0}
|
|
213
|
+
className={cn('menu-item im-row-compact', isActive && 'active')}
|
|
214
|
+
onClick={() => {
|
|
215
|
+
setSelection({ type: 'channel', id: ch.id, name: ch.name, channelType: ch.channelType })
|
|
216
|
+
if (typeof window !== 'undefined' && window.innerWidth < 768) setShowChannelList(false)
|
|
217
|
+
}}
|
|
218
|
+
onKeyDown={(e) => {
|
|
219
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
220
|
+
e.preventDefault()
|
|
221
|
+
setSelection({ type: 'channel', id: ch.id, name: ch.name, channelType: ch.channelType })
|
|
222
|
+
if (typeof window !== 'undefined' && window.innerWidth < 768) setShowChannelList(false)
|
|
223
|
+
}
|
|
224
|
+
}}
|
|
225
|
+
>
|
|
226
|
+
<span className="shrink-0 opacity-90">{getChannelIcon(ch.channelType)}</span>
|
|
227
|
+
<div className="flex-1 min-w-0 text-left">
|
|
228
|
+
<div className="text-sm font-medium truncate leading-tight">{ch.name}</div>
|
|
229
|
+
<div className="text-[11px] text-muted-foreground truncate mt-0.5">
|
|
230
|
+
{ch.channelType === 'private' ? '私聊' : ch.channelType === 'group' ? '群聊' : '频道'}
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
)
|
|
235
|
+
})}
|
|
236
|
+
|
|
237
|
+
<div className="pt-2 mt-2 border-t border-border/50 space-y-0.5">
|
|
238
|
+
<div
|
|
239
|
+
role="button"
|
|
240
|
+
tabIndex={0}
|
|
241
|
+
className={cn('menu-item im-row-compact', selection?.type === 'requests' && 'active')}
|
|
242
|
+
onClick={() => {
|
|
243
|
+
setSelection({ type: 'requests' })
|
|
244
|
+
if (typeof window !== 'undefined' && window.innerWidth < 768) setShowChannelList(false)
|
|
245
|
+
}}
|
|
246
|
+
>
|
|
247
|
+
<UserPlus size={16} className="shrink-0" />
|
|
248
|
+
<div className="flex-1 min-w-0 text-left">
|
|
249
|
+
<div className="text-sm font-medium">请求</div>
|
|
250
|
+
<div className="text-[11px] text-muted-foreground">好友/群邀请</div>
|
|
251
|
+
</div>
|
|
252
|
+
{requestList.length > 0 && (
|
|
253
|
+
<span className="inline-flex items-center justify-center h-5 min-w-5 rounded-full bg-destructive text-destructive-foreground text-[10px] font-medium px-1">
|
|
254
|
+
{requestList.length}
|
|
255
|
+
</span>
|
|
256
|
+
)}
|
|
257
|
+
</div>
|
|
258
|
+
<div
|
|
259
|
+
role="button"
|
|
260
|
+
tabIndex={0}
|
|
261
|
+
className={cn('menu-item im-row-compact', selection?.type === 'notices' && 'active')}
|
|
262
|
+
onClick={() => {
|
|
263
|
+
setSelection({ type: 'notices' })
|
|
264
|
+
if (typeof window !== 'undefined' && window.innerWidth < 768) setShowChannelList(false)
|
|
265
|
+
}}
|
|
266
|
+
>
|
|
267
|
+
<Bell size={16} className="shrink-0" />
|
|
268
|
+
<div className="flex-1 min-w-0 text-left">
|
|
269
|
+
<div className="text-sm font-medium">通知</div>
|
|
270
|
+
<div className="text-[11px] text-muted-foreground">群管/撤回等</div>
|
|
271
|
+
</div>
|
|
272
|
+
{noticeList.length > 0 && (
|
|
273
|
+
<span className="inline-flex items-center justify-center h-5 min-w-5 rounded-full bg-destructive text-destructive-foreground text-[10px] font-medium px-1">
|
|
274
|
+
{noticeList.length}
|
|
275
|
+
</span>
|
|
276
|
+
)}
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
|
|
281
|
+
<div className="p-2 border-t border-border/60">
|
|
282
|
+
<Button
|
|
283
|
+
variant="outline"
|
|
284
|
+
size="sm"
|
|
285
|
+
className="w-full border-dashed text-xs h-8"
|
|
286
|
+
onClick={() => void loadLists()}
|
|
287
|
+
disabled={listLoading || !connected}
|
|
288
|
+
>
|
|
289
|
+
刷新好友/群
|
|
290
|
+
</Button>
|
|
291
|
+
</div>
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
{showChannelList && (
|
|
295
|
+
<div
|
|
296
|
+
className="channel-overlay md:hidden"
|
|
297
|
+
onClick={() => setShowChannelList(false)}
|
|
298
|
+
aria-hidden
|
|
299
|
+
/>
|
|
300
|
+
)}
|
|
301
|
+
|
|
302
|
+
<div className="im-main-split">
|
|
303
|
+
<div className="im-center">
|
|
304
|
+
{selection?.type === 'channel' && (
|
|
305
|
+
<>
|
|
306
|
+
<header className="im-chat-header px-3 py-2.5 flex items-center justify-between gap-2">
|
|
307
|
+
<div className="flex items-center gap-3 min-w-0">
|
|
308
|
+
<div className="p-2 rounded-full bg-muted/80 text-muted-foreground shrink-0">
|
|
309
|
+
{getChannelIcon(selection.channelType)}
|
|
310
|
+
</div>
|
|
311
|
+
<div className="min-w-0">
|
|
312
|
+
<h2 className="text-[15px] font-semibold truncate leading-tight">{selection.name}</h2>
|
|
313
|
+
<p className="text-[11px] text-muted-foreground truncate mt-0.5">
|
|
314
|
+
{selection.channelType === 'private' ? '私聊' : selection.channelType === 'group' ? '群聊' : '频道'}{' '}
|
|
315
|
+
· {selection.id}
|
|
316
|
+
</p>
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
320
|
+
{selection.channelType === 'private' && (
|
|
321
|
+
<Button
|
|
322
|
+
variant="ghost"
|
|
323
|
+
size="icon"
|
|
324
|
+
className="h-8 w-8 text-destructive hover:text-destructive"
|
|
325
|
+
title="删除好友"
|
|
326
|
+
onClick={() => void deleteFriend()}
|
|
327
|
+
>
|
|
328
|
+
<UserMinus className="h-4 w-4" />
|
|
329
|
+
</Button>
|
|
330
|
+
)}
|
|
331
|
+
<Button variant="ghost" size="icon" className="h-8 w-8 opacity-50" disabled title="更多">
|
|
332
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
333
|
+
</Button>
|
|
334
|
+
</div>
|
|
335
|
+
</header>
|
|
336
|
+
|
|
337
|
+
<div className="flex-1 overflow-y-auto px-3 py-2 min-h-0 flex flex-col">
|
|
338
|
+
{inboxMessagesEnabled && inboxMessagesHasMore && (
|
|
339
|
+
<div className="flex-shrink-0 py-2 flex justify-center">
|
|
340
|
+
<Button
|
|
341
|
+
variant="ghost"
|
|
342
|
+
size="sm"
|
|
343
|
+
className="text-xs"
|
|
344
|
+
disabled={inboxMessagesLoading}
|
|
345
|
+
onClick={() => {
|
|
346
|
+
const oldest = Math.min(...inboxMessages.map((m) => m.created_at))
|
|
347
|
+
void loadInboxMessages(oldest)
|
|
348
|
+
}}
|
|
349
|
+
>
|
|
350
|
+
{inboxMessagesLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : '加载更早消息'}
|
|
351
|
+
</Button>
|
|
352
|
+
</div>
|
|
353
|
+
)}
|
|
354
|
+
{channelMessages.length === 0 && !inboxMessagesLoading ? (
|
|
355
|
+
<div className="flex flex-col items-center justify-center flex-1 gap-2 text-muted-foreground text-sm py-12">
|
|
356
|
+
<MessageSquare className="h-10 w-10 opacity-35" />
|
|
357
|
+
<span>
|
|
358
|
+
{inboxMessagesEnabled ? '暂无消息' : '暂无消息,对方发送的消息会显示在此处'}
|
|
359
|
+
</span>
|
|
360
|
+
</div>
|
|
361
|
+
) : (
|
|
362
|
+
<div className="flex flex-col gap-1 pb-2">
|
|
363
|
+
{channelMessages.map((m) => {
|
|
364
|
+
const dk = dayKey(m.timestamp)
|
|
365
|
+
const showDate = dk !== lastDay
|
|
366
|
+
if (showDate) lastDay = dk
|
|
367
|
+
const out = m.outgoing === true
|
|
368
|
+
return (
|
|
369
|
+
<Fragment key={m.id}>
|
|
370
|
+
{showDate && <div className="im-date-pill">{dayLabel(m.timestamp)}</div>}
|
|
371
|
+
<div className={cn('flex w-full', out ? 'justify-end' : 'justify-start')}>
|
|
372
|
+
<div className={cn(out ? 'im-bubble-out' : 'im-bubble-in')}>
|
|
373
|
+
<div
|
|
374
|
+
className={cn(
|
|
375
|
+
'im-meta flex items-center gap-2 text-[10px] mb-0.5',
|
|
376
|
+
out ? '' : 'text-muted-foreground',
|
|
377
|
+
)}
|
|
378
|
+
>
|
|
379
|
+
<span className={cn('font-medium', out ? '' : 'text-foreground/90')}>
|
|
380
|
+
{out ? '我' : m.sender?.name || m.sender?.id || '未知'}
|
|
381
|
+
</span>
|
|
382
|
+
<span className="tabular-nums opacity-80">
|
|
383
|
+
{new Date(m.timestamp).toLocaleTimeString('zh-CN', {
|
|
384
|
+
hour: '2-digit',
|
|
385
|
+
minute: '2-digit',
|
|
386
|
+
})}
|
|
387
|
+
</span>
|
|
388
|
+
</div>
|
|
389
|
+
<div className="text-[14px] leading-snug break-words">
|
|
390
|
+
<MessageBody content={m.content} />
|
|
391
|
+
</div>
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
</Fragment>
|
|
395
|
+
)
|
|
396
|
+
})}
|
|
397
|
+
</div>
|
|
398
|
+
)}
|
|
399
|
+
</div>
|
|
400
|
+
|
|
401
|
+
<div className="im-composer p-3 shrink-0 space-y-2">
|
|
402
|
+
<input
|
|
403
|
+
ref={imageFileRef}
|
|
404
|
+
type="file"
|
|
405
|
+
accept="image/*"
|
|
406
|
+
className="hidden"
|
|
407
|
+
onChange={onPickImageFile}
|
|
408
|
+
/>
|
|
409
|
+
<div className="flex flex-wrap items-center gap-1">
|
|
410
|
+
<Button
|
|
411
|
+
type="button"
|
|
412
|
+
variant="outline"
|
|
413
|
+
size="sm"
|
|
414
|
+
className="h-8 px-2 text-xs"
|
|
415
|
+
title="选择本地图片(插入为 data URL)"
|
|
416
|
+
onClick={() => imageFileRef.current?.click()}
|
|
417
|
+
>
|
|
418
|
+
<Image className="w-3.5 h-3.5 mr-1" />
|
|
419
|
+
图片文件
|
|
420
|
+
</Button>
|
|
421
|
+
<Button
|
|
422
|
+
type="button"
|
|
423
|
+
variant={mediaPanel === 'image' ? 'secondary' : 'outline'}
|
|
424
|
+
size="sm"
|
|
425
|
+
className="h-8 px-2 text-xs"
|
|
426
|
+
onClick={() => {
|
|
427
|
+
setMediaPanel((p) => (p === 'image' ? null : 'image'))
|
|
428
|
+
}}
|
|
429
|
+
>
|
|
430
|
+
图片链接
|
|
431
|
+
</Button>
|
|
432
|
+
<Button
|
|
433
|
+
type="button"
|
|
434
|
+
variant={mediaPanel === 'video' ? 'secondary' : 'outline'}
|
|
435
|
+
size="sm"
|
|
436
|
+
className="h-8 px-2 text-xs"
|
|
437
|
+
onClick={() => {
|
|
438
|
+
setMediaPanel((p) => (p === 'video' ? null : 'video'))
|
|
439
|
+
}}
|
|
440
|
+
>
|
|
441
|
+
<Video className="w-3.5 h-3.5 mr-1" />
|
|
442
|
+
视频
|
|
443
|
+
</Button>
|
|
444
|
+
<Button
|
|
445
|
+
type="button"
|
|
446
|
+
variant={mediaPanel === 'audio' ? 'secondary' : 'outline'}
|
|
447
|
+
size="sm"
|
|
448
|
+
className="h-8 px-2 text-xs"
|
|
449
|
+
onClick={() => {
|
|
450
|
+
setMediaPanel((p) => (p === 'audio' ? null : 'audio'))
|
|
451
|
+
}}
|
|
452
|
+
>
|
|
453
|
+
<Music className="w-3.5 h-3.5 mr-1" />
|
|
454
|
+
音频
|
|
455
|
+
</Button>
|
|
456
|
+
</div>
|
|
457
|
+
{mediaPanel && (
|
|
458
|
+
<div className="flex flex-wrap items-end gap-2 rounded-md border border-border/80 bg-muted/20 p-2">
|
|
459
|
+
<Input
|
|
460
|
+
value={mediaUrl}
|
|
461
|
+
onChange={(e) => setMediaUrl(e.target.value)}
|
|
462
|
+
placeholder={
|
|
463
|
+
mediaPanel === 'image'
|
|
464
|
+
? '图片 URL 或 base64://…'
|
|
465
|
+
: mediaPanel === 'video'
|
|
466
|
+
? '视频直链 URL'
|
|
467
|
+
: '音频直链 URL'
|
|
468
|
+
}
|
|
469
|
+
className="flex-1 min-w-[12rem] h-9 text-sm"
|
|
470
|
+
onKeyDown={(e) => {
|
|
471
|
+
if (e.key === 'Enter') {
|
|
472
|
+
e.preventDefault()
|
|
473
|
+
commitMediaUrl()
|
|
474
|
+
}
|
|
475
|
+
}}
|
|
476
|
+
/>
|
|
477
|
+
<Button type="button" size="sm" className="h-9" onClick={commitMediaUrl} disabled={!mediaUrl.trim()}>
|
|
478
|
+
<Check className="w-3.5 h-3.5 mr-1" />
|
|
479
|
+
插入
|
|
480
|
+
</Button>
|
|
481
|
+
</div>
|
|
482
|
+
)}
|
|
483
|
+
<div className="flex gap-2 items-end">
|
|
484
|
+
<Textarea
|
|
485
|
+
placeholder="文字消息,或使用上方插入图片/音视频… 也可手写 [image:URL]、[video:URL]、[audio:URL]"
|
|
486
|
+
value={msgContent}
|
|
487
|
+
onChange={(e) => setMsgContent(e.target.value)}
|
|
488
|
+
onKeyDown={(e) => {
|
|
489
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
490
|
+
e.preventDefault()
|
|
491
|
+
void handleSend()
|
|
492
|
+
}
|
|
493
|
+
}}
|
|
494
|
+
className="flex-1 min-h-[44px] max-h-[160px] text-sm resize-y bg-background font-mono text-[13px]"
|
|
495
|
+
rows={2}
|
|
496
|
+
/>
|
|
497
|
+
<Button
|
|
498
|
+
className="shrink-0 h-10 w-10 p-0 rounded-full"
|
|
499
|
+
onClick={() => void handleSend()}
|
|
500
|
+
disabled={!canSend}
|
|
501
|
+
>
|
|
502
|
+
{sending ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />}
|
|
503
|
+
</Button>
|
|
504
|
+
</div>
|
|
505
|
+
<p className="text-[10px] text-muted-foreground">
|
|
506
|
+
Enter 发送 · Shift+Enter 换行 · 发送内容为消息段数组(含多媒体),由适配器实际发送
|
|
507
|
+
</p>
|
|
508
|
+
</div>
|
|
509
|
+
</>
|
|
510
|
+
)}
|
|
511
|
+
|
|
512
|
+
{selection?.type === 'requests' && (
|
|
513
|
+
<div className="flex flex-col flex-1 min-h-0 overflow-hidden bg-card m-0 border-0 rounded-none">
|
|
514
|
+
<header className="im-chat-header px-4 py-3 flex items-center justify-between">
|
|
515
|
+
<h2 className="text-base font-semibold flex items-center gap-2">
|
|
516
|
+
<UserPlus size={18} />
|
|
517
|
+
请求
|
|
518
|
+
</h2>
|
|
519
|
+
<Button size="sm" variant="outline" onClick={() => void loadRequestsFromServer()}>
|
|
520
|
+
刷新
|
|
521
|
+
</Button>
|
|
522
|
+
</header>
|
|
523
|
+
<Tabs
|
|
524
|
+
value={requestsTab}
|
|
525
|
+
onValueChange={(v) => {
|
|
526
|
+
setRequestsTab(v as 'pending' | 'history')
|
|
527
|
+
if (v === 'history' && inboxRequests.length === 0 && !inboxRequestsLoading)
|
|
528
|
+
void loadInboxRequests(false)
|
|
529
|
+
}}
|
|
530
|
+
className="flex flex-col flex-1 min-h-0"
|
|
531
|
+
>
|
|
532
|
+
<TabsList className="mx-3 mt-2 w-auto justify-start">
|
|
533
|
+
<TabsTrigger value="pending">待处理</TabsTrigger>
|
|
534
|
+
<TabsTrigger value="history">历史</TabsTrigger>
|
|
535
|
+
</TabsList>
|
|
536
|
+
<TabsContent value="pending" className="flex-1 overflow-y-auto p-4 space-y-3 mt-0">
|
|
537
|
+
{requestList.length === 0 && (
|
|
538
|
+
<div className="flex flex-col items-center justify-center py-16 gap-2 text-muted-foreground text-sm">
|
|
539
|
+
<UserPlus size={40} className="opacity-25" />
|
|
540
|
+
<span>暂无未处理请求</span>
|
|
541
|
+
</div>
|
|
542
|
+
)}
|
|
543
|
+
{requestList.map((r) => (
|
|
544
|
+
<div key={r.id} className="border border-border/80 rounded-lg p-3 space-y-2 bg-background/50">
|
|
545
|
+
<div className="flex flex-wrap gap-2 text-sm">
|
|
546
|
+
<Badge>{r.type}</Badge>
|
|
547
|
+
<span>来自 {r.sender.name || r.sender.id}</span>
|
|
548
|
+
<span className="text-muted-foreground text-xs">
|
|
549
|
+
{new Date(r.timestamp).toLocaleString()}
|
|
550
|
+
</span>
|
|
551
|
+
</div>
|
|
552
|
+
{r.comment && <p className="text-sm">{r.comment}</p>}
|
|
553
|
+
<div className="flex flex-wrap gap-2">
|
|
554
|
+
{r.canAct === true && (
|
|
555
|
+
<>
|
|
556
|
+
<Button size="sm" onClick={() => void approve(r.platformRequestId, true)}>
|
|
557
|
+
同意
|
|
558
|
+
</Button>
|
|
559
|
+
<Button size="sm" variant="outline" onClick={() => void approve(r.platformRequestId, false)}>
|
|
560
|
+
拒绝
|
|
561
|
+
</Button>
|
|
562
|
+
</>
|
|
563
|
+
)}
|
|
564
|
+
<Button size="sm" variant="ghost" onClick={() => void dismissRequest(r.id)}>
|
|
565
|
+
标记已处理
|
|
566
|
+
</Button>
|
|
567
|
+
</div>
|
|
568
|
+
</div>
|
|
569
|
+
))}
|
|
570
|
+
</TabsContent>
|
|
571
|
+
<TabsContent value="history" className="flex-1 overflow-y-auto p-4 space-y-3 mt-0 min-h-0">
|
|
572
|
+
{!inboxRequestsEnabled && !inboxRequestsLoading && (
|
|
573
|
+
<div className="flex flex-col items-center justify-center py-12 gap-2 text-muted-foreground text-sm">
|
|
574
|
+
<span>未启用统一收件箱,无历史记录</span>
|
|
575
|
+
</div>
|
|
576
|
+
)}
|
|
577
|
+
{inboxRequestsLoading && inboxRequests.length === 0 && (
|
|
578
|
+
<div className="flex justify-center py-8">
|
|
579
|
+
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
|
580
|
+
</div>
|
|
581
|
+
)}
|
|
582
|
+
{inboxRequestsEnabled && inboxRequests.length === 0 && !inboxRequestsLoading && (
|
|
583
|
+
<div className="flex flex-col items-center justify-center py-12 gap-2 text-muted-foreground text-sm">
|
|
584
|
+
<span>暂无请求历史</span>
|
|
585
|
+
</div>
|
|
586
|
+
)}
|
|
587
|
+
{inboxRequests.length > 0 && (
|
|
588
|
+
<>
|
|
589
|
+
{inboxRequests.map((r) => (
|
|
590
|
+
<div key={r.id} className="border border-border/80 rounded-lg p-3 space-y-1 text-sm bg-background/50">
|
|
591
|
+
<div className="flex flex-wrap gap-2">
|
|
592
|
+
<Badge variant="outline">{r.type}</Badge>
|
|
593
|
+
<span>{r.sender_name || r.sender_id}</span>
|
|
594
|
+
<span className="text-muted-foreground text-xs">
|
|
595
|
+
{new Date(r.created_at).toLocaleString()}
|
|
596
|
+
</span>
|
|
597
|
+
{r.resolved ? <Badge variant="secondary">已处理</Badge> : null}
|
|
598
|
+
</div>
|
|
599
|
+
{r.comment && <p className="text-muted-foreground text-sm">{r.comment}</p>}
|
|
600
|
+
</div>
|
|
601
|
+
))}
|
|
602
|
+
<div className="flex justify-center pt-2">
|
|
603
|
+
<Button
|
|
604
|
+
variant="ghost"
|
|
605
|
+
size="sm"
|
|
606
|
+
disabled={inboxRequestsLoading}
|
|
607
|
+
onClick={() => void loadInboxRequests(true)}
|
|
608
|
+
>
|
|
609
|
+
{inboxRequestsLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : '加载更多'}
|
|
610
|
+
</Button>
|
|
611
|
+
</div>
|
|
612
|
+
</>
|
|
613
|
+
)}
|
|
614
|
+
</TabsContent>
|
|
615
|
+
</Tabs>
|
|
616
|
+
</div>
|
|
617
|
+
)}
|
|
618
|
+
|
|
619
|
+
{selection?.type === 'notices' && (
|
|
620
|
+
<div className="flex flex-col flex-1 min-h-0 overflow-hidden bg-card m-0 border-0 rounded-none">
|
|
621
|
+
<header className="im-chat-header px-4 py-3">
|
|
622
|
+
<h2 className="text-base font-semibold flex items-center gap-2">
|
|
623
|
+
<Bell size={18} />
|
|
624
|
+
通知
|
|
625
|
+
</h2>
|
|
626
|
+
</header>
|
|
627
|
+
<Tabs
|
|
628
|
+
value={noticesTab}
|
|
629
|
+
onValueChange={(v) => {
|
|
630
|
+
setNoticesTab(v as 'unread' | 'history')
|
|
631
|
+
if (v === 'history' && inboxNotices.length === 0 && !inboxNoticesLoading)
|
|
632
|
+
void loadInboxNotices(false)
|
|
633
|
+
}}
|
|
634
|
+
className="flex flex-col flex-1 min-h-0"
|
|
635
|
+
>
|
|
636
|
+
<TabsList className="mx-3 mt-2 w-auto justify-start">
|
|
637
|
+
<TabsTrigger value="unread">未读</TabsTrigger>
|
|
638
|
+
<TabsTrigger value="history">历史</TabsTrigger>
|
|
639
|
+
</TabsList>
|
|
640
|
+
<TabsContent value="unread" className="flex-1 overflow-y-auto p-4 space-y-3 mt-0">
|
|
641
|
+
{noticeList.length === 0 && (
|
|
642
|
+
<div className="flex flex-col items-center justify-center py-16 gap-2 text-muted-foreground text-sm">
|
|
643
|
+
<Bell size={40} className="opacity-25" />
|
|
644
|
+
<span>暂无未读通知</span>
|
|
645
|
+
</div>
|
|
646
|
+
)}
|
|
647
|
+
{noticeList.map((n) => (
|
|
648
|
+
<div
|
|
649
|
+
key={n.id}
|
|
650
|
+
className="border border-border/80 rounded-lg p-3 flex justify-between gap-2 bg-background/50"
|
|
651
|
+
>
|
|
652
|
+
<div className="min-w-0">
|
|
653
|
+
<Badge className="mb-1">{n.noticeType}</Badge>
|
|
654
|
+
<p className="text-xs text-muted-foreground font-mono truncate max-w-md">
|
|
655
|
+
{n.payload.slice(0, 200)}
|
|
656
|
+
</p>
|
|
657
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
658
|
+
{new Date(n.timestamp).toLocaleString()}
|
|
659
|
+
</p>
|
|
660
|
+
</div>
|
|
661
|
+
<Button size="sm" variant="outline" onClick={() => void dismissNotice(n.id)}>
|
|
662
|
+
已读
|
|
663
|
+
</Button>
|
|
664
|
+
</div>
|
|
665
|
+
))}
|
|
666
|
+
</TabsContent>
|
|
667
|
+
<TabsContent value="history" className="flex-1 overflow-y-auto p-4 space-y-3 mt-0 min-h-0">
|
|
668
|
+
{!inboxNoticesEnabled && !inboxNoticesLoading && (
|
|
669
|
+
<div className="flex flex-col items-center justify-center py-12 gap-2 text-muted-foreground text-sm">
|
|
670
|
+
<span>未启用统一收件箱,无历史记录</span>
|
|
671
|
+
</div>
|
|
672
|
+
)}
|
|
673
|
+
{inboxNoticesLoading && inboxNotices.length === 0 && (
|
|
674
|
+
<div className="flex justify-center py-8">
|
|
675
|
+
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
|
676
|
+
</div>
|
|
677
|
+
)}
|
|
678
|
+
{inboxNoticesEnabled && inboxNotices.length === 0 && !inboxNoticesLoading && (
|
|
679
|
+
<div className="flex flex-col items-center justify-center py-12 gap-2 text-muted-foreground text-sm">
|
|
680
|
+
<span>暂无通知历史</span>
|
|
681
|
+
</div>
|
|
682
|
+
)}
|
|
683
|
+
{inboxNotices.length > 0 && (
|
|
684
|
+
<>
|
|
685
|
+
{inboxNotices.map((n) => (
|
|
686
|
+
<div key={n.id} className="border border-border/80 rounded-lg p-3 space-y-1 text-sm bg-background/50">
|
|
687
|
+
<div className="flex flex-wrap gap-2">
|
|
688
|
+
<Badge variant="outline">{n.type}</Badge>
|
|
689
|
+
<span className="text-muted-foreground text-xs">
|
|
690
|
+
{new Date(n.created_at).toLocaleString()}
|
|
691
|
+
</span>
|
|
692
|
+
</div>
|
|
693
|
+
<p className="text-muted-foreground font-mono text-xs truncate max-w-full">
|
|
694
|
+
{String(n.payload ?? '').slice(0, 200)}
|
|
695
|
+
</p>
|
|
696
|
+
</div>
|
|
697
|
+
))}
|
|
698
|
+
<div className="flex justify-center pt-2">
|
|
699
|
+
<Button
|
|
700
|
+
variant="ghost"
|
|
701
|
+
size="sm"
|
|
702
|
+
disabled={inboxNoticesLoading}
|
|
703
|
+
onClick={() => void loadInboxNotices(true)}
|
|
704
|
+
>
|
|
705
|
+
{inboxNoticesLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : '加载更多'}
|
|
706
|
+
</Button>
|
|
707
|
+
</div>
|
|
708
|
+
</>
|
|
709
|
+
)}
|
|
710
|
+
</TabsContent>
|
|
711
|
+
</Tabs>
|
|
712
|
+
</div>
|
|
713
|
+
)}
|
|
714
|
+
|
|
715
|
+
{!selection && (
|
|
716
|
+
<div className="flex flex-col items-center justify-center flex-1 gap-3 text-muted-foreground px-6 text-center">
|
|
717
|
+
<MessageSquare className="h-14 w-14 opacity-20" />
|
|
718
|
+
<p className="text-sm font-medium text-foreground/80">选择会话或查看请求 / 通知</p>
|
|
719
|
+
<p className="text-xs max-w-sm">
|
|
720
|
+
左侧列表与 Telegram Web 类似:点选好友或群开始聊天;请求与通知在列表下方分组。
|
|
721
|
+
</p>
|
|
722
|
+
</div>
|
|
723
|
+
)}
|
|
724
|
+
</div>
|
|
725
|
+
|
|
726
|
+
{showRightPanel && (
|
|
727
|
+
<aside className={cn('im-right-panel im-right-visible')}>
|
|
728
|
+
<div className="p-3 border-b border-border/60">
|
|
729
|
+
<h3 className="text-sm font-semibold">群成员与管理</h3>
|
|
730
|
+
<p className="text-[11px] text-muted-foreground mt-0.5">仅 ICQQ 群聊</p>
|
|
731
|
+
</div>
|
|
732
|
+
<div className="p-2 border-b border-border/60">
|
|
733
|
+
<Button
|
|
734
|
+
size="sm"
|
|
735
|
+
variant="outline"
|
|
736
|
+
className="w-full"
|
|
737
|
+
onClick={() => void loadMembers()}
|
|
738
|
+
disabled={membersLoading}
|
|
739
|
+
>
|
|
740
|
+
{membersLoading ? <Loader2 className="h-4 w-4 animate-spin mr-1" /> : null}
|
|
741
|
+
加载成员
|
|
742
|
+
</Button>
|
|
743
|
+
</div>
|
|
744
|
+
<div className="flex-1 overflow-y-auto p-2 space-y-2 min-h-0">
|
|
745
|
+
{members.map((m, i) => {
|
|
746
|
+
const uid = m.user_id ?? (m as { id?: string }).id ?? i
|
|
747
|
+
return (
|
|
748
|
+
<div
|
|
749
|
+
key={`${uid}-${i}`}
|
|
750
|
+
className="flex flex-col gap-1.5 text-xs p-2 border border-border/70 rounded-md bg-background/60"
|
|
751
|
+
>
|
|
752
|
+
<div className="flex flex-wrap items-center gap-1">
|
|
753
|
+
<span className="font-medium text-sm">
|
|
754
|
+
{m.nickname ?? (m as { name?: string }).name ?? uid}
|
|
755
|
+
</span>
|
|
756
|
+
<span className="text-muted-foreground">{uid}</span>
|
|
757
|
+
{(m.role ?? (m as { role?: string }).role) != null && (
|
|
758
|
+
<Badge variant="outline" className="text-[10px]">
|
|
759
|
+
{String(m.role ?? (m as { role?: string }).role)}
|
|
760
|
+
</Badge>
|
|
761
|
+
)}
|
|
762
|
+
</div>
|
|
763
|
+
<div className="flex flex-wrap gap-1">
|
|
764
|
+
<Button
|
|
765
|
+
size="sm"
|
|
766
|
+
variant="destructive"
|
|
767
|
+
className="h-7 text-[10px] px-2"
|
|
768
|
+
onClick={() => void groupAction('bot:groupKick', uid)}
|
|
769
|
+
>
|
|
770
|
+
踢
|
|
771
|
+
</Button>
|
|
772
|
+
<Button
|
|
773
|
+
size="sm"
|
|
774
|
+
variant="outline"
|
|
775
|
+
className="h-7 text-[10px] px-2"
|
|
776
|
+
onClick={() => void groupAction('bot:groupMute', uid)}
|
|
777
|
+
>
|
|
778
|
+
禁言
|
|
779
|
+
</Button>
|
|
780
|
+
<Button
|
|
781
|
+
size="sm"
|
|
782
|
+
variant="outline"
|
|
783
|
+
className="h-7 text-[10px] px-2"
|
|
784
|
+
onClick={() => void groupAction('bot:groupAdmin', uid, { enable: true })}
|
|
785
|
+
>
|
|
786
|
+
管理
|
|
787
|
+
</Button>
|
|
788
|
+
</div>
|
|
789
|
+
</div>
|
|
790
|
+
)
|
|
791
|
+
})}
|
|
792
|
+
</div>
|
|
793
|
+
</aside>
|
|
794
|
+
)}
|
|
795
|
+
</div>
|
|
796
|
+
</div>
|
|
797
|
+
)
|
|
798
|
+
}
|