OhMyDitzzy
commited on
Commit
·
83f446a
1
Parent(s):
f535bab
disabled: komiku
Browse files- src/components/PluginCard.tsx +95 -31
- src/server/plugin-loader.ts +39 -10
- src/server/plugins/anime/komiku_get_details.js +2 -0
- src/server/plugins/anime/komiku_get_manga.js +2 -0
- src/server/plugins/anime/komiku_get_manhua.js +2 -0
- src/server/plugins/anime/komiku_get_manhwa.js +2 -0
- src/server/plugins/anime/komiku_read_chapter.js +2 -0
- src/server/plugins/anime/komiku_search.js +2 -0
- src/server/types/plugin.ts +12 -13
src/components/PluginCard.tsx
CHANGED
|
@@ -6,7 +6,7 @@ import { Input } from "@/components/ui/input";
|
|
| 6 |
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
| 7 |
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
| 8 |
import { PluginMetadata } from "@/client/hooks/usePlugin";
|
| 9 |
-
import { Play, ChevronDown, ChevronUp, Copy, Check } from "lucide-react";
|
| 10 |
import { CodeBlock } from "@/components/CodeBlock";
|
| 11 |
import { getApiUrl } from "@/lib/api-url";
|
| 12 |
|
|
@@ -32,11 +32,27 @@ export function PluginCard({ plugin }: PluginCardProps) {
|
|
| 32 |
const [copiedUrl, setCopiedUrl] = useState(false);
|
| 33 |
const [copiedRequestUrl, setCopiedRequestUrl] = useState(false);
|
| 34 |
|
|
|
|
|
|
|
|
|
|
| 35 |
const handleParamChange = (paramName: string, value: string) => {
|
| 36 |
setParamValues((prev) => ({ ...prev, [paramName]: value }));
|
| 37 |
};
|
| 38 |
|
| 39 |
const handleExecute = async () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
setLoading(true);
|
| 41 |
|
| 42 |
try {
|
|
@@ -123,8 +139,9 @@ export function PluginCard({ plugin }: PluginCardProps) {
|
|
| 123 |
<Select
|
| 124 |
value={paramValues[param.name] || ""}
|
| 125 |
onValueChange={(value) => handleParamChange(param.name, value)}
|
|
|
|
| 126 |
>
|
| 127 |
-
<SelectTrigger className="bg-black/50 border-white/10 text-white focus:border-purple-500">
|
| 128 |
<SelectValue placeholder={`Select ${param.name}...`} />
|
| 129 |
</SelectTrigger>
|
| 130 |
<SelectContent className="bg-zinc-900 border-white/10">
|
|
@@ -147,7 +164,8 @@ export function PluginCard({ plugin }: PluginCardProps) {
|
|
| 147 |
placeholder={param.example?.toString() || param.description}
|
| 148 |
value={paramValues[param.name] || ""}
|
| 149 |
onChange={(e) => handleParamChange(param.name, e.target.value)}
|
| 150 |
-
|
|
|
|
| 151 |
/>
|
| 152 |
);
|
| 153 |
}
|
|
@@ -210,7 +228,7 @@ export function PluginCard({ plugin }: PluginCardProps) {
|
|
| 210 |
};
|
| 211 |
|
| 212 |
return (
|
| 213 |
-
<Card className=
|
| 214 |
{/* Collapsible Header */}
|
| 215 |
<div
|
| 216 |
className="p-4 border-b border-white/10 cursor-pointer hover:bg-white/[0.02] transition-colors"
|
|
@@ -221,6 +239,21 @@ export function PluginCard({ plugin }: PluginCardProps) {
|
|
| 221 |
{plugin.method}
|
| 222 |
</Badge>
|
| 223 |
<code className="text-sm text-purple-400 font-mono flex-1 min-w-0 break-all">{plugin.endpoint}</code>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
<div className="flex items-center gap-1 flex-shrink-0">
|
| 225 |
<button
|
| 226 |
onClick={(e) => {
|
|
@@ -257,6 +290,27 @@ export function PluginCard({ plugin }: PluginCardProps) {
|
|
| 257 |
<h3 className="text-xl font-bold text-white mb-2">{plugin.name}</h3>
|
| 258 |
<p className="text-gray-400 text-sm leading-relaxed">{plugin.description || "No description provided"}</p>
|
| 259 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
{/* Tags */}
|
| 261 |
{plugin.tags && plugin.tags.length > 0 && (
|
| 262 |
<div className="flex flex-wrap gap-2 mt-3">
|
|
@@ -313,7 +367,6 @@ export function PluginCard({ plugin }: PluginCardProps) {
|
|
| 313 |
</tr>
|
| 314 |
</thead>
|
| 315 |
<tbody>
|
| 316 |
-
{/* Path Parameters */}
|
| 317 |
{plugin.parameters?.path?.map((param) => (
|
| 318 |
<tr key={param.name} className="border-b border-white/5">
|
| 319 |
<td className="py-3 pr-4 text-white font-mono">{param.name}</td>
|
|
@@ -333,7 +386,6 @@ export function PluginCard({ plugin }: PluginCardProps) {
|
|
| 333 |
<td className="py-3 text-gray-400">{param.description}</td>
|
| 334 |
</tr>
|
| 335 |
))}
|
| 336 |
-
{/* Query Parameters */}
|
| 337 |
{plugin.parameters?.query?.map((param) => (
|
| 338 |
<tr key={param.name} className="border-b border-white/5">
|
| 339 |
<td className="py-3 pr-4 text-white font-mono">{param.name}</td>
|
|
@@ -353,7 +405,6 @@ export function PluginCard({ plugin }: PluginCardProps) {
|
|
| 353 |
<td className="py-3 text-gray-400">{param.description}</td>
|
| 354 |
</tr>
|
| 355 |
))}
|
| 356 |
-
{/* Body Parameters */}
|
| 357 |
{plugin.parameters?.body?.map((param) => (
|
| 358 |
<tr key={param.name} className="border-b border-white/5">
|
| 359 |
<td className="py-3 pr-4 text-white font-mono">{param.name}</td>
|
|
@@ -439,7 +490,6 @@ export function PluginCard({ plugin }: PluginCardProps) {
|
|
| 439 |
{/* Parameters Input */}
|
| 440 |
{hasAnyParams ? (
|
| 441 |
<div className="space-y-4 mb-4">
|
| 442 |
-
{/* Query Parameters */}
|
| 443 |
{plugin.parameters?.query?.map((param) => (
|
| 444 |
<div key={param.name}>
|
| 445 |
<label className="block text-sm text-gray-300 mb-2">
|
|
@@ -457,7 +507,6 @@ export function PluginCard({ plugin }: PluginCardProps) {
|
|
| 457 |
</div>
|
| 458 |
))}
|
| 459 |
|
| 460 |
-
{/* Body Parameters */}
|
| 461 |
{plugin.parameters?.body?.map((param) => (
|
| 462 |
<div key={param.name}>
|
| 463 |
<label className="block text-sm text-gray-300 mb-2">
|
|
@@ -482,36 +531,51 @@ export function PluginCard({ plugin }: PluginCardProps) {
|
|
| 482 |
{/* Execute Button */}
|
| 483 |
<Button
|
| 484 |
onClick={handleExecute}
|
| 485 |
-
disabled={loading}
|
| 486 |
-
className="w-full bg-purple-500 hover:bg-purple-600 text-white py-6 text-base font-semibold"
|
| 487 |
>
|
| 488 |
<Play className="w-5 h-5 mr-2" />
|
| 489 |
-
{loading ? "Executing..." : "Execute"}
|
| 490 |
</Button>
|
| 491 |
|
| 492 |
{/* Response Display */}
|
| 493 |
{response && (
|
| 494 |
<div className="mt-6 space-y-4">
|
| 495 |
-
{/*
|
| 496 |
-
|
| 497 |
-
<div className="flex items-
|
| 498 |
-
<
|
| 499 |
-
<
|
| 500 |
-
|
| 501 |
-
className="text-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
<Check className="w-4 h-4 text-green-400" />
|
| 506 |
-
) : (
|
| 507 |
-
<Copy className="w-4 h-4" />
|
| 508 |
-
)}
|
| 509 |
-
</button>
|
| 510 |
</div>
|
| 511 |
-
|
| 512 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 513 |
</div>
|
| 514 |
-
|
| 515 |
|
| 516 |
{/* Response Status */}
|
| 517 |
<div className="flex items-center justify-between">
|
|
@@ -539,7 +603,7 @@ export function PluginCard({ plugin }: PluginCardProps) {
|
|
| 539 |
</div>
|
| 540 |
)}
|
| 541 |
|
| 542 |
-
{/* Response Body
|
| 543 |
<div>
|
| 544 |
<h5 className="text-sm text-gray-400 mb-2">Response Body</h5>
|
| 545 |
<CodeBlock
|
|
|
|
| 6 |
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
| 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 |
|
|
|
|
| 32 |
const [copiedUrl, setCopiedUrl] = useState(false);
|
| 33 |
const [copiedRequestUrl, setCopiedRequestUrl] = useState(false);
|
| 34 |
|
| 35 |
+
const isDisabled = plugin.disabled;
|
| 36 |
+
const isDeprecated = plugin.deprecated;
|
| 37 |
+
|
| 38 |
const handleParamChange = (paramName: string, value: string) => {
|
| 39 |
setParamValues((prev) => ({ ...prev, [paramName]: value }));
|
| 40 |
};
|
| 41 |
|
| 42 |
const handleExecute = async () => {
|
| 43 |
+
if (isDisabled) {
|
| 44 |
+
setResponse({
|
| 45 |
+
status: 503,
|
| 46 |
+
statusText: "Service Unavailable",
|
| 47 |
+
data: {
|
| 48 |
+
success: false,
|
| 49 |
+
message: "Plugin is disabled",
|
| 50 |
+
reason: plugin.disabledReason || "This plugin has been disabled"
|
| 51 |
+
}
|
| 52 |
+
});
|
| 53 |
+
return;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
setLoading(true);
|
| 57 |
|
| 58 |
try {
|
|
|
|
| 139 |
<Select
|
| 140 |
value={paramValues[param.name] || ""}
|
| 141 |
onValueChange={(value) => handleParamChange(param.name, value)}
|
| 142 |
+
disabled={isDisabled}
|
| 143 |
>
|
| 144 |
+
<SelectTrigger className="bg-black/50 border-white/10 text-white focus:border-purple-500 disabled:opacity-50">
|
| 145 |
<SelectValue placeholder={`Select ${param.name}...`} />
|
| 146 |
</SelectTrigger>
|
| 147 |
<SelectContent className="bg-zinc-900 border-white/10">
|
|
|
|
| 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 |
}
|
|
|
|
| 228 |
};
|
| 229 |
|
| 230 |
return (
|
| 231 |
+
<Card className={`bg-white/[0.02] border-white/10 overflow-hidden w-full ${isDisabled ? 'opacity-60' : ''}`}>
|
| 232 |
{/* Collapsible Header */}
|
| 233 |
<div
|
| 234 |
className="p-4 border-b border-white/10 cursor-pointer hover:bg-white/[0.02] transition-colors"
|
|
|
|
| 239 |
{plugin.method}
|
| 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" />
|
| 247 |
+
Disabled
|
| 248 |
+
</Badge>
|
| 249 |
+
)}
|
| 250 |
+
{isDeprecated && !isDisabled && (
|
| 251 |
+
<Badge className="bg-yellow-500/20 text-yellow-400 border-yellow-500/50 border flex items-center gap-1 flex-shrink-0">
|
| 252 |
+
<AlertTriangle className="w-3 h-3" />
|
| 253 |
+
Deprecated
|
| 254 |
+
</Badge>
|
| 255 |
+
)}
|
| 256 |
+
|
| 257 |
<div className="flex items-center gap-1 flex-shrink-0">
|
| 258 |
<button
|
| 259 |
onClick={(e) => {
|
|
|
|
| 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" />
|
| 297 |
+
<div>
|
| 298 |
+
<p className="text-sm font-semibold text-red-400">Plugin Disabled</p>
|
| 299 |
+
<p className="text-xs text-red-300 mt-1">{plugin.disabledReason}</p>
|
| 300 |
+
</div>
|
| 301 |
+
</div>
|
| 302 |
+
)}
|
| 303 |
+
|
| 304 |
+
{isDeprecated && !isDisabled && plugin.deprecatedReason && (
|
| 305 |
+
<div className="mt-3 p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg flex items-start gap-2">
|
| 306 |
+
<AlertTriangle className="w-5 h-5 text-yellow-400 flex-shrink-0 mt-0.5" />
|
| 307 |
+
<div>
|
| 308 |
+
<p className="text-sm font-semibold text-yellow-400">Plugin Deprecated</p>
|
| 309 |
+
<p className="text-xs text-yellow-300 mt-1">{plugin.deprecatedReason}</p>
|
| 310 |
+
</div>
|
| 311 |
+
</div>
|
| 312 |
+
)}
|
| 313 |
+
|
| 314 |
{/* Tags */}
|
| 315 |
{plugin.tags && plugin.tags.length > 0 && (
|
| 316 |
<div className="flex flex-wrap gap-2 mt-3">
|
|
|
|
| 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>
|
|
|
|
| 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>
|
|
|
|
| 405 |
<td className="py-3 text-gray-400">{param.description}</td>
|
| 406 |
</tr>
|
| 407 |
))}
|
|
|
|
| 408 |
{plugin.parameters?.body?.map((param) => (
|
| 409 |
<tr key={param.name} className="border-b border-white/5">
|
| 410 |
<td className="py-3 pr-4 text-white font-mono">{param.name}</td>
|
|
|
|
| 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 |
<label className="block text-sm text-gray-300 mb-2">
|
|
|
|
| 507 |
</div>
|
| 508 |
))}
|
| 509 |
|
|
|
|
| 510 |
{plugin.parameters?.body?.map((param) => (
|
| 511 |
<div key={param.name}>
|
| 512 |
<label className="block text-sm text-gray-300 mb-2">
|
|
|
|
| 531 |
{/* Execute Button */}
|
| 532 |
<Button
|
| 533 |
onClick={handleExecute}
|
| 534 |
+
disabled={loading || isDisabled}
|
| 535 |
+
className="w-full bg-purple-500 hover:bg-purple-600 text-white py-6 text-base font-semibold disabled:opacity-50 disabled:cursor-not-allowed"
|
| 536 |
>
|
| 537 |
<Play className="w-5 h-5 mr-2" />
|
| 538 |
+
{loading ? "Executing..." : isDisabled ? "Plugin Disabled" : "Execute"}
|
| 539 |
</Button>
|
| 540 |
|
| 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" />
|
| 548 |
+
<div>
|
| 549 |
+
<p className="text-sm font-semibold text-yellow-400">Deprecation Warning</p>
|
| 550 |
+
<p className="text-xs text-yellow-300 mt-1">
|
| 551 |
+
{responseHeaders['x-deprecation-reason'] || 'This plugin is deprecated'}
|
| 552 |
+
</p>
|
| 553 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 554 |
</div>
|
| 555 |
+
)}
|
| 556 |
+
|
| 557 |
+
{/* Request URL */}
|
| 558 |
+
{requestUrl && (
|
| 559 |
+
<div>
|
| 560 |
+
<div className="flex items-center justify-between mb-2">
|
| 561 |
+
<span className="text-sm text-gray-400">Request URL</span>
|
| 562 |
+
<button
|
| 563 |
+
onClick={copyRequestUrl}
|
| 564 |
+
className="text-gray-400 hover:text-white transition-colors p-1"
|
| 565 |
+
title="Copy Request URL"
|
| 566 |
+
>
|
| 567 |
+
{copiedRequestUrl ? (
|
| 568 |
+
<Check className="w-4 h-4 text-green-400" />
|
| 569 |
+
) : (
|
| 570 |
+
<Copy className="w-4 h-4" />
|
| 571 |
+
)}
|
| 572 |
+
</button>
|
| 573 |
+
</div>
|
| 574 |
+
<div className="bg-black/50 border border-white/10 rounded p-3 overflow-x-auto">
|
| 575 |
+
<code className="text-xs text-purple-300 break-all">{requestUrl}</code>
|
| 576 |
+
</div>
|
| 577 |
</div>
|
| 578 |
+
)}
|
| 579 |
|
| 580 |
{/* Response Status */}
|
| 581 |
<div className="flex items-center justify-between">
|
|
|
|
| 603 |
</div>
|
| 604 |
)}
|
| 605 |
|
| 606 |
+
{/* Response Body */}
|
| 607 |
<div>
|
| 608 |
<h5 className="text-sm text-gray-400 mb-2">Response Body</h5>
|
| 609 |
<CodeBlock
|
src/server/plugin-loader.ts
CHANGED
|
@@ -43,7 +43,7 @@ export class PluginLoader {
|
|
| 43 |
let reloadTimeout: NodeJS.Timeout | null = null;
|
| 44 |
|
| 45 |
this.watcher = watch(this.pluginsDir, {
|
| 46 |
-
ignored: /(^|[\/\\])\../,
|
| 47 |
persistent: true,
|
| 48 |
ignoreInitial: true,
|
| 49 |
awaitWriteFinish: {
|
|
@@ -87,7 +87,6 @@ export class PluginLoader {
|
|
| 87 |
try {
|
| 88 |
await this.scanDirectory(this.pluginsDir, newRouter);
|
| 89 |
|
| 90 |
-
// If successful, replace old router with new one
|
| 91 |
this.removeOldRouter();
|
| 92 |
this.router = newRouter;
|
| 93 |
this.app.use("/api", this.router);
|
|
@@ -109,7 +108,6 @@ export class PluginLoader {
|
|
| 109 |
if (!this.app) return;
|
| 110 |
|
| 111 |
try {
|
| 112 |
-
// Express 5 uses app._router differently
|
| 113 |
const stack = (this.app as any)._router?.stack || [];
|
| 114 |
|
| 115 |
for (let i = stack.length - 1; i >= 0; i--) {
|
|
@@ -119,7 +117,6 @@ export class PluginLoader {
|
|
| 119 |
}
|
| 120 |
}
|
| 121 |
} catch (error) {
|
| 122 |
-
// if _router structure is different, just log warning
|
| 123 |
console.warn("⚠️ Could not remove old router, continuing anyway...");
|
| 124 |
}
|
| 125 |
}
|
|
@@ -136,10 +133,6 @@ export class PluginLoader {
|
|
| 136 |
if (stat.isDirectory()) {
|
| 137 |
this.clearModuleCache(fullPath);
|
| 138 |
} else if (stat.isFile() && (extname(item) === ".ts" || extname(item) === ".js")) {
|
| 139 |
-
// In ES modules, we can't clear cache like CommonJS.
|
| 140 |
-
// Hot Reload also doesn't seem to have any effect on API serving.
|
| 141 |
-
// For now, Just log and mark as reload, We have to restart the server in "development" mode.
|
| 142 |
-
// TODO: Find another way. If hot reloading doesn't work, try restarting automatically.
|
| 143 |
const relativePath = relative(process.cwd(), fullPath);
|
| 144 |
console.log(`♻️ Marked for reload: ${relativePath}`);
|
| 145 |
}
|
|
@@ -207,6 +200,17 @@ export class PluginLoader {
|
|
| 207 |
return;
|
| 208 |
}
|
| 209 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
const metadataValidation = this.isValidPluginMetadata(handler, fileName);
|
| 211 |
const shouldShowInDocs = metadataValidation.valid;
|
| 212 |
|
|
@@ -222,7 +226,26 @@ export class PluginLoader {
|
|
| 222 |
const primaryEndpoint = basePath ? `${basePath}/${primaryAlias}` : `/${primaryAlias}`;
|
| 223 |
const method = handler.method.toLowerCase() as "get" | "post" | "put" | "delete" | "patch";
|
| 224 |
|
|
|
|
| 225 |
const wrappedExec = async (req: any, res: any, next: any) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
try {
|
| 227 |
await handler.exec(req, res, next);
|
| 228 |
} catch (error) {
|
|
@@ -241,7 +264,9 @@ export class PluginLoader {
|
|
| 241 |
for (const alias of handler.alias) {
|
| 242 |
const endpoint = basePath ? `${basePath}/${alias}` : `/${alias}`;
|
| 243 |
router[method](endpoint, wrappedExec);
|
| 244 |
-
|
|
|
|
|
|
|
| 245 |
}
|
| 246 |
|
| 247 |
if (shouldShowInDocs) {
|
|
@@ -260,7 +285,11 @@ export class PluginLoader {
|
|
| 260 |
headers: [],
|
| 261 |
path: []
|
| 262 |
},
|
| 263 |
-
responses: handler.responses || {}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
};
|
| 265 |
|
| 266 |
this.pluginRegistry[primaryEndpoint] = { handler, metadata };
|
|
|
|
| 43 |
let reloadTimeout: NodeJS.Timeout | null = null;
|
| 44 |
|
| 45 |
this.watcher = watch(this.pluginsDir, {
|
| 46 |
+
ignored: /(^|[\/\\])\../,
|
| 47 |
persistent: true,
|
| 48 |
ignoreInitial: true,
|
| 49 |
awaitWriteFinish: {
|
|
|
|
| 87 |
try {
|
| 88 |
await this.scanDirectory(this.pluginsDir, newRouter);
|
| 89 |
|
|
|
|
| 90 |
this.removeOldRouter();
|
| 91 |
this.router = newRouter;
|
| 92 |
this.app.use("/api", this.router);
|
|
|
|
| 108 |
if (!this.app) return;
|
| 109 |
|
| 110 |
try {
|
|
|
|
| 111 |
const stack = (this.app as any)._router?.stack || [];
|
| 112 |
|
| 113 |
for (let i = stack.length - 1; i >= 0; i--) {
|
|
|
|
| 117 |
}
|
| 118 |
}
|
| 119 |
} catch (error) {
|
|
|
|
| 120 |
console.warn("⚠️ Could not remove old router, continuing anyway...");
|
| 121 |
}
|
| 122 |
}
|
|
|
|
| 133 |
if (stat.isDirectory()) {
|
| 134 |
this.clearModuleCache(fullPath);
|
| 135 |
} else if (stat.isFile() && (extname(item) === ".ts" || extname(item) === ".js")) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
const relativePath = relative(process.cwd(), fullPath);
|
| 137 |
console.log(`♻️ Marked for reload: ${relativePath}`);
|
| 138 |
}
|
|
|
|
| 200 |
return;
|
| 201 |
}
|
| 202 |
|
| 203 |
+
if (handler.disabled) {
|
| 204 |
+
const reason = handler.disabledReason || "This plugin has been disabled";
|
| 205 |
+
console.log(`🚫 Plugin '${handler.name}' is disabled: ${reason}`);
|
| 206 |
+
// Still register it but with disabled flag
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
if (handler.deprecated) {
|
| 210 |
+
const reason = handler.deprecatedReason || "This plugin is deprecated and may be removed in future versions";
|
| 211 |
+
console.warn(`⚠️ Plugin '${handler.name}' is deprecated: ${reason}`);
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
const metadataValidation = this.isValidPluginMetadata(handler, fileName);
|
| 215 |
const shouldShowInDocs = metadataValidation.valid;
|
| 216 |
|
|
|
|
| 226 |
const primaryEndpoint = basePath ? `${basePath}/${primaryAlias}` : `/${primaryAlias}`;
|
| 227 |
const method = handler.method.toLowerCase() as "get" | "post" | "put" | "delete" | "patch";
|
| 228 |
|
| 229 |
+
// Wrap exec function to handle disabled/deprecated plugins
|
| 230 |
const wrappedExec = async (req: any, res: any, next: any) => {
|
| 231 |
+
// If plugin is disabled, return error response
|
| 232 |
+
if (handler.disabled) {
|
| 233 |
+
const reason = handler.disabledReason || "This plugin has been disabled";
|
| 234 |
+
return res.status(503).json({
|
| 235 |
+
success: false,
|
| 236 |
+
message: "Plugin is disabled",
|
| 237 |
+
reason: reason,
|
| 238 |
+
plugin: handler.name || 'unknown',
|
| 239 |
+
});
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
// If plugin is deprecated, add warning header
|
| 243 |
+
if (handler.deprecated) {
|
| 244 |
+
const reason = handler.deprecatedReason || "This plugin is deprecated and may be removed in future versions";
|
| 245 |
+
res.setHeader('X-Plugin-Deprecated', 'true');
|
| 246 |
+
res.setHeader('X-Deprecation-Reason', reason);
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
try {
|
| 250 |
await handler.exec(req, res, next);
|
| 251 |
} catch (error) {
|
|
|
|
| 264 |
for (const alias of handler.alias) {
|
| 265 |
const endpoint = basePath ? `${basePath}/${alias}` : `/${alias}`;
|
| 266 |
router[method](endpoint, wrappedExec);
|
| 267 |
+
|
| 268 |
+
const statusIcon = handler.disabled ? '🚫' : handler.deprecated ? '⚠️' : '✓';
|
| 269 |
+
console.log(`${statusIcon} [${handler.method}] ${endpoint} -> ${handler.name || 'unnamed'}`);
|
| 270 |
}
|
| 271 |
|
| 272 |
if (shouldShowInDocs) {
|
|
|
|
| 285 |
headers: [],
|
| 286 |
path: []
|
| 287 |
},
|
| 288 |
+
responses: handler.responses || {},
|
| 289 |
+
disabled: handler.disabled,
|
| 290 |
+
deprecated: handler.deprecated,
|
| 291 |
+
disabledReason: handler.disabledReason,
|
| 292 |
+
deprecatedReason: handler.deprecatedReason
|
| 293 |
};
|
| 294 |
|
| 295 |
this.pluginRegistry[primaryEndpoint] = { handler, metadata };
|
src/server/plugins/anime/komiku_get_details.js
CHANGED
|
@@ -9,6 +9,8 @@ const handler = {
|
|
| 9 |
category: ["komiku"],
|
| 10 |
alias: ["getDetail"],
|
| 11 |
tags: ["comic"],
|
|
|
|
|
|
|
| 12 |
parameters: {
|
| 13 |
query: [
|
| 14 |
{
|
|
|
|
| 9 |
category: ["komiku"],
|
| 10 |
alias: ["getDetail"],
|
| 11 |
tags: ["comic"],
|
| 12 |
+
disabled: true,
|
| 13 |
+
disabledReason: "Komiku server is temporarily unavailable, Maybe this feature will not be removed until the server is back.",
|
| 14 |
parameters: {
|
| 15 |
query: [
|
| 16 |
{
|
src/server/plugins/anime/komiku_get_manga.js
CHANGED
|
@@ -9,6 +9,8 @@ const handler = {
|
|
| 9 |
category: ["komiku"],
|
| 10 |
alias: ["getLatestManga"],
|
| 11 |
tags: ["comic"],
|
|
|
|
|
|
|
| 12 |
responses: {
|
| 13 |
200: {
|
| 14 |
status: 200,
|
|
|
|
| 9 |
category: ["komiku"],
|
| 10 |
alias: ["getLatestManga"],
|
| 11 |
tags: ["comic"],
|
| 12 |
+
disabled: true,
|
| 13 |
+
disabledReason: "Komiku server is temporarily unavailable, Maybe this feature will not be removed until the server is back.",
|
| 14 |
responses: {
|
| 15 |
200: {
|
| 16 |
status: 200,
|
src/server/plugins/anime/komiku_get_manhua.js
CHANGED
|
@@ -9,6 +9,8 @@ const handler = {
|
|
| 9 |
category: ["komiku"],
|
| 10 |
alias: ["getLatestManhua"],
|
| 11 |
tags: ["comic"],
|
|
|
|
|
|
|
| 12 |
responses: {
|
| 13 |
200: {
|
| 14 |
status: 200,
|
|
|
|
| 9 |
category: ["komiku"],
|
| 10 |
alias: ["getLatestManhua"],
|
| 11 |
tags: ["comic"],
|
| 12 |
+
disabled: true,
|
| 13 |
+
disabledReason: "Komiku server is temporarily unavailable, Maybe this feature will not be removed until the server is back.",
|
| 14 |
responses: {
|
| 15 |
200: {
|
| 16 |
status: 200,
|
src/server/plugins/anime/komiku_get_manhwa.js
CHANGED
|
@@ -9,6 +9,8 @@ const handler = {
|
|
| 9 |
category: ["komiku"],
|
| 10 |
alias: ["getLatestManhwa"],
|
| 11 |
tags: ["comic"],
|
|
|
|
|
|
|
| 12 |
responses: {
|
| 13 |
200: {
|
| 14 |
status: 200,
|
|
|
|
| 9 |
category: ["komiku"],
|
| 10 |
alias: ["getLatestManhwa"],
|
| 11 |
tags: ["comic"],
|
| 12 |
+
disabled: true,
|
| 13 |
+
disabledReason: "Komiku server is temporarily unavailable, Maybe this feature will not be removed until the server is back.",
|
| 14 |
responses: {
|
| 15 |
200: {
|
| 16 |
status: 200,
|
src/server/plugins/anime/komiku_read_chapter.js
CHANGED
|
@@ -9,6 +9,8 @@ const handler = {
|
|
| 9 |
category: ["komiku"],
|
| 10 |
alias: ["readChapter"],
|
| 11 |
tags: ["comic"],
|
|
|
|
|
|
|
| 12 |
parameters: {
|
| 13 |
query: [
|
| 14 |
{
|
|
|
|
| 9 |
category: ["komiku"],
|
| 10 |
alias: ["readChapter"],
|
| 11 |
tags: ["comic"],
|
| 12 |
+
disabled: true,
|
| 13 |
+
disabledReason: "Komiku server is temporarily unavailable, Maybe this feature will not be removed until the server is back.",
|
| 14 |
parameters: {
|
| 15 |
query: [
|
| 16 |
{
|
src/server/plugins/anime/komiku_search.js
CHANGED
|
@@ -9,6 +9,8 @@ const handler = {
|
|
| 9 |
category: ["komiku"],
|
| 10 |
alias: ["search"],
|
| 11 |
tags: ["comic"],
|
|
|
|
|
|
|
| 12 |
parameters: {
|
| 13 |
query: [
|
| 14 |
{
|
|
|
|
| 9 |
category: ["komiku"],
|
| 10 |
alias: ["search"],
|
| 11 |
tags: ["comic"],
|
| 12 |
+
disabled: true,
|
| 13 |
+
disabledReason: "Komiku server is temporarily unavailable, Maybe this feature will not be removed until the server is back.",
|
| 14 |
parameters: {
|
| 15 |
query: [
|
| 16 |
{
|
src/server/types/plugin.ts
CHANGED
|
@@ -27,17 +27,21 @@ export interface PluginParameters {
|
|
| 27 |
export interface ApiPluginHandler {
|
| 28 |
name: string;
|
| 29 |
description: string;
|
| 30 |
-
version
|
| 31 |
category: string[];
|
| 32 |
method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
|
| 33 |
alias: string[];
|
| 34 |
-
tags?: string[];
|
| 35 |
parameters?: PluginParameters;
|
| 36 |
responses?: {
|
| 37 |
[statusCode: number]: PluginResponse;
|
| 38 |
};
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
}
|
| 42 |
|
| 43 |
export interface PluginMetadata {
|
|
@@ -53,6 +57,10 @@ export interface PluginMetadata {
|
|
| 53 |
responses?: {
|
| 54 |
[statusCode: number]: PluginResponse;
|
| 55 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
}
|
| 57 |
|
| 58 |
export interface PluginRegistry {
|
|
@@ -60,13 +68,4 @@ export interface PluginRegistry {
|
|
| 60 |
handler: ApiPluginHandler;
|
| 61 |
metadata: PluginMetadata;
|
| 62 |
};
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
export interface ApiResponse<T = any> {
|
| 66 |
-
status: number;
|
| 67 |
-
message?: string;
|
| 68 |
-
author?: string;
|
| 69 |
-
note?: string;
|
| 70 |
-
results?: T;
|
| 71 |
-
error?: string;
|
| 72 |
}
|
|
|
|
| 27 |
export interface ApiPluginHandler {
|
| 28 |
name: string;
|
| 29 |
description: string;
|
| 30 |
+
version?: string;
|
| 31 |
category: string[];
|
| 32 |
method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
|
| 33 |
alias: string[];
|
| 34 |
+
tags?: string[];
|
| 35 |
parameters?: PluginParameters;
|
| 36 |
responses?: {
|
| 37 |
[statusCode: number]: PluginResponse;
|
| 38 |
};
|
| 39 |
+
disabled?: boolean;
|
| 40 |
+
deprecated?: boolean;
|
| 41 |
+
disabledReason?: string;
|
| 42 |
+
deprecatedReason?: string;
|
| 43 |
+
|
| 44 |
+
exec: (req: Request, res: Response, next: NextFunction) => Promise<void> | void;
|
| 45 |
}
|
| 46 |
|
| 47 |
export interface PluginMetadata {
|
|
|
|
| 57 |
responses?: {
|
| 58 |
[statusCode: number]: PluginResponse;
|
| 59 |
};
|
| 60 |
+
disabled?: boolean;
|
| 61 |
+
deprecated?: boolean;
|
| 62 |
+
disabledReason?: string;
|
| 63 |
+
deprecatedReason?: string;
|
| 64 |
}
|
| 65 |
|
| 66 |
export interface PluginRegistry {
|
|
|
|
| 68 |
handler: ApiPluginHandler;
|
| 69 |
metadata: PluginMetadata;
|
| 70 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
}
|