@zhin.js/console 2.0.0 → 2.0.2
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 +29 -0
- package/client/src/layouts/dashboard.tsx +1 -1
- package/client/src/main.tsx +24 -6
- package/client/src/pages/cron.tsx +529 -0
- package/client/src/pages/dashboard.tsx +60 -1
- package/client/src/pages/marketplace.tsx +464 -0
- package/dist/assets/index-BMkFI-uN.js +124 -0
- package/dist/assets/style-CtySe6_R.css +3 -0
- package/dist/client.js +10 -2
- package/dist/index.html +2 -2
- package/dist/style.css +1 -1
- package/lib/index.js +146 -6
- package/lib/websocket.js +146 -6
- package/package.json +7 -6
- package/src/bot-hub.ts +18 -4
- package/src/websocket.ts +151 -2
- package/dist/assets/index-B1ihXBk4.js +0 -124
- package/dist/assets/style-kkLO-vsa.css +0 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# @zhin.js/console
|
|
2
2
|
|
|
3
|
+
## 2.0.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 5073d4c: chore: chore: update TypeScript version to ^5.9.3 across all plugins and packages
|
|
8
|
+
feat: enhance ai-text-as-image output registration with off handler for cleanup
|
|
9
|
+
fix: remove unnecessary logging in ensureBuiltinFontsCached function
|
|
10
|
+
refactor: simplify action handlers in html-renderer tools
|
|
11
|
+
chore: add README files for queue-sandbox-poc and event-delivery packages
|
|
12
|
+
chore: adjust pnpm workspace configuration to exclude games directory
|
|
13
|
+
chore: update tsconfig to include plugins directory for TypeScript compilation
|
|
14
|
+
- Updated dependencies [5073d4c]
|
|
15
|
+
- @zhin.js/agent@0.1.2
|
|
16
|
+
- @zhin.js/core@1.1.2
|
|
17
|
+
- zhin.js@1.0.60
|
|
18
|
+
- @zhin.js/http@1.0.55
|
|
19
|
+
|
|
20
|
+
## 2.0.1
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- c212bf7: fix: 适配器优化
|
|
25
|
+
- Updated dependencies [c212bf7]
|
|
26
|
+
- @zhin.js/agent@0.1.1
|
|
27
|
+
- @zhin.js/client@1.0.15
|
|
28
|
+
- @zhin.js/core@1.1.1
|
|
29
|
+
- zhin.js@1.0.59
|
|
30
|
+
- @zhin.js/http@1.0.54
|
|
31
|
+
|
|
3
32
|
## 2.0.0
|
|
4
33
|
|
|
5
34
|
### Patch Changes
|
|
@@ -43,7 +43,7 @@ export default function DashboardLayout() {
|
|
|
43
43
|
const navigate = useNavigate()
|
|
44
44
|
const sidebarOpen = useSelector((state) => state.ui.sidebarOpen)
|
|
45
45
|
const activeMenu = useSelector((state) => state.ui.activeMenu)
|
|
46
|
-
const routes = useSelector((state) => state.route
|
|
46
|
+
const routes = useSelector((state) => state.route?.routes || [])
|
|
47
47
|
const [searchQ, setSearchQ] = useState("")
|
|
48
48
|
|
|
49
49
|
const menuRoutes = useMemo(() => {
|
package/client/src/main.tsx
CHANGED
|
@@ -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, Clock, Store } 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,8 @@ import ConfigPage from './pages/config'
|
|
|
14
14
|
import EnvManagePage from './pages/env'
|
|
15
15
|
import FileManagePage from './pages/files'
|
|
16
16
|
import DatabasePage from './pages/database'
|
|
17
|
+
import CronPage from './pages/cron'
|
|
18
|
+
import MarketplacePage from './pages/marketplace'
|
|
17
19
|
import LoginPage from './pages/login'
|
|
18
20
|
import { hasToken } from './utils/auth'
|
|
19
21
|
import './style.css'
|
|
@@ -90,13 +92,21 @@ function RouteInitializer() {
|
|
|
90
92
|
element: <LogsPage />,
|
|
91
93
|
meta: { group: '系统', order: 2, fullWidth: true },
|
|
92
94
|
},
|
|
95
|
+
{
|
|
96
|
+
key: 'cronPage',
|
|
97
|
+
path: '/cron',
|
|
98
|
+
title: '定时任务',
|
|
99
|
+
icon: <Clock className="w-4 h-4" />,
|
|
100
|
+
element: <CronPage />,
|
|
101
|
+
meta: { group: '系统', order: 3 },
|
|
102
|
+
},
|
|
93
103
|
{
|
|
94
104
|
key: 'pluginsPage',
|
|
95
105
|
path: '/plugins',
|
|
96
106
|
title: '插件管理',
|
|
97
107
|
icon: <Package className="w-4 h-4" />,
|
|
98
108
|
element: <PluginsPage />,
|
|
99
|
-
meta: { group: '扩展', order:
|
|
109
|
+
meta: { group: '扩展', order: 4 },
|
|
100
110
|
},
|
|
101
111
|
{
|
|
102
112
|
key: 'pluginDetailPage',
|
|
@@ -105,13 +115,21 @@ function RouteInitializer() {
|
|
|
105
115
|
element: <PluginDetailPage />,
|
|
106
116
|
meta: { hideInMenu: true },
|
|
107
117
|
},
|
|
118
|
+
{
|
|
119
|
+
key: 'marketplacePage',
|
|
120
|
+
path: '/marketplace',
|
|
121
|
+
title: '插件市场',
|
|
122
|
+
icon: <Store className="w-4 h-4" />,
|
|
123
|
+
element: <MarketplacePage />,
|
|
124
|
+
meta: { group: '扩展', order: 5 },
|
|
125
|
+
},
|
|
108
126
|
{
|
|
109
127
|
key: 'configPage',
|
|
110
128
|
path: '/config',
|
|
111
129
|
title: '配置管理',
|
|
112
130
|
icon: <Settings className="w-4 h-4" />,
|
|
113
131
|
element: <ConfigPage />,
|
|
114
|
-
meta: { group: '配置与数据', order:
|
|
132
|
+
meta: { group: '配置与数据', order: 6 },
|
|
115
133
|
},
|
|
116
134
|
{
|
|
117
135
|
key: 'envManagePage',
|
|
@@ -119,7 +137,7 @@ function RouteInitializer() {
|
|
|
119
137
|
title: '环境变量',
|
|
120
138
|
icon: <KeyRound className="w-4 h-4" />,
|
|
121
139
|
element: <EnvManagePage />,
|
|
122
|
-
meta: { group: '配置与数据', order:
|
|
140
|
+
meta: { group: '配置与数据', order: 6 },
|
|
123
141
|
},
|
|
124
142
|
{
|
|
125
143
|
key: 'fileManagePage',
|
|
@@ -127,7 +145,7 @@ function RouteInitializer() {
|
|
|
127
145
|
title: '文件管理',
|
|
128
146
|
icon: <FolderOpen className="w-4 h-4" />,
|
|
129
147
|
element: <FileManagePage />,
|
|
130
|
-
meta: { group: '配置与数据', order:
|
|
148
|
+
meta: { group: '配置与数据', order: 7 },
|
|
131
149
|
},
|
|
132
150
|
{
|
|
133
151
|
key: 'databasePage',
|
|
@@ -135,7 +153,7 @@ function RouteInitializer() {
|
|
|
135
153
|
title: '数据库',
|
|
136
154
|
icon: <Database className="w-4 h-4" />,
|
|
137
155
|
element: <DatabasePage />,
|
|
138
|
-
meta: { group: '配置与数据', order:
|
|
156
|
+
meta: { group: '配置与数据', order: 8, fullWidth: true },
|
|
139
157
|
},
|
|
140
158
|
{
|
|
141
159
|
key: 'botDetailPage',
|
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
import { useEffect, useState, useCallback, type ChangeEvent } from 'react'
|
|
2
|
+
import { Clock, Plus, Trash2, AlertCircle, Pause, Play, RefreshCw, Timer, Cpu, ChevronDown, ChevronUp, Copy, Check } from 'lucide-react'
|
|
3
|
+
import { useWebSocket, useSelector, selectConfigConnected } from '@zhin.js/client'
|
|
4
|
+
import { Button } from '../components/ui/button'
|
|
5
|
+
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '../components/ui/card'
|
|
6
|
+
import { Badge } from '../components/ui/badge'
|
|
7
|
+
import { Alert, AlertDescription } from '../components/ui/alert'
|
|
8
|
+
import { Skeleton } from '../components/ui/skeleton'
|
|
9
|
+
import { Input } from '../components/ui/input'
|
|
10
|
+
import { Textarea } from '../components/ui/textarea'
|
|
11
|
+
import { Separator } from '../components/ui/separator'
|
|
12
|
+
import {
|
|
13
|
+
Dialog, DialogContent, DialogHeader, DialogFooter,
|
|
14
|
+
DialogTitle, DialogDescription, DialogClose,
|
|
15
|
+
} from '../components/ui/dialog'
|
|
16
|
+
|
|
17
|
+
interface MemoryCron {
|
|
18
|
+
type: 'memory'
|
|
19
|
+
expression: string
|
|
20
|
+
running: boolean
|
|
21
|
+
nextExecution: string | null
|
|
22
|
+
plugin: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface CronJobContext {
|
|
26
|
+
platform?: string
|
|
27
|
+
botId?: string
|
|
28
|
+
senderId?: string
|
|
29
|
+
sceneId?: string
|
|
30
|
+
scope?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface PersistentCron {
|
|
34
|
+
type: 'persistent'
|
|
35
|
+
id: string
|
|
36
|
+
cronExpression: string
|
|
37
|
+
prompt: string
|
|
38
|
+
label?: string
|
|
39
|
+
enabled: boolean
|
|
40
|
+
context?: CronJobContext
|
|
41
|
+
createdAt: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface BotInfo {
|
|
45
|
+
name: string
|
|
46
|
+
adapter: string
|
|
47
|
+
connected: boolean
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const EMPTY_CONTEXT: CronJobContext = { platform: '', botId: '', senderId: '', sceneId: '', scope: '' }
|
|
51
|
+
|
|
52
|
+
export default function CronPage() {
|
|
53
|
+
const [memoryCrons, setMemoryCrons] = useState<MemoryCron[]>([])
|
|
54
|
+
const [persistentCrons, setPersistentCrons] = useState<PersistentCron[]>([])
|
|
55
|
+
const [bots, setBots] = useState<BotInfo[]>([])
|
|
56
|
+
const [loading, setLoading] = useState(true)
|
|
57
|
+
const [error, setError] = useState<string | null>(null)
|
|
58
|
+
const [addDialogOpen, setAddDialogOpen] = useState(false)
|
|
59
|
+
const [deleteTarget, setDeleteTarget] = useState<PersistentCron | null>(null)
|
|
60
|
+
const [submitting, setSubmitting] = useState(false)
|
|
61
|
+
const [newCron, setNewCron] = useState({ cronExpression: '', prompt: '', label: '', context: { ...EMPTY_CONTEXT } })
|
|
62
|
+
const [expandedId, setExpandedId] = useState<string | null>(null)
|
|
63
|
+
const [expandedMemIdx, setExpandedMemIdx] = useState<number | null>(null)
|
|
64
|
+
const [copiedId, setCopiedId] = useState<string | null>(null)
|
|
65
|
+
const connected = useSelector(selectConfigConnected)
|
|
66
|
+
const { sendRequest } = useWebSocket()
|
|
67
|
+
|
|
68
|
+
const fetchCrons = useCallback(async () => {
|
|
69
|
+
if (!connected) {
|
|
70
|
+
setLoading(false)
|
|
71
|
+
setError('WebSocket 未连接,请刷新页面')
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const data = await sendRequest<{ memory: MemoryCron[]; persistent: PersistentCron[] }>({ type: 'cron:list' })
|
|
76
|
+
setMemoryCrons(data.memory || [])
|
|
77
|
+
setPersistentCrons(data.persistent || [])
|
|
78
|
+
// Also fetch bots for context selector
|
|
79
|
+
try {
|
|
80
|
+
const botData = await sendRequest<{ bots: BotInfo[] }>({ type: 'bot:list' })
|
|
81
|
+
setBots(botData.bots || [])
|
|
82
|
+
} catch { /* ignore */ }
|
|
83
|
+
setError(null)
|
|
84
|
+
} catch (err) {
|
|
85
|
+
setError((err as Error).message)
|
|
86
|
+
} finally {
|
|
87
|
+
setLoading(false)
|
|
88
|
+
}
|
|
89
|
+
}, [connected, sendRequest])
|
|
90
|
+
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (connected) {
|
|
93
|
+
setLoading(true)
|
|
94
|
+
fetchCrons()
|
|
95
|
+
}
|
|
96
|
+
}, [connected, fetchCrons])
|
|
97
|
+
|
|
98
|
+
const handleAdd = async () => {
|
|
99
|
+
if (!newCron.cronExpression || !newCron.prompt) return
|
|
100
|
+
setSubmitting(true)
|
|
101
|
+
try {
|
|
102
|
+
// Build context, omitting empty fields
|
|
103
|
+
const ctx: CronJobContext = {}
|
|
104
|
+
if (newCron.context.platform) ctx.platform = newCron.context.platform
|
|
105
|
+
if (newCron.context.botId) ctx.botId = newCron.context.botId
|
|
106
|
+
if (newCron.context.senderId) ctx.senderId = newCron.context.senderId
|
|
107
|
+
if (newCron.context.sceneId) ctx.sceneId = newCron.context.sceneId
|
|
108
|
+
if (newCron.context.scope) ctx.scope = newCron.context.scope
|
|
109
|
+
const hasContext = Object.keys(ctx).length > 0
|
|
110
|
+
await sendRequest({
|
|
111
|
+
type: 'cron:add',
|
|
112
|
+
cronExpression: newCron.cronExpression,
|
|
113
|
+
prompt: newCron.prompt,
|
|
114
|
+
label: newCron.label,
|
|
115
|
+
context: hasContext ? ctx : undefined,
|
|
116
|
+
})
|
|
117
|
+
setAddDialogOpen(false)
|
|
118
|
+
setNewCron({ cronExpression: '', prompt: '', label: '', context: { ...EMPTY_CONTEXT } })
|
|
119
|
+
await fetchCrons()
|
|
120
|
+
} catch (err) {
|
|
121
|
+
setError((err as Error).message)
|
|
122
|
+
} finally {
|
|
123
|
+
setSubmitting(false)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const handleDelete = async () => {
|
|
128
|
+
if (!deleteTarget) return
|
|
129
|
+
setSubmitting(true)
|
|
130
|
+
try {
|
|
131
|
+
await sendRequest({ type: 'cron:remove', id: deleteTarget.id })
|
|
132
|
+
setDeleteTarget(null)
|
|
133
|
+
await fetchCrons()
|
|
134
|
+
} catch (err) {
|
|
135
|
+
setError((err as Error).message)
|
|
136
|
+
} finally {
|
|
137
|
+
setSubmitting(false)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const handleToggle = async (job: PersistentCron) => {
|
|
142
|
+
try {
|
|
143
|
+
if (job.enabled) {
|
|
144
|
+
await sendRequest({ type: 'cron:pause', id: job.id })
|
|
145
|
+
} else {
|
|
146
|
+
await sendRequest({ type: 'cron:resume', id: job.id })
|
|
147
|
+
}
|
|
148
|
+
await fetchCrons()
|
|
149
|
+
} catch (err) {
|
|
150
|
+
setError((err as Error).message)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (loading && connected) {
|
|
155
|
+
return (
|
|
156
|
+
<div className="space-y-6">
|
|
157
|
+
<Skeleton className="h-8 w-48" />
|
|
158
|
+
<div className="grid grid-cols-1 gap-4">
|
|
159
|
+
{[...Array(3)].map((_, i) => <Skeleton key={i} className="h-32" />)}
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (error) {
|
|
166
|
+
return (
|
|
167
|
+
<div className="space-y-4">
|
|
168
|
+
<Alert variant="destructive">
|
|
169
|
+
<AlertCircle className="h-4 w-4" />
|
|
170
|
+
<AlertDescription>{error}</AlertDescription>
|
|
171
|
+
</Alert>
|
|
172
|
+
<Button variant="outline" size="sm" onClick={() => { setError(null); setLoading(true); fetchCrons() }}>
|
|
173
|
+
<RefreshCw className="w-4 h-4 mr-1" /> 重试
|
|
174
|
+
</Button>
|
|
175
|
+
</div>
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<div className="space-y-6">
|
|
181
|
+
{/* Header */}
|
|
182
|
+
<div className="flex items-center justify-between">
|
|
183
|
+
<div>
|
|
184
|
+
<h2 className="text-2xl font-bold tracking-tight">定时任务</h2>
|
|
185
|
+
<p className="text-muted-foreground text-sm mt-1">
|
|
186
|
+
管理持久化定时任务和查看插件注册的内存任务
|
|
187
|
+
</p>
|
|
188
|
+
</div>
|
|
189
|
+
<div className="flex items-center gap-2">
|
|
190
|
+
<Button variant="outline" size="sm" onClick={() => { setLoading(true); fetchCrons() }}>
|
|
191
|
+
<RefreshCw className="w-4 h-4 mr-1" /> 刷新
|
|
192
|
+
</Button>
|
|
193
|
+
<Button size="sm" onClick={() => setAddDialogOpen(true)}>
|
|
194
|
+
<Plus className="w-4 h-4 mr-1" /> 新建任务
|
|
195
|
+
</Button>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
{/* Persistent Cron Jobs */}
|
|
200
|
+
<div>
|
|
201
|
+
<div className="flex items-center gap-2 mb-3">
|
|
202
|
+
<Timer className="w-5 h-5 text-primary" />
|
|
203
|
+
<h3 className="text-lg font-semibold">持久化任务</h3>
|
|
204
|
+
<Badge variant="secondary">{persistentCrons.length}</Badge>
|
|
205
|
+
</div>
|
|
206
|
+
{persistentCrons.length === 0 ? (
|
|
207
|
+
<Card>
|
|
208
|
+
<CardContent className="py-8 text-center text-muted-foreground">
|
|
209
|
+
<Clock className="w-10 h-10 mx-auto mb-3 opacity-40" />
|
|
210
|
+
<p>暂无持久化定时任务</p>
|
|
211
|
+
<p className="text-xs mt-1">点击「新建任务」添加一个定时 AI 任务</p>
|
|
212
|
+
</CardContent>
|
|
213
|
+
</Card>
|
|
214
|
+
) : (
|
|
215
|
+
<div className="grid grid-cols-1 gap-3">
|
|
216
|
+
{persistentCrons.map((job) => {
|
|
217
|
+
const isExpanded = expandedId === job.id
|
|
218
|
+
return (
|
|
219
|
+
<Card key={job.id} className={!job.enabled ? 'opacity-60' : ''}>
|
|
220
|
+
<CardContent className="py-4">
|
|
221
|
+
<div
|
|
222
|
+
className="flex items-start justify-between gap-4 cursor-pointer"
|
|
223
|
+
onClick={() => setExpandedId(isExpanded ? null : job.id)}
|
|
224
|
+
>
|
|
225
|
+
<div className="flex-1 min-w-0">
|
|
226
|
+
<div className="flex items-center gap-2 mb-1">
|
|
227
|
+
{isExpanded ? <ChevronUp className="w-4 h-4 shrink-0 text-muted-foreground" /> : <ChevronDown className="w-4 h-4 shrink-0 text-muted-foreground" />}
|
|
228
|
+
<span className="font-medium truncate">
|
|
229
|
+
{job.label || job.id}
|
|
230
|
+
</span>
|
|
231
|
+
<Badge variant={job.enabled ? 'default' : 'outline'} className="text-xs shrink-0">
|
|
232
|
+
{job.enabled ? '运行中' : '已暂停'}
|
|
233
|
+
</Badge>
|
|
234
|
+
</div>
|
|
235
|
+
<div className="flex items-center gap-3 text-xs text-muted-foreground mb-2 ml-6">
|
|
236
|
+
<code className="bg-muted px-1.5 py-0.5 rounded">{job.cronExpression}</code>
|
|
237
|
+
<span>创建于 {new Date(job.createdAt).toLocaleString()}</span>
|
|
238
|
+
</div>
|
|
239
|
+
{!isExpanded && (
|
|
240
|
+
<p className="text-sm text-muted-foreground line-clamp-1 ml-6">{job.prompt}</p>
|
|
241
|
+
)}
|
|
242
|
+
</div>
|
|
243
|
+
<div className="flex items-center gap-1 shrink-0" onClick={(e) => e.stopPropagation()}>
|
|
244
|
+
<Button
|
|
245
|
+
variant="ghost"
|
|
246
|
+
size="icon"
|
|
247
|
+
className="h-8 w-8"
|
|
248
|
+
title={job.enabled ? '暂停' : '恢复'}
|
|
249
|
+
onClick={() => handleToggle(job)}
|
|
250
|
+
>
|
|
251
|
+
{job.enabled ? <Pause className="w-4 h-4" /> : <Play className="w-4 h-4" />}
|
|
252
|
+
</Button>
|
|
253
|
+
<Button
|
|
254
|
+
variant="ghost"
|
|
255
|
+
size="icon"
|
|
256
|
+
className="h-8 w-8 text-destructive hover:text-destructive"
|
|
257
|
+
title="删除"
|
|
258
|
+
onClick={() => setDeleteTarget(job)}
|
|
259
|
+
>
|
|
260
|
+
<Trash2 className="w-4 h-4" />
|
|
261
|
+
</Button>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
{isExpanded && (
|
|
265
|
+
<div className="mt-3 ml-6 space-y-3 border-t pt-3">
|
|
266
|
+
<div>
|
|
267
|
+
<div className="flex items-center justify-between mb-1">
|
|
268
|
+
<span className="text-xs font-medium text-muted-foreground">任务 ID</span>
|
|
269
|
+
<Button
|
|
270
|
+
variant="ghost"
|
|
271
|
+
size="icon"
|
|
272
|
+
className="h-6 w-6"
|
|
273
|
+
title="复制 ID"
|
|
274
|
+
onClick={() => {
|
|
275
|
+
navigator.clipboard.writeText(job.id)
|
|
276
|
+
setCopiedId(job.id)
|
|
277
|
+
setTimeout(() => setCopiedId(null), 1500)
|
|
278
|
+
}}
|
|
279
|
+
>
|
|
280
|
+
{copiedId === job.id ? <Check className="w-3 h-3 text-green-500" /> : <Copy className="w-3 h-3" />}
|
|
281
|
+
</Button>
|
|
282
|
+
</div>
|
|
283
|
+
<code className="text-xs bg-muted px-2 py-1 rounded block">{job.id}</code>
|
|
284
|
+
</div>
|
|
285
|
+
<div>
|
|
286
|
+
<span className="text-xs font-medium text-muted-foreground block mb-1">Cron 表达式</span>
|
|
287
|
+
<code className="text-sm bg-muted px-2 py-1 rounded block">{job.cronExpression}</code>
|
|
288
|
+
</div>
|
|
289
|
+
<div>
|
|
290
|
+
<span className="text-xs font-medium text-muted-foreground block mb-1">Prompt</span>
|
|
291
|
+
<pre className="text-sm bg-muted px-3 py-2 rounded whitespace-pre-wrap break-words max-h-60 overflow-y-auto">{job.prompt}</pre>
|
|
292
|
+
</div>
|
|
293
|
+
{job.context && Object.values(job.context).some(Boolean) && (
|
|
294
|
+
<div>
|
|
295
|
+
<span className="text-xs font-medium text-muted-foreground block mb-1">执行上下文</span>
|
|
296
|
+
<div className="bg-muted px-3 py-2 rounded text-xs space-y-1">
|
|
297
|
+
{job.context.platform && <p><span className="text-muted-foreground">平台:</span> {job.context.platform}</p>}
|
|
298
|
+
{job.context.botId && <p><span className="text-muted-foreground">Bot:</span> {job.context.botId}</p>}
|
|
299
|
+
{job.context.senderId && <p><span className="text-muted-foreground">发送者:</span> {job.context.senderId}</p>}
|
|
300
|
+
{job.context.sceneId && <p><span className="text-muted-foreground">场景:</span> {job.context.sceneId}</p>}
|
|
301
|
+
{job.context.scope && <p><span className="text-muted-foreground">类型:</span> {job.context.scope}</p>}
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
)}
|
|
305
|
+
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
|
306
|
+
<span>状态: {job.enabled ? '✅ 运行中' : '⏸️ 已暂停'}</span>
|
|
307
|
+
<span>创建于: {new Date(job.createdAt).toLocaleString()}</span>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
)}
|
|
311
|
+
</CardContent>
|
|
312
|
+
</Card>
|
|
313
|
+
)
|
|
314
|
+
})}
|
|
315
|
+
</div>
|
|
316
|
+
)}
|
|
317
|
+
</div>
|
|
318
|
+
|
|
319
|
+
<Separator />
|
|
320
|
+
|
|
321
|
+
{/* Memory Cron Jobs (read-only) */}
|
|
322
|
+
<div>
|
|
323
|
+
<div className="flex items-center gap-2 mb-3">
|
|
324
|
+
<Cpu className="w-5 h-5 text-muted-foreground" />
|
|
325
|
+
<h3 className="text-lg font-semibold">内存任务(插件注册)</h3>
|
|
326
|
+
<Badge variant="outline">{memoryCrons.length}</Badge>
|
|
327
|
+
</div>
|
|
328
|
+
{memoryCrons.length === 0 ? (
|
|
329
|
+
<Card>
|
|
330
|
+
<CardContent className="py-6 text-center text-muted-foreground text-sm">
|
|
331
|
+
暂无插件注册的内存定时任务
|
|
332
|
+
</CardContent>
|
|
333
|
+
</Card>
|
|
334
|
+
) : (
|
|
335
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
336
|
+
{memoryCrons.map((cron, idx) => {
|
|
337
|
+
const isExpanded = expandedMemIdx === idx
|
|
338
|
+
return (
|
|
339
|
+
<Card key={idx} className={!cron.running ? 'opacity-60' : ''}>
|
|
340
|
+
<CardContent className="py-3">
|
|
341
|
+
<div
|
|
342
|
+
className="flex items-center justify-between mb-1 cursor-pointer"
|
|
343
|
+
onClick={() => setExpandedMemIdx(isExpanded ? null : idx)}
|
|
344
|
+
>
|
|
345
|
+
<div className="flex items-center gap-2">
|
|
346
|
+
{isExpanded ? <ChevronUp className="w-4 h-4 shrink-0 text-muted-foreground" /> : <ChevronDown className="w-4 h-4 shrink-0 text-muted-foreground" />}
|
|
347
|
+
<code className="text-sm bg-muted px-1.5 py-0.5 rounded">{cron.expression}</code>
|
|
348
|
+
</div>
|
|
349
|
+
<Badge variant={cron.running ? 'default' : 'outline'} className="text-xs">
|
|
350
|
+
{cron.running ? '运行中' : '已停止'}
|
|
351
|
+
</Badge>
|
|
352
|
+
</div>
|
|
353
|
+
<div className="text-xs text-muted-foreground space-y-0.5 ml-6">
|
|
354
|
+
<p>插件: {cron.plugin}</p>
|
|
355
|
+
{cron.nextExecution && (
|
|
356
|
+
<p>下次执行: {new Date(cron.nextExecution).toLocaleString()}</p>
|
|
357
|
+
)}
|
|
358
|
+
</div>
|
|
359
|
+
{isExpanded && (
|
|
360
|
+
<div className="mt-3 ml-6 space-y-2 border-t pt-3">
|
|
361
|
+
<div>
|
|
362
|
+
<span className="text-xs font-medium text-muted-foreground block mb-1">Cron 表达式</span>
|
|
363
|
+
<code className="text-sm bg-muted px-2 py-1 rounded block">{cron.expression}</code>
|
|
364
|
+
</div>
|
|
365
|
+
<div>
|
|
366
|
+
<span className="text-xs font-medium text-muted-foreground block mb-1">所属插件</span>
|
|
367
|
+
<code className="text-sm bg-muted px-2 py-1 rounded block">{cron.plugin}</code>
|
|
368
|
+
</div>
|
|
369
|
+
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
|
370
|
+
<span>状态: {cron.running ? '✅ 运行中' : '⏹️ 已停止'}</span>
|
|
371
|
+
{cron.nextExecution && (
|
|
372
|
+
<span>下次执行: {new Date(cron.nextExecution).toLocaleString()}</span>
|
|
373
|
+
)}
|
|
374
|
+
</div>
|
|
375
|
+
</div>
|
|
376
|
+
)}
|
|
377
|
+
</CardContent>
|
|
378
|
+
</Card>
|
|
379
|
+
)
|
|
380
|
+
})}
|
|
381
|
+
</div>
|
|
382
|
+
)}
|
|
383
|
+
</div>
|
|
384
|
+
|
|
385
|
+
{/* Add Dialog */}
|
|
386
|
+
<Dialog open={addDialogOpen} onOpenChange={setAddDialogOpen}>
|
|
387
|
+
<DialogContent>
|
|
388
|
+
<DialogHeader>
|
|
389
|
+
<DialogTitle>新建定时任务</DialogTitle>
|
|
390
|
+
<DialogDescription>
|
|
391
|
+
创建一个持久化定时任务,到点时会将 Prompt 发送给 AI 执行。
|
|
392
|
+
</DialogDescription>
|
|
393
|
+
</DialogHeader>
|
|
394
|
+
<div className="space-y-4 py-2">
|
|
395
|
+
<div>
|
|
396
|
+
<label className="text-sm font-medium mb-1.5 block">标签(可选)</label>
|
|
397
|
+
<Input
|
|
398
|
+
placeholder="例如:每日摘要"
|
|
399
|
+
value={newCron.label}
|
|
400
|
+
onChange={(e) => setNewCron((p) => ({ ...p, label: e.target.value }))}
|
|
401
|
+
/>
|
|
402
|
+
</div>
|
|
403
|
+
<div>
|
|
404
|
+
<label className="text-sm font-medium mb-1.5 block">Cron 表达式</label>
|
|
405
|
+
<Input
|
|
406
|
+
placeholder="分 时 日 月 周,例如:0 9 * * *"
|
|
407
|
+
value={newCron.cronExpression}
|
|
408
|
+
onChange={(e) => setNewCron((p) => ({ ...p, cronExpression: e.target.value }))}
|
|
409
|
+
/>
|
|
410
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
411
|
+
5 字段格式:分(0-59) 时(0-23) 日(1-31) 月(1-12) 周(0-7)
|
|
412
|
+
</p>
|
|
413
|
+
</div>
|
|
414
|
+
<div>
|
|
415
|
+
<label className="text-sm font-medium mb-1.5 block">Prompt</label>
|
|
416
|
+
<Textarea
|
|
417
|
+
placeholder="触发时发送给 AI 的指令..."
|
|
418
|
+
rows={4}
|
|
419
|
+
value={newCron.prompt}
|
|
420
|
+
onChange={(e) => setNewCron((p) => ({ ...p, prompt: e.target.value }))}
|
|
421
|
+
/>
|
|
422
|
+
</div>
|
|
423
|
+
<Separator />
|
|
424
|
+
<div>
|
|
425
|
+
<label className="text-sm font-medium mb-1.5 block">执行上下文(可选)</label>
|
|
426
|
+
<p className="text-xs text-muted-foreground mb-3">
|
|
427
|
+
指定任务执行时的身份信息,如不填则以 system 身份执行
|
|
428
|
+
</p>
|
|
429
|
+
<div className="grid grid-cols-2 gap-3">
|
|
430
|
+
<div>
|
|
431
|
+
<label className="text-xs text-muted-foreground mb-1 block">适配器</label>
|
|
432
|
+
<select
|
|
433
|
+
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
434
|
+
value={newCron.context.platform || ''}
|
|
435
|
+
onChange={(e) => {
|
|
436
|
+
const platform = e.target.value
|
|
437
|
+
setNewCron((p) => ({ ...p, context: { ...p.context, platform, botId: '' } }))
|
|
438
|
+
}}
|
|
439
|
+
>
|
|
440
|
+
<option value="">不指定</option>
|
|
441
|
+
{[...new Set(bots.map((b) => b.adapter))].map((adapter) => (
|
|
442
|
+
<option key={adapter} value={adapter}>{adapter}</option>
|
|
443
|
+
))}
|
|
444
|
+
</select>
|
|
445
|
+
</div>
|
|
446
|
+
<div>
|
|
447
|
+
<label className="text-xs text-muted-foreground mb-1 block">Bot</label>
|
|
448
|
+
<select
|
|
449
|
+
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
450
|
+
value={newCron.context.botId || ''}
|
|
451
|
+
disabled={!newCron.context.platform}
|
|
452
|
+
onChange={(e) => setNewCron((p) => ({ ...p, context: { ...p.context, botId: e.target.value } }))}
|
|
453
|
+
>
|
|
454
|
+
<option value="">不指定</option>
|
|
455
|
+
{bots
|
|
456
|
+
.filter((b) => b.adapter === newCron.context.platform)
|
|
457
|
+
.map((b) => (
|
|
458
|
+
<option key={b.name} value={b.name}>{b.name}</option>
|
|
459
|
+
))}
|
|
460
|
+
</select>
|
|
461
|
+
</div>
|
|
462
|
+
<div>
|
|
463
|
+
<label className="text-xs text-muted-foreground mb-1 block">场景类型</label>
|
|
464
|
+
<select
|
|
465
|
+
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
466
|
+
value={newCron.context.scope || ''}
|
|
467
|
+
onChange={(e) => setNewCron((p) => ({ ...p, context: { ...p.context, scope: e.target.value } }))}
|
|
468
|
+
>
|
|
469
|
+
<option value="">不指定</option>
|
|
470
|
+
<option value="private">私聊 (private)</option>
|
|
471
|
+
<option value="group">群聊 (group)</option>
|
|
472
|
+
<option value="channel">频道 (channel)</option>
|
|
473
|
+
</select>
|
|
474
|
+
</div>
|
|
475
|
+
<div>
|
|
476
|
+
<label className="text-xs text-muted-foreground mb-1 block">发送者 ID</label>
|
|
477
|
+
<Input
|
|
478
|
+
placeholder="用户 ID"
|
|
479
|
+
value={newCron.context.senderId || ''}
|
|
480
|
+
onChange={(e) => setNewCron((p) => ({ ...p, context: { ...p.context, senderId: e.target.value } }))}
|
|
481
|
+
/>
|
|
482
|
+
</div>
|
|
483
|
+
<div>
|
|
484
|
+
<label className="text-xs text-muted-foreground mb-1 block">场景 ID</label>
|
|
485
|
+
<Input
|
|
486
|
+
placeholder="群号/频道ID"
|
|
487
|
+
value={newCron.context.sceneId || ''}
|
|
488
|
+
onChange={(e) => setNewCron((p) => ({ ...p, context: { ...p.context, sceneId: e.target.value } }))}
|
|
489
|
+
/>
|
|
490
|
+
</div>
|
|
491
|
+
</div>
|
|
492
|
+
</div>
|
|
493
|
+
</div>
|
|
494
|
+
<DialogFooter>
|
|
495
|
+
<DialogClose asChild>
|
|
496
|
+
<Button variant="outline">取消</Button>
|
|
497
|
+
</DialogClose>
|
|
498
|
+
<Button
|
|
499
|
+
onClick={handleAdd}
|
|
500
|
+
disabled={submitting || !newCron.cronExpression || !newCron.prompt}
|
|
501
|
+
>
|
|
502
|
+
{submitting ? '创建中...' : '创建'}
|
|
503
|
+
</Button>
|
|
504
|
+
</DialogFooter>
|
|
505
|
+
</DialogContent>
|
|
506
|
+
</Dialog>
|
|
507
|
+
|
|
508
|
+
{/* Delete Confirm Dialog */}
|
|
509
|
+
<Dialog open={!!deleteTarget} onOpenChange={(open) => !open && setDeleteTarget(null)}>
|
|
510
|
+
<DialogContent>
|
|
511
|
+
<DialogHeader>
|
|
512
|
+
<DialogTitle>确认删除</DialogTitle>
|
|
513
|
+
<DialogDescription>
|
|
514
|
+
确定要删除任务「{deleteTarget?.label || deleteTarget?.id}」吗?此操作不可撤销。
|
|
515
|
+
</DialogDescription>
|
|
516
|
+
</DialogHeader>
|
|
517
|
+
<DialogFooter>
|
|
518
|
+
<DialogClose asChild>
|
|
519
|
+
<Button variant="outline">取消</Button>
|
|
520
|
+
</DialogClose>
|
|
521
|
+
<Button variant="destructive" onClick={handleDelete} disabled={submitting}>
|
|
522
|
+
{submitting ? '删除中...' : '删除'}
|
|
523
|
+
</Button>
|
|
524
|
+
</DialogFooter>
|
|
525
|
+
</DialogContent>
|
|
526
|
+
</Dialog>
|
|
527
|
+
</div>
|
|
528
|
+
)
|
|
529
|
+
}
|