@zhin.js/adapter-sandbox 1.0.61 → 1.0.63

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 CHANGED
@@ -1,5 +1,25 @@
1
1
  # @zhin.js/adapter-process
2
2
 
3
+ ## 1.0.63
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [bb6bfa8]
8
+ - Updated dependencies [bb6bfa8]
9
+ - Updated dependencies [bb6bfa8]
10
+ - @zhin.js/core@1.0.52
11
+ - zhin.js@1.0.52
12
+ - @zhin.js/console@1.0.52
13
+ - @zhin.js/client@1.0.13
14
+ - @zhin.js/http@1.0.46
15
+
16
+ ## 1.0.62
17
+
18
+ ### Patch Changes
19
+
20
+ - Updated dependencies [a451abf]
21
+ - @zhin.js/console@1.0.51
22
+
3
23
  ## 1.0.61
4
24
 
5
25
  ### Patch Changes
@@ -1,7 +1,7 @@
1
1
  import { useRef, useEffect, forwardRef, useImperativeHandle } from 'react'
2
2
 
3
3
  export interface MessageSegment {
4
- type: 'text' | 'at' | 'face' | 'image' | 'video' | 'audio' | 'file'
4
+ type: 'text' | 'at' | 'face' | 'image' | 'video' | 'audio' | 'record' | 'file'
5
5
  data: Record<string, any>
6
6
  }
7
7
 
