@zhin.js/console 1.0.49 → 1.0.51

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,19 @@
1
1
  # @zhin.js/console
2
2
 
3
+ ## 1.0.51
4
+
5
+ ### Patch Changes
6
+
7
+ - a451abf: fix: console 白屏
8
+
9
+ ## 1.0.50
10
+
11
+ ### Patch Changes
12
+
13
+ - zhin.js@1.0.51
14
+ - @zhin.js/http@1.0.45
15
+ - @zhin.js/core@1.0.51
16
+
3
17
  ## 1.0.49
4
18
 
5
19
  ### Patch Changes
@@ -1,7 +1,7 @@
1
1
  import { StrictMode, useCallback, useEffect, useState } from 'react'
2
2
  import { createRoot } from 'react-dom/client'
3
3
  import { Provider as ReduxProvider } from 'react-redux'
4
- import { Home, Package, Bot, FileText, Settings, KeyRound, FolderOpen, Database } from 'lucide-react'
4
+ import { Home, Package, Bot, FileText, Settings, KeyRound, FolderOpen, Database, LogIn } from 'lucide-react'
5
5
  import { store, DynamicRouter, persistor, addPage, useSelector, useWebSocket } from '@zhin.js/client'
6
6
  import DashboardLayout from './layouts/dashboard'
7
7
  import HomePage from './pages/dashboard'
@@ -14,6 +14,7 @@ import EnvMangePage from './pages/env'
14
14
  import FileMangePage from './pages/files'
15
15
  import DatabasePage from './pages/database'
16
16
  import LoginPage from './pages/login'
17
+ import LoginAssistPage from './pages/login-assist'
17
18
  import { hasToken } from './utils/auth'
18
19
  import './style.css'
19
20
  import { PersistGate } from 'redux-persist/integration/react'
@@ -72,6 +73,14 @@ function RouteInitializer() {
72
73
  icon: <Home className="w-4 h-4" />,
73
74
  element: <HomePage />,
74
75
  },
