|
|
@@ -7,7 +7,7 @@
|
|
|
* • 卸载 pack(DELETE)
|
|
|
* • 一键"从此包创建智能体"(POST /{id}/create-agent → 跳转 /agents)
|
|
|
*/
|
|
|
-import { useState } from 'react'
|
|
|
+import { useRef, useState } from 'react'
|
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
|
import { useNavigate } from 'react-router-dom'
|
|
|
import {
|
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
FolderOpen,
|
|
|
RefreshCw,
|
|
|
ShieldCheck,
|
|
|
+ Upload,
|
|
|
WifiOff,
|
|
|
Terminal,
|
|
|
} from 'lucide-react'
|
|
|
@@ -77,11 +78,17 @@ function InstallModal({
|
|
|
onClose: () => void
|
|
|
onInstalled: (pack: AgentPack) => void
|
|
|
}) {
|
|
|
- const [zipPath, setZipPath] = useState('')
|
|
|
+ const [file, setFile] = useState<File | null>(null)
|
|
|
const [allowShell, setAllowShell] = useState(false)
|
|
|
+ const [showPathFallback, setShowPathFallback] = useState(false)
|
|
|
+ const [zipPath, setZipPath] = useState('')
|
|
|
+ const fileInputRef = useRef<HTMLInputElement>(null)
|
|
|
|
|
|
const mut = useMutation({
|
|
|
- mutationFn: () => agentpacksApi.install(zipPath.trim(), allowShell),
|
|
|
+ mutationFn: () => {
|
|
|
+ if (file) return agentpacksApi.installUpload(file, allowShell)
|
|
|
+ return agentpacksApi.install(zipPath.trim(), allowShell)
|
|
|
+ },
|
|
|
onSuccess: (res) => {
|
|
|
toast.success(`已安装:${res.installed.name} v${res.installed.version}`)
|
|
|
onInstalled(res.installed)
|
|
|
@@ -89,37 +96,96 @@ function InstallModal({
|
|
|
onError: (e: any) => toast.error(e.message ?? '安装失败'),
|
|
|
})
|
|
|
|
|
|
+ const canInstall = file !== null || zipPath.trim().length > 0
|
|
|
+
|
|
|
return (
|
|
|
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
|
|
|
<div className="bg-white rounded-xl shadow-xl w-full max-w-md p-6">
|
|
|
<h3 className="text-base font-semibold mb-1">安装 AgentPack</h3>
|
|
|
<p className="text-xs text-gray-500 mb-4">
|
|
|
- 填写本机 .zip 路径(仅支持 127.0.0.1 回环地址请求)
|
|
|
+ 选择本机 .zip 文件即可安装(仅限本机访问)
|
|
|
</p>
|
|
|
- <div className="space-y-4">
|
|
|
- <div>
|
|
|
- <label className="block text-xs font-medium text-gray-700 mb-1">
|
|
|
- ZIP 文件路径 *
|
|
|
- </label>
|
|
|
+
|
|
|
+ {/* File picker — primary */}
|
|
|
+ <div
|
|
|
+ onClick={() => fileInputRef.current?.click()}
|
|
|
+ className={`
|
|
|
+ border-2 border-dashed rounded-xl px-4 py-8 text-center cursor-pointer transition-colors
|
|
|
+ ${file
|
|
|
+ ? 'border-indigo-400 bg-indigo-50'
|
|
|
+ : 'border-gray-200 hover:border-indigo-300 hover:bg-gray-50'
|
|
|
+ }
|
|
|
+ `}
|
|
|
+ >
|
|
|
+ <input
|
|
|
+ ref={fileInputRef}
|
|
|
+ type="file"
|
|
|
+ accept=".zip"
|
|
|
+ className="hidden"
|
|
|
+ onChange={(e) => {
|
|
|
+ const f = e.target.files?.[0] ?? null
|
|
|
+ setFile(f)
|
|
|
+ if (f) setZipPath('') // 选了文件就清空路径输入
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ {file ? (
|
|
|
+ <div className="space-y-1">
|
|
|
+ <div className="flex items-center justify-center gap-2 text-indigo-600">
|
|
|
+ <Package size={20} />
|
|
|
+ <span className="text-sm font-medium truncate max-w-[260px]">{file.name}</span>
|
|
|
+ </div>
|
|
|
+ <p className="text-xs text-gray-400">{(file.size / 1024).toFixed(1)} KB</p>
|
|
|
+ <button
|
|
|
+ onClick={(e) => { e.stopPropagation(); setFile(null); if (fileInputRef.current) fileInputRef.current.value = '' }}
|
|
|
+ className="text-xs text-red-400 hover:text-red-600 mt-1"
|
|
|
+ >
|
|
|
+ 重新选择
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ ) : (
|
|
|
+ <div className="space-y-1 text-gray-400">
|
|
|
+ <FolderOpen size={28} className="mx-auto" />
|
|
|
+ <p className="text-sm font-medium text-gray-600">点击选择 .zip 文件</p>
|
|
|
+ <p className="text-xs">或将文件拖到此处</p>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* Drag-and-drop support */}
|
|
|
+ {/* (handled by the div above via onClick; real drag support below) */}
|
|
|
+
|
|
|
+ {/* Advanced: path input fallback */}
|
|
|
+ <button
|
|
|
+ onClick={() => setShowPathFallback(v => !v)}
|
|
|
+ className="mt-3 text-xs text-gray-400 hover:text-gray-600 underline underline-offset-2 block"
|
|
|
+ >
|
|
|
+ {showPathFallback ? '▲ 隐藏路径输入' : '▼ 高级:直接填写文件路径'}
|
|
|
+ </button>
|
|
|
+
|
|
|
+ {showPathFallback && (
|
|
|
+ <div className="mt-2">
|
|
|
<input
|
|
|
value={zipPath}
|
|
|
- onChange={(e) => setZipPath(e.target.value)}
|
|
|
+ onChange={(e) => { setZipPath(e.target.value); setFile(null) }}
|
|
|
placeholder="/Users/me/packs/research.reviewer-0.1.0.zip"
|
|
|
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm font-mono focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none"
|
|
|
/>
|
|
|
</div>
|
|
|
- <label className="flex items-center gap-2 cursor-pointer select-none">
|
|
|
- <input
|
|
|
- type="checkbox"
|
|
|
- checked={allowShell}
|
|
|
- onChange={(e) => setAllowShell(e.target.checked)}
|
|
|
- className="rounded text-indigo-600"
|
|
|
- />
|
|
|
- <span className="text-xs text-gray-600">
|
|
|
- 允许 shell 权限(危险:仅限受信任来源)
|
|
|
- </span>
|
|
|
- </label>
|
|
|
- </div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {/* Shell permission */}
|
|
|
+ <label className="flex items-center gap-2 cursor-pointer select-none mt-4">
|
|
|
+ <input
|
|
|
+ type="checkbox"
|
|
|
+ checked={allowShell}
|
|
|
+ onChange={(e) => setAllowShell(e.target.checked)}
|
|
|
+ className="rounded text-indigo-600"
|
|
|
+ />
|
|
|
+ <span className="text-xs text-gray-600">
|
|
|
+ 允许 shell 权限(危险:仅限受信任来源)
|
|
|
+ </span>
|
|
|
+ </label>
|
|
|
+
|
|
|
<div className="flex justify-end gap-2 mt-5">
|
|
|
<button
|
|
|
onClick={onClose}
|
|
|
@@ -129,7 +195,7 @@ function InstallModal({
|
|
|
</button>
|
|
|
<button
|
|
|
onClick={() => mut.mutate()}
|
|
|
- disabled={!zipPath.trim() || mut.isPending}
|
|
|
+ disabled={!canInstall || mut.isPending}
|
|
|
className="px-4 py-2 text-sm bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50 flex items-center gap-2"
|
|
|
>
|
|
|
{mut.isPending && <RefreshCw size={14} className="animate-spin" />}
|
|
|
@@ -331,7 +397,7 @@ export default function AgentPacks() {
|
|
|
</Button>
|
|
|
<Button
|
|
|
size="sm"
|
|
|
- icon={<Plus size={14} />}
|
|
|
+ icon={<Upload size={14} />}
|
|
|
onClick={() => setShowInstall(true)}
|
|
|
>
|
|
|
安装 Pack
|