OhMyDitzzy
commited on
Commit
·
c0ee8b0
1
Parent(s):
83f446a
test
Browse files- package.json +3 -0
- src/client/hooks/usePlugin.ts +13 -1
- src/components/FileUpload.tsx +284 -0
- src/components/PluginCard.tsx +226 -110
- src/server/index.ts +2 -1
- src/server/plugins/ai/ai_image_editor.js +264 -0
- src/server/plugins/ai/ai_image_utils.js +359 -0
- src/server/types/plugin.ts +9 -1
package.json
CHANGED
|
@@ -41,10 +41,13 @@
|
|
| 41 |
"cheerio": "^1.1.2",
|
| 42 |
"class-variance-authority": "^0.7.1",
|
| 43 |
"clsx": "^2.1.1",
|
|
|
|
| 44 |
"express": "^5.2.1",
|
|
|
|
| 45 |
"form-data": "^4.0.5",
|
| 46 |
"framer-motion": "^12.26.2",
|
| 47 |
"lucide-react": "^0.562.0",
|
|
|
|
| 48 |
"prismjs": "^1.30.0",
|
| 49 |
"react": "^19.2.3",
|
| 50 |
"react-dom": "^19.2.3",
|
|
|
|
| 41 |
"cheerio": "^1.1.2",
|
| 42 |
"class-variance-authority": "^0.7.1",
|
| 43 |
"clsx": "^2.1.1",
|
| 44 |
+
"crypto-js": "^4.2.0",
|
| 45 |
"express": "^5.2.1",
|
| 46 |
+
"file-type": "^21.3.0",
|
| 47 |
"form-data": "^4.0.5",
|
| 48 |
"framer-motion": "^12.26.2",
|
| 49 |
"lucide-react": "^0.562.0",
|
| 50 |
+
"multer": "^2.0.2",
|
| 51 |
"prismjs": "^1.30.0",
|
| 52 |
"react": "^19.2.3",
|
| 53 |
"react-dom": "^19.2.3",
|
src/client/hooks/usePlugin.ts
CHANGED
|
@@ -1,14 +1,22 @@
|
|
| 1 |
import { useState, useEffect } from "react";
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
export interface PluginParameter {
|
| 4 |
name: string;
|
| 5 |
-
type: "string" | "number" | "boolean" | "array" | "object";
|
| 6 |
required: boolean;
|
| 7 |
description: string;
|
| 8 |
example?: any;
|
| 9 |
default?: any;
|
| 10 |
enum?: any[];
|
| 11 |
pattern?: string;
|
|
|
|
|
|
|
| 12 |
}
|
| 13 |
|
| 14 |
export interface PluginResponse {
|
|
@@ -37,6 +45,10 @@ export interface PluginMetadata {
|
|
| 37 |
responses?: {
|
| 38 |
[statusCode: number]: PluginResponse;
|
| 39 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
}
|
| 41 |
|
| 42 |
export interface ApiStats {
|
|
|
|
| 1 |
import { useState, useEffect } from "react";
|
| 2 |
|
| 3 |
+
export interface FileConstraints {
|
| 4 |
+
maxSize?: number;
|
| 5 |
+
acceptedTypes?: string[];
|
| 6 |
+
acceptedExtensions?: string[];
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
export interface PluginParameter {
|
| 10 |
name: string;
|
| 11 |
+
type: "string" | "number" | "boolean" | "array" | "object" | "file";
|
| 12 |
required: boolean;
|
| 13 |
description: string;
|
| 14 |
example?: any;
|
| 15 |
default?: any;
|
| 16 |
enum?: any[];
|
| 17 |
pattern?: string;
|
| 18 |
+
fileConstraints?: FileConstraints;
|
| 19 |
+
acceptUrl?: boolean;
|
| 20 |
}
|
| 21 |
|
| 22 |
export interface PluginResponse {
|
|
|
|
| 45 |
responses?: {
|
| 46 |
[statusCode: number]: PluginResponse;
|
| 47 |
};
|
| 48 |
+
disabled?: boolean;
|
| 49 |
+
deprecated?: boolean;
|
| 50 |
+
disabledReason?: string;
|
| 51 |
+
deprecatedReason?: string;
|
| 52 |
}
|
| 53 |
|
| 54 |
export interface ApiStats {
|
src/components/FileUpload.tsx
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useRef, useCallback } from "react";
|
| 2 |
+
import { Upload, X, File, AlertCircle, Link as LinkIcon } from "lucide-react";
|
| 3 |
+
import { Input } from "@/components/ui/input";
|
| 4 |
+
import { Button } from "@/components/ui/button";
|
| 5 |
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
| 6 |
+
import { FileConstraints } from "@/server/types/plugin";
|
| 7 |
+
|
| 8 |
+
interface FileUploadProps {
|
| 9 |
+
name: string;
|
| 10 |
+
description: string;
|
| 11 |
+
fileConstraints?: FileConstraints;
|
| 12 |
+
acceptUrl?: boolean;
|
| 13 |
+
onFileChange: (file: File | null) => void;
|
| 14 |
+
onUrlChange?: (url: string) => void;
|
| 15 |
+
disabled?: boolean;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
export function FileUpload({
|
| 19 |
+
name,
|
| 20 |
+
description,
|
| 21 |
+
fileConstraints,
|
| 22 |
+
acceptUrl = false,
|
| 23 |
+
onFileChange,
|
| 24 |
+
onUrlChange,
|
| 25 |
+
disabled = false,
|
| 26 |
+
}: FileUploadProps) {
|
| 27 |
+
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
| 28 |
+
const [fileUrl, setFileUrl] = useState<string>("");
|
| 29 |
+
const [error, setError] = useState<string | null>(null);
|
| 30 |
+
const [dragActive, setDragActive] = useState(false);
|
| 31 |
+
const [mode, setMode] = useState<"upload" | "url">("upload");
|
| 32 |
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
| 33 |
+
|
| 34 |
+
const formatFileSize = (bytes: number): string => {
|
| 35 |
+
if (bytes === 0) return "0 Bytes";
|
| 36 |
+
const k = 1024;
|
| 37 |
+
const sizes = ["Bytes", "KB", "MB", "GB"];
|
| 38 |
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
| 39 |
+
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + " " + sizes[i];
|
| 40 |
+
};
|
| 41 |
+
|
| 42 |
+
const validateFile = (file: File): string | null => {
|
| 43 |
+
if (!fileConstraints) return null;
|
| 44 |
+
|
| 45 |
+
if (fileConstraints.maxSize && file.size > fileConstraints.maxSize) {
|
| 46 |
+
return `File size (${formatFileSize(file.size)}) exceeds maximum allowed size (${formatFileSize(fileConstraints.maxSize)})`;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
if (fileConstraints.acceptedTypes && fileConstraints.acceptedTypes.length > 0) {
|
| 50 |
+
if (!fileConstraints.acceptedTypes.includes(file.type)) {
|
| 51 |
+
return `File type ${file.type} is not accepted. Allowed types: ${fileConstraints.acceptedTypes.join(", ")}`;
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
if (fileConstraints.acceptedExtensions && fileConstraints.acceptedExtensions.length > 0) {
|
| 56 |
+
const extension = "." + file.name.split(".").pop()?.toLowerCase();
|
| 57 |
+
if (!fileConstraints.acceptedExtensions.includes(extension)) {
|
| 58 |
+
return `File extension ${extension} is not accepted. Allowed extensions: ${fileConstraints.acceptedExtensions.join(", ")}`;
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
return null;
|
| 63 |
+
};
|
| 64 |
+
|
| 65 |
+
const handleFileSelect = useCallback((file: File) => {
|
| 66 |
+
const validationError = validateFile(file);
|
| 67 |
+
|
| 68 |
+
if (validationError) {
|
| 69 |
+
setError(validationError);
|
| 70 |
+
setSelectedFile(null);
|
| 71 |
+
onFileChange(null);
|
| 72 |
+
return;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
setError(null);
|
| 76 |
+
setSelectedFile(file);
|
| 77 |
+
onFileChange(file);
|
| 78 |
+
}, [fileConstraints, onFileChange]);
|
| 79 |
+
|
| 80 |
+
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
| 81 |
+
const file = e.target.files?.[0];
|
| 82 |
+
if (file) {
|
| 83 |
+
handleFileSelect(file);
|
| 84 |
+
}
|
| 85 |
+
};
|
| 86 |
+
|
| 87 |
+
const handleDrag = (e: React.DragEvent) => {
|
| 88 |
+
e.preventDefault();
|
| 89 |
+
e.stopPropagation();
|
| 90 |
+
if (e.type === "dragenter" || e.type === "dragover") {
|
| 91 |
+
setDragActive(true);
|
| 92 |
+
} else if (e.type === "dragleave") {
|
| 93 |
+
setDragActive(false);
|
| 94 |
+
}
|
| 95 |
+
};
|
| 96 |
+
|
| 97 |
+
const handleDrop = (e: React.DragEvent) => {
|
| 98 |
+
e.preventDefault();
|
| 99 |
+
e.stopPropagation();
|
| 100 |
+
setDragActive(false);
|
| 101 |
+
|
| 102 |
+
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
|
| 103 |
+
handleFileSelect(e.dataTransfer.files[0]);
|
| 104 |
+
}
|
| 105 |
+
};
|
| 106 |
+
|
| 107 |
+
const handleRemoveFile = () => {
|
| 108 |
+
setSelectedFile(null);
|
| 109 |
+
setError(null);
|
| 110 |
+
onFileChange(null);
|
| 111 |
+
if (fileInputRef.current) {
|
| 112 |
+
fileInputRef.current.value = "";
|
| 113 |
+
}
|
| 114 |
+
};
|
| 115 |
+
|
| 116 |
+
const handleUrlChange = (url: string) => {
|
| 117 |
+
setFileUrl(url);
|
| 118 |
+
if (onUrlChange) {
|
| 119 |
+
onUrlChange(url);
|
| 120 |
+
}
|
| 121 |
+
};
|
| 122 |
+
|
| 123 |
+
const getAcceptAttribute = (): string | undefined => {
|
| 124 |
+
if (!fileConstraints) return undefined;
|
| 125 |
+
|
| 126 |
+
const types = [];
|
| 127 |
+
if (fileConstraints.acceptedTypes) {
|
| 128 |
+
types.push(...fileConstraints.acceptedTypes);
|
| 129 |
+
}
|
| 130 |
+
if (fileConstraints.acceptedExtensions) {
|
| 131 |
+
types.push(...fileConstraints.acceptedExtensions);
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
return types.length > 0 ? types.join(",") : undefined;
|
| 135 |
+
};
|
| 136 |
+
|
| 137 |
+
const renderContent = () => {
|
| 138 |
+
if (!acceptUrl) {
|
| 139 |
+
return (
|
| 140 |
+
<div className="space-y-3">
|
| 141 |
+
{renderUploadArea()}
|
| 142 |
+
{renderConstraints()}
|
| 143 |
+
</div>
|
| 144 |
+
);
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
return (
|
| 148 |
+
<Tabs value={mode} onValueChange={(v) => setMode(v as "upload" | "url")} className="w-full">
|
| 149 |
+
<TabsList className="grid w-full grid-cols-2 bg-black/30">
|
| 150 |
+
<TabsTrigger value="upload" className="data-[state=active]:bg-purple-500/20">
|
| 151 |
+
<Upload className="w-4 h-4 mr-2" />
|
| 152 |
+
Upload File
|
| 153 |
+
</TabsTrigger>
|
| 154 |
+
<TabsTrigger value="url" className="data-[state=active]:bg-purple-500/20">
|
| 155 |
+
<LinkIcon className="w-4 h-4 mr-2" />
|
| 156 |
+
Use URL
|
| 157 |
+
</TabsTrigger>
|
| 158 |
+
</TabsList>
|
| 159 |
+
|
| 160 |
+
<TabsContent value="upload" className="mt-3 space-y-3">
|
| 161 |
+
{renderUploadArea()}
|
| 162 |
+
{renderConstraints()}
|
| 163 |
+
</TabsContent>
|
| 164 |
+
|
| 165 |
+
<TabsContent value="url" className="mt-3 space-y-3">
|
| 166 |
+
<Input
|
| 167 |
+
type="url"
|
| 168 |
+
placeholder="https://example.com/file.jpg"
|
| 169 |
+
value={fileUrl}
|
| 170 |
+
onChange={(e) => handleUrlChange(e.target.value)}
|
| 171 |
+
disabled={disabled}
|
| 172 |
+
className="bg-black/50 border-white/10 text-white focus:border-purple-500"
|
| 173 |
+
/>
|
| 174 |
+
<p className="text-xs text-gray-500">
|
| 175 |
+
Enter the URL of the file you want to process
|
| 176 |
+
</p>
|
| 177 |
+
{renderConstraints()}
|
| 178 |
+
</TabsContent>
|
| 179 |
+
</Tabs>
|
| 180 |
+
);
|
| 181 |
+
};
|
| 182 |
+
|
| 183 |
+
const renderUploadArea = () => (
|
| 184 |
+
<>
|
| 185 |
+
<div
|
| 186 |
+
className={`relative border-2 border-dashed rounded-lg p-8 transition-all ${
|
| 187 |
+
dragActive
|
| 188 |
+
? "border-purple-500 bg-purple-500/10"
|
| 189 |
+
: "border-white/20 bg-black/30"
|
| 190 |
+
} ${disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer hover:border-purple-500/50"}`}
|
| 191 |
+
onDragEnter={handleDrag}
|
| 192 |
+
onDragLeave={handleDrag}
|
| 193 |
+
onDragOver={handleDrag}
|
| 194 |
+
onDrop={handleDrop}
|
| 195 |
+
onClick={() => !disabled && fileInputRef.current?.click()}
|
| 196 |
+
>
|
| 197 |
+
<input
|
| 198 |
+
ref={fileInputRef}
|
| 199 |
+
type="file"
|
| 200 |
+
onChange={handleFileChange}
|
| 201 |
+
accept={getAcceptAttribute()}
|
| 202 |
+
disabled={disabled}
|
| 203 |
+
className="hidden"
|
| 204 |
+
/>
|
| 205 |
+
|
| 206 |
+
{selectedFile ? (
|
| 207 |
+
<div className="flex items-center justify-between">
|
| 208 |
+
<div className="flex items-center gap-3">
|
| 209 |
+
<div className="p-2 bg-purple-500/20 rounded">
|
| 210 |
+
<File className="w-6 h-6 text-purple-400" />
|
| 211 |
+
</div>
|
| 212 |
+
<div>
|
| 213 |
+
<p className="text-sm font-medium text-white">{selectedFile.name}</p>
|
| 214 |
+
<p className="text-xs text-gray-400">{formatFileSize(selectedFile.size)}</p>
|
| 215 |
+
</div>
|
| 216 |
+
</div>
|
| 217 |
+
<Button
|
| 218 |
+
type="button"
|
| 219 |
+
variant="ghost"
|
| 220 |
+
size="sm"
|
| 221 |
+
onClick={(e) => {
|
| 222 |
+
e.stopPropagation();
|
| 223 |
+
handleRemoveFile();
|
| 224 |
+
}}
|
| 225 |
+
disabled={disabled}
|
| 226 |
+
className="text-red-400 hover:text-red-300 hover:bg-red-500/10"
|
| 227 |
+
>
|
| 228 |
+
<X className="w-4 h-4" />
|
| 229 |
+
</Button>
|
| 230 |
+
</div>
|
| 231 |
+
) : (
|
| 232 |
+
<div className="text-center">
|
| 233 |
+
<Upload className="w-12 h-12 text-gray-400 mx-auto mb-3" />
|
| 234 |
+
<p className="text-sm text-gray-300 mb-1">
|
| 235 |
+
{dragActive ? "Drop file here" : "Click to upload or drag and drop"}
|
| 236 |
+
</p>
|
| 237 |
+
<p className="text-xs text-gray-500">{description}</p>
|
| 238 |
+
</div>
|
| 239 |
+
)}
|
| 240 |
+
</div>
|
| 241 |
+
|
| 242 |
+
{error && (
|
| 243 |
+
<div className="flex items-start gap-2 p-3 bg-red-500/10 border border-red-500/30 rounded-lg">
|
| 244 |
+
<AlertCircle className="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5" />
|
| 245 |
+
<p className="text-sm text-red-300">{error}</p>
|
| 246 |
+
</div>
|
| 247 |
+
)}
|
| 248 |
+
</>
|
| 249 |
+
);
|
| 250 |
+
|
| 251 |
+
const renderConstraints = () => {
|
| 252 |
+
if (!fileConstraints) return null;
|
| 253 |
+
|
| 254 |
+
return (
|
| 255 |
+
<div className="space-y-2">
|
| 256 |
+
{fileConstraints.maxSize && (
|
| 257 |
+
<p className="text-xs text-gray-500">
|
| 258 |
+
• Max file size: {formatFileSize(fileConstraints.maxSize)}
|
| 259 |
+
</p>
|
| 260 |
+
)}
|
| 261 |
+
{fileConstraints.acceptedTypes && fileConstraints.acceptedTypes.length > 0 && (
|
| 262 |
+
<p className="text-xs text-gray-500">
|
| 263 |
+
• Accepted types: {fileConstraints.acceptedTypes.join(", ")}
|
| 264 |
+
</p>
|
| 265 |
+
)}
|
| 266 |
+
{fileConstraints.acceptedExtensions && fileConstraints.acceptedExtensions.length > 0 && (
|
| 267 |
+
<p className="text-xs text-gray-500">
|
| 268 |
+
• Accepted extensions: {fileConstraints.acceptedExtensions.join(", ")}
|
| 269 |
+
</p>
|
| 270 |
+
)}
|
| 271 |
+
</div>
|
| 272 |
+
);
|
| 273 |
+
};
|
| 274 |
+
|
| 275 |
+
return (
|
| 276 |
+
<div className="space-y-2">
|
| 277 |
+
<label className="block text-sm text-gray-300">
|
| 278 |
+
{name}
|
| 279 |
+
<span className="text-xs text-gray-500 ml-2">(file)</span>
|
| 280 |
+
</label>
|
| 281 |
+
{renderContent()}
|
| 282 |
+
</div>
|
| 283 |
+
);
|
| 284 |
+
}
|
src/components/PluginCard.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
| 8 |
import { PluginMetadata } from "@/client/hooks/usePlugin";
|
| 9 |
import { Play, ChevronDown, ChevronUp, Copy, Check, AlertTriangle, XCircle } from "lucide-react";
|
| 10 |
import { CodeBlock } from "@/components/CodeBlock";
|
|
|
|
| 11 |
import { getApiUrl } from "@/lib/api-url";
|
| 12 |
|
| 13 |
interface PluginCardProps {
|
|
@@ -24,6 +25,8 @@ const methodColors: Record<string, string> = {
|
|
| 24 |
|
| 25 |
export function PluginCard({ plugin }: PluginCardProps) {
|
| 26 |
const [paramValues, setParamValues] = useState<Record<string, string>>({});
|
|
|
|
|
|
|
| 27 |
const [response, setResponse] = useState<any>(null);
|
| 28 |
const [responseHeaders, setResponseHeaders] = useState<Record<string, string>>({});
|
| 29 |
const [requestUrl, setRequestUrl] = useState<string>("");
|
|
@@ -39,6 +42,14 @@ export function PluginCard({ plugin }: PluginCardProps) {
|
|
| 39 |
setParamValues((prev) => ({ ...prev, [paramName]: value }));
|
| 40 |
};
|
| 41 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
const handleExecute = async () => {
|
| 43 |
if (isDisabled) {
|
| 44 |
setResponse({
|
|
@@ -59,12 +70,23 @@ export function PluginCard({ plugin }: PluginCardProps) {
|
|
| 59 |
let url = "/api" + plugin.endpoint;
|
| 60 |
let fullUrl = getApiUrl(plugin.endpoint);
|
| 61 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
if (plugin.method === "GET" && plugin.parameters?.query) {
|
| 63 |
const queryParams = new URLSearchParams();
|
| 64 |
plugin.parameters.query.forEach((param) => {
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
}
|
| 69 |
});
|
| 70 |
|
|
@@ -80,7 +102,34 @@ export function PluginCard({ plugin }: PluginCardProps) {
|
|
| 80 |
method: plugin.method,
|
| 81 |
};
|
| 82 |
|
| 83 |
-
if (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
const bodyData: Record<string, any> = {};
|
| 85 |
plugin.parameters.body.forEach((param) => {
|
| 86 |
const value = paramValues[param.name];
|
|
@@ -94,13 +143,53 @@ export function PluginCard({ plugin }: PluginCardProps) {
|
|
| 94 |
};
|
| 95 |
}
|
| 96 |
|
| 97 |
-
|
| 98 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
|
| 100 |
const headers: Record<string, string> = {};
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
|
| 105 |
setResponseHeaders(headers);
|
| 106 |
setResponse({
|
|
@@ -134,6 +223,20 @@ export function PluginCard({ plugin }: PluginCardProps) {
|
|
| 134 |
};
|
| 135 |
|
| 136 |
const renderParameterInput = (param: any) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
if (param.enum && Array.isArray(param.enum) && param.enum.length > 0) {
|
| 138 |
return (
|
| 139 |
<Select
|
|
@@ -157,18 +260,18 @@ export function PluginCard({ plugin }: PluginCardProps) {
|
|
| 157 |
</SelectContent>
|
| 158 |
</Select>
|
| 159 |
);
|
| 160 |
-
} else {
|
| 161 |
-
return (
|
| 162 |
-
<Input
|
| 163 |
-
type="text"
|
| 164 |
-
placeholder={param.example?.toString() || param.description}
|
| 165 |
-
value={paramValues[param.name] || ""}
|
| 166 |
-
onChange={(e) => handleParamChange(param.name, e.target.value)}
|
| 167 |
-
disabled={isDisabled}
|
| 168 |
-
className="bg-black/50 border-white/10 text-white focus:border-purple-500 disabled:opacity-50"
|
| 169 |
-
/>
|
| 170 |
-
);
|
| 171 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
};
|
| 173 |
|
| 174 |
const hasQueryParams = plugin.parameters?.query && plugin.parameters.query.length > 0;
|
|
@@ -181,7 +284,12 @@ export function PluginCard({ plugin }: PluginCardProps) {
|
|
| 181 |
|
| 182 |
if (hasQueryParams) {
|
| 183 |
const exampleParams = plugin.parameters!.query!
|
| 184 |
-
.map((p) =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
.join('&');
|
| 186 |
curl += `?${exampleParams}`;
|
| 187 |
}
|
|
@@ -189,31 +297,80 @@ export function PluginCard({ plugin }: PluginCardProps) {
|
|
| 189 |
curl += '"';
|
| 190 |
|
| 191 |
if (hasBodyParams) {
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
}
|
| 200 |
|
| 201 |
return curl;
|
| 202 |
};
|
| 203 |
|
| 204 |
const generateNodeExample = () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
let code = `const response = await fetch("${getApiUrl(plugin.endpoint)}`;
|
| 206 |
|
| 207 |
if (hasQueryParams) {
|
| 208 |
const exampleParams = plugin.parameters!.query!
|
| 209 |
-
.map((p) =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
.join('&');
|
| 211 |
code += `?${exampleParams}`;
|
| 212 |
}
|
| 213 |
|
| 214 |
code += '", {\n method: "' + plugin.method + '"';
|
| 215 |
|
| 216 |
-
if (hasBodyParams) {
|
| 217 |
code += ',\n headers: {\n "Content-Type": "application/json"\n },\n body: JSON.stringify(';
|
| 218 |
const bodyExample: Record<string, any> = {};
|
| 219 |
plugin.parameters!.body!.forEach((p) => {
|
|
@@ -229,7 +386,7 @@ export function PluginCard({ plugin }: PluginCardProps) {
|
|
| 229 |
|
| 230 |
return (
|
| 231 |
<Card className={`bg-white/[0.02] border-white/10 overflow-hidden w-full ${isDisabled ? 'opacity-60' : ''}`}>
|
| 232 |
-
{/*
|
| 233 |
<div
|
| 234 |
className="p-4 border-b border-white/10 cursor-pointer hover:bg-white/[0.02] transition-colors"
|
| 235 |
onClick={() => setIsExpanded(!isExpanded)}
|
|
@@ -240,7 +397,6 @@ export function PluginCard({ plugin }: PluginCardProps) {
|
|
| 240 |
</Badge>
|
| 241 |
<code className="text-sm text-purple-400 font-mono flex-1 min-w-0 break-all">{plugin.endpoint}</code>
|
| 242 |
|
| 243 |
-
{/* Status Badges */}
|
| 244 |
{isDisabled && (
|
| 245 |
<Badge className="bg-red-500/20 text-red-400 border-red-500/50 border flex items-center gap-1 flex-shrink-0">
|
| 246 |
<XCircle className="w-3 h-3" />
|
|
@@ -290,7 +446,6 @@ export function PluginCard({ plugin }: PluginCardProps) {
|
|
| 290 |
<h3 className="text-xl font-bold text-white mb-2">{plugin.name}</h3>
|
| 291 |
<p className="text-gray-400 text-sm leading-relaxed">{plugin.description || "No description provided"}</p>
|
| 292 |
|
| 293 |
-
{/* Disabled/Deprecated Warnings */}
|
| 294 |
{isDisabled && plugin.disabledReason && (
|
| 295 |
<div className="mt-3 p-3 bg-red-500/10 border border-red-500/30 rounded-lg flex items-start gap-2">
|
| 296 |
<XCircle className="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5" />
|
|
@@ -311,7 +466,6 @@ export function PluginCard({ plugin }: PluginCardProps) {
|
|
| 311 |
</div>
|
| 312 |
)}
|
| 313 |
|
| 314 |
-
{/* Tags */}
|
| 315 |
{plugin.tags && plugin.tags.length > 0 && (
|
| 316 |
<div className="flex flex-wrap gap-2 mt-3">
|
| 317 |
{plugin.tags.map((tag) => (
|
|
@@ -322,7 +476,6 @@ export function PluginCard({ plugin }: PluginCardProps) {
|
|
| 322 |
</div>
|
| 323 |
)}
|
| 324 |
|
| 325 |
-
{/* API URL Display */}
|
| 326 |
<div className="mt-3 flex items-start gap-2">
|
| 327 |
<span className="text-xs text-gray-500 flex-shrink-0">API URL:</span>
|
| 328 |
<code className="text-xs text-gray-300 bg-black/30 px-2 py-1 rounded break-all flex-1">
|
|
@@ -352,7 +505,6 @@ export function PluginCard({ plugin }: PluginCardProps) {
|
|
| 352 |
|
| 353 |
{/* Documentation Tab */}
|
| 354 |
<TabsContent value="documentation" className="p-6 space-y-6">
|
| 355 |
-
{/* Parameters Table */}
|
| 356 |
{hasAnyParams && (
|
| 357 |
<div>
|
| 358 |
<h4 className="text-purple-400 font-semibold mb-3">Parameters</h4>
|
|
@@ -367,30 +519,25 @@ export function PluginCard({ plugin }: PluginCardProps) {
|
|
| 367 |
</tr>
|
| 368 |
</thead>
|
| 369 |
<tbody>
|
| 370 |
-
{plugin.parameters?.path?.map((param) => (
|
| 371 |
<tr key={param.name} className="border-b border-white/5">
|
| 372 |
<td className="py-3 pr-4 text-white font-mono">{param.name}</td>
|
| 373 |
<td className="py-3 pr-4">
|
| 374 |
<span className="text-blue-400 font-mono text-xs">{param.type}</span>
|
| 375 |
-
{param.
|
| 376 |
-
<
|
| 377 |
-
|
| 378 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 379 |
)}
|
| 380 |
-
</td>
|
| 381 |
-
<td className="py-3 pr-4">
|
| 382 |
-
<span className={param.required ? "text-red-400" : "text-gray-500"}>
|
| 383 |
-
{param.required ? "Yes" : "No"}
|
| 384 |
-
</span>
|
| 385 |
-
</td>
|
| 386 |
-
<td className="py-3 text-gray-400">{param.description}</td>
|
| 387 |
-
</tr>
|
| 388 |
-
))}
|
| 389 |
-
{plugin.parameters?.query?.map((param) => (
|
| 390 |
-
<tr key={param.name} className="border-b border-white/5">
|
| 391 |
-
<td className="py-3 pr-4 text-white font-mono">{param.name}</td>
|
| 392 |
-
<td className="py-3 pr-4">
|
| 393 |
-
<span className="text-blue-400 font-mono text-xs">{param.type}</span>
|
| 394 |
{param.enum && (
|
| 395 |
<span className="ml-2 text-xs text-gray-500">
|
| 396 |
(options: {param.enum.join(', ')})
|
|
@@ -402,26 +549,14 @@ export function PluginCard({ plugin }: PluginCardProps) {
|
|
| 402 |
{param.required ? "Yes" : "No"}
|
| 403 |
</span>
|
| 404 |
</td>
|
| 405 |
-
<td className="py-3 text-gray-400">
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
<td className="py-3 pr-4 text-white font-mono">{param.name}</td>
|
| 411 |
-
<td className="py-3 pr-4">
|
| 412 |
-
<span className="text-blue-400 font-mono text-xs">{param.type}</span>
|
| 413 |
-
{param.enum && (
|
| 414 |
-
<span className="ml-2 text-xs text-gray-500">
|
| 415 |
-
(options: {param.enum.join(', ')})
|
| 416 |
</span>
|
| 417 |
)}
|
| 418 |
</td>
|
| 419 |
-
<td className="py-3 pr-4">
|
| 420 |
-
<span className={param.required ? "text-red-400" : "text-gray-500"}>
|
| 421 |
-
{param.required ? "Yes" : "No"}
|
| 422 |
-
</span>
|
| 423 |
-
</td>
|
| 424 |
-
<td className="py-3 text-gray-400">{param.description}</td>
|
| 425 |
</tr>
|
| 426 |
))}
|
| 427 |
</tbody>
|
|
@@ -430,7 +565,7 @@ export function PluginCard({ plugin }: PluginCardProps) {
|
|
| 430 |
</div>
|
| 431 |
)}
|
| 432 |
|
| 433 |
-
{/* Responses */}
|
| 434 |
{plugin.responses && Object.keys(plugin.responses).length > 0 && (
|
| 435 |
<div>
|
| 436 |
<h4 className="text-purple-400 font-semibold mb-3">Responses</h4>
|
|
@@ -487,40 +622,27 @@ export function PluginCard({ plugin }: PluginCardProps) {
|
|
| 487 |
|
| 488 |
{/* Try It Out Tab */}
|
| 489 |
<TabsContent value="try" className="p-6">
|
| 490 |
-
{/* Parameters Input */}
|
| 491 |
{hasAnyParams ? (
|
| 492 |
<div className="space-y-4 mb-4">
|
| 493 |
-
{plugin.parameters?.query?.map((param) => (
|
| 494 |
<div key={param.name}>
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
<label className="block text-sm text-gray-300 mb-2">
|
| 513 |
-
{param.name}
|
| 514 |
-
{param.required && <span className="text-red-400 ml-1">*</span>}
|
| 515 |
-
<span className="text-xs text-gray-500 ml-2">({param.type})</span>
|
| 516 |
-
{param.enum && (
|
| 517 |
-
<span className="text-xs text-purple-400 ml-2">
|
| 518 |
-
• Select from options
|
| 519 |
-
</span>
|
| 520 |
-
)}
|
| 521 |
-
</label>
|
| 522 |
-
{renderParameterInput(param)}
|
| 523 |
-
<p className="text-xs text-gray-500 mt-1">{param.description}</p>
|
| 524 |
</div>
|
| 525 |
))}
|
| 526 |
</div>
|
|
@@ -528,7 +650,6 @@ export function PluginCard({ plugin }: PluginCardProps) {
|
|
| 528 |
<p className="text-sm text-gray-400 mb-4">No parameters required</p>
|
| 529 |
)}
|
| 530 |
|
| 531 |
-
{/* Execute Button */}
|
| 532 |
<Button
|
| 533 |
onClick={handleExecute}
|
| 534 |
disabled={loading || isDisabled}
|
|
@@ -541,7 +662,6 @@ export function PluginCard({ plugin }: PluginCardProps) {
|
|
| 541 |
{/* Response Display */}
|
| 542 |
{response && (
|
| 543 |
<div className="mt-6 space-y-4">
|
| 544 |
-
{/* Deprecation Warning in Response */}
|
| 545 |
{isDeprecated && responseHeaders['x-plugin-deprecated'] && (
|
| 546 |
<div className="p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg flex items-start gap-2">
|
| 547 |
<AlertTriangle className="w-5 h-5 text-yellow-400 flex-shrink-0 mt-0.5" />
|
|
@@ -554,7 +674,6 @@ export function PluginCard({ plugin }: PluginCardProps) {
|
|
| 554 |
</div>
|
| 555 |
)}
|
| 556 |
|
| 557 |
-
{/* Request URL */}
|
| 558 |
{requestUrl && (
|
| 559 |
<div>
|
| 560 |
<div className="flex items-center justify-between mb-2">
|
|
@@ -577,7 +696,6 @@ export function PluginCard({ plugin }: PluginCardProps) {
|
|
| 577 |
</div>
|
| 578 |
)}
|
| 579 |
|
| 580 |
-
{/* Response Status */}
|
| 581 |
<div className="flex items-center justify-between">
|
| 582 |
<span className="text-sm text-gray-400">Response Status</span>
|
| 583 |
<Badge className={`${response.status >= 200 && response.status < 300
|
|
@@ -588,7 +706,6 @@ export function PluginCard({ plugin }: PluginCardProps) {
|
|
| 588 |
</Badge>
|
| 589 |
</div>
|
| 590 |
|
| 591 |
-
{/* Response Headers */}
|
| 592 |
{Object.keys(responseHeaders).length > 0 && (
|
| 593 |
<div>
|
| 594 |
<h5 className="text-sm text-gray-400 mb-2">Response Headers</h5>
|
|
@@ -603,7 +720,6 @@ export function PluginCard({ plugin }: PluginCardProps) {
|
|
| 603 |
</div>
|
| 604 |
)}
|
| 605 |
|
| 606 |
-
{/* Response Body */}
|
| 607 |
<div>
|
| 608 |
<h5 className="text-sm text-gray-400 mb-2">Response Body</h5>
|
| 609 |
<CodeBlock
|
|
|
|
| 8 |
import { PluginMetadata } from "@/client/hooks/usePlugin";
|
| 9 |
import { Play, ChevronDown, ChevronUp, Copy, Check, AlertTriangle, XCircle } from "lucide-react";
|
| 10 |
import { CodeBlock } from "@/components/CodeBlock";
|
| 11 |
+
import { FileUpload } from "@/components/FileUpload";
|
| 12 |
import { getApiUrl } from "@/lib/api-url";
|
| 13 |
|
| 14 |
interface PluginCardProps {
|
|
|
|
| 25 |
|
| 26 |
export function PluginCard({ plugin }: PluginCardProps) {
|
| 27 |
const [paramValues, setParamValues] = useState<Record<string, string>>({});
|
| 28 |
+
const [fileParams, setFileParams] = useState<Record<string, File | null>>({});
|
| 29 |
+
const [urlParams, setUrlParams] = useState<Record<string, string>>({});
|
| 30 |
const [response, setResponse] = useState<any>(null);
|
| 31 |
const [responseHeaders, setResponseHeaders] = useState<Record<string, string>>({});
|
| 32 |
const [requestUrl, setRequestUrl] = useState<string>("");
|
|
|
|
| 42 |
setParamValues((prev) => ({ ...prev, [paramName]: value }));
|
| 43 |
};
|
| 44 |
|
| 45 |
+
const handleFileChange = (paramName: string, file: File | null) => {
|
| 46 |
+
setFileParams((prev) => ({ ...prev, [paramName]: file }));
|
| 47 |
+
};
|
| 48 |
+
|
| 49 |
+
const handleUrlParamChange = (paramName: string, url: string) => {
|
| 50 |
+
setUrlParams((prev) => ({ ...prev, [paramName]: url }));
|
| 51 |
+
};
|
| 52 |
+
|
| 53 |
const handleExecute = async () => {
|
| 54 |
if (isDisabled) {
|
| 55 |
setResponse({
|
|
|
|
| 70 |
let url = "/api" + plugin.endpoint;
|
| 71 |
let fullUrl = getApiUrl(plugin.endpoint);
|
| 72 |
|
| 73 |
+
// Check if we have any file parameters
|
| 74 |
+
const hasFiles = Object.values(fileParams).some(file => file !== null);
|
| 75 |
+
const hasUrls = Object.values(urlParams).some(url => url !== "");
|
| 76 |
+
|
| 77 |
if (plugin.method === "GET" && plugin.parameters?.query) {
|
| 78 |
const queryParams = new URLSearchParams();
|
| 79 |
plugin.parameters.query.forEach((param) => {
|
| 80 |
+
if (param.type === "file" && param.acceptUrl) {
|
| 81 |
+
const urlValue = urlParams[param.name];
|
| 82 |
+
if (urlValue) {
|
| 83 |
+
queryParams.append(param.name, urlValue);
|
| 84 |
+
}
|
| 85 |
+
} else {
|
| 86 |
+
const value = paramValues[param.name];
|
| 87 |
+
if (value) {
|
| 88 |
+
queryParams.append(param.name, value);
|
| 89 |
+
}
|
| 90 |
}
|
| 91 |
});
|
| 92 |
|
|
|
|
| 102 |
method: plugin.method,
|
| 103 |
};
|
| 104 |
|
| 105 |
+
if (hasFiles || (hasUrls && plugin.method !== "GET")) {
|
| 106 |
+
const formData = new FormData();
|
| 107 |
+
|
| 108 |
+
Object.entries(fileParams).forEach(([name, file]) => {
|
| 109 |
+
if (file) {
|
| 110 |
+
formData.append(name, file);
|
| 111 |
+
}
|
| 112 |
+
});
|
| 113 |
+
|
| 114 |
+
Object.entries(urlParams).forEach(([name, url]) => {
|
| 115 |
+
if (url && !fileParams[name]) {
|
| 116 |
+
formData.append(name, url);
|
| 117 |
+
}
|
| 118 |
+
});
|
| 119 |
+
|
| 120 |
+
if (plugin.parameters?.body) {
|
| 121 |
+
plugin.parameters.body.forEach((param) => {
|
| 122 |
+
if (param.type !== "file") {
|
| 123 |
+
const value = paramValues[param.name];
|
| 124 |
+
if (value) {
|
| 125 |
+
formData.append(param.name, value);
|
| 126 |
+
}
|
| 127 |
+
}
|
| 128 |
+
});
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
fetchOptions.body = formData;
|
| 132 |
+
} else if (["POST", "PUT", "PATCH"].includes(plugin.method) && plugin.parameters?.body) {
|
| 133 |
const bodyData: Record<string, any> = {};
|
| 134 |
plugin.parameters.body.forEach((param) => {
|
| 135 |
const value = paramValues[param.name];
|
|
|
|
| 143 |
};
|
| 144 |
}
|
| 145 |
|
| 146 |
+
// Add XMLHttpRequest fallback for FormData to avoid HTTP/2 issues
|
| 147 |
+
let res;
|
| 148 |
+
let data;
|
| 149 |
+
|
| 150 |
+
if (fetchOptions.body instanceof FormData) {
|
| 151 |
+
// Use XMLHttpRequest for FormData to ensure compatibility
|
| 152 |
+
const xhr = new XMLHttpRequest();
|
| 153 |
+
|
| 154 |
+
const xhrPromise = new Promise((resolve, reject) => {
|
| 155 |
+
xhr.onload = () => {
|
| 156 |
+
try {
|
| 157 |
+
const responseData = JSON.parse(xhr.responseText);
|
| 158 |
+
resolve({
|
| 159 |
+
status: xhr.status,
|
| 160 |
+
statusText: xhr.statusText,
|
| 161 |
+
headers: new Headers(),
|
| 162 |
+
json: async () => responseData
|
| 163 |
+
});
|
| 164 |
+
} catch (e) {
|
| 165 |
+
reject(new Error('Failed to parse response'));
|
| 166 |
+
}
|
| 167 |
+
};
|
| 168 |
+
|
| 169 |
+
xhr.onerror = () => reject(new Error('Network request failed'));
|
| 170 |
+
xhr.ontimeout = () => reject(new Error('Request timeout'));
|
| 171 |
+
|
| 172 |
+
xhr.open(plugin.method, url, true);
|
| 173 |
+
xhr.timeout = 60000; // 60 second timeout
|
| 174 |
+
xhr.send(fetchOptions.body as FormData);
|
| 175 |
+
});
|
| 176 |
+
|
| 177 |
+
res = await xhrPromise as any;
|
| 178 |
+
data = await res.json();
|
| 179 |
+
} else {
|
| 180 |
+
// Use fetch for non-FormData requests
|
| 181 |
+
res = await fetch(url, fetchOptions);
|
| 182 |
+
data = await res.json();
|
| 183 |
+
}
|
| 184 |
|
| 185 |
const headers: Record<string, string> = {};
|
| 186 |
+
|
| 187 |
+
// Get headers from Response object
|
| 188 |
+
if (res.headers && typeof res.headers.forEach === 'function') {
|
| 189 |
+
res.headers.forEach((value: string, key: string) => {
|
| 190 |
+
headers[key] = value;
|
| 191 |
+
});
|
| 192 |
+
}
|
| 193 |
|
| 194 |
setResponseHeaders(headers);
|
| 195 |
setResponse({
|
|
|
|
| 223 |
};
|
| 224 |
|
| 225 |
const renderParameterInput = (param: any) => {
|
| 226 |
+
if (param.type === "file") {
|
| 227 |
+
return (
|
| 228 |
+
<FileUpload
|
| 229 |
+
name={param.name}
|
| 230 |
+
description={param.description}
|
| 231 |
+
fileConstraints={param.fileConstraints}
|
| 232 |
+
acceptUrl={param.acceptUrl}
|
| 233 |
+
onFileChange={(file) => handleFileChange(param.name, file)}
|
| 234 |
+
onUrlChange={param.acceptUrl ? (url) => handleUrlParamChange(param.name, url) : undefined}
|
| 235 |
+
disabled={isDisabled}
|
| 236 |
+
/>
|
| 237 |
+
);
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
if (param.enum && Array.isArray(param.enum) && param.enum.length > 0) {
|
| 241 |
return (
|
| 242 |
<Select
|
|
|
|
| 260 |
</SelectContent>
|
| 261 |
</Select>
|
| 262 |
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
}
|
| 264 |
+
|
| 265 |
+
return (
|
| 266 |
+
<Input
|
| 267 |
+
type="text"
|
| 268 |
+
placeholder={param.example?.toString() || param.description}
|
| 269 |
+
value={paramValues[param.name] || ""}
|
| 270 |
+
onChange={(e) => handleParamChange(param.name, e.target.value)}
|
| 271 |
+
disabled={isDisabled}
|
| 272 |
+
className="bg-black/50 border-white/10 text-white focus:border-purple-500 disabled:opacity-50"
|
| 273 |
+
/>
|
| 274 |
+
);
|
| 275 |
};
|
| 276 |
|
| 277 |
const hasQueryParams = plugin.parameters?.query && plugin.parameters.query.length > 0;
|
|
|
|
| 284 |
|
| 285 |
if (hasQueryParams) {
|
| 286 |
const exampleParams = plugin.parameters!.query!
|
| 287 |
+
.map((p) => {
|
| 288 |
+
if (p.type === "file" && p.acceptUrl) {
|
| 289 |
+
return `${p.name}=https://example.com/file.jpg`;
|
| 290 |
+
}
|
| 291 |
+
return `${p.name}=${p.example || 'value'}`;
|
| 292 |
+
})
|
| 293 |
.join('&');
|
| 294 |
curl += `?${exampleParams}`;
|
| 295 |
}
|
|
|
|
| 297 |
curl += '"';
|
| 298 |
|
| 299 |
if (hasBodyParams) {
|
| 300 |
+
const hasFileParams = plugin.parameters!.body!.some(p => p.type === "file");
|
| 301 |
+
|
| 302 |
+
if (hasFileParams) {
|
| 303 |
+
curl += ' \\\n';
|
| 304 |
+
plugin.parameters!.body!.forEach((p) => {
|
| 305 |
+
if (p.type === "file") {
|
| 306 |
+
if (p.acceptUrl) {
|
| 307 |
+
curl += ` -F "${p.name}=https://example.com/file.jpg" \\\n`;
|
| 308 |
+
} else {
|
| 309 |
+
curl += ` -F "${p.name}=@/path/to/file" \\\n`;
|
| 310 |
+
}
|
| 311 |
+
} else {
|
| 312 |
+
curl += ` -F "${p.name}=${p.example || 'value'}" \\\n`;
|
| 313 |
+
}
|
| 314 |
+
});
|
| 315 |
+
curl = curl.slice(0, -3); // Remove last \\\n
|
| 316 |
+
} else {
|
| 317 |
+
curl += ' \\\n -H "Content-Type: application/json" \\\n -d \'';
|
| 318 |
+
const bodyExample: Record<string, any> = {};
|
| 319 |
+
plugin.parameters!.body!.forEach((p) => {
|
| 320 |
+
bodyExample[p.name] = p.example || 'value';
|
| 321 |
+
});
|
| 322 |
+
curl += JSON.stringify(bodyExample, null, 2);
|
| 323 |
+
curl += "'";
|
| 324 |
+
}
|
| 325 |
}
|
| 326 |
|
| 327 |
return curl;
|
| 328 |
};
|
| 329 |
|
| 330 |
const generateNodeExample = () => {
|
| 331 |
+
const hasFileParams = plugin.parameters?.body?.some(p => p.type === "file") || false;
|
| 332 |
+
|
| 333 |
+
if (hasFileParams) {
|
| 334 |
+
let code = 'const formData = new FormData();\n';
|
| 335 |
+
|
| 336 |
+
plugin.parameters!.body!.forEach((p) => {
|
| 337 |
+
if (p.type === "file") {
|
| 338 |
+
if (p.acceptUrl) {
|
| 339 |
+
code += `formData.append("${p.name}", "https://example.com/file.jpg");\n`;
|
| 340 |
+
} else {
|
| 341 |
+
code += `// For file upload from input: <input type="file" id="fileInput">\n`;
|
| 342 |
+
code += `const fileInput = document.getElementById("fileInput");\n`;
|
| 343 |
+
code += `formData.append("${p.name}", fileInput.files[0]);\n`;
|
| 344 |
+
}
|
| 345 |
+
} else {
|
| 346 |
+
code += `formData.append("${p.name}", "${p.example || 'value'}");\n`;
|
| 347 |
+
}
|
| 348 |
+
});
|
| 349 |
+
|
| 350 |
+
code += `\nconst response = await fetch("${getApiUrl(plugin.endpoint)}", {\n`;
|
| 351 |
+
code += ` method: "${plugin.method}",\n`;
|
| 352 |
+
code += ` body: formData\n`;
|
| 353 |
+
code += '});\n\nconst data = await response.json();\nconsole.log(data);';
|
| 354 |
+
return code;
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
let code = `const response = await fetch("${getApiUrl(plugin.endpoint)}`;
|
| 358 |
|
| 359 |
if (hasQueryParams) {
|
| 360 |
const exampleParams = plugin.parameters!.query!
|
| 361 |
+
.map((p) => {
|
| 362 |
+
if (p.type === "file" && p.acceptUrl) {
|
| 363 |
+
return `${p.name}=https://example.com/file.jpg`;
|
| 364 |
+
}
|
| 365 |
+
return `${p.name}=${p.example || 'value'}`;
|
| 366 |
+
})
|
| 367 |
.join('&');
|
| 368 |
code += `?${exampleParams}`;
|
| 369 |
}
|
| 370 |
|
| 371 |
code += '", {\n method: "' + plugin.method + '"';
|
| 372 |
|
| 373 |
+
if (hasBodyParams && !hasFileParams) {
|
| 374 |
code += ',\n headers: {\n "Content-Type": "application/json"\n },\n body: JSON.stringify(';
|
| 375 |
const bodyExample: Record<string, any> = {};
|
| 376 |
plugin.parameters!.body!.forEach((p) => {
|
|
|
|
| 386 |
|
| 387 |
return (
|
| 388 |
<Card className={`bg-white/[0.02] border-white/10 overflow-hidden w-full ${isDisabled ? 'opacity-60' : ''}`}>
|
| 389 |
+
{/* Header */}
|
| 390 |
<div
|
| 391 |
className="p-4 border-b border-white/10 cursor-pointer hover:bg-white/[0.02] transition-colors"
|
| 392 |
onClick={() => setIsExpanded(!isExpanded)}
|
|
|
|
| 397 |
</Badge>
|
| 398 |
<code className="text-sm text-purple-400 font-mono flex-1 min-w-0 break-all">{plugin.endpoint}</code>
|
| 399 |
|
|
|
|
| 400 |
{isDisabled && (
|
| 401 |
<Badge className="bg-red-500/20 text-red-400 border-red-500/50 border flex items-center gap-1 flex-shrink-0">
|
| 402 |
<XCircle className="w-3 h-3" />
|
|
|
|
| 446 |
<h3 className="text-xl font-bold text-white mb-2">{plugin.name}</h3>
|
| 447 |
<p className="text-gray-400 text-sm leading-relaxed">{plugin.description || "No description provided"}</p>
|
| 448 |
|
|
|
|
| 449 |
{isDisabled && plugin.disabledReason && (
|
| 450 |
<div className="mt-3 p-3 bg-red-500/10 border border-red-500/30 rounded-lg flex items-start gap-2">
|
| 451 |
<XCircle className="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5" />
|
|
|
|
| 466 |
</div>
|
| 467 |
)}
|
| 468 |
|
|
|
|
| 469 |
{plugin.tags && plugin.tags.length > 0 && (
|
| 470 |
<div className="flex flex-wrap gap-2 mt-3">
|
| 471 |
{plugin.tags.map((tag) => (
|
|
|
|
| 476 |
</div>
|
| 477 |
)}
|
| 478 |
|
|
|
|
| 479 |
<div className="mt-3 flex items-start gap-2">
|
| 480 |
<span className="text-xs text-gray-500 flex-shrink-0">API URL:</span>
|
| 481 |
<code className="text-xs text-gray-300 bg-black/30 px-2 py-1 rounded break-all flex-1">
|
|
|
|
| 505 |
|
| 506 |
{/* Documentation Tab */}
|
| 507 |
<TabsContent value="documentation" className="p-6 space-y-6">
|
|
|
|
| 508 |
{hasAnyParams && (
|
| 509 |
<div>
|
| 510 |
<h4 className="text-purple-400 font-semibold mb-3">Parameters</h4>
|
|
|
|
| 519 |
</tr>
|
| 520 |
</thead>
|
| 521 |
<tbody>
|
| 522 |
+
{[...(plugin.parameters?.path || []), ...(plugin.parameters?.query || []), ...(plugin.parameters?.body || [])].map((param) => (
|
| 523 |
<tr key={param.name} className="border-b border-white/5">
|
| 524 |
<td className="py-3 pr-4 text-white font-mono">{param.name}</td>
|
| 525 |
<td className="py-3 pr-4">
|
| 526 |
<span className="text-blue-400 font-mono text-xs">{param.type}</span>
|
| 527 |
+
{param.type === "file" && param.fileConstraints && (
|
| 528 |
+
<div className="mt-1 space-y-1">
|
| 529 |
+
{param.fileConstraints.maxSize && (
|
| 530 |
+
<div className="text-xs text-gray-500">
|
| 531 |
+
Max: {(param.fileConstraints.maxSize / 1024 / 1024).toFixed(1)}MB
|
| 532 |
+
</div>
|
| 533 |
+
)}
|
| 534 |
+
{param.fileConstraints.acceptedTypes && (
|
| 535 |
+
<div className="text-xs text-gray-500">
|
| 536 |
+
Types: {param.fileConstraints.acceptedTypes.join(', ')}
|
| 537 |
+
</div>
|
| 538 |
+
)}
|
| 539 |
+
</div>
|
| 540 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 541 |
{param.enum && (
|
| 542 |
<span className="ml-2 text-xs text-gray-500">
|
| 543 |
(options: {param.enum.join(', ')})
|
|
|
|
| 549 |
{param.required ? "Yes" : "No"}
|
| 550 |
</span>
|
| 551 |
</td>
|
| 552 |
+
<td className="py-3 text-gray-400">
|
| 553 |
+
{param.description}
|
| 554 |
+
{param.type === "file" && param.acceptUrl && (
|
| 555 |
+
<span className="block mt-1 text-xs text-purple-400">
|
| 556 |
+
✓ Can also accept URL
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 557 |
</span>
|
| 558 |
)}
|
| 559 |
</td>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 560 |
</tr>
|
| 561 |
))}
|
| 562 |
</tbody>
|
|
|
|
| 565 |
</div>
|
| 566 |
)}
|
| 567 |
|
| 568 |
+
{/* Responses section */}
|
| 569 |
{plugin.responses && Object.keys(plugin.responses).length > 0 && (
|
| 570 |
<div>
|
| 571 |
<h4 className="text-purple-400 font-semibold mb-3">Responses</h4>
|
|
|
|
| 622 |
|
| 623 |
{/* Try It Out Tab */}
|
| 624 |
<TabsContent value="try" className="p-6">
|
|
|
|
| 625 |
{hasAnyParams ? (
|
| 626 |
<div className="space-y-4 mb-4">
|
| 627 |
+
{[...(plugin.parameters?.query || []), ...(plugin.parameters?.body || [])].map((param) => (
|
| 628 |
<div key={param.name}>
|
| 629 |
+
{param.type !== "file" && (
|
| 630 |
+
<>
|
| 631 |
+
<label className="block text-sm text-gray-300 mb-2">
|
| 632 |
+
{param.name}
|
| 633 |
+
{param.required && <span className="text-red-400 ml-1">*</span>}
|
| 634 |
+
<span className="text-xs text-gray-500 ml-2">({param.type})</span>
|
| 635 |
+
{param.enum && (
|
| 636 |
+
<span className="text-xs text-purple-400 ml-2">
|
| 637 |
+
• Select from options
|
| 638 |
+
</span>
|
| 639 |
+
)}
|
| 640 |
+
</label>
|
| 641 |
+
{renderParameterInput(param)}
|
| 642 |
+
<p className="text-xs text-gray-500 mt-1">{param.description}</p>
|
| 643 |
+
</>
|
| 644 |
+
)}
|
| 645 |
+
{param.type === "file" && renderParameterInput(param)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 646 |
</div>
|
| 647 |
))}
|
| 648 |
</div>
|
|
|
|
| 650 |
<p className="text-sm text-gray-400 mb-4">No parameters required</p>
|
| 651 |
)}
|
| 652 |
|
|
|
|
| 653 |
<Button
|
| 654 |
onClick={handleExecute}
|
| 655 |
disabled={loading || isDisabled}
|
|
|
|
| 662 |
{/* Response Display */}
|
| 663 |
{response && (
|
| 664 |
<div className="mt-6 space-y-4">
|
|
|
|
| 665 |
{isDeprecated && responseHeaders['x-plugin-deprecated'] && (
|
| 666 |
<div className="p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg flex items-start gap-2">
|
| 667 |
<AlertTriangle className="w-5 h-5 text-yellow-400 flex-shrink-0 mt-0.5" />
|
|
|
|
| 674 |
</div>
|
| 675 |
)}
|
| 676 |
|
|
|
|
| 677 |
{requestUrl && (
|
| 678 |
<div>
|
| 679 |
<div className="flex items-center justify-between mb-2">
|
|
|
|
| 696 |
</div>
|
| 697 |
)}
|
| 698 |
|
|
|
|
| 699 |
<div className="flex items-center justify-between">
|
| 700 |
<span className="text-sm text-gray-400">Response Status</span>
|
| 701 |
<Badge className={`${response.status >= 200 && response.status < 300
|
|
|
|
| 706 |
</Badge>
|
| 707 |
</div>
|
| 708 |
|
|
|
|
| 709 |
{Object.keys(responseHeaders).length > 0 && (
|
| 710 |
<div>
|
| 711 |
<h5 className="text-sm text-gray-400 mb-2">Response Headers</h5>
|
|
|
|
| 720 |
</div>
|
| 721 |
)}
|
| 722 |
|
|
|
|
| 723 |
<div>
|
| 724 |
<h5 className="text-sm text-gray-400 mb-2">Response Body</h5>
|
| 725 |
<CodeBlock
|
src/server/index.ts
CHANGED
|
@@ -19,10 +19,11 @@ app.use(
|
|
| 19 |
verify: (req, _res, buf) => {
|
| 20 |
req.rawBody = buf;
|
| 21 |
},
|
|
|
|
| 22 |
}),
|
| 23 |
);
|
| 24 |
|
| 25 |
-
app.use(express.urlencoded({ extended: false }));
|
| 26 |
|
| 27 |
export function log(message: string, source = "express") {
|
| 28 |
const formattedTime = new Date().toLocaleTimeString("id-ID", {
|
|
|
|
| 19 |
verify: (req, _res, buf) => {
|
| 20 |
req.rawBody = buf;
|
| 21 |
},
|
| 22 |
+
limit: '10mb'
|
| 23 |
}),
|
| 24 |
);
|
| 25 |
|
| 26 |
+
app.use(express.urlencoded({ limit: '10mb', extended: false }));
|
| 27 |
|
| 28 |
export function log(message: string, source = "express") {
|
| 29 |
const formattedTime = new Date().toLocaleTimeString("id-ID", {
|
src/server/plugins/ai/ai_image_editor.js
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { sendSuccess, ErrorResponses } from "../../lib/response-helper.js";
|
| 2 |
+
import { AIEnhancer } from "./ai_image_utils.js";
|
| 3 |
+
import multer from "multer";
|
| 4 |
+
import axios from "axios";
|
| 5 |
+
import path from "path";
|
| 6 |
+
|
| 7 |
+
const storage = multer.memoryStorage();
|
| 8 |
+
const upload = multer({
|
| 9 |
+
storage: storage,
|
| 10 |
+
limits: {
|
| 11 |
+
fileSize: 5 * 1024 * 1024, // 5MB
|
| 12 |
+
},
|
| 13 |
+
fileFilter: (req, file, cb) => {
|
| 14 |
+
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
| 15 |
+
if (allowedTypes.includes(file.mimetype)) {
|
| 16 |
+
cb(null, true);
|
| 17 |
+
} else {
|
| 18 |
+
cb(new Error('Invalid file type. Only JPEG, PNG, GIF, and WebP are allowed.'));
|
| 19 |
+
}
|
| 20 |
+
}
|
| 21 |
+
});
|
| 22 |
+
|
| 23 |
+
const ModelMap = {
|
| 24 |
+
nano_banana: 2,
|
| 25 |
+
seed_dream: 5,
|
| 26 |
+
flux: 8,
|
| 27 |
+
qwen_image: 9
|
| 28 |
+
};
|
| 29 |
+
|
| 30 |
+
/** @type {import("../../types/plugin.ts").ApiPluginHandler}*/
|
| 31 |
+
const handler = {
|
| 32 |
+
name: "AI Image Editor",
|
| 33 |
+
description: "Edit your images using AI",
|
| 34 |
+
method: "POST",
|
| 35 |
+
version: "1.0.0",
|
| 36 |
+
category: ["ai"],
|
| 37 |
+
alias: ["aiImageEditor"],
|
| 38 |
+
tags: ["ai", "image"],
|
| 39 |
+
parameters: {
|
| 40 |
+
body: [
|
| 41 |
+
{
|
| 42 |
+
name: "image",
|
| 43 |
+
type: "file",
|
| 44 |
+
required: true,
|
| 45 |
+
description: "Image file to edit (or URL)",
|
| 46 |
+
fileConstraints: {
|
| 47 |
+
maxSize: 5 * 1024 * 1024, // 5MB
|
| 48 |
+
acceptedTypes: ["image/jpeg", "image/png", "image/gif", "image/webp"],
|
| 49 |
+
acceptedExtensions: [".jpg", ".jpeg", ".png", ".gif", ".webp"]
|
| 50 |
+
},
|
| 51 |
+
acceptUrl: true
|
| 52 |
+
},
|
| 53 |
+
{
|
| 54 |
+
name: "model",
|
| 55 |
+
type: "string",
|
| 56 |
+
required: true,
|
| 57 |
+
description: "Select an AI model to edit your image.",
|
| 58 |
+
example: "nano_banana",
|
| 59 |
+
enum: ["nano_banana", "seed_dream", "flux", "qwen_image"],
|
| 60 |
+
default: "nano_banana"
|
| 61 |
+
},
|
| 62 |
+
{
|
| 63 |
+
name: "prompt",
|
| 64 |
+
type: "string",
|
| 65 |
+
required: true,
|
| 66 |
+
description: "Tell AI how your image will look edited",
|
| 67 |
+
example: "Put Cristiano Ronaldo next to that person"
|
| 68 |
+
},
|
| 69 |
+
{
|
| 70 |
+
name: "output_size",
|
| 71 |
+
type: "string",
|
| 72 |
+
required: true,
|
| 73 |
+
description: "Output size",
|
| 74 |
+
example: "2K",
|
| 75 |
+
enum: ["2K", "4K", "8K"],
|
| 76 |
+
default: "2K"
|
| 77 |
+
},
|
| 78 |
+
{
|
| 79 |
+
name: "disable_safety_checker",
|
| 80 |
+
type: "boolean",
|
| 81 |
+
required: false,
|
| 82 |
+
description: "If you want to disable safety checker for indecent images, set to true (default false)",
|
| 83 |
+
example: false,
|
| 84 |
+
default: false
|
| 85 |
+
}
|
| 86 |
+
]
|
| 87 |
+
},
|
| 88 |
+
responses: {
|
| 89 |
+
200: {
|
| 90 |
+
status: 200,
|
| 91 |
+
description: "Successfully retrieved data",
|
| 92 |
+
example: {
|
| 93 |
+
status: 200,
|
| 94 |
+
author: "Ditzzy",
|
| 95 |
+
note: "Thank you for using this API!",
|
| 96 |
+
results: {
|
| 97 |
+
image_url: "https://example.com/edited-image.jpg",
|
| 98 |
+
filename: "edited-image.jpg",
|
| 99 |
+
size: "2K"
|
| 100 |
+
}
|
| 101 |
+
}
|
| 102 |
+
},
|
| 103 |
+
404: {
|
| 104 |
+
status: 404,
|
| 105 |
+
description: "Missing required parameter",
|
| 106 |
+
example: {
|
| 107 |
+
status: 404,
|
| 108 |
+
message: "Missing required parameter"
|
| 109 |
+
}
|
| 110 |
+
},
|
| 111 |
+
400: {
|
| 112 |
+
status: 400,
|
| 113 |
+
description: "Invalid file or URL",
|
| 114 |
+
example: {
|
| 115 |
+
success: false,
|
| 116 |
+
message: "Invalid image file or URL"
|
| 117 |
+
}
|
| 118 |
+
},
|
| 119 |
+
413: {
|
| 120 |
+
status: 413,
|
| 121 |
+
description: "File too large",
|
| 122 |
+
example: {
|
| 123 |
+
success: false,
|
| 124 |
+
message: "File size exceeds 5MB limit"
|
| 125 |
+
}
|
| 126 |
+
},
|
| 127 |
+
500: {
|
| 128 |
+
status: 500,
|
| 129 |
+
description: "Server error or unavailable",
|
| 130 |
+
example: {
|
| 131 |
+
status: 500,
|
| 132 |
+
message: "An error occurred, please try again later."
|
| 133 |
+
}
|
| 134 |
+
}
|
| 135 |
+
},
|
| 136 |
+
exec: async (req, res) => {
|
| 137 |
+
console.log("🔵 [1] Request received at AI Image Editor");
|
| 138 |
+
console.log("🔵 [1] Headers:", req.headers);
|
| 139 |
+
|
| 140 |
+
upload.single('image')(req, res, async (err) => {
|
| 141 |
+
console.log("🔵 [2] Inside multer middleware");
|
| 142 |
+
|
| 143 |
+
if (err) {
|
| 144 |
+
console.error("❌ [2] Multer error:", err);
|
| 145 |
+
if (err instanceof multer.MulterError) {
|
| 146 |
+
if (err.code === 'LIMIT_FILE_SIZE') {
|
| 147 |
+
return res.status(413).json({
|
| 148 |
+
success: false,
|
| 149 |
+
message: "File size exceeds 5MB limit"
|
| 150 |
+
});
|
| 151 |
+
}
|
| 152 |
+
}
|
| 153 |
+
return res.status(400).json({
|
| 154 |
+
success: false,
|
| 155 |
+
message: err.message || "File upload error"
|
| 156 |
+
});
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
console.log("🔵 [3] Multer successful");
|
| 160 |
+
console.log("🔵 [3] Body:", req.body);
|
| 161 |
+
console.log("🔵 [3] File:", req.file ? `${req.file.originalname} (${req.file.size} bytes)` : "No file");
|
| 162 |
+
|
| 163 |
+
try {
|
| 164 |
+
const { prompt, output_size, disable_safety_checker, model } = req.body;
|
| 165 |
+
|
| 166 |
+
console.log("🔵 [4] Extracted params:", { prompt, output_size, model, disable_safety_checker });
|
| 167 |
+
|
| 168 |
+
// Validate required parameters
|
| 169 |
+
if (!prompt) {
|
| 170 |
+
console.log("❌ [4] Missing prompt");
|
| 171 |
+
return ErrorResponses.missingParameter(res, "prompt");
|
| 172 |
+
}
|
| 173 |
+
if (!output_size) {
|
| 174 |
+
console.log("❌ [4] Missing output_size");
|
| 175 |
+
return ErrorResponses.missingParameter(res, "output_size");
|
| 176 |
+
}
|
| 177 |
+
if (!model) {
|
| 178 |
+
console.log("❌ [4] Missing model");
|
| 179 |
+
return ErrorResponses.missingParameter(res, "model");
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
// Validate model
|
| 183 |
+
if (!ModelMap[model]) {
|
| 184 |
+
console.log("❌ [4] Invalid model:", model);
|
| 185 |
+
return res.status(400).json({
|
| 186 |
+
success: false,
|
| 187 |
+
message: `Invalid model. Must be one of: ${Object.keys(ModelMap).join(', ')}`
|
| 188 |
+
});
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
console.log("🔵 [5] Creating AIEnhancer instance");
|
| 192 |
+
const ai = new AIEnhancer();
|
| 193 |
+
console.log("✅ [5] AIEnhancer created");
|
| 194 |
+
|
| 195 |
+
let imageBuffer;
|
| 196 |
+
let filename;
|
| 197 |
+
|
| 198 |
+
// Handle uploaded file
|
| 199 |
+
if (req.file) {
|
| 200 |
+
console.log("🔵 [6] Using uploaded file");
|
| 201 |
+
imageBuffer = req.file.buffer;
|
| 202 |
+
filename = req.file.originalname;
|
| 203 |
+
}
|
| 204 |
+
// Handle URL
|
| 205 |
+
else if (req.body.image && typeof req.body.image === 'string') {
|
| 206 |
+
console.log("🔵 [6] Downloading from URL:", req.body.image);
|
| 207 |
+
const imageUrl = req.body.image;
|
| 208 |
+
|
| 209 |
+
// Validate URL
|
| 210 |
+
try {
|
| 211 |
+
new URL(imageUrl);
|
| 212 |
+
} catch {
|
| 213 |
+
console.log("❌ [6] Invalid URL");
|
| 214 |
+
return ErrorResponses.invalidUrl(res, "Invalid image URL");
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
// Download image from URL
|
| 218 |
+
const response = await axios.get(imageUrl, {
|
| 219 |
+
responseType: 'arraybuffer',
|
| 220 |
+
maxContentLength: 5 * 1024 * 1024,
|
| 221 |
+
timeout: 10000,
|
| 222 |
+
headers: {
|
| 223 |
+
'User-Agent': 'Mozilla/5.0 (compatible; AIImageEditor/1.0)'
|
| 224 |
+
}
|
| 225 |
+
});
|
| 226 |
+
|
| 227 |
+
imageBuffer = Buffer.from(response.data);
|
| 228 |
+
filename = path.basename(new URL(imageUrl).pathname) || 'image';
|
| 229 |
+
console.log("✅ [6] Downloaded from URL:", filename);
|
| 230 |
+
} else {
|
| 231 |
+
console.log("❌ [6] No image provided");
|
| 232 |
+
return ErrorResponses.missingParameter(res, "image (file or URL)");
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
console.log("🔵 [7] Image buffer size:", imageBuffer.length);
|
| 236 |
+
|
| 237 |
+
// Get model ID from map
|
| 238 |
+
const modelId = ModelMap[model];
|
| 239 |
+
console.log("🔵 [8] Model ID:", modelId);
|
| 240 |
+
|
| 241 |
+
// Call AI Image Editor
|
| 242 |
+
console.log("🔵 [9] Calling ImageAIEditor...");
|
| 243 |
+
const edit = await ai.ImageAIEditor(imageBuffer, modelId, {
|
| 244 |
+
size: output_size,
|
| 245 |
+
aspect_ratio: "match_input_image",
|
| 246 |
+
go_fast: true,
|
| 247 |
+
prompt: prompt,
|
| 248 |
+
output_quality: 100,
|
| 249 |
+
disable_safety_checker: disable_safety_checker === 'true' || disable_safety_checker === true,
|
| 250 |
+
});
|
| 251 |
+
|
| 252 |
+
console.log("✅ [9] ImageAIEditor response:", edit);
|
| 253 |
+
|
| 254 |
+
return sendSuccess(res, edit.results || edit);
|
| 255 |
+
} catch (e) {
|
| 256 |
+
console.error("❌ [ERROR] Exception caught:", e);
|
| 257 |
+
console.error("❌ [ERROR] Stack trace:", e.stack);
|
| 258 |
+
return ErrorResponses.serverError(res, "An error occurred, try again later.");
|
| 259 |
+
}
|
| 260 |
+
});
|
| 261 |
+
}
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
export default handler;
|
src/server/plugins/ai/ai_image_utils.js
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const ModelMap = {
|
| 2 |
+
nano_banana: 2,
|
| 3 |
+
seed_dream: 5,
|
| 4 |
+
flux: 8,
|
| 5 |
+
qwen_image: 9
|
| 6 |
+
};
|
| 7 |
+
|
| 8 |
+
const IMAGE_TO_ANIME_PRESETS = {
|
| 9 |
+
manga: {
|
| 10 |
+
size: "2K",
|
| 11 |
+
aspect_ratio: "match_input_image",
|
| 12 |
+
output_format: "jpg",
|
| 13 |
+
sequential_image_generation: "disabled",
|
| 14 |
+
max_images: 1,
|
| 15 |
+
prompt: "Convert the provided image into a KOREAN-STYLE MANGA illustration. Apply strong stylization with clear and noticeable differences from the original image."
|
| 16 |
+
},
|
| 17 |
+
|
| 18 |
+
anime: {
|
| 19 |
+
size: "2K",
|
| 20 |
+
aspect_ratio: "match_input_image",
|
| 21 |
+
output_format: "jpg",
|
| 22 |
+
sequential_image_generation: "disabled",
|
| 23 |
+
max_images: 1,
|
| 24 |
+
prompt: "Convert the provided image into an ANIME-STYLE illustration. Apply strong stylization with clear and noticeable differences from the original image."
|
| 25 |
+
},
|
| 26 |
+
|
| 27 |
+
ghibli: {
|
| 28 |
+
size: "2K",
|
| 29 |
+
aspect_ratio: "match_input_image",
|
| 30 |
+
output_format: "jpg",
|
| 31 |
+
sequential_image_generation: "disabled",
|
| 32 |
+
max_images: 1,
|
| 33 |
+
prompt: "Convert the provided image into a STUDIO GHIBLI-STYLE illustration. Apply strong stylization with clear and noticeable differences from the original image."
|
| 34 |
+
},
|
| 35 |
+
|
| 36 |
+
cyberpunk: {
|
| 37 |
+
size: "2K",
|
| 38 |
+
aspect_ratio: "match_input_image",
|
| 39 |
+
output_format: "jpg",
|
| 40 |
+
sequential_image_generation: "disabled",
|
| 41 |
+
max_images: 1,
|
| 42 |
+
prompt: "Convert the provided image into a CYBERPUNK-STYLE illustration with neon colors, futuristic elements, and dark atmosphere."
|
| 43 |
+
},
|
| 44 |
+
|
| 45 |
+
watercolor: {
|
| 46 |
+
size: "2K",
|
| 47 |
+
aspect_ratio: "match_input_image",
|
| 48 |
+
output_format: "png",
|
| 49 |
+
sequential_image_generation: "disabled",
|
| 50 |
+
max_images: 1,
|
| 51 |
+
prompt: "Convert the provided image into a WATERCOLOR painting style with soft brush strokes and pastel colors."
|
| 52 |
+
},
|
| 53 |
+
|
| 54 |
+
pixelart: {
|
| 55 |
+
size: "2K",
|
| 56 |
+
aspect_ratio: "match_input_image",
|
| 57 |
+
output_format: "png",
|
| 58 |
+
sequential_image_generation: "disabled",
|
| 59 |
+
max_images: 1,
|
| 60 |
+
prompt: "Convert the provided image into PIXEL ART style with 8-bit retro gaming aesthetic."
|
| 61 |
+
},
|
| 62 |
+
|
| 63 |
+
sketch: {
|
| 64 |
+
size: "2K",
|
| 65 |
+
aspect_ratio: "match_input_image",
|
| 66 |
+
output_format: "jpg",
|
| 67 |
+
sequential_image_generation: "disabled",
|
| 68 |
+
max_images: 1,
|
| 69 |
+
prompt: "Convert the provided image into a detailed PENCIL SKETCH with realistic shading and artistic strokes."
|
| 70 |
+
},
|
| 71 |
+
|
| 72 |
+
oilpainting: {
|
| 73 |
+
size: "2K",
|
| 74 |
+
aspect_ratio: "match_input_image",
|
| 75 |
+
output_format: "jpg",
|
| 76 |
+
sequential_image_generation: "disabled",
|
| 77 |
+
max_images: 1,
|
| 78 |
+
prompt: "Convert the provided image into an OIL PAINTING style with thick brush strokes and rich colors."
|
| 79 |
+
}
|
| 80 |
+
};
|
| 81 |
+
|
| 82 |
+
const IMAGE_TO_ANIME_ENCRYPTED_PAYLOADS = {
|
| 83 |
+
manga: "L7p91uXhVyp5OOJthAyqjSqhlbM+RPZ8+h2Uq9tz6Y+4Agarugz8f4JjxjEycxEzuj/7+6Q0YY9jUvrfmqkucENhHAkMq1EOilzosQlw2msQpW2yRqV3C/WqvP/jrmSu3aUVAyeFhSbK3ARzowBzQYPVHtxwBbTWwlSR4tehnodUasnmftnY77c8gIFtL2ArNdzmPLx5H8O9un2U8WE4s0+xiFV3y4sbetHMN7rHh7DRIpuIQD4rKISR/vE+HeaHpRavXfsilr5P7Y6bsIo+RRFIPgX2ofbYYiATziqsjDeie4IlcOAVf1Pudqz8uk6YKM78CGxjF9iPLYQnkW+c6j96PNsg1Yk4Xz8/ZcdmHF4GGZe8ILYH/D0yyM1dsCkK1zY8ciL+6pAk4dHIZ/4k9A==",
|
| 84 |
+
ghibli: "L7p91uXhVyp5OOJthAyqjSqhlbM+RPZ8+h2Uq9tz6Y+4Agarugz8f4JjxjEycxEzuj/7+6Q0YY9jUvrfmqkucENhHAkMq1EOilzosQlw2msQpW2yRqV3C/WqvP/jrmSu3aUVAyeFhSbK3ARzowBzQYPVHtxwBbTWwlSR4tehnodUasnmftnY77c8gIFtL2ArNdzmPLx5H8O9un2U8WE4syzL5EYHGJWC1rlQM9xhNe1PViOsBSxmwHVwOdqtxZtcAJmGuzTgG7JVU7Hr9ZRwajhYK5yxQwSdJGwwR4jjS1yF9s9wKUQqgI+fYxaw7FZziLS+9JG5pTEjch4D0fpl+LO7vIynHN4cyu4DDeAUwNeYfbGMn2QQs+5OgMdViCAM1GkJk2jhlQm10rESTjDryw==",
|
| 85 |
+
anime: "L7p91uXhVyp5OOJthAyqjSqhlbM+RPZ8+h2Uq9tz6Y+4Agarugz8f4JjxjEycxEzuj/7+6Q0YY9jUvrfmqkucENhHAkMq1EOilzosQlw2msQpW2yRqV3C/WqvP/jrmSu3aUVAyeFhSbK3ARzowBzQYPVHtxwBbTWwlSR4tehnodUasnmftnY77c8gIFtL2ArNdzmPLx5H8O9un2U8WE4s7O2FxvQPCjt2uGmHPMOx1DsNSnLvzCKPVdz8Ob1cPHePmmquQZlsb/p+8gGv+cizSiOL4ts6GD2RxWN+K5MmpA/F3rQXanFUm4EL0g7qZCQbChRRQyaAyZuxtIdTKsmsMzkVKM5Sx96eV7bEjUAJ52j6NcP96INv2DhnWTP7gB6tltFQe8B8SPS2LuLRuPghA=="
|
| 86 |
+
};
|
| 87 |
+
|
| 88 |
+
export class AIEnhancer {
|
| 89 |
+
constructor() {
|
| 90 |
+
this.CREATED_BY = "Ditzzy";
|
| 91 |
+
this.NOTE = "Thank you for using this scrape, I hope you appreciate me for making this scrape by not deleting wm";
|
| 92 |
+
|
| 93 |
+
this.AES_KEY = "ai-enhancer-web__aes-key";
|
| 94 |
+
this.AES_IV = "aienhancer-aesiv";
|
| 95 |
+
|
| 96 |
+
this.HEADERS = {
|
| 97 |
+
"accept": "*/*",
|
| 98 |
+
"content-type": "application/json",
|
| 99 |
+
"Referer": "https://aienhancer.ai",
|
| 100 |
+
"Referrer-Policy": "strict-origin-when-cross-origin"
|
| 101 |
+
};
|
| 102 |
+
|
| 103 |
+
this.POLLING_INTERVAL = 2000;
|
| 104 |
+
this.MAX_POLLING_ATTEMPTS = 120;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
encrypt(data) {
|
| 108 |
+
const plaintext = typeof data === "string" ? data : JSON.stringify(data);
|
| 109 |
+
|
| 110 |
+
return CryptoJS.AES.encrypt(
|
| 111 |
+
plaintext,
|
| 112 |
+
CryptoJS.enc.Utf8.parse(this.AES_KEY),
|
| 113 |
+
{
|
| 114 |
+
iv: CryptoJS.enc.Utf8.parse(this.AES_IV),
|
| 115 |
+
mode: CryptoJS.mode.CBC,
|
| 116 |
+
padding: CryptoJS.pad.Pkcs7
|
| 117 |
+
}
|
| 118 |
+
).toString();
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
decrypt(encryptedData) {
|
| 122 |
+
const decrypted = CryptoJS.AES.decrypt(
|
| 123 |
+
encryptedData,
|
| 124 |
+
CryptoJS.enc.Utf8.parse(this.AES_KEY),
|
| 125 |
+
{
|
| 126 |
+
iv: CryptoJS.enc.Utf8.parse(this.AES_IV),
|
| 127 |
+
mode: CryptoJS.mode.CBC,
|
| 128 |
+
padding: CryptoJS.pad.Pkcs7
|
| 129 |
+
}
|
| 130 |
+
);
|
| 131 |
+
|
| 132 |
+
return decrypted.toString(CryptoJS.enc.Utf8);
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
decryptToJSON(encryptedData) {
|
| 136 |
+
const decrypted = this.decrypt(encryptedData);
|
| 137 |
+
return JSON.parse(decrypted);
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
wrapResponse(data) {
|
| 141 |
+
return {
|
| 142 |
+
created_by: this.CREATED_BY,
|
| 143 |
+
note: this.NOTE,
|
| 144 |
+
results: data
|
| 145 |
+
};
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
async sleep(ms) {
|
| 149 |
+
return new Promise(resolve => setTimeout(resolve, ms));
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
async processImage(image) {
|
| 153 |
+
let img;
|
| 154 |
+
let type;
|
| 155 |
+
|
| 156 |
+
if (typeof image === "string") {
|
| 157 |
+
img = readFileSync(image);
|
| 158 |
+
type = await fileTypeFromBuffer(img);
|
| 159 |
+
} else if (Buffer.isBuffer(image)) {
|
| 160 |
+
img = image;
|
| 161 |
+
type = await fileTypeFromBuffer(image);
|
| 162 |
+
} else {
|
| 163 |
+
throw new Error("Invalid image input: must be file path (string) or Buffer");
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
if (!type) {
|
| 167 |
+
throw new Error("Could not detect file type");
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
const allowedImageTypes = [
|
| 171 |
+
'image/jpeg',
|
| 172 |
+
'image/jpg',
|
| 173 |
+
'image/png',
|
| 174 |
+
'image/webp',
|
| 175 |
+
'image/gif',
|
| 176 |
+
'image/bmp'
|
| 177 |
+
];
|
| 178 |
+
|
| 179 |
+
if (!allowedImageTypes.includes(type.mime)) {
|
| 180 |
+
throw new Error(
|
| 181 |
+
`Unsupported format: ${type.mime}. Allowed: jpeg, jpg, png, webp, gif, bmp`
|
| 182 |
+
);
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
const imgbase64 = img.toString("base64");
|
| 186 |
+
return {
|
| 187 |
+
base64: `data:${type.mime};base64,${imgbase64}`,
|
| 188 |
+
mime: type.mime
|
| 189 |
+
};
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
async createTask(apiUrl, model, image, config) {
|
| 193 |
+
const { base64 } = await this.processImage(image);
|
| 194 |
+
|
| 195 |
+
const settings = typeof config === "string"
|
| 196 |
+
? config
|
| 197 |
+
: this.encrypt(config);
|
| 198 |
+
|
| 199 |
+
const requestConfig = {
|
| 200 |
+
url: apiUrl,
|
| 201 |
+
method: "POST",
|
| 202 |
+
headers: this.HEADERS,
|
| 203 |
+
data: {
|
| 204 |
+
model,
|
| 205 |
+
image: base64,
|
| 206 |
+
settings
|
| 207 |
+
}
|
| 208 |
+
};
|
| 209 |
+
|
| 210 |
+
const { data } = await axios.request(requestConfig);
|
| 211 |
+
|
| 212 |
+
if (data.code !== 100000 || !data.data.id) {
|
| 213 |
+
throw new Error(`Task creation failed: ${data.message}`);
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
return data.data.id;
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
async checkTaskStatus(resultUrl, taskId) {
|
| 220 |
+
const config = {
|
| 221 |
+
url: resultUrl,
|
| 222 |
+
method: "POST",
|
| 223 |
+
headers: this.HEADERS,
|
| 224 |
+
data: { task_id: taskId }
|
| 225 |
+
};
|
| 226 |
+
|
| 227 |
+
const { data } = await axios.request(config);
|
| 228 |
+
return data;
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
async pollTaskResult(resultUrl, taskId) {
|
| 232 |
+
let attempts = 0;
|
| 233 |
+
|
| 234 |
+
while (attempts < this.MAX_POLLING_ATTEMPTS) {
|
| 235 |
+
const response = await this.checkTaskStatus(resultUrl, taskId);
|
| 236 |
+
|
| 237 |
+
if (response.code !== 100000) {
|
| 238 |
+
throw new Error(`Status check failed: ${response.message}`);
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
const { status, error, output, input, completed_at } = response.data;
|
| 242 |
+
|
| 243 |
+
if ((status === "succeeded" || status === "success") && output && input) {
|
| 244 |
+
return {
|
| 245 |
+
id: taskId,
|
| 246 |
+
output,
|
| 247 |
+
input,
|
| 248 |
+
error,
|
| 249 |
+
status,
|
| 250 |
+
created_at: response.data.created_at,
|
| 251 |
+
started_at: response.data.started_at,
|
| 252 |
+
completed_at: completed_at
|
| 253 |
+
};
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
if (status === "failed" || status === "fail" || error) {
|
| 257 |
+
throw new Error(`Task failed: ${error || "Unknown error"}`);
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
console.log(`[${taskId} | ${status}] Polling attempt ${attempts + 1}/${this.MAX_POLLING_ATTEMPTS}`);
|
| 261 |
+
|
| 262 |
+
await this.sleep(this.POLLING_INTERVAL);
|
| 263 |
+
attempts++;
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
throw new Error(`Task timeout after ${this.MAX_POLLING_ATTEMPTS} attempts`);
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
async imageToAnime(image, preset = "anime") {
|
| 270 |
+
try {
|
| 271 |
+
const apiUrl = "https://aienhancer.ai/api/v1/r/image-enhance/create";
|
| 272 |
+
const resultUrl = "https://aienhancer.ai/api/v1/r/image-enhance/result";
|
| 273 |
+
const model = 5;
|
| 274 |
+
|
| 275 |
+
let config;
|
| 276 |
+
|
| 277 |
+
if (typeof preset === "string") {
|
| 278 |
+
if (IMAGE_TO_ANIME_PRESETS[preset]) {
|
| 279 |
+
config = IMAGE_TO_ANIME_PRESETS[preset];
|
| 280 |
+
}
|
| 281 |
+
else if (IMAGE_TO_ANIME_ENCRYPTED_PAYLOADS[preset]) {
|
| 282 |
+
config = IMAGE_TO_ANIME_ENCRYPTED_PAYLOADS[preset];
|
| 283 |
+
}
|
| 284 |
+
else {
|
| 285 |
+
config = preset;
|
| 286 |
+
}
|
| 287 |
+
} else {
|
| 288 |
+
config = preset;
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
const taskId = await this.createTask(apiUrl, model, image, config);
|
| 292 |
+
console.log(`✓ Task created: ${taskId}`);
|
| 293 |
+
|
| 294 |
+
const result = await this.pollTaskResult(resultUrl, taskId);
|
| 295 |
+
return this.wrapResponse(result);
|
| 296 |
+
} catch (error) {
|
| 297 |
+
throw new Error(`Image to anime conversion failed: ${error}`);
|
| 298 |
+
}
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
async ImageAIEditor(image, model, preset) {
|
| 302 |
+
try {
|
| 303 |
+
const apiUrl = "https://aienhancer.ai/api/v1/k/image-enhance/create";
|
| 304 |
+
const resultUrl = "https://aienhancer.ai/api/v1/k/image-enhance/result";
|
| 305 |
+
|
| 306 |
+
const modelId = ModelMap[model];
|
| 307 |
+
const taskId = await this.createTask(apiUrl, modelId, image, preset);
|
| 308 |
+
const result = await this.pollTaskResult(resultUrl, taskId);
|
| 309 |
+
|
| 310 |
+
return this.wrapResponse(result);
|
| 311 |
+
} catch (e) {
|
| 312 |
+
throw new Error("Image editor failed: " + e);
|
| 313 |
+
}
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
async AIImageRestoration(image, model, config) {
|
| 317 |
+
try {
|
| 318 |
+
const apiUrl = "https://aienhancer.ai/api/v1/r/image-enhance/create";
|
| 319 |
+
const resultUrl = "https://aienhancer.ai/api/v1/r/image-enhance/result";
|
| 320 |
+
|
| 321 |
+
const taskId = await this.createTask(apiUrl, model, image, config);
|
| 322 |
+
const result = await this.pollTaskResult(resultUrl, taskId);
|
| 323 |
+
|
| 324 |
+
return this.wrapResponse(result);
|
| 325 |
+
} catch (e) {
|
| 326 |
+
throw new Error(`An error occurred while upscaling the image: ${e}`);
|
| 327 |
+
}
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
async RemoveBackground(image, config = {}) {
|
| 331 |
+
try {
|
| 332 |
+
const apiUrl = "https://aienhancer.ai/api/v1/r/image-enhance/create";
|
| 333 |
+
const resultUrl = "https://aienhancer.ai/api/v1/r/image-enhance/result";
|
| 334 |
+
const model = 4;
|
| 335 |
+
|
| 336 |
+
const payloadConfig = config ? config : {
|
| 337 |
+
threshold: 0,
|
| 338 |
+
reverse: false,
|
| 339 |
+
background_type: "rgba",
|
| 340 |
+
format: "png",
|
| 341 |
+
};
|
| 342 |
+
|
| 343 |
+
const taskId = await this.createTask(apiUrl, model, image, payloadConfig);
|
| 344 |
+
const result = await this.pollTaskResult(resultUrl, taskId);
|
| 345 |
+
|
| 346 |
+
return this.wrapResponse(result);
|
| 347 |
+
} catch (e) {
|
| 348 |
+
throw new Error(`Remove background error: ${e}`);
|
| 349 |
+
}
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
decryptPayload(encryptedPayload) {
|
| 353 |
+
return this.decryptToJSON(encryptedPayload);
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
encryptConfig(config) {
|
| 357 |
+
return this.encrypt(config);
|
| 358 |
+
}
|
| 359 |
+
}
|
src/server/types/plugin.ts
CHANGED
|
@@ -1,14 +1,22 @@
|
|
| 1 |
import { Request, Response, NextFunction } from "express";
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
export interface PluginParameter {
|
| 4 |
name: string;
|
| 5 |
-
type: "string" | "number" | "boolean" | "array" | "object";
|
| 6 |
required: boolean;
|
| 7 |
description: string;
|
| 8 |
example?: any;
|
| 9 |
default?: any;
|
| 10 |
enum?: any[];
|
| 11 |
pattern?: string;
|
|
|
|
|
|
|
| 12 |
}
|
| 13 |
|
| 14 |
export interface PluginResponse {
|
|
|
|
| 1 |
import { Request, Response, NextFunction } from "express";
|
| 2 |
|
| 3 |
+
export interface FileConstraints {
|
| 4 |
+
maxSize?: number; // NOTE: in bytes, e.g., 5 * 1024 * 1024 for 5MB
|
| 5 |
+
acceptedTypes?: string[]; // NOTE: MIME types, e.g., ['image/jpeg', 'image/png']
|
| 6 |
+
acceptedExtensions?: string[]; // e.g., ['.jpg', '.png', '.pdf']
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
export interface PluginParameter {
|
| 10 |
name: string;
|
| 11 |
+
type: "string" | "number" | "boolean" | "array" | "object" | "file";
|
| 12 |
required: boolean;
|
| 13 |
description: string;
|
| 14 |
example?: any;
|
| 15 |
default?: any;
|
| 16 |
enum?: any[];
|
| 17 |
pattern?: string;
|
| 18 |
+
fileConstraints?: FileConstraints;
|
| 19 |
+
acceptUrl?: boolean;
|
| 20 |
}
|
| 21 |
|
| 22 |
export interface PluginResponse {
|