76
+ {
77
+ key: 'loginAssistPage',
78
+ path: '/login-assist',
79
+ title: '登录辅助',
80
+ icon: <LogIn className="w-4 h-4" />,
81
+ element: <LoginAssistPage />,
82
+ meta: { order: 1 }
83
+ },
75
84
  {
76
85
  key: 'pluginsPage',
77
86
  path: '/plugins',
@@ -0,0 +1,225 @@
1
+ import { useEffect, useState, useCallback } from 'react'
2
+ import { LogIn, QrCode, MessageSquare, MousePointer, Smartphone, AlertCircle } from 'lucide-react'
3
+ import { apiFetch } from '../utils/auth'
4
+ import { Card, CardContent, CardHeader, CardTitle } from '../components/ui/card'
5
+ import { Badge } from '../components/ui/badge'
6
+ import { Button } from '../components/ui/button'
7
+ import { Input } from '../components/ui/input'
8
+ import { Alert, AlertDescription } from '../components/ui/alert'
9
+ import { Skeleton } from '../components/ui/skeleton'
10
+ import { Separator } from '../components/ui/separator'
11
+
12
+ interface PendingLoginTask {
13
+ id: string
14
+ adapter: string
15
+ botId: string
16
+ type: string
17
+ payload?: {
18
+ message?: string
19
+ image?: string
20
+ url?: string
21
+ [key: string]: unknown
22
+ }
23
+ createdAt: number
24
+ }
25
+
26
+ const POLL_INTERVAL_MS = 2000
27
+
28
+ export default function LoginAssistPage() {
29
+ const [pending, setPending] = useState<PendingLoginTask[]>([])
30
+ const [loading, setLoading] = useState(true)
31
+ const [error, setError] = useState<string | null>(null)
32
+ const [submitting, setSubmitting] = useState<Record<string, boolean>>({})
33
+ const [inputValues, setInputValues] = useState<Record<string, string>>({})
34
+
35
+ const fetchPending = useCallback(async () => {
36
+ try {
37
+ const res = await apiFetch('/api/login-assist/pending')
38
+ if (!res.ok) throw new Error('获取待办失败')
39
+ const data = await res.json()
40
+ setPending(Array.isArray(data) ? data : [])
41
+ setError(null)
42
+ } catch (err) {
43
+ setError((err as Error).message)
44
+ } finally {
45
+ setLoading(false)
46
+ }
47
+ }, [])
48
+
49
+ useEffect(() => {
50
+ fetchPending()
51
+ const interval = setInterval(fetchPending, POLL_INTERVAL_MS)
52
+ return () => clearInterval(interval)
53
+ }, [fetchPending])
54
+
55
+ const handleSubmit = async (id: string, value: string | Record<string, unknown>) => {
56
+ setSubmitting((s) => ({ ...s, [id]: true }))
57
+ try {
58
+ const res = await apiFetch('/api/login-assist/submit', {
59
+ method: 'POST',
60
+ headers: { 'Content-Type': 'application/json' },
61
+ body: JSON.stringify({ id, value }),
62
+ })
63
+ if (!res.ok) throw new Error('提交失败')
64
+ setInputValues((v) => {
65
+ const next = { ...v }
66
+ delete next[id]
67
+ return next
68
+ })
69
+ await fetchPending()
70
+ } catch (err) {
71
+ setError((err as Error).message)
72
+ } finally {
73
+ setSubmitting((s) => ({ ...s, [id]: false }))
74
+ }
75
+ }
76
+
77
+ const handleCancel = async (id: string) => {
78
+ try {
79
+ const res = await apiFetch('/api/login-assist/cancel', {
80
+ method: 'POST',
81
+ headers: { 'Content-Type': 'application/json' },
82
+ body: JSON.stringify({ id }),
83
+ })
84
+ if (res.ok) await fetchPending()
85
+ } catch {
86
+ // ignore
87
+ }
88
+ }
89
+
90
+ const typeIcon: Record<string, React.ReactNode> = {
91
+ qrcode: <QrCode className="w-4 h-4" />,
92
+ sms: <MessageSquare className="w-4 h-4" />,
93
+ device: <Smartphone className="w-4 h-4" />,
94
+ slider: <MousePointer className="w-4 h-4" />,
95
+ }
96
+ const typeLabel: Record<string, string> = {
97
+ qrcode: '扫码登录',
98
+ sms: '短信验证码',
99
+ device: '设备验证',
100
+ slider: '滑块验证',
101
+ other: '其他',
102
+ }
103
+
104
+ if (loading && pending.length === 0) {
105
+ return (
106
+ <div className="space-y-6">
107
+ <Skeleton className="h-8 w-48" />
108
+ <Skeleton className="h-32 w-full" />
109
+ </div>
110
+ )
111
+ }
112
+
113
+ return (
114
+ <div className="space-y-6">
115
+ <div>
116
+ <h1 className="text-2xl font-bold tracking-tight">登录辅助</h1>
117
+ <p className="text-sm text-muted-foreground mt-1">
118
+ 需要人为辅助登录的待办会出现在下方,在 Web 完成操作或刷新页面后仍可继续处理。
119
+ </p>
120
+ </div>
121
+
122
+ <Separator />
123
+
124
+ {error && (
125
+ <Alert variant="destructive">
126
+ <AlertCircle className="h-4 w-4" />
127
+ <AlertDescription>{error}</AlertDescription>
128
+ </Alert>
129
+ )}
130
+
131
+ {pending.length === 0 ? (
132
+ <Card>
133
+ <CardContent className="flex flex-col items-center gap-4 py-12">
134
+ <LogIn className="w-16 h-16 text-muted-foreground/30" />
135
+ <div className="text-center">
136
+ <h3 className="text-lg font-semibold">暂无待办</h3>
137
+ <p className="text-sm text-muted-foreground">当有机器人需要扫码、短信或滑块验证时,待办会显示在这里</p>
138
+ </div>
139
+ </CardContent>
140
+ </Card>
141
+ ) : (
142
+ <div className="grid gap-4">
143
+ {pending.map((task) => (
144
+ <Card key={task.id}>
145
+ <CardHeader className="pb-2">
146
+ <div className="flex items-center justify-between">
147
+ <CardTitle className="text-base flex items-center gap-2">
148
+ <span className="shrink-0">{typeIcon[task.type] ?? <LogIn className="w-4 h-4" />}</span>
149
+ {typeLabel[task.type] ?? task.type}
150
+ </CardTitle>
151
+ <div className="flex items-center gap-2">
152
+ <Badge variant="outline">{task.adapter}</Badge>
153
+ <Badge variant="secondary">{task.botId}</Badge>
154
+ </div>
155
+ </div>
156
+ {task.payload?.message && (
157
+ <p className="text-sm text-muted-foreground mt-1">{task.payload.message}</p>
158
+ )}
159
+ </CardHeader>
160
+ <CardContent className="space-y-4">
161
+ {task.type === 'qrcode' && task.payload?.image && (
162
+ <div className="flex justify-center p-4 bg-muted/30 rounded-lg">
163
+ <img
164
+ src={task.payload.image}
165
+ alt="登录二维码"
166
+ className="max-w-[200px] w-full h-auto"
167
+ />
168
+ </div>
169
+ )}
170
+ {task.type === 'slider' && task.payload?.url && (
171
+ <p className="text-sm break-all">
172
+ <span className="text-muted-foreground">滑块链接:</span>{' '}
173
+ <a href={task.payload.url} target="_blank" rel="noopener noreferrer" className="text-primary underline">
174
+ {task.payload.url}
175
+ </a>
176
+ </p>
177
+ )}
178
+ {(task.type === 'sms' || task.type === 'device' || task.type === 'slider') && (
179
+ <div className="flex flex-wrap items-center gap-2">
180
+ <Input
181
+ placeholder={task.type === 'slider' ? '输入 ticket' : '输入验证码'}
182
+ className="max-w-xs"
183
+ value={inputValues[task.id] ?? ''}
184
+ onChange={(e) => setInputValues((v) => ({ ...v, [task.id]: e.target.value }))}
185
+ onKeyDown={(e) => {
186
+ if (e.key === 'Enter') {
187
+ const val = inputValues[task.id]?.trim()
188
+ if (val) handleSubmit(task.id, task.type === 'slider' ? { ticket: val } : val)
189
+ }
190
+ }}
191
+ />
192
+ <Button
193
+ size="sm"
194
+ disabled={submitting[task.id] || !(inputValues[task.id]?.trim())}
195
+ onClick={() => {
196
+ const val = inputValues[task.id]?.trim()
197
+ if (val) handleSubmit(task.id, task.type === 'slider' ? { ticket: val } : val)
198
+ }}
199
+ >
200
+ {submitting[task.id] ? '提交中…' : '提交'}
201
+ </Button>
202
+ </div>
203
+ )}
204
+ {task.type === 'qrcode' && (
205
+ <div className="flex gap-2">
206
+ <Button
207
+ size="sm"
208
+ disabled={submitting[task.id]}
209
+ onClick={() => handleSubmit(task.id, { done: true })}
210
+ >
211
+ {submitting[task.id] ? '提交中…' : '我已扫码'}
212
+ </Button>
213
+ <Button size="sm" variant="outline" onClick={() => handleCancel(task.id)}>
214
+ 取消
215
+ </Button>
216
+ </div>
217
+ )}
218
+ </CardContent>
219
+ </Card>
220
+ ))}
221
+ </div>
222
+ )}
223
+ </div>
224
+ )
225
+ }