@@ -19,6 +19,8 @@ export interface RichTextEditorRef {
19
19
  clear: () => void
20
20
  insertFace: (faceId: number) => void
21
21
  insertImage: (url: string) => void
22
+ insertVideo: (url: string) => void
23
+ insertAudio: (url: string) => void
22
24
  insertAt: (name: string, id?: string) => void
23
25
  replaceAtTrigger: (name: string, id?: string) => void
24
26
  getContent: () => { text: string; segments: MessageSegment[] }
@@ -55,6 +57,14 @@ const RichTextEditor = forwardRef<RichTextEditorRef, RichTextEditorProps>(
55
57
  const imageUrl = el.dataset.url
56
58
  text += `[image:${imageUrl}]`
57
59
  segments.push({ type: 'image', data: { url: imageUrl } })
60
+ } else if (el.classList.contains('editor-video')) {
61
+ const u = el.dataset.url || ''
62
+ text += `[video:${u}]`
63
+ segments.push({ type: 'video', data: { url: u } })
64
+ } else if (el.classList.contains('editor-audio')) {
65
+ const u = el.dataset.url || ''
66
+ text += `[audio:${u}]`
67
+ segments.push({ type: 'audio', data: { url: u } })
58
68
  } else if (el.classList.contains('editor-at')) {
59
69
  const name = el.dataset.name
60
70
  const id = el.dataset.id
@@ -99,6 +109,30 @@ const RichTextEditor = forwardRef<RichTextEditorRef, RichTextEditorProps>(
99
109
  handleChange()
100
110
  }
101
111
 
112
+ const insertVideo = (url: string) => {
113
+ if (!editorRef.current || !url.trim()) return
114
+ const u = url.trim()
115
+ const span = document.createElement('span')
116
+ span.className = 'editor-video'
117
+ span.dataset.url = u
118
+ span.contentEditable = 'false'
119
+ span.textContent = '📹 视频'
120
+ insertNodeAtCursor(span)
121
+ handleChange()
122
+ }
123
+
124
+ const insertAudio = (url: string) => {
125
+ if (!editorRef.current || !url.trim()) return
126
+ const u = url.trim()
127
+ const span = document.createElement('span')
128
+ span.className = 'editor-audio'
129
+ span.dataset.url = u
130
+ span.contentEditable = 'false'
131
+ span.textContent = '🎵 音频'
132
+ insertNodeAtCursor(span)
133
+ handleChange()
134
+ }
135
+
102
136
  // 插入 @ 提及
103
137
  const insertAt = (name: string, id?: string) => {
104
138
  if (!editorRef.current || !name.trim()) return
@@ -319,6 +353,8 @@ const RichTextEditor = forwardRef<RichTextEditorRef, RichTextEditorProps>(
319
353
  clear,
320
354
  insertFace,
321
355
  insertImage,
356
+ insertVideo,
357
+ insertAudio,
322
358
  insertAt,
323
359
  replaceAtTrigger,
324
360
  getContent
@@ -1,6 +1,6 @@
1
1
  import React, { useState, useEffect, useRef } from 'react';
2
- import { MessageSegment, cn } from '@zhin.js/client';
3
- import { User, Users, Trash2, Send, Hash, MessageSquare, Wifi, WifiOff, Smile, Image, AtSign, X, Upload, Check, Info, Search, Bot } from 'lucide-react';
2
+ import { MessageSegment, cn, resolveMediaSrc, pickMediaRawUrl } from '@zhin.js/client';
3
+ import { User, Users, Trash2, Send, Hash, MessageSquare, Wifi, WifiOff, Smile, Image, X, Check, Info, Search, Bot, UserPlus, Bell, Video, Music } from 'lucide-react';
4
4
  import RichTextEditor, { RichTextEditorRef } from './RichTextEditor';
5
5
 
6
6
  interface Message {
@@ -25,12 +25,13 @@ export default function Sandbox() {
25
25
  const [botName, setBotName] = useState('ProcessBot')
26
26
  const [connected, setConnected] = useState(false)
27
27
  const [showFacePicker, setShowFacePicker] = useState(false)
28
- const [showImageUpload, setShowImageUpload] = useState(false)
28
+ /** 输入区:插入图片 / 视频 / 音频 URL */
29
+ const [mediaPanel, setMediaPanel] = useState<null | 'image' | 'video' | 'audio'>(null)
30
+ const [mediaUrl, setMediaUrl] = useState('')
29
31
  const [showAtPicker, setShowAtPicker] = useState(false)
30
32
  const [atPopoverPosition, setAtPopoverPosition] = useState<{ top: number; left: number } | null>(null)
31
33
  const [atSearchQuery, setAtSearchQuery] = useState('')
32
34
  const [faceSearchQuery, setFaceSearchQuery] = useState('')
33
- const [imageUrl, setImageUrl] = useState('')
34
35
  const [atUserName, setAtUserName] = useState('')
35
36
  const [atSuggestions] = useState([
36
37
  { id: '10001', name: '张三' }, { id: '10002', name: '李四' }, { id: '10003', name: '王五' },
@@ -39,6 +40,7 @@ export default function Sandbox() {
39
40
  ])
40
41
  const [previewSegments, setPreviewSegments] = useState<MessageSegment[]>([])
41
42
  const [showChannelList, setShowChannelList] = useState(false)
43
+ const [viewMode, setViewMode] = useState<'chat' | 'requests' | 'notices'>('chat')
42
44
  const messagesEndRef = useRef<HTMLDivElement>(null)
43
45
  const wsRef = useRef<WebSocket | null>(null)
44
46
  const editorRef = useRef<RichTextEditorRef>(null)
@@ -85,39 +87,101 @@ export default function Sandbox() {
85
87
  useEffect(() => { setPreviewSegments(inputText.trim() ? parseTextToSegments(inputText) : []) }, [inputText])
86
88
 
87
89
  const parseTextToSegments = (text: string): MessageSegment[] => {
88
- const segments: MessageSegment[] = []; const regex = /\[@([^\]]+)\]|\[face:(\d+)\]|\[image:([^\]]+)\]/g
89
- let lastIndex = 0; let match
90
+ const segments: MessageSegment[] = []
91
+ const regex = /\[@([^\]]+)\]|\[face:(\d+)\]|\[image:([^\]]+)\]|\[video:([^\]]+)\]|\[audio:([^\]]+)\]/g
92
+ let lastIndex = 0
93
+ let match: RegExpExecArray | null
90
94
  while ((match = regex.exec(text)) !== null) {
91
- if (match.index > lastIndex) { const t = text.substring(lastIndex, match.index); if (t) segments.push({ type: 'text', data: { text: t } }) }
95
+ if (match.index > lastIndex) {
96
+ const t = text.substring(lastIndex, match.index)
97
+ if (t) segments.push({ type: 'text', data: { text: t } })
98
+ }
92
99
  if (match[1]) segments.push({ type: 'at', data: { qq: match[1], name: match[1] } })
93
- else if (match[2]) segments.push({ type: 'face', data: { id: parseInt(match[2]) } })
100
+ else if (match[2]) segments.push({ type: 'face', data: { id: parseInt(match[2], 10) } })
94
101
  else if (match[3]) segments.push({ type: 'image', data: { url: match[3] } })
102
+ else if (match[4]) segments.push({ type: 'video', data: { url: match[4] } })
103
+ else if (match[5]) segments.push({ type: 'audio', data: { url: match[5] } })
95
104
  lastIndex = regex.lastIndex
96
105
  }
97
- if (lastIndex < text.length) { const r = text.substring(lastIndex); if (r) segments.push({ type: 'text', data: { text: r } }) }
106
+ if (lastIndex < text.length) {
107
+ const r = text.substring(lastIndex)
108
+ if (r) segments.push({ type: 'text', data: { text: r } })
109
+ }
98
110
  return segments.length > 0 ? segments : [{ type: 'text', data: { text } }]
99
111
  }
100
112
 
101
- const renderMessageSegments = (segments: (MessageSegment | string)[]) => {
113
+ const hasRenderableSegments = (segments: MessageSegment[]) => {
114
+ if (segments.length === 0) return false
115
+ return segments.some((s) => {
116
+ if (s.type === 'text') return Boolean(String(s.data?.text ?? '').trim())
117
+ return true
118
+ })
119
+ }
120
+
121
+ const renderMessageSegments = (segments: (MessageSegment | string)[], isSent: boolean) => {
122
+ const ring = isSent ? 'ring-1 ring-primary-foreground/25' : 'ring-1 ring-border/60'
102
123
  return segments.map((segment, index) => {
103
124
  if (typeof segment === 'string') {
104
125
  return <span key={index}>{segment.split('\n').map((part, i) => <React.Fragment key={i}>{part}{i < segment.split('\n').length - 1 && <br />}</React.Fragment>)}</span>
105
126
  }
127
+ const d = segment.data as Record<string, unknown>
106
128
  switch (segment.type) {
107
- case 'text': return <span key={index}>{segment.data.text.split('\n').map((part: string, i: number) => <React.Fragment key={i}>{part}{i < segment.data.text.split('\n').length - 1 && <br />}</React.Fragment>)}</span>
108
- case 'at': return <span key={index} className="inline-flex items-center px-1.5 py-0.5 rounded bg-accent text-accent-foreground text-xs mx-0.5">@{segment.data.name || segment.data.qq}</span>
109
- case 'face': return <img key={index} src={`https://face.viki.moe/apng/${segment.data.id}.png`} alt={`face${segment.data.id}`} className="w-6 h-6 inline-block align-middle mx-0.5" />
110
- case 'image': return <img key={index} src={segment.data.url} alt="image" className="max-w-[300px] rounded-lg my-1 block" onError={(e) => { e.currentTarget.style.display = 'none' }} />
111
- case 'video': return <span key={index} className="inline-flex items-center px-1.5 py-0.5 rounded border text-xs mx-0.5">📹 视频</span>
112
- case 'audio': return <span key={index} className="inline-flex items-center px-1.5 py-0.5 rounded border text-xs mx-0.5">🎵 语音</span>
113
- case 'file': return <span key={index} className="inline-flex items-center px-1.5 py-0.5 rounded border text-xs mx-0.5">📎 {segment.data.name || '文件'}</span>
114
- default: return <span key={index}>[未知消息类型]</span>
129
+ case 'text':
130
+ return <span key={index}>{String(d.text ?? '').split('\n').map((part: string, i: number) => <React.Fragment key={i}>{part}{i < String(d.text ?? '').split('\n').length - 1 && <br />}</React.Fragment>)}</span>
131
+ case 'at':
132
+ return <span key={index} className="inline-flex items-center px-1.5 py-0.5 rounded bg-accent text-accent-foreground text-xs mx-0.5">@{String(d.name ?? d.qq ?? '')}</span>
133
+ case 'face':
134
+ return <img key={index} src={`https://face.viki.moe/apng/${d.id}.png`} alt="" className="w-6 h-6 inline-block align-middle mx-0.5" />
135
+ case 'image': {
136
+ const raw = pickMediaRawUrl(d)
137
+ const src = resolveMediaSrc(raw, 'image')
138
+ if (!src) return <span key={index} className="text-xs opacity-70">[图片]</span>
139
+ return (
140
+ <a key={index} href={src} target="_blank" rel="noreferrer" className="block my-1">
141
+ <img src={src} alt="" className={cn('max-w-[min(320px,88vw)] rounded-lg block', ring, 'ring-offset-0')} onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }} />
142
+ </a>
143
+ )
144
+ }
145
+ case 'video': {
146
+ const raw = pickMediaRawUrl(d)
147
+ const src = resolveMediaSrc(raw, 'video')
148
+ if (!src) return <span key={index} className="text-xs opacity-70">[视频无地址]</span>
149
+ return (
150
+ <video
151
+ key={index}
152
+ src={src}
153
+ controls
154
+ playsInline
155
+ preload="metadata"
156
+ className={cn('max-w-[min(360px,92vw)] max-h-72 rounded-lg my-1 bg-black/10', ring)}
157
+ />
158
+ )
159
+ }
160
+ case 'audio':
161
+ case 'record': {
162
+ const raw = pickMediaRawUrl(d)
163
+ const src = resolveMediaSrc(raw, 'audio')
164
+ if (!src) return <span key={index} className="text-xs opacity-70">[音频无地址]</span>
165
+ return (
166
+ <audio
167
+ key={index}
168
+ src={src}
169
+ controls
170
+ preload="metadata"
171
+ className={cn('w-full max-w-sm my-2 h-10', isSent && 'opacity-95')}
172
+ />
173
+ )
174
+ }
175
+ case 'file':
176
+ return <span key={index} className="inline-flex items-center px-1.5 py-0.5 rounded border text-xs mx-0.5">📎 {String(d.name || '文件')}</span>
177
+ default:
178
+ return <span key={index} className="text-xs opacity-70">[{segment.type}]</span>
115
179
  }
116
180
  })
117
181
  }
118
182
 
119
183
  const handleSendMessage = (text: string, segments: MessageSegment[]) => {
120
- if (!text.trim() || segments.length === 0) return
184
+ if (!hasRenderableSegments(segments)) return
121
185
  const newMessage: Message = { id: `msg_${Date.now()}`, type: 'sent', channelType: activeChannel.type, channelId: activeChannel.id, channelName: activeChannel.name, senderId: 'test_user', senderName: '测试用户', content: segments, timestamp: Date.now() }
122
186
  setMessages((prev) => [...prev, newMessage]); setInputText(''); setPreviewSegments([])
123
187
  editorRef.current?.clear()
@@ -125,7 +189,7 @@ export default function Sandbox() {
125
189
  }
126
190
 
127
191
  const clearMessages = () => { if (confirm('确定清空所有消息记录?')) setMessages([]) }
128
- const switchChannel = (channel: Channel) => { setActiveChannel(channel); setChannels((prev) => prev.map((c) => c.id === channel.id ? { ...c, unread: 0 } : c)); if (window.innerWidth < 768) setShowChannelList(false) }
192
+ const switchChannel = (channel: Channel) => { setViewMode('chat'); setActiveChannel(channel); setChannels((prev) => prev.map((c) => c.id === channel.id ? { ...c, unread: 0 } : c)); if (window.innerWidth < 768) setShowChannelList(false) }
129
193
  const addChannel = () => {
130
194
  const types: Array<'private' | 'group' | 'channel'> = ['private', 'group', 'channel']
131
195
  const type = types[Math.floor(Math.random() * types.length)]
@@ -134,7 +198,15 @@ export default function Sandbox() {
134
198
  }
135
199
  const getChannelIcon = (type: string) => { switch (type) { case 'private': return <User size={16} />; case 'group': return <Users size={16} />; case 'channel': return <Hash size={16} />; default: return <MessageSquare size={16} /> } }
136
200
  const insertFace = (faceId: number) => { editorRef.current?.insertFace(faceId); setShowFacePicker(false) }
137
- const insertImageUrl = () => { if (!imageUrl.trim()) return; editorRef.current?.insertImage(imageUrl.trim()); setImageUrl(''); setShowImageUpload(false) }
201
+ const commitMediaUrl = () => {
202
+ const u = mediaUrl.trim()
203
+ if (!u || !mediaPanel) return
204
+ if (mediaPanel === 'image') editorRef.current?.insertImage(u)
205
+ else if (mediaPanel === 'video') editorRef.current?.insertVideo(u)
206
+ else editorRef.current?.insertAudio(u)
207
+ setMediaUrl('')
208
+ setMediaPanel(null)
209
+ }
138
210
  const insertAtUser = () => { if (!atUserName.trim()) return; editorRef.current?.insertAt(atUserName.trim()); setAtUserName(''); setShowAtPicker(false) }
139
211
  const selectAtUser = (user: { id: string; name: string }) => { editorRef.current?.replaceAtTrigger(user.name, user.id); setAtPopoverPosition(null); setAtSearchQuery('') }
140
212
  const handleAtTrigger = (show: boolean, searchQuery: string, position?: { top: number; left: number }) => {
@@ -147,7 +219,7 @@ export default function Sandbox() {
147
219
  const channelMessages = messages.filter((msg) => msg.channelId === activeChannel.id)
148
220
 
149
221
  return (
150
- <div className="sandbox-container">
222
+ <div className="sandbox-container rounded-xl border border-border/70 bg-card/30 shadow-sm">
151
223
  <button className="mobile-channel-toggle md:hidden" onClick={() => setShowChannelList(!showChannelList)}>
152
224
  <MessageSquare size={20} /> 频道列表
153
225
  </button>
@@ -169,7 +241,7 @@ export default function Sandbox() {
169
241
 
170
242
  <div className="flex-1 overflow-y-auto p-2 space-y-1">
171
243
  {channels.map((channel) => {
172
- const isActive = activeChannel.id === channel.id
244
+ const isActive = viewMode === 'chat' && activeChannel.id === channel.id
173
245
  return (
174
246
  <div key={channel.id} className={cn("menu-item", isActive && "active")} onClick={() => switchChannel(channel)}>
175
247
  <span className="shrink-0">{getChannelIcon(channel.type)}</span>
@@ -181,6 +253,22 @@ export default function Sandbox() {
181
253
  </div>
182
254
  )
183
255
  })}
256
+ <div className="pt-2 mt-2 border-t space-y-1">
257
+ <div className={cn("menu-item", viewMode === 'requests' && "active")} onClick={() => { setViewMode('requests'); if (window.innerWidth < 768) setShowChannelList(false) }}>
258
+ <UserPlus size={16} className="shrink-0" />
259
+ <div className="flex-1 min-w-0">
260
+ <div className="text-sm font-medium">请求</div>
261
+ <div className="text-xs text-muted-foreground">好友/群邀请等</div>
262
+ </div>
263
+ </div>
264
+ <div className={cn("menu-item", viewMode === 'notices' && "active")} onClick={() => { setViewMode('notices'); if (window.innerWidth < 768) setShowChannelList(false) }}>
265
+ <Bell size={16} className="shrink-0" />
266
+ <div className="flex-1 min-w-0">
267
+ <div className="text-sm font-medium">通知</div>
268
+ <div className="text-xs text-muted-foreground">群管/撤回等</div>
269
+ </div>
270
+ </div>
271
+ </div>
184
272
  </div>
185
273
 
186
274
  <div className="p-2 border-t">
@@ -190,8 +278,40 @@ export default function Sandbox() {
190
278
 
191
279
  {showChannelList && <div className="channel-overlay md:hidden" onClick={() => setShowChannelList(false)} />}
192
280
 
193
- {/* Chat area */}
281
+ {/* Main area: Chat / Requests / Notices */}
194
282
  <div className="chat-area">
283
+ {viewMode === 'requests' && (
284
+ <div className="rounded-lg border bg-card flex-1 flex flex-col min-h-0 overflow-hidden">
285
+ <div className="p-3 border-b flex-shrink-0">
286
+ <h2 className="text-lg font-bold flex items-center gap-2">
287
+ <UserPlus size={20} /> 请求
288
+ </h2>
289
+ </div>
290
+ <div className="flex-1 overflow-y-auto p-4 flex flex-col items-center justify-center gap-3 text-muted-foreground text-center">
291
+ <UserPlus size={48} className="opacity-30" />
292
+ <span>沙盒为模拟环境,暂无请求数据</span>
293
+ <span className="text-sm">实际好友/群邀请等请求请到侧边栏 <strong>机器人</strong> 页面进入对应机器人管理查看</span>
294
+ </div>
295
+ </div>
296
+ )}
297
+
298
+ {viewMode === 'notices' && (
299
+ <div className="rounded-lg border bg-card flex-1 flex flex-col min-h-0 overflow-hidden">
300
+ <div className="p-3 border-b flex-shrink-0">
301
+ <h2 className="text-lg font-bold flex items-center gap-2">
302
+ <Bell size={20} /> 通知
303
+ </h2>
304
+ </div>
305
+ <div className="flex-1 overflow-y-auto p-4 flex flex-col items-center justify-center gap-3 text-muted-foreground text-center">
306
+ <Bell size={48} className="opacity-30" />
307
+ <span>沙盒为模拟环境,暂无通知数据</span>
308
+ <span className="text-sm">实际群管、撤回等通知请到侧边栏 <strong>机器人</strong> 页面进入对应机器人管理查看</span>
309
+ </div>
310
+ </div>
311
+ )}
312
+
313
+ {viewMode === 'chat' && (
314
+ <>
195
315
  {/* Top bar */}
196
316
  <div className="rounded-lg border bg-card p-3 flex-shrink-0">
197
317
  <div className="flex justify-between items-center flex-wrap gap-2">
@@ -238,7 +358,7 @@ export default function Sandbox() {
238
358
  <span className="text-xs font-medium opacity-90">{msg.senderName}</span>
239
359
  <span className="text-xs opacity-70">{new Date(msg.timestamp).toLocaleTimeString()}</span>
240
360
  </div>
241
- <div className="text-sm">{renderMessageSegments(msg.content)}</div>
361
+ <div className="text-sm space-y-1">{renderMessageSegments(msg.content, msg.type === 'sent')}</div>
242
362
  </div>
243
363
  </div>
244
364
  ))}
@@ -251,16 +371,24 @@ export default function Sandbox() {
251
371
  {/* Input area */}
252
372
  <div className="rounded-lg border bg-card p-3 flex-shrink-0 space-y-3">
253
373
  {/* Toolbar */}
254
- <div className="flex gap-2 items-center">
255
- <button className={cn("h-8 w-8 rounded-md flex items-center justify-center border transition-colors", showFacePicker ? "bg-primary text-primary-foreground" : "hover:bg-accent")}
256
- onClick={() => { setShowFacePicker(!showFacePicker); setShowImageUpload(false) }} title="插入表情">
374
+ <div className="flex gap-2 items-center flex-wrap">
375
+ <button type="button" className={cn("h-8 w-8 rounded-md flex items-center justify-center border transition-colors", showFacePicker ? "bg-primary text-primary-foreground" : "hover:bg-accent")}
376
+ onClick={() => { setShowFacePicker(!showFacePicker); setMediaPanel(null) }} title="插入表情">
257
377
  <Smile size={16} />
258
378
  </button>
259
- <button className={cn("h-8 w-8 rounded-md flex items-center justify-center border transition-colors", showImageUpload ? "bg-primary text-primary-foreground" : "hover:bg-accent")}
260
- onClick={() => { setShowImageUpload(!showImageUpload); setShowFacePicker(false) }} title="插入图片">
379
+ <button type="button" className={cn("h-8 w-8 rounded-md flex items-center justify-center border transition-colors", mediaPanel === 'image' ? "bg-primary text-primary-foreground" : "hover:bg-accent")}
380
+ onClick={() => { setMediaPanel((p) => (p === 'image' ? null : 'image')); setShowFacePicker(false) }} title="插入图片 URL">
261
381
  <Image size={16} />
262
382
  </button>
263
- <div className="flex-1" />
383
+ <button type="button" className={cn("h-8 w-8 rounded-md flex items-center justify-center border transition-colors", mediaPanel === 'video' ? "bg-primary text-primary-foreground" : "hover:bg-accent")}
384
+ onClick={() => { setMediaPanel((p) => (p === 'video' ? null : 'video')); setShowFacePicker(false) }} title="插入视频 URL">
385
+ <Video size={16} />
386
+ </button>
387
+ <button type="button" className={cn("h-8 w-8 rounded-md flex items-center justify-center border transition-colors", mediaPanel === 'audio' ? "bg-primary text-primary-foreground" : "hover:bg-accent")}
388
+ onClick={() => { setMediaPanel((p) => (p === 'audio' ? null : 'audio')); setShowFacePicker(false) }} title="插入音频 URL">
389
+ <Music size={16} />
390
+ </button>
391
+ <div className="flex-1 min-w-[1rem]" />
264
392
  {inputText && (
265
393
  <button className="h-8 w-8 rounded-md flex items-center justify-center hover:bg-accent transition-colors"
266
394
  onClick={() => { setInputText(''); setPreviewSegments([]) }}><X size={16} /></button>
@@ -289,15 +417,27 @@ export default function Sandbox() {
289
417
  </div>
290
418
  )}
291
419
 
292
- {/* Image upload */}
293
- {showImageUpload && (
420
+ {mediaPanel && (
294
421
  <div className="p-3 rounded-md border bg-muted/30 space-y-2">
295
- <input value={imageUrl} onChange={(e) => setImageUrl(e.target.value)} placeholder="输入图片 URL..."
296
- className="w-full h-8 rounded-md border bg-transparent px-2 text-sm"
297
- onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); insertImageUrl() } }} />
298
- <button className="inline-flex items-center gap-1 h-8 px-3 rounded-md bg-primary text-primary-foreground text-sm disabled:opacity-50"
299
- onClick={insertImageUrl} disabled={!imageUrl.trim()}>
300
- <Check size={14} /> 插入
422
+ <p className="text-xs text-muted-foreground">
423
+ {mediaPanel === 'image' && '支持 http(s) 图片链接或 data URL'}
424
+ {mediaPanel === 'video' && '支持浏览器可解码的视频直链(如 .mp4、.webm)'}
425
+ {mediaPanel === 'audio' && '支持 .mp3、.ogg、.wav 等音频直链'}
426
+ </p>
427
+ <input
428
+ value={mediaUrl}
429
+ onChange={(e) => setMediaUrl(e.target.value)}
430
+ placeholder={mediaPanel === 'image' ? '图片 URL…' : mediaPanel === 'video' ? '视频 URL…' : '音频 URL…'}
431
+ className="w-full h-8 rounded-md border border-input bg-background px-2 text-sm"
432
+ onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); commitMediaUrl() } }}
433
+ />
434
+ <button
435
+ type="button"
436
+ className="inline-flex items-center gap-1 h-8 px-3 rounded-md bg-primary text-primary-foreground text-sm disabled:opacity-50"
437
+ onClick={commitMediaUrl}
438
+ disabled={!mediaUrl.trim()}
439
+ >
440
+ <Check size={14} /> 插入到输入框
301
441
  </button>
302
442
  </div>
303
443
  )}
@@ -330,7 +470,7 @@ export default function Sandbox() {
330
470
  <button
331
471
  className="inline-flex items-center gap-1.5 h-10 px-4 rounded-md bg-primary text-primary-foreground text-sm font-medium disabled:opacity-50 transition-colors hover:bg-primary/90"
332
472
  onClick={() => { const c = editorRef.current?.getContent(); if (c) handleSendMessage(c.text, c.segments) }}
333
- disabled={!inputText.trim() || previewSegments.length === 0}>
473
+ disabled={!hasRenderableSegments(previewSegments)}>
334
474
  <Send size={16} /> 发送
335
475
  </button>
336
476
  </div>
@@ -341,8 +481,12 @@ export default function Sandbox() {
341
481
  <span className="px-1 py-0.5 rounded border text-[10px]">Enter</span> 发送
342
482
  <span className="px-1 py-0.5 rounded border text-[10px]">Shift+Enter</span> 换行
343
483
  <span className="px-1 py-0.5 rounded border text-[10px]">[@名称]</span> @某人
484
+ <span className="px-1 py-0.5 rounded border text-[10px]">[video:URL]</span>
485
+ <span className="px-1 py-0.5 rounded border text-[10px]">[audio:URL]</span>
344
486
  </div>
345
487
  </div>
488
+ </>
489
+ )}
346
490
  </div>
347
491
  </div>
348
492
  )
@@ -1,19 +1,7 @@
1
1
  {
2
- "compilerOptions": {
3
- "outDir": "../dist",
4
- "baseUrl": ".",
5
- "declaration": true,
6
- "module": "ESNext",
7
- "moduleResolution": "bundler",
8
- "target": "ES2022",
9
- "jsx":"react-jsx",
10
- "declarationMap": true,
11
- "sourceMap": true,
12
- "skipLibCheck": true,
13
- "noEmit": false
14
- },
15
- "include": [
16
- "./**/*"
17
- ]
18
- }
19
-
2
+ "extends": "@zhin.js/console/browser.tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "../dist"
5
+ },
6
+ "include": ["./**/*"]
7
+ }
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{addPage as e,cn as t}from"@zhin.js/client";import{Bot as n,Check as r,Hash as i,Image as a,Info as o,MessageSquare as s,Search as c,Send as l,Smile as u,Terminal as d,Trash2 as f,User as p,Users as m,Wifi as ee,WifiOff as te,X as h}from"lucide-react";import g,{forwardRef as _,useEffect as v,useImperativeHandle as y,useRef as b,useState as x}from"react";import{jsx as S,jsxs as C}from"react/jsx-runtime";var w=_(({placeholder:e2="输入消息...",onSend:t2,onChange:n2,onAtTrigger:r2,minHeight:i2="44px",maxHeight:a2="200px"},o2)=>{let s2=b(null),c2=b(null),l2=()=>{if(!s2.current)return{text:"",segments:[]};let e3="",t3=[],n3=Array.from(s2.current.childNodes);for(let r3 of n3)if(r3.nodeType===Node.TEXT_NODE){let n4=r3.textContent||"";n4&&(e3+=n4,t3.push({type:"text",data:{text:n4}}))}else if(r3.nodeType===Node.ELEMENT_NODE){let n4=r3;if(n4.classList.contains("editor-face")){let r4=n4.dataset.id;e3+=`[face:${r4}]`,t3.push({type:"face",data:{id:Number(r4)}})}else if(n4.classList.contains("editor-image")){let r4=n4.dataset.url;e3+=`[image:${r4}]`,t3.push({type:"image",data:{url:r4}})}else if(n4.classList.contains("editor-at")){let r4=n4.dataset.name,i3=n4.dataset.id;e3+=`[@${r4}]`,t3.push({type:"at",data:{name:r4,qq:i3}})}else n4.tagName==="BR"&&(e3+="\n")}return{text:e3,segments:t3}},u2=e3=>{if(!s2.current)return;let t3=document.createElement("img");t3.src=`https://face.viki.moe/apng/${e3}.png`,t3.alt=`[face:${e3}]`,t3.dataset.type="face",t3.dataset.id=String(e3),t3.className="editor-face",p2(t3),g2()},d2=e3=>{if(!s2.current||!e3.trim())return;let t3=document.createElement("img");t3.src=e3.trim(),t3.alt=`[image:${e3.trim()}]`,t3.dataset.type="image",t3.dataset.url=e3.trim(),t3.className="editor-image",p2(t3),g2()},f2=(e3,t3)=>{if(!s2.current||!e3.trim())return;let n3=document.createElement("span");n3.dataset.type="at",n3.dataset.name=e3,t3&&(n3.dataset.id=t3),n3.className="editor-at",n3.contentEditable="false";let r3=document.createElement("span");r3.textContent="@",r3.className="editor-at-symbol";let i3=document.createElement("span");i3.textContent=e3,i3.className="editor-at-name",n3.appendChild(r3),n3.appendChild(i3),p2(n3),g2()},p2=e3=>{if(!s2.current)return;s2.current.focus();let t3=window.getSelection();if(t3&&t3.rangeCount>0){let n3=t3.getRangeAt(0);if(s2.current.contains(n3.commonAncestorContainer))n3.deleteContents(),n3.insertNode(e3),n3.collapse(false),t3.removeAllRanges(),t3.addRange(n3);else{s2.current.appendChild(e3);let n4=document.createRange();n4.setStartAfter(e3),n4.collapse(true),t3.removeAllRanges(),t3.addRange(n4)}}else{s2.current.appendChild(e3);let t4=window.getSelection();if(t4){let n3=document.createRange();n3.setStartAfter(e3),n3.collapse(true),t4.removeAllRanges(),t4.addRange(n3)}}},m2=()=>{s2.current&&(s2.current.innerHTML="",g2())},ee2=()=>{s2.current?.focus()},te2=()=>l2(),h2=()=>{if(!s2.current||!r2)return;let e3=window.getSelection();if(!e3||e3.rangeCount===0){r2(false,""),c2.current=null;return}let t3=e3.getRangeAt(0);if(!s2.current.contains(t3.commonAncestorContainer)){r2(false,""),c2.current=null;return}let n3=t3.startContainer;if(n3.nodeType!==Node.TEXT_NODE){r2(false,""),c2.current=null;return}let i3=n3,a3=i3.textContent?.substring(0,t3.startOffset)||"",o3=a3.lastIndexOf("@");if(o3!==-1){let e4=a3.substring(o3+1);if(e4.includes(" ")||e4.includes("\n")){r2(false,""),c2.current=null;return}c2.current=i3;let t4=document.createRange();t4.setStart(i3,o3),t4.setEnd(i3,o3+1);let n4=t4.getBoundingClientRect(),l3=s2.current.getBoundingClientRect();r2(true,e4,{top:n4.bottom-l3.top,left:n4.left-l3.left})}else r2(false,""),c2.current=null},g2=()=>{if(h2(),n2){let{text:e3,segments:t3}=l2();n2(e3,t3)}},_2=(e3,t3)=>{if(!c2.current)return;let n3=c2.current,r3=n3.textContent||"",i3=r3.lastIndexOf("@");if(i3!==-1){let e4=r3.substring(i3+1),t4=i3+1+e4.split(/[\s\n]/)[0].length;n3.textContent=r3.substring(0,i3)+r3.substring(t4);let a3=window.getSelection();if(a3){let e5=document.createRange();e5.setStart(n3,i3),e5.collapse(true),a3.removeAllRanges(),a3.addRange(e5)}}c2.current=null,f2(e3,t3)};return y(o2,()=>({focus:ee2,clear:m2,insertFace:u2,insertImage:d2,insertAt:f2,replaceAtTrigger:_2,getContent:te2})),S("div",{ref:s2,contentEditable:true,suppressContentEditableWarning:true,onInput:g2,onKeyDown:e3=>{if(e3.key==="Enter"&&!e3.shiftKey&&(e3.preventDefault(),t2)){let{text:e4,segments:n3}=l2();t2(e4,n3)}},"data-placeholder":e2,className:"rich-text-editor",style:{width:"100%",minHeight:i2,maxHeight:a2,padding:"0.5rem 0.75rem",border:"1px solid var(--gray-6)",borderRadius:"6px",backgroundColor:"var(--gray-1)",fontSize:"var(--font-size-2)",outline:"none",overflowY:"auto",lineHeight:"1.5",wordWrap:"break-word",color:"var(--gray-12)"}})});w.displayName="RichTextEditor";function T(){let[e2,d2]=x([]),[_2,y2]=x([{id:"user_1001",name:"测试用户",type:"private",unread:0},{id:"group_2001",name:"测试群组",type:"group",unread:0},{id:"channel_3001",name:"测试频道",type:"channel",unread:0}]),[T2,ne]=x([]),[E,D]=x(_2[0]),[O,k]=x(""),[A,re]=x("ProcessBot"),[j,M]=x(false),[N,P]=x(false),[F,I]=x(false),[ie,ae]=x(false),[L,R]=x(null),[z,B]=x(""),[V,oe]=x(""),[H,U]=x(""),[se,ce]=x(""),[le]=x([{id:"10001",name:"张三"},{id:"10002",name:"李四"},{id:"10003",name:"王五"},{id:"10004",name:"赵六"},{id:"10005",name:"测试用户"},{id:"10086",name:"Admin"},{id:"10010",name:"Test User"}]),[ue,W]=x([]),[G,K]=x(false),q=b(null),J=b(null),Y=b(null),de=async()=>{try{ne(await(await fetch("https://face.viki.moe/metadata.json")).json())}catch(e3){console.error("[Sandbox] Failed to fetch face list:",e3)}};v(()=>{de()},[]),v(()=>{let e3=window.location.protocol==="https:"?"wss:":"ws:";return J.current=new WebSocket(`${e3}//${window.location.host}/sandbox`),J.current.onopen=()=>M(true),J.current.onmessage=e4=>{try{let t2=JSON.parse(e4.data),n2=typeof t2.content=="string"?X(t2.content):Array.isArray(t2.content)?t2.content:X(String(t2.content)),r2=_2.find(e5=>e5.id===t2.id);if(!r2){let e5=t2.type==="private"?`私聊-${t2.bot||A}`:t2.type==="group"?`群组-${t2.id}`:`频道-${t2.id}`;r2={id:t2.id,name:e5,type:t2.type,unread:0},y2(e6=>[...e6,r2]),D(r2)}let i2={id:`bot_${t2.timestamp}`,type:"received",channelType:t2.type,channelId:t2.id,channelName:r2.name,senderId:"bot",senderName:t2.bot||A,content:n2,timestamp:t2.timestamp};d2(e5=>[...e5,i2])}catch(e5){console.error("[Sandbox] Failed to parse message:",e5)}},J.current.onclose=()=>M(false),()=>{J.current?.close()}},[A,_2]),v(()=>{q.current?.scrollIntoView({behavior:"smooth"})},[e2]),v(()=>{W(O.trim()?X(O):[])},[O]);let X=e3=>{let t2=[],n2=/\[@([^\]]+)\]|\[face:(\d+)\]|\[image:([^\]]+)\]/g,r2=0,i2;for(;(i2=n2.exec(e3))!==null;){if(i2.index>r2){let n3=e3.substring(r2,i2.index);n3&&t2.push({type:"text",data:{text:n3}})}i2[1]?t2.push({type:"at",data:{qq:i2[1],name:i2[1]}}):i2[2]?t2.push({type:"face",data:{id:parseInt(i2[2])}}):i2[3]&&t2.push({type:"image",data:{url:i2[3]}}),r2=n2.lastIndex}if(r2<e3.length){let n3=e3.substring(r2);n3&&t2.push({type:"text",data:{text:n3}})}return t2.length>0?t2:[{type:"text",data:{text:e3}}]},fe=e3=>e3.map((e4,t2)=>{if(typeof e4=="string")return S("span",{children:e4.split("\n").map((t3,n2)=>C(g.Fragment,{children:[t3,n2<e4.split("\n").length-1&&S("br",{})]},n2))},t2);switch(e4.type){case"text":return S("span",{children:e4.data.text.split("\n").map((t3,n2)=>C(g.Fragment,{children:[t3,n2<e4.data.text.split("\n").length-1&&S("br",{})]},n2))},t2);case"at":return C("span",{className:"inline-flex items-center px-1.5 py-0.5 rounded bg-accent text-accent-foreground text-xs mx-0.5",children:["@",e4.data.name||e4.data.qq]},t2);case"face":return S("img",{src:`https://face.viki.moe/apng/${e4.data.id}.png`,alt:`face${e4.data.id}`,className:"w-6 h-6 inline-block align-middle mx-0.5"},t2);case"image":return S("img",{src:e4.data.url,alt:"image",className:"max-w-[300px] rounded-lg my-1 block",onError:e5=>{e5.currentTarget.style.display="none"}},t2);case"video":return S("span",{className:"inline-flex items-center px-1.5 py-0.5 rounded border text-xs mx-0.5",children:"📹 视频"},t2);case"audio":return S("span",{className:"inline-flex items-center px-1.5 py-0.5 rounded border text-xs mx-0.5",children:"🎵 语音"},t2);case"file":return C("span",{className:"inline-flex items-center px-1.5 py-0.5 rounded border text-xs mx-0.5",children:["📎 ",e4.data.name||"文件"]},t2);default:return S("span",{children:"[未知消息类型]"},t2)}}),Z=(e3,t2)=>{if(!e3.trim()||t2.length===0)return;let n2={id:`msg_${Date.now()}`,type:"sent",channelType:E.type,channelId:E.id,channelName:E.name,senderId:"test_user",senderName:"测试用户",content:t2,timestamp:Date.now()};d2(e4=>[...e4,n2]),k(""),W([]),Y.current?.clear(),J.current?.send(JSON.stringify({type:E.type,id:E.id,content:t2,timestamp:Date.now()}))},pe=()=>{confirm("确定清空所有消息记录?")&&d2([])},me=e3=>{D(e3),y2(t2=>t2.map(t3=>t3.id===e3.id?{...t3,unread:0}:t3)),window.innerWidth<768&&K(false)},he=()=>{let e3=["private","group","channel"],t2=e3[Math.floor(Math.random()*e3.length)],n2=prompt("请输入频道名称:");if(n2){let e4={id:`${t2}_${Date.now()}`,name:n2,type:t2,unread:0};y2(t3=>[...t3,e4]),D(e4)}},Q=e3=>{switch(e3){case"private":return S(p,{size:16});case"group":return S(m,{size:16});case"channel":return S(i,{size:16});default:return S(s,{size:16})}},ge=e3=>{Y.current?.insertFace(e3),P(false)},_e=()=>{H.trim()&&(Y.current?.insertImage(H.trim()),U(""),I(false))},ve=e3=>{Y.current?.replaceAtTrigger(e3.name,e3.id),R(null),B("")},ye=(e3,t2,n2)=>{if(E.type==="private"){R(null),B("");return}e3&&n2?(R(n2),B(t2)):(R(null),B(""))},be=le.filter(e3=>{if(!z.trim())return true;let t2=z.toLowerCase();return e3.name.toLowerCase().includes(t2)||e3.id.toLowerCase().includes(t2)}),xe=(e3,t2)=>{k(e3),W(t2)},Se=T2.filter(e3=>e3.name.toLowerCase().includes(V.toLowerCase())||e3.describe.toLowerCase().includes(V.toLowerCase())),$=e2.filter(e3=>e3.channelId===E.id);return C("div",{className:"sandbox-container",children:[C("button",{className:"mobile-channel-toggle md:hidden",onClick:()=>K(!G),children:[S(s,{size:20})," 频道列表"]}),C("div",{className:t("channel-sidebar rounded-lg border bg-card",G&&"show"),children:[S("div",{className:"p-3 border-b",children:C("div",{className:"flex justify-between items-center",children:[C("div",{className:"flex items-center gap-2",children:[S("div",{className:"p-1 rounded-md bg-secondary",children:S(s,{size:16,className:"text-muted-foreground"})}),S("h3",{className:"font-semibold",children:"频道列表"})]}),C("span",{className:t("inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium border",j?"bg-emerald-100 text-emerald-800 border-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400 dark:border-emerald-800":"bg-muted text-muted-foreground"),children:[S(j?ee:te,{size:12}),j?"已连接":"未连接"]})]})}),S("div",{className:"flex-1 overflow-y-auto p-2 space-y-1",children:_2.map(e3=>C("div",{className:t("menu-item",E.id===e3.id&&"active"),onClick:()=>me(e3),children:[S("span",{className:"shrink-0",children:Q(e3.type)}),C("div",{className:"flex-1 min-w-0",children:[S("div",{className:"text-sm font-medium truncate",children:e3.name}),S("div",{className:"text-xs text-muted-foreground",children:e3.type==="private"?"私聊":e3.type==="group"?"群聊":"频道"})]}),e3.unread>0&&S("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",children:e3.unread})]},e3.id))}),S("div",{className:"p-2 border-t",children:S("button",{className:"w-full py-2 px-3 rounded-md border border-dashed text-sm text-muted-foreground hover:bg-accent transition-colors",onClick:he,children:"+ 添加频道"})})]}),G&&S("div",{className:"channel-overlay md:hidden",onClick:()=>K(false)}),C("div",{className:"chat-area",children:[S("div",{className:"rounded-lg border bg-card p-3 flex-shrink-0",children:C("div",{className:"flex justify-between items-center flex-wrap gap-2",children:[C("div",{className:"flex items-center gap-3",children:[S("div",{className:"p-2 rounded-lg bg-secondary",children:Q(E.type)}),C("div",{children:[S("h2",{className:"text-lg font-bold",children:E.name}),C("div",{className:"flex items-center gap-2 text-xs text-muted-foreground",children:[S("span",{children:E.id}),S("span",{className:"inline-flex items-center px-1.5 py-0.5 rounded border text-[10px]",children:$.length}),S("span",{children:"条消息"})]})]}),S("span",{className:"inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-secondary text-secondary-foreground",children:E.type==="private"?"私聊":E.type==="group"?"群聊":"频道"})]}),C("div",{className:"flex items-center gap-2",children:[S("input",{value:A,onChange:e3=>re(e3.target.value),placeholder:"机器人名称",className:"h-8 w-28 rounded-md border bg-transparent px-2 text-sm"}),C("button",{className:"inline-flex items-center gap-1 h-8 px-3 rounded-md bg-secondary text-secondary-foreground text-sm hover:bg-secondary/80",onClick:pe,children:[S(f,{size:14})," 清空"]})]})]})}),S("div",{className:"rounded-lg border bg-card flex-1 flex flex-col min-h-0",children:S("div",{className:"flex-1 overflow-y-auto p-4",children:$.length===0?C("div",{className:"flex flex-col items-center justify-center h-full gap-3",children:[S(s,{size:64,className:"text-muted-foreground/20"}),S("span",{className:"text-muted-foreground",children:"暂无消息,开始对话吧!"})]}):C("div",{className:"space-y-2",children:[$.map(e3=>S("div",{className:t("flex",e3.type==="sent"?"justify-end":"justify-start"),children:C("div",{className:t("max-w-[70%] p-3 rounded-2xl",e3.type==="sent"?"bg-primary text-primary-foreground":"bg-muted"),children:[C("div",{className:"flex items-center gap-2 mb-1",children:[e3.type==="received"&&S(n,{size:14}),e3.type==="sent"&&S(p,{size:14}),S("span",{className:"text-xs font-medium opacity-90",children:e3.senderName}),S("span",{className:"text-xs opacity-70",children:new Date(e3.timestamp).toLocaleTimeString()})]}),S("div",{className:"text-sm",children:fe(e3.content)})]})},e3.id)),S("div",{ref:q})]})})}),C("div",{className:"rounded-lg border bg-card p-3 flex-shrink-0 space-y-3",children:[C("div",{className:"flex gap-2 items-center",children:[S("button",{className:t("h-8 w-8 rounded-md flex items-center justify-center border transition-colors",N?"bg-primary text-primary-foreground":"hover:bg-accent"),onClick:()=>{P(!N),I(false)},title:"插入表情",children:S(u,{size:16})}),S("button",{className:t("h-8 w-8 rounded-md flex items-center justify-center border transition-colors",F?"bg-primary text-primary-foreground":"hover:bg-accent"),onClick:()=>{I(!F),P(false)},title:"插入图片",children:S(a,{size:16})}),S("div",{className:"flex-1"}),O&&S("button",{className:"h-8 w-8 rounded-md flex items-center justify-center hover:bg-accent transition-colors",onClick:()=>{k(""),W([])},children:S(h,{size:16})})]}),N&&C("div",{className:"p-3 rounded-md border bg-muted/30 max-h-64 overflow-y-auto space-y-2",children:[S("input",{value:V,onChange:e3=>oe(e3.target.value),placeholder:"搜索表情...",className:"w-full h-8 rounded-md border bg-transparent px-2 text-sm"}),S("div",{className:"grid grid-cols-8 gap-1",children:Se.slice(0,80).map(e3=>S("button",{onClick:()=>ge(e3.id),title:e3.name,className:"w-10 h-10 rounded-md border flex items-center justify-center hover:bg-accent transition-colors",children:S("img",{src:`https://face.viki.moe/apng/${e3.id}.png`,alt:e3.name,className:"w-8 h-8"})},e3.id))}),Se.length===0&&C("div",{className:"flex flex-col items-center gap-2 py-4",children:[S(c,{size:32,className:"text-muted-foreground/30"}),S("span",{className:"text-sm text-muted-foreground",children:"未找到匹配的表情"})]})]}),F&&C("div",{className:"p-3 rounded-md border bg-muted/30 space-y-2",children:[S("input",{value:H,onChange:e3=>U(e3.target.value),placeholder:"输入图片 URL...",className:"w-full h-8 rounded-md border bg-transparent px-2 text-sm",onKeyDown:e3=>{e3.key==="Enter"&&(e3.preventDefault(),_e())}}),C("button",{className:"inline-flex items-center gap-1 h-8 px-3 rounded-md bg-primary text-primary-foreground text-sm disabled:opacity-50",onClick:_e,disabled:!H.trim(),children:[S(r,{size:14})," 插入"]})]}),C("div",{className:"flex gap-2 items-start",children:[C("div",{className:"flex-1 relative",children:[S(w,{ref:Y,placeholder:`向 ${E.name} 发送消息...`,onSend:Z,onChange:xe,onAtTrigger:ye,minHeight:"44px",maxHeight:"200px"}),L&&S("div",{className:"absolute z-50 rounded-lg border bg-popover shadow-md min-w-60 max-h-72 overflow-y-auto p-1",style:{top:`${L.top}px`,left:`${L.left}px`},children:be.length>0?be.map(e3=>C("div",{className:"flex items-center gap-2 p-2 rounded-md cursor-pointer hover:bg-accent transition-colors",onClick:()=>ve(e3),children:[S(p,{size:16,className:"text-muted-foreground"}),C("div",{className:"flex-1",children:[S("div",{className:"text-sm font-medium",children:e3.name}),C("div",{className:"text-xs text-muted-foreground",children:["ID: ",e3.id]})]})]},e3.id)):C("div",{className:"flex flex-col items-center gap-2 p-4",children:[S(c,{size:20,className:"text-muted-foreground/50"}),S("span",{className:"text-xs text-muted-foreground",children:"未找到匹配的用户"})]})})]}),C("button",{className:"inline-flex items-center gap-1.5 h-10 px-4 rounded-md bg-primary text-primary-foreground text-sm font-medium disabled:opacity-50 transition-colors hover:bg-primary/90",onClick:()=>{let e3=Y.current?.getContent();e3&&Z(e3.text,e3.segments)},disabled:!O.trim()||ue.length===0,children:[S(l,{size:16})," 发送"]})]}),C("div",{className:"flex items-center gap-2 flex-wrap text-xs text-muted-foreground",children:[S(o,{size:12})," 快捷操作:",S("span",{className:"px-1 py-0.5 rounded border text-[10px]",children:"Enter"})," 发送",S("span",{className:"px-1 py-0.5 rounded border text-[10px]",children:"Shift+Enter"})," 换行",S("span",{className:"px-1 py-0.5 rounded border text-[10px]",children:"[@名称]"})," @某人"]})]})]})]})}e({key:"process-sandbox",path:"/sandbox",title:"沙盒",icon:S(d,{className:"w-5 h-5"}),element:S(T,{})});
1
+ import{addPage as e,cn as t,pickMediaRawUrl as n,resolveMediaSrc as r}from"@zhin.js/client";import{Bell as i,Bot as a,Check as o,Hash as s,Image as c,Info as l,MessageSquare as u,Music as d,Search as f,Send as ee,Smile as p,Terminal as m,Trash2 as h,User as g,UserPlus as _,Users as v,Video as y,Wifi as b,WifiOff as te,X as ne}from"lucide-react";import re,{forwardRef as x,useEffect as S,useImperativeHandle as C,useRef as w,useState as T}from"react";import{Fragment as ie,jsx as E,jsxs as D}from"react/jsx-runtime";var ae=x(({placeholder:e2="输入消息...",onSend:t2,onChange:n2,onAtTrigger:r2,minHeight:i2="44px",maxHeight:a2="200px"},o2)=>{let s2=w(null),c2=w(null),l2=()=>{if(!s2.current)return{text:"",segments:[]};let e3="",t3=[],n3=Array.from(s2.current.childNodes);for(let r3 of n3)if(r3.nodeType===Node.TEXT_NODE){let n4=r3.textContent||"";n4&&(e3+=n4,t3.push({type:"text",data:{text:n4}}))}else if(r3.nodeType===Node.ELEMENT_NODE){let n4=r3;if(n4.classList.contains("editor-face")){let r4=n4.dataset.id;e3+=`[face:${r4}]`,t3.push({type:"face",data:{id:Number(r4)}})}else if(n4.classList.contains("editor-image")){let r4=n4.dataset.url;e3+=`[image:${r4}]`,t3.push({type:"image",data:{url:r4}})}else if(n4.classList.contains("editor-video")){let r4=n4.dataset.url||"";e3+=`[video:${r4}]`,t3.push({type:"video",data:{url:r4}})}else if(n4.classList.contains("editor-audio")){let r4=n4.dataset.url||"";e3+=`[audio:${r4}]`,t3.push({type:"audio",data:{url:r4}})}else if(n4.classList.contains("editor-at")){let r4=n4.dataset.name,i3=n4.dataset.id;e3+=`[@${r4}]`,t3.push({type:"at",data:{name:r4,qq:i3}})}else n4.tagName==="BR"&&(e3+="\n")}return{text:e3,segments:t3}},u2=e3=>{if(!s2.current)return;let t3=document.createElement("img");t3.src=`https://face.viki.moe/apng/${e3}.png`,t3.alt=`[face:${e3}]`,t3.dataset.type="face",t3.dataset.id=String(e3),t3.className="editor-face",m2(t3),y2()},d2=e3=>{if(!s2.current||!e3.trim())return;let t3=document.createElement("img");t3.src=e3.trim(),t3.alt=`[image:${e3.trim()}]`,t3.dataset.type="image",t3.dataset.url=e3.trim(),t3.className="editor-image",m2(t3),y2()},f2=e3=>{if(!s2.current||!e3.trim())return;let t3=e3.trim(),n3=document.createElement("span");n3.className="editor-video",n3.dataset.url=t3,n3.contentEditable="false",n3.textContent="📹 视频",m2(n3),y2()},ee2=e3=>{if(!s2.current||!e3.trim())return;let t3=e3.trim(),n3=document.createElement("span");n3.className="editor-audio",n3.dataset.url=t3,n3.contentEditable="false",n3.textContent="🎵 音频",m2(n3),y2()},p2=(e3,t3)=>{if(!s2.current||!e3.trim())return;let n3=document.createElement("span");n3.dataset.type="at",n3.dataset.name=e3,t3&&(n3.dataset.id=t3),n3.className="editor-at",n3.contentEditable="false";let r3=document.createElement("span");r3.textContent="@",r3.className="editor-at-symbol";let i3=document.createElement("span");i3.textContent=e3,i3.className="editor-at-name",n3.appendChild(r3),n3.appendChild(i3),m2(n3),y2()},m2=e3=>{if(!s2.current)return;s2.current.focus();let t3=window.getSelection();if(t3&&t3.rangeCount>0){let n3=t3.getRangeAt(0);if(s2.current.contains(n3.commonAncestorContainer))n3.deleteContents(),n3.insertNode(e3),n3.collapse(false),t3.removeAllRanges(),t3.addRange(n3);else{s2.current.appendChild(e3);let n4=document.createRange();n4.setStartAfter(e3),n4.collapse(true),t3.removeAllRanges(),t3.addRange(n4)}}else{s2.current.appendChild(e3);let t4=window.getSelection();if(t4){let n3=document.createRange();n3.setStartAfter(e3),n3.collapse(true),t4.removeAllRanges(),t4.addRange(n3)}}},h2=()=>{s2.current&&(s2.current.innerHTML="",y2())},g2=()=>{s2.current?.focus()},_2=()=>l2(),v2=()=>{if(!s2.current||!r2)return;let e3=window.getSelection();if(!e3||e3.rangeCount===0){r2(false,""),c2.current=null;return}let t3=e3.getRangeAt(0);if(!s2.current.contains(t3.commonAncestorContainer)){r2(false,""),c2.current=null;return}let n3=t3.startContainer;if(n3.nodeType!==Node.TEXT_NODE){r2(false,""),c2.current=null;return}let i3=n3,a3=i3.textContent?.substring(0,t3.startOffset)||"",o3=a3.lastIndexOf("@");if(o3!==-1){let e4=a3.substring(o3+1);if(e4.includes(" ")||e4.includes("\n")){r2(false,""),c2.current=null;return}c2.current=i3;let t4=document.createRange();t4.setStart(i3,o3),t4.setEnd(i3,o3+1);let n4=t4.getBoundingClientRect(),l3=s2.current.getBoundingClientRect();r2(true,e4,{top:n4.bottom-l3.top,left:n4.left-l3.left})}else r2(false,""),c2.current=null},y2=()=>{if(v2(),n2){let{text:e3,segments:t3}=l2();n2(e3,t3)}},b2=(e3,t3)=>{if(!c2.current)return;let n3=c2.current,r3=n3.textContent||"",i3=r3.lastIndexOf("@");if(i3!==-1){let e4=r3.substring(i3+1),t4=i3+1+e4.split(/[\s\n]/)[0].length;n3.textContent=r3.substring(0,i3)+r3.substring(t4);let a3=window.getSelection();if(a3){let e5=document.createRange();e5.setStart(n3,i3),e5.collapse(true),a3.removeAllRanges(),a3.addRange(e5)}}c2.current=null,p2(e3,t3)};return C(o2,()=>({focus:g2,clear:h2,insertFace:u2,insertImage:d2,insertVideo:f2,insertAudio:ee2,insertAt:p2,replaceAtTrigger:b2,getContent:_2})),E("div",{ref:s2,contentEditable:true,suppressContentEditableWarning:true,onInput:y2,onKeyDown:e3=>{if(e3.key==="Enter"&&!e3.shiftKey&&(e3.preventDefault(),t2)){let{text:e4,segments:n3}=l2();t2(e4,n3)}},"data-placeholder":e2,className:"rich-text-editor",style:{width:"100%",minHeight:i2,maxHeight:a2,padding:"0.5rem 0.75rem",border:"1px solid var(--gray-6)",borderRadius:"6px",backgroundColor:"var(--gray-1)",fontSize:"var(--font-size-2)",outline:"none",overflowY:"auto",lineHeight:"1.5",wordWrap:"break-word",color:"var(--gray-12)"}})});ae.displayName="RichTextEditor";function oe(){let[e2,m2]=T([]),[x2,C2]=T([{id:"user_1001",name:"测试用户",type:"private",unread:0},{id:"group_2001",name:"测试群组",type:"group",unread:0},{id:"channel_3001",name:"测试频道",type:"channel",unread:0}]),[oe2,se]=T([]),[O,k]=T(x2[0]),[A,j]=T(""),[M,ce]=T("ProcessBot"),[N,P]=T(false),[F,I]=T(false),[L,R]=T(null),[z,B]=T(""),[le,ue]=T(false),[V,H]=T(null),[de,U]=T(""),[W,fe]=T(""),[pe,me]=T(""),[he]=T([{id:"10001",name:"张三"},{id:"10002",name:"李四"},{id:"10003",name:"王五"},{id:"10004",name:"赵六"},{id:"10005",name:"测试用户"},{id:"10086",name:"Admin"},{id:"10010",name:"Test User"}]),[ge,G]=T([]),[K,q]=T(false),[J,Y]=T("chat"),_e=w(null),X=w(null),Z=w(null),ve=async()=>{try{se(await(await fetch("https://face.viki.moe/metadata.json")).json())}catch(e3){console.error("[Sandbox] Failed to fetch face list:",e3)}};S(()=>{ve()},[]),S(()=>{let e3=window.location.protocol==="https:"?"wss:":"ws:";return X.current=new WebSocket(`${e3}//${window.location.host}/sandbox`),X.current.onopen=()=>P(true),X.current.onmessage=e4=>{try{let t2=JSON.parse(e4.data),n2=typeof t2.content=="string"?Q(t2.content):Array.isArray(t2.content)?t2.content:Q(String(t2.content)),r2=x2.find(e5=>e5.id===t2.id);if(!r2){let e5=t2.type==="private"?`私聊-${t2.bot||M}`:t2.type==="group"?`群组-${t2.id}`:`频道-${t2.id}`;r2={id:t2.id,name:e5,type:t2.type,unread:0},C2(e6=>[...e6,r2]),k(r2)}let i2={id:`bot_${t2.timestamp}`,type:"received",channelType:t2.type,channelId:t2.id,channelName:r2.name,senderId:"bot",senderName:t2.bot||M,content:n2,timestamp:t2.timestamp};m2(e5=>[...e5,i2])}catch(e5){console.error("[Sandbox] Failed to parse message:",e5)}},X.current.onclose=()=>P(false),()=>{X.current?.close()}},[M,x2]),S(()=>{_e.current?.scrollIntoView({behavior:"smooth"})},[e2]),S(()=>{G(A.trim()?Q(A):[])},[A]);let Q=e3=>{let t2=[],n2=/\[@([^\]]+)\]|\[face:(\d+)\]|\[image:([^\]]+)\]|\[video:([^\]]+)\]|\[audio:([^\]]+)\]/g,r2=0,i2;for(;(i2=n2.exec(e3))!==null;){if(i2.index>r2){let n3=e3.substring(r2,i2.index);n3&&t2.push({type:"text",data:{text:n3}})}i2[1]?t2.push({type:"at",data:{qq:i2[1],name:i2[1]}}):i2[2]?t2.push({type:"face",data:{id:parseInt(i2[2],10)}}):i2[3]?t2.push({type:"image",data:{url:i2[3]}}):i2[4]?t2.push({type:"video",data:{url:i2[4]}}):i2[5]&&t2.push({type:"audio",data:{url:i2[5]}}),r2=n2.lastIndex}if(r2<e3.length){let n3=e3.substring(r2);n3&&t2.push({type:"text",data:{text:n3}})}return t2.length>0?t2:[{type:"text",data:{text:e3}}]},ye=e3=>e3.length===0?false:e3.some(e4=>e4.type==="text"?!!String(e4.data?.text??"").trim():true),be=(e3,i2)=>{let a2=i2?"ring-1 ring-primary-foreground/25":"ring-1 ring-border/60";return e3.map((e4,o2)=>{if(typeof e4=="string")return E("span",{children:e4.split("\n").map((t2,n2)=>D(re.Fragment,{children:[t2,n2<e4.split("\n").length-1&&E("br",{})]},n2))},o2);let s2=e4.data;switch(e4.type){case"text":return E("span",{children:String(s2.text??"").split("\n").map((e5,t2)=>D(re.Fragment,{children:[e5,t2<String(s2.text??"").split("\n").length-1&&E("br",{})]},t2))},o2);case"at":return D("span",{className:"inline-flex items-center px-1.5 py-0.5 rounded bg-accent text-accent-foreground text-xs mx-0.5",children:["@",String(s2.name??s2.qq??"")]},o2);case"face":return E("img",{src:`https://face.viki.moe/apng/${s2.id}.png`,alt:"",className:"w-6 h-6 inline-block align-middle mx-0.5"},o2);case"image":{let e5=r(n(s2),"image");return e5?E("a",{href:e5,target:"_blank",rel:"noreferrer",className:"block my-1",children:E("img",{src:e5,alt:"",className:t("max-w-[min(320px,88vw)] rounded-lg block",a2,"ring-offset-0"),onError:e6=>{e6.target.style.display="none"}})},o2):E("span",{className:"text-xs opacity-70",children:"[图片]"},o2)}case"video":{let e5=r(n(s2),"video");return e5?E("video",{src:e5,controls:true,playsInline:true,preload:"metadata",className:t("max-w-[min(360px,92vw)] max-h-72 rounded-lg my-1 bg-black/10",a2)},o2):E("span",{className:"text-xs opacity-70",children:"[视频无地址]"},o2)}case"audio":case"record":{let e5=r(n(s2),"audio");return e5?E("audio",{src:e5,controls:true,preload:"metadata",className:t("w-full max-w-sm my-2 h-10",i2&&"opacity-95")},o2):E("span",{className:"text-xs opacity-70",children:"[音频无地址]"},o2)}case"file":return D("span",{className:"inline-flex items-center px-1.5 py-0.5 rounded border text-xs mx-0.5",children:["📎 ",String(s2.name||"文件")]},o2);default:return D("span",{className:"text-xs opacity-70",children:["[",e4.type,"]"]},o2)}})},xe=(e3,t2)=>{if(!ye(t2))return;let n2={id:`msg_${Date.now()}`,type:"sent",channelType:O.type,channelId:O.id,channelName:O.name,senderId:"test_user",senderName:"测试用户",content:t2,timestamp:Date.now()};m2(e4=>[...e4,n2]),j(""),G([]),Z.current?.clear(),X.current?.send(JSON.stringify({type:O.type,id:O.id,content:t2,timestamp:Date.now()}))},Se=()=>{confirm("确定清空所有消息记录?")&&m2([])},Ce=e3=>{Y("chat"),k(e3),C2(t2=>t2.map(t3=>t3.id===e3.id?{...t3,unread:0}:t3)),window.innerWidth<768&&q(false)},we=()=>{let e3=["private","group","channel"],t2=e3[Math.floor(Math.random()*e3.length)],n2=prompt("请输入频道名称:");if(n2){let e4={id:`${t2}_${Date.now()}`,name:n2,type:t2,unread:0};C2(t3=>[...t3,e4]),k(e4)}},Te=e3=>{switch(e3){case"private":return E(g,{size:16});case"group":return E(v,{size:16});case"channel":return E(s,{size:16});default:return E(u,{size:16})}},Ee=e3=>{Z.current?.insertFace(e3),I(false)},De=()=>{let e3=z.trim();!e3||!L||(L==="image"?Z.current?.insertImage(e3):L==="video"?Z.current?.insertVideo(e3):Z.current?.insertAudio(e3),B(""),R(null))},Oe=e3=>{Z.current?.replaceAtTrigger(e3.name,e3.id),H(null),U("")},ke=(e3,t2,n2)=>{if(O.type==="private"){H(null),U("");return}e3&&n2?(H(n2),U(t2)):(H(null),U(""))},Ae=he.filter(e3=>{if(!de.trim())return true;let t2=de.toLowerCase();return e3.name.toLowerCase().includes(t2)||e3.id.toLowerCase().includes(t2)}),je=(e3,t2)=>{j(e3),G(t2)},Me=oe2.filter(e3=>e3.name.toLowerCase().includes(W.toLowerCase())||e3.describe.toLowerCase().includes(W.toLowerCase())),$=e2.filter(e3=>e3.channelId===O.id);return D("div",{className:"sandbox-container rounded-xl border border-border/70 bg-card/30 shadow-sm",children:[D("button",{className:"mobile-channel-toggle md:hidden",onClick:()=>q(!K),children:[E(u,{size:20})," 频道列表"]}),D("div",{className:t("channel-sidebar rounded-lg border bg-card",K&&"show"),children:[E("div",{className:"p-3 border-b",children:D("div",{className:"flex justify-between items-center",children:[D("div",{className:"flex items-center gap-2",children:[E("div",{className:"p-1 rounded-md bg-secondary",children:E(u,{size:16,className:"text-muted-foreground"})}),E("h3",{className:"font-semibold",children:"频道列表"})]}),D("span",{className:t("inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium border",N?"bg-emerald-100 text-emerald-800 border-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400 dark:border-emerald-800":"bg-muted text-muted-foreground"),children:[E(N?b:te,{size:12}),N?"已连接":"未连接"]})]})}),D("div",{className:"flex-1 overflow-y-auto p-2 space-y-1",children:[x2.map(e3=>D("div",{className:t("menu-item",J==="chat"&&O.id===e3.id&&"active"),onClick:()=>Ce(e3),children:[E("span",{className:"shrink-0",children:Te(e3.type)}),D("div",{className:"flex-1 min-w-0",children:[E("div",{className:"text-sm font-medium truncate",children:e3.name}),E("div",{className:"text-xs text-muted-foreground",children:e3.type==="private"?"私聊":e3.type==="group"?"群聊":"频道"})]}),e3.unread>0&&E("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",children:e3.unread})]},e3.id)),D("div",{className:"pt-2 mt-2 border-t space-y-1",children:[D("div",{className:t("menu-item",J==="requests"&&"active"),onClick:()=>{Y("requests"),window.innerWidth<768&&q(false)},children:[E(_,{size:16,className:"shrink-0"}),D("div",{className:"flex-1 min-w-0",children:[E("div",{className:"text-sm font-medium",children:"请求"}),E("div",{className:"text-xs text-muted-foreground",children:"好友/群邀请等"})]})]}),D("div",{className:t("menu-item",J==="notices"&&"active"),onClick:()=>{Y("notices"),window.innerWidth<768&&q(false)},children:[E(i,{size:16,className:"shrink-0"}),D("div",{className:"flex-1 min-w-0",children:[E("div",{className:"text-sm font-medium",children:"通知"}),E("div",{className:"text-xs text-muted-foreground",children:"群管/撤回等"})]})]})]})]}),E("div",{className:"p-2 border-t",children:E("button",{className:"w-full py-2 px-3 rounded-md border border-dashed text-sm text-muted-foreground hover:bg-accent transition-colors",onClick:we,children:"+ 添加频道"})})]}),K&&E("div",{className:"channel-overlay md:hidden",onClick:()=>q(false)}),D("div",{className:"chat-area",children:[J==="requests"&&D("div",{className:"rounded-lg border bg-card flex-1 flex flex-col min-h-0 overflow-hidden",children:[E("div",{className:"p-3 border-b flex-shrink-0",children:D("h2",{className:"text-lg font-bold flex items-center gap-2",children:[E(_,{size:20})," 请求"]})}),D("div",{className:"flex-1 overflow-y-auto p-4 flex flex-col items-center justify-center gap-3 text-muted-foreground text-center",children:[E(_,{size:48,className:"opacity-30"}),E("span",{children:"沙盒为模拟环境,暂无请求数据"}),D("span",{className:"text-sm",children:["实际好友/群邀请等请求请到侧边栏 ",E("strong",{children:"机器人"})," 页面进入对应机器人管理查看"]})]})]}),J==="notices"&&D("div",{className:"rounded-lg border bg-card flex-1 flex flex-col min-h-0 overflow-hidden",children:[E("div",{className:"p-3 border-b flex-shrink-0",children:D("h2",{className:"text-lg font-bold flex items-center gap-2",children:[E(i,{size:20})," 通知"]})}),D("div",{className:"flex-1 overflow-y-auto p-4 flex flex-col items-center justify-center gap-3 text-muted-foreground text-center",children:[E(i,{size:48,className:"opacity-30"}),E("span",{children:"沙盒为模拟环境,暂无通知数据"}),D("span",{className:"text-sm",children:["实际群管、撤回等通知请到侧边栏 ",E("strong",{children:"机器人"})," 页面进入对应机器人管理查看"]})]})]}),J==="chat"&&D(ie,{children:[E("div",{className:"rounded-lg border bg-card p-3 flex-shrink-0",children:D("div",{className:"flex justify-between items-center flex-wrap gap-2",children:[D("div",{className:"flex items-center gap-3",children:[E("div",{className:"p-2 rounded-lg bg-secondary",children:Te(O.type)}),D("div",{children:[E("h2",{className:"text-lg font-bold",children:O.name}),D("div",{className:"flex items-center gap-2 text-xs text-muted-foreground",children:[E("span",{children:O.id}),E("span",{className:"inline-flex items-center px-1.5 py-0.5 rounded border text-[10px]",children:$.length}),E("span",{children:"条消息"})]})]}),E("span",{className:"inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-secondary text-secondary-foreground",children:O.type==="private"?"私聊":O.type==="group"?"群聊":"频道"})]}),D("div",{className:"flex items-center gap-2",children:[E("input",{value:M,onChange:e3=>ce(e3.target.value),placeholder:"机器人名称",className:"h-8 w-28 rounded-md border bg-transparent px-2 text-sm"}),D("button",{className:"inline-flex items-center gap-1 h-8 px-3 rounded-md bg-secondary text-secondary-foreground text-sm hover:bg-secondary/80",onClick:Se,children:[E(h,{size:14})," 清空"]})]})]})}),E("div",{className:"rounded-lg border bg-card flex-1 flex flex-col min-h-0",children:E("div",{className:"flex-1 overflow-y-auto p-4",children:$.length===0?D("div",{className:"flex flex-col items-center justify-center h-full gap-3",children:[E(u,{size:64,className:"text-muted-foreground/20"}),E("span",{className:"text-muted-foreground",children:"暂无消息,开始对话吧!"})]}):D("div",{className:"space-y-2",children:[$.map(e3=>E("div",{className:t("flex",e3.type==="sent"?"justify-end":"justify-start"),children:D("div",{className:t("max-w-[70%] p-3 rounded-2xl",e3.type==="sent"?"bg-primary text-primary-foreground":"bg-muted"),children:[D("div",{className:"flex items-center gap-2 mb-1",children:[e3.type==="received"&&E(a,{size:14}),e3.type==="sent"&&E(g,{size:14}),E("span",{className:"text-xs font-medium opacity-90",children:e3.senderName}),E("span",{className:"text-xs opacity-70",children:new Date(e3.timestamp).toLocaleTimeString()})]}),E("div",{className:"text-sm space-y-1",children:be(e3.content,e3.type==="sent")})]})},e3.id)),E("div",{ref:_e})]})})}),D("div",{className:"rounded-lg border bg-card p-3 flex-shrink-0 space-y-3",children:[D("div",{className:"flex gap-2 items-center flex-wrap",children:[E("button",{type:"button",className:t("h-8 w-8 rounded-md flex items-center justify-center border transition-colors",F?"bg-primary text-primary-foreground":"hover:bg-accent"),onClick:()=>{I(!F),R(null)},title:"插入表情",children:E(p,{size:16})}),E("button",{type:"button",className:t("h-8 w-8 rounded-md flex items-center justify-center border transition-colors",L==="image"?"bg-primary text-primary-foreground":"hover:bg-accent"),onClick:()=>{R(e3=>e3==="image"?null:"image"),I(false)},title:"插入图片 URL",children:E(c,{size:16})}),E("button",{type:"button",className:t("h-8 w-8 rounded-md flex items-center justify-center border transition-colors",L==="video"?"bg-primary text-primary-foreground":"hover:bg-accent"),onClick:()=>{R(e3=>e3==="video"?null:"video"),I(false)},title:"插入视频 URL",children:E(y,{size:16})}),E("button",{type:"button",className:t("h-8 w-8 rounded-md flex items-center justify-center border transition-colors",L==="audio"?"bg-primary text-primary-foreground":"hover:bg-accent"),onClick:()=>{R(e3=>e3==="audio"?null:"audio"),I(false)},title:"插入音频 URL",children:E(d,{size:16})}),E("div",{className:"flex-1 min-w-[1rem]"}),A&&E("button",{className:"h-8 w-8 rounded-md flex items-center justify-center hover:bg-accent transition-colors",onClick:()=>{j(""),G([])},children:E(ne,{size:16})})]}),F&&D("div",{className:"p-3 rounded-md border bg-muted/30 max-h-64 overflow-y-auto space-y-2",children:[E("input",{value:W,onChange:e3=>fe(e3.target.value),placeholder:"搜索表情...",className:"w-full h-8 rounded-md border bg-transparent px-2 text-sm"}),E("div",{className:"grid grid-cols-8 gap-1",children:Me.slice(0,80).map(e3=>E("button",{onClick:()=>Ee(e3.id),title:e3.name,className:"w-10 h-10 rounded-md border flex items-center justify-center hover:bg-accent transition-colors",children:E("img",{src:`https://face.viki.moe/apng/${e3.id}.png`,alt:e3.name,className:"w-8 h-8"})},e3.id))}),Me.length===0&&D("div",{className:"flex flex-col items-center gap-2 py-4",children:[E(f,{size:32,className:"text-muted-foreground/30"}),E("span",{className:"text-sm text-muted-foreground",children:"未找到匹配的表情"})]})]}),L&&D("div",{className:"p-3 rounded-md border bg-muted/30 space-y-2",children:[D("p",{className:"text-xs text-muted-foreground",children:[L==="image"&&"支持 http(s) 图片链接或 data URL",L==="video"&&"支持浏览器可解码的视频直链(如 .mp4、.webm)",L==="audio"&&"支持 .mp3、.ogg、.wav 等音频直链"]}),E("input",{value:z,onChange:e3=>B(e3.target.value),placeholder:L==="image"?"图片 URL…":L==="video"?"视频 URL…":"音频 URL…",className:"w-full h-8 rounded-md border border-input bg-background px-2 text-sm",onKeyDown:e3=>{e3.key==="Enter"&&(e3.preventDefault(),De())}}),D("button",{type:"button",className:"inline-flex items-center gap-1 h-8 px-3 rounded-md bg-primary text-primary-foreground text-sm disabled:opacity-50",onClick:De,disabled:!z.trim(),children:[E(o,{size:14})," 插入到输入框"]})]}),D("div",{className:"flex gap-2 items-start",children:[D("div",{className:"flex-1 relative",children:[E(ae,{ref:Z,placeholder:`向 ${O.name} 发送消息...`,onSend:xe,onChange:je,onAtTrigger:ke,minHeight:"44px",maxHeight:"200px"}),V&&E("div",{className:"absolute z-50 rounded-lg border bg-popover shadow-md min-w-60 max-h-72 overflow-y-auto p-1",style:{top:`${V.top}px`,left:`${V.left}px`},children:Ae.length>0?Ae.map(e3=>D("div",{className:"flex items-center gap-2 p-2 rounded-md cursor-pointer hover:bg-accent transition-colors",onClick:()=>Oe(e3),children:[E(g,{size:16,className:"text-muted-foreground"}),D("div",{className:"flex-1",children:[E("div",{className:"text-sm font-medium",children:e3.name}),D("div",{className:"text-xs text-muted-foreground",children:["ID: ",e3.id]})]})]},e3.id)):D("div",{className:"flex flex-col items-center gap-2 p-4",children:[E(f,{size:20,className:"text-muted-foreground/50"}),E("span",{className:"text-xs text-muted-foreground",children:"未找到匹配的用户"})]})})]}),D("button",{className:"inline-flex items-center gap-1.5 h-10 px-4 rounded-md bg-primary text-primary-foreground text-sm font-medium disabled:opacity-50 transition-colors hover:bg-primary/90",onClick:()=>{let e3=Z.current?.getContent();e3&&xe(e3.text,e3.segments)},disabled:!ye(ge),children:[E(ee,{size:16})," 发送"]})]}),D("div",{className:"flex items-center gap-2 flex-wrap text-xs text-muted-foreground",children:[E(l,{size:12})," 快捷操作:",E("span",{className:"px-1 py-0.5 rounded border text-[10px]",children:"Enter"})," 发送",E("span",{className:"px-1 py-0.5 rounded border text-[10px]",children:"Shift+Enter"})," 换行",E("span",{className:"px-1 py-0.5 rounded border text-[10px]",children:"[@名称]"})," @某人",E("span",{className:"px-1 py-0.5 rounded border text-[10px]",children:"[video:URL]"}),E("span",{className:"px-1 py-0.5 rounded border text-[10px]",children:"[audio:URL]"})]})]})]})]})]})}e({key:"process-sandbox",path:"/sandbox",title:"沙盒",icon:E(m,{className:"w-5 h-5"}),element:E(oe,{})});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-sandbox",
3
- "version": "1.0.61",
3
+ "version": "1.0.63",
4
4
  "description": "Zhin.js adapter for local testing and development",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -13,12 +13,13 @@
13
13
  }
14
14
  },
15
15
  "files": [
16
+ "src",
16
17
  "lib",
17
- "README.md",
18
- "CHANGELOG.md",
19
18
  "client",
20
- "src",
21
- "dist"
19
+ "dist",
20
+ "skills",
21
+ "README.md",
22
+ "CHANGELOG.md"
22
23
  ],
23
24
  "keywords": [
24
25
  "zhin",
@@ -38,7 +39,7 @@
38
39
  "repository": {
39
40
  "url": "git+https://github.com/zhinjs/zhin.git",
40
41
  "type": "git",
41
- "directory": "plugins/adapters/process"
42
+ "directory": "plugins/adapters/sandbox"
42
43
  },
43
44
  "devDependencies": {
44
45
  "@types/react": "^19.2.2",
@@ -46,14 +47,14 @@
46
47
  "radix-ui": "^1.4.3",
47
48
  "lucide-react": "^0.469.0",
48
49
  "typescript": "^5.3.0",
49
- "zhin.js": "1.0.51"
50
+ "zhin.js": "1.0.52"
50
51
  },
51
52
  "peerDependencies": {
52
- "@zhin.js/core": "1.0.51",
53
- "@zhin.js/client": "1.0.12",
54
- "@zhin.js/console": "1.0.50",
55
- "zhin.js": "1.0.51",
56
- "@zhin.js/http": "1.0.45"
53
+ "@zhin.js/core": "1.0.52",
54
+ "@zhin.js/client": "1.0.13",
55
+ "@zhin.js/http": "1.0.46",
56
+ "zhin.js": "1.0.52",
57
+ "@zhin.js/console": "1.0.52"
57
58
  },
58
59
  "peerDependenciesMeta": {
59
60
  "@zhin.js/http": {