Ferndiagnose (MCP) erweitert: Live-Provider-Laufzeitzustand + Conversion-Log
Motiviert durch die Mega-Debrid-Cooldown-Diagnose: der account-weite Cooldown musste aus Log-Arithmetik rekonstruiert werden (alle Fehlerzeilen zeigten ms-genau auf dieselbe Deadline). Genau diese Sicht-Luecke wird geschlossen. Neu: - GET /providers (debrid.ts getProviderRuntimeSnapshot): Live-Laufzeitzustand pro Mega-Account / Debrid-Link-Key — AKTIVER Cooldown (untilMs/remainingMs/Grund/ Kategorie/untilRestart), in-flight-Tiefe, Mega-Rotationscursor + Sticky-Count, Empty-Response-Streaks, Debrid-Link Key-/Host-Cooldowns + Runtime-Status. Account- Keys sind nicht-umkehrbare Hashes (mda_<fnv1a64>), keine Logins/Tokens. - GET /logs/conversion: der pro-Item Link-Aufloesungs-Lebenszyklus (Token, API getLink, Web, Account-Rotation, Abbrueche mit Zeiten) aus conversion.log. - Beide auch im /diagnostics-Aggregat (providers + logs.conversion) -> rd_diagnostics zeigt die Cooldowns jetzt direkt. - MCP-Bridge: neues Tool rd_providers + "conversion" in der rd_logs-Enum + README. Test: getProviderRuntimeSnapshot spiegelt einen geprimten Account-Cooldown (until/remaining/Grund) und ist vorher null. Suite 935 gruen, tsc 6.
This commit is contained in:
parent
471e40b87f
commit
4543ac3c1a
@ -423,6 +423,106 @@ export function getMegaDebridAccountCooldownState(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProviderRuntimeCooldown {
|
||||||
|
untilMs: number;
|
||||||
|
remainingMs: number;
|
||||||
|
message: string;
|
||||||
|
category: string;
|
||||||
|
untilRestart?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProviderRuntimeSnapshot {
|
||||||
|
capturedAtMs: number;
|
||||||
|
megaDebrid: {
|
||||||
|
rotationCursor: number;
|
||||||
|
stickyCount: number;
|
||||||
|
accounts: Array<{
|
||||||
|
key: string;
|
||||||
|
cooldown: ProviderRuntimeCooldown | null;
|
||||||
|
inFlight: number;
|
||||||
|
emptyResponseStreak: number;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
debridLink: {
|
||||||
|
keys: Array<{
|
||||||
|
keyId: string;
|
||||||
|
cooldown: ProviderRuntimeCooldown | null;
|
||||||
|
runtimeStatus: { state: string; detail: string; updatedAt: number } | null;
|
||||||
|
}>;
|
||||||
|
hostCooldowns: Array<{ key: string; cooldown: ProviderRuntimeCooldown }>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProviderRuntimeSnapshot(now = Date.now()): ProviderRuntimeSnapshot {
|
||||||
|
const megaKeys = new Set<string>([
|
||||||
|
...megaDebridAccountCooldowns.keys(),
|
||||||
|
...megaDebridInFlight.keys(),
|
||||||
|
...megaDebridEmptyResponseStreaks.keys()
|
||||||
|
]);
|
||||||
|
const megaAccounts = [...megaKeys].sort().map((key) => {
|
||||||
|
const detail = megaDebridAccountCooldowns.get(key);
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
cooldown: detail
|
||||||
|
? {
|
||||||
|
untilMs: detail.until,
|
||||||
|
remainingMs: Math.max(0, detail.until - now),
|
||||||
|
message: detail.message,
|
||||||
|
category: detail.category,
|
||||||
|
untilRestart: detail.untilRestart === true
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
inFlight: megaDebridInFlight.get(key) ?? 0,
|
||||||
|
emptyResponseStreak: megaDebridEmptyResponseStreaks.get(key) ?? 0
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const dlKeyIds = new Set<string>([
|
||||||
|
...debridLinkKeyCooldowns.keys(),
|
||||||
|
...debridLinkKeyRuntimeStatuses.keys()
|
||||||
|
]);
|
||||||
|
const dlKeys = [...dlKeyIds].sort().map((keyId) => {
|
||||||
|
const until = Number(debridLinkKeyCooldowns.get(keyId) || 0);
|
||||||
|
const detail = debridLinkKeyCooldownDetails.get(keyId);
|
||||||
|
const status = debridLinkKeyRuntimeStatuses.get(keyId) || null;
|
||||||
|
return {
|
||||||
|
keyId,
|
||||||
|
cooldown: until > 0
|
||||||
|
? {
|
||||||
|
untilMs: until,
|
||||||
|
remainingMs: Math.max(0, until - now),
|
||||||
|
message: detail?.message ?? "",
|
||||||
|
category: detail?.category ?? "temporary"
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
runtimeStatus: status ? { state: status.state, detail: status.detail, updatedAt: status.updatedAt } : null
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const dlHostCooldowns = [...debridLinkKeyHostCooldowns].map(([key, until]) => {
|
||||||
|
const detail = debridLinkKeyHostCooldownDetails.get(key);
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
cooldown: {
|
||||||
|
untilMs: until,
|
||||||
|
remainingMs: Math.max(0, until - now),
|
||||||
|
message: detail?.message ?? "",
|
||||||
|
category: detail?.category ?? "temporary"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
capturedAtMs: now,
|
||||||
|
megaDebrid: {
|
||||||
|
rotationCursor: megaDebridRotationCursor,
|
||||||
|
stickyCount: megaDebridStickyCount,
|
||||||
|
accounts: megaAccounts
|
||||||
|
},
|
||||||
|
debridLink: { keys: dlKeys, hostCooldowns: dlHostCooldowns }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const LINKSNAPPY_API_BASE = "https://linksnappy.com/api";
|
const LINKSNAPPY_API_BASE = "https://linksnappy.com/api";
|
||||||
|
|
||||||
const PROVIDER_LABELS: Record<DebridProvider, string> = {
|
const PROVIDER_LABELS: Record<DebridProvider, string> = {
|
||||||
|
|||||||
@ -15,6 +15,8 @@ import { createStoragePaths, loadHistory, loadSettings } from "./storage";
|
|||||||
import { buildAccountSummary, buildRedactedSettingsPayload, buildStatsPayload, summarizeHistoryEntry } from "./support-data";
|
import { buildAccountSummary, buildRedactedSettingsPayload, buildStatsPayload, summarizeHistoryEntry } from "./support-data";
|
||||||
import { buildSupportBundle, getSupportBundleDefaultFileName } from "./support-bundle";
|
import { buildSupportBundle, getSupportBundleDefaultFileName } from "./support-bundle";
|
||||||
import { getTraceConfig, getTraceConfigPath, getTraceLogPath, logTraceEvent, setTraceEnabled, updateTraceConfig } from "./trace-log";
|
import { getTraceConfig, getTraceConfigPath, getTraceLogPath, logTraceEvent, setTraceEnabled, updateTraceConfig } from "./trace-log";
|
||||||
|
import { getConversionLogPath } from "./conversion-trace";
|
||||||
|
import { getProviderRuntimeSnapshot } from "./debrid";
|
||||||
import { getWindowsHostDiagnostics } from "./windows-host-diagnostics";
|
import { getWindowsHostDiagnostics } from "./windows-host-diagnostics";
|
||||||
import type { DownloadManager } from "./download-manager";
|
import type { DownloadManager } from "./download-manager";
|
||||||
import type { DownloadItem, PackageEntry, UiSnapshot } from "../shared/types";
|
import type { DownloadItem, PackageEntry, UiSnapshot } from "../shared/types";
|
||||||
@ -43,12 +45,14 @@ const DEBUG_ENDPOINTS: DebugEndpointDescriptor[] = [
|
|||||||
{ method: "GET", path: "/logs/rename", queryExample: "lines=100&grep=keyword", description: "Reads the dedicated rename and MKV move log." },
|
{ method: "GET", path: "/logs/rename", queryExample: "lines=100&grep=keyword", description: "Reads the dedicated rename and MKV move log." },
|
||||||
{ method: "GET", path: "/logs/trace", queryExample: "lines=100&grep=keyword", description: "Reads the optional support trace log." },
|
{ method: "GET", path: "/logs/trace", queryExample: "lines=100&grep=keyword", description: "Reads the optional support trace log." },
|
||||||
{ method: "GET", path: "/logs/session", queryExample: "lines=100&grep=keyword", description: "Reads the session log tail." },
|
{ method: "GET", path: "/logs/session", queryExample: "lines=100&grep=keyword", description: "Reads the session log tail." },
|
||||||
|
{ method: "GET", path: "/logs/conversion", queryExample: "lines=100&grep=keyword", description: "Reads the per-item link conversion/unrestrict lifecycle log (token, API getLink, web, account rotation, aborts with timings)." },
|
||||||
{ method: "GET", path: "/logs/package", queryExample: "package=Release&lines=100&grep=keyword", description: "Reads the package log for a specific package name or id." },
|
{ method: "GET", path: "/logs/package", queryExample: "package=Release&lines=100&grep=keyword", description: "Reads the package log for a specific package name or id." },
|
||||||
{ method: "GET", path: "/logs/item", queryExample: "item=episode.part2.rar&lines=100&grep=keyword", description: "Reads the item log for a specific file name or item id." },
|
{ method: "GET", path: "/logs/item", queryExample: "item=episode.part2.rar&lines=100&grep=keyword", description: "Reads the item log for a specific file name or item id." },
|
||||||
{ method: "GET", path: "/errors", queryExample: "level=ERROR&limit=100", description: "Returns the in-memory ring of the most recent WARN/ERROR log lines." },
|
{ method: "GET", path: "/errors", queryExample: "level=ERROR&limit=100", description: "Returns the in-memory ring of the most recent WARN/ERROR log lines." },
|
||||||
{ method: "GET", path: "/trace/config", queryExample: "enable=1¬e=support&durationMinutes=120", description: "Reads or updates the support trace configuration." },
|
{ method: "GET", path: "/trace/config", queryExample: "enable=1¬e=support&durationMinutes=120", description: "Reads or updates the support trace configuration." },
|
||||||
{ method: "GET", path: "/settings", description: "Returns a redacted settings snapshot without raw secrets." },
|
{ method: "GET", path: "/settings", description: "Returns a redacted settings snapshot without raw secrets." },
|
||||||
{ method: "GET", path: "/accounts", description: "Returns a redacted account/provider configuration summary." },
|
{ method: "GET", path: "/accounts", description: "Returns a redacted account/provider configuration summary." },
|
||||||
|
{ method: "GET", path: "/providers", description: "Live provider runtime state: per-account/key cooldowns (until/remaining/reason/category), in-flight depth, Mega rotation cursor, empty-response streaks. The 'why is it cooling down right now' view." },
|
||||||
{ method: "GET", path: "/stats", description: "Returns live session stats plus persisted all-time totals." },
|
{ method: "GET", path: "/stats", description: "Returns live session stats plus persisted all-time totals." },
|
||||||
{ method: "GET", path: "/history", queryExample: "limit=50&status=completed", description: "Returns history entries with optional filters." },
|
{ method: "GET", path: "/history", queryExample: "limit=50&status=completed", description: "Returns history entries with optional filters." },
|
||||||
{ method: "GET", path: "/status", description: "Returns a live high-level status overview." },
|
{ method: "GET", path: "/status", description: "Returns a live high-level status overview." },
|
||||||
@ -356,6 +360,7 @@ function buildAiManifest(baseDir: string): Record<string, unknown> {
|
|||||||
"Call /meta first to confirm the server is reachable and to re-read the endpoint list.",
|
"Call /meta first to confirm the server is reachable and to re-read the endpoint list.",
|
||||||
"Use /self-check or /debug/setup to quickly verify whether token, host, manifest, trace, disk space, and log sizes are in a good support state.",
|
"Use /self-check or /debug/setup to quickly verify whether token, host, manifest, trace, disk space, and log sizes are in a good support state.",
|
||||||
"Use /diagnostics for an overview, then drill into /logs/item, /logs/package, /logs/rename, /status, /packages, /items, /settings, /accounts, /stats, /history, or /logs/trace.",
|
"Use /diagnostics for an overview, then drill into /logs/item, /logs/package, /logs/rename, /status, /packages, /items, /settings, /accounts, /stats, /history, or /logs/trace.",
|
||||||
|
"For provider stalls/cooldowns, call /providers for the live cooldown state (until/remaining/reason per account/key) and /logs/conversion for the per-item resolve lifecycle (token, API, web, rotation, aborts with timings).",
|
||||||
"If a full handoff is needed, download /support/bundle as a ZIP."
|
"If a full handoff is needed, download /support/bundle as a ZIP."
|
||||||
],
|
],
|
||||||
auth: {
|
auth: {
|
||||||
@ -707,6 +712,25 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pathname === "/logs/conversion") {
|
||||||
|
const count = normalizeLinesParam(url.searchParams.get("lines"), 100);
|
||||||
|
const grep = url.searchParams.get("grep") || "";
|
||||||
|
const logPath = getConversionLogPath();
|
||||||
|
const lines = logPath ? filterLines(readLogTailFromFile(logPath, count), grep) : [];
|
||||||
|
jsonResponse(res, 200, {
|
||||||
|
path: logPath,
|
||||||
|
available: Boolean(logPath),
|
||||||
|
lines,
|
||||||
|
count: lines.length
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathname === "/providers") {
|
||||||
|
jsonResponse(res, 200, getProviderRuntimeSnapshot());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (pathname === "/trace/config") {
|
if (pathname === "/trace/config") {
|
||||||
const patch: Record<string, unknown> = {};
|
const patch: Record<string, unknown> = {};
|
||||||
const enabled = toBooleanQuery(url.searchParams.get("enable"));
|
const enabled = toBooleanQuery(url.searchParams.get("enable"));
|
||||||
@ -992,6 +1016,7 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
|
|||||||
settings: buildRedactedSettingsPayload(readSupportSettings()),
|
settings: buildRedactedSettingsPayload(readSupportSettings()),
|
||||||
stats: buildStatsPayload(snapshot),
|
stats: buildStatsPayload(snapshot),
|
||||||
accounts: buildAccountSummary(readSupportSettings()),
|
accounts: buildAccountSummary(readSupportSettings()),
|
||||||
|
providers: getProviderRuntimeSnapshot(),
|
||||||
history: {
|
history: {
|
||||||
total: readSupportHistory().length,
|
total: readSupportHistory().length,
|
||||||
recent: readSupportHistory()
|
recent: readSupportHistory()
|
||||||
@ -1023,6 +1048,10 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
|
|||||||
path: sessionLogPath,
|
path: sessionLogPath,
|
||||||
lines: filterLines(readLogTailFromFile(sessionLogPath, lineCount), grep)
|
lines: filterLines(readLogTailFromFile(sessionLogPath, lineCount), grep)
|
||||||
},
|
},
|
||||||
|
conversion: {
|
||||||
|
path: getConversionLogPath(),
|
||||||
|
lines: getConversionLogPath() ? filterLines(readLogTailFromFile(getConversionLogPath() as string, lineCount), grep) : []
|
||||||
|
},
|
||||||
package: selectedPackage ? {
|
package: selectedPackage ? {
|
||||||
path: packageLogPath,
|
path: packageLogPath,
|
||||||
lines: filterLines(readLogTailFromFile(packageLogPath, lineCount), grep)
|
lines: filterLines(readLogTailFromFile(packageLogPath, lineCount), grep)
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { parseDebridLinkApiKeys } from "../src/shared/debrid-link-keys";
|
|||||||
import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts";
|
import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts";
|
||||||
import { getProviderUsageDayKey } from "../src/shared/provider-daily-limits";
|
import { getProviderUsageDayKey } from "../src/shared/provider-daily-limits";
|
||||||
import { isMegaDebridTransientResolveFailure } from "../src/shared/mega-debrid-errors";
|
import { isMegaDebridTransientResolveFailure } from "../src/shared/mega-debrid-errors";
|
||||||
import { classifyMegaDebridAccountFailureForTests, clearMegaDebridEmptyResponseStreak, DebridService, extractRapidgatorFilenameFromHtml, fetchAllDebridHostInfo, fetchDebridLinkHostLimits, filenameFromRapidgatorUrlPath, getDebridLinkKeyCooldownStateForTests, getDebridLinkKeyRuntimeStateForTests, getMegaDebridAccountCooldownState, leadProviderChainWith, MEGA_DEBRID_EMPTY_STREAK_UNTIL_RESTART, MEGA_DEBRID_STICKY_LINKS, normalizeResolvedFilename, primeMegaDebridUntilRestartForTests, recordMegaDebridEmptyResponseStreak, resetDebridLinkRuntimeStateForTests, resetMegaDebridRuntimeStateForTests } from "../src/main/debrid";
|
import { classifyMegaDebridAccountFailureForTests, clearMegaDebridEmptyResponseStreak, DebridService, extractRapidgatorFilenameFromHtml, fetchAllDebridHostInfo, fetchDebridLinkHostLimits, filenameFromRapidgatorUrlPath, getDebridLinkKeyCooldownStateForTests, getDebridLinkKeyRuntimeStateForTests, getMegaDebridAccountCooldownState, getProviderRuntimeSnapshot, leadProviderChainWith, MEGA_DEBRID_EMPTY_STREAK_UNTIL_RESTART, MEGA_DEBRID_STICKY_LINKS, normalizeResolvedFilename, primeMegaDebridRuntimeCooldownForTests, primeMegaDebridUntilRestartForTests, recordMegaDebridEmptyResponseStreak, resetDebridLinkRuntimeStateForTests, resetMegaDebridRuntimeStateForTests } from "../src/main/debrid";
|
||||||
|
|
||||||
const originalFetch = globalThis.fetch;
|
const originalFetch = globalThis.fetch;
|
||||||
|
|
||||||
@ -2071,6 +2071,24 @@ describe("debrid service", () => {
|
|||||||
expect(calls).toBeGreaterThanOrEqual(1);
|
expect(calls).toBeGreaterThanOrEqual(1);
|
||||||
}, 20000);
|
}, 20000);
|
||||||
|
|
||||||
|
it("getProviderRuntimeSnapshot surfaces a live Mega-Debrid account cooldown (until/remaining/reason) for the diagnostics endpoint", () => {
|
||||||
|
const accId = getMegaDebridAccountId("user");
|
||||||
|
const key = `${accId}:web`;
|
||||||
|
expect(getProviderRuntimeSnapshot().megaDebrid.accounts.find((a) => a.key === key)?.cooldown ?? null).toBeNull();
|
||||||
|
|
||||||
|
primeMegaDebridRuntimeCooldownForTests(key, 90_000, "Abbruch/Timeout nach 60s");
|
||||||
|
|
||||||
|
const snap = getProviderRuntimeSnapshot();
|
||||||
|
expect(typeof snap.capturedAtMs).toBe("number");
|
||||||
|
const acc = snap.megaDebrid.accounts.find((a) => a.key === key);
|
||||||
|
expect(acc).toBeTruthy();
|
||||||
|
expect(acc!.cooldown).not.toBeNull();
|
||||||
|
expect(acc!.cooldown!.remainingMs).toBeGreaterThan(0);
|
||||||
|
expect(acc!.cooldown!.remainingMs).toBeLessThanOrEqual(90_000);
|
||||||
|
expect(acc!.cooldown!.untilMs).toBeGreaterThan(snap.capturedAtMs);
|
||||||
|
expect(acc!.cooldown!.message).toContain("Abbruch");
|
||||||
|
});
|
||||||
|
|
||||||
it("single Mega-Debrid account: a long Web abort parks only the slow link and does NOT freeze the sole account", async () => {
|
it("single Mega-Debrid account: a long Web abort parks only the slow link and does NOT freeze the sole account", async () => {
|
||||||
process.env.RD_MEGA_ABORT_MIN_RUN_MS = "0";
|
process.env.RD_MEGA_ABORT_MIN_RUN_MS = "0";
|
||||||
const settings = {
|
const settings = {
|
||||||
|
|||||||
@ -29,8 +29,10 @@ Without env config, every tool simply takes a `code` argument.
|
|||||||
|
|
||||||
## Tools
|
## Tools
|
||||||
|
|
||||||
`rd_servers`, `rd_ping`, `rd_diagnostics`, `rd_status`, `rd_items`, `rd_packages`, `rd_errors`, `rd_logs`,
|
`rd_servers`, `rd_ping`, `rd_diagnostics`, `rd_status`, `rd_items`, `rd_packages`, `rd_errors`, `rd_logs`
|
||||||
`rd_history`, `rd_accounts`, `rd_host`, `rd_self_check`, `rd_get` (raw escape-hatch, any read-only path).
|
(`main|audit|rename|trace|session|conversion|package|item`), `rd_history`, `rd_accounts`, `rd_providers`
|
||||||
|
(live per-account/key cooldown + in-flight + rotation state), `rd_host`, `rd_self_check`,
|
||||||
|
`rd_get` (raw escape-hatch, any read-only path).
|
||||||
|
|
||||||
Each tool accepts `code` or `server` to pick the target.
|
Each tool accepts `code` or `server` to pick the target.
|
||||||
|
|
||||||
|
|||||||
@ -219,6 +219,7 @@ const LOG_PATHS = {
|
|||||||
rename: "/logs/rename",
|
rename: "/logs/rename",
|
||||||
trace: "/logs/trace",
|
trace: "/logs/trace",
|
||||||
session: "/logs/session",
|
session: "/logs/session",
|
||||||
|
conversion: "/logs/conversion",
|
||||||
package: "/logs/package",
|
package: "/logs/package",
|
||||||
item: "/logs/item"
|
item: "/logs/item"
|
||||||
};
|
};
|
||||||
@ -227,10 +228,10 @@ server.registerTool(
|
|||||||
"rd_logs",
|
"rd_logs",
|
||||||
{
|
{
|
||||||
title: "Log lesen",
|
title: "Log lesen",
|
||||||
description: "Liest das Ende eines Logs (GET /logs/<name>). name: main|audit|rename|trace|session|package|item. Fuer package/item zusaetzlich package/item angeben.",
|
description: "Liest das Ende eines Logs (GET /logs/<name>). name: main|audit|rename|trace|session|conversion|package|item. conversion = Pro-Item Link-Aufloesungs-Lebenszyklus (Token, API, Web, Rotation, Abbrueche mit Zeiten). Fuer package/item zusaetzlich package/item angeben.",
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
...CODE_FIELD,
|
...CODE_FIELD,
|
||||||
name: z.enum(["main", "audit", "rename", "trace", "session", "package", "item"]).describe("Welches Log."),
|
name: z.enum(["main", "audit", "rename", "trace", "session", "conversion", "package", "item"]).describe("Welches Log."),
|
||||||
lines: z.number().int().positive().optional().describe("Anzahl Zeilen vom Ende (Default 100)."),
|
lines: z.number().int().positive().optional().describe("Anzahl Zeilen vom Ende (Default 100)."),
|
||||||
grep: z.string().optional().describe("Filter."),
|
grep: z.string().optional().describe("Filter."),
|
||||||
package: z.string().optional().describe("Nur fuer name=package."),
|
package: z.string().optional().describe("Nur fuer name=package."),
|
||||||
@ -268,6 +269,16 @@ server.registerTool(
|
|||||||
async (args) => requestTool(args, "/accounts", {})
|
async (args) => requestTool(args, "/accounts", {})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
server.registerTool(
|
||||||
|
"rd_providers",
|
||||||
|
{
|
||||||
|
title: "Provider-Laufzeitzustand",
|
||||||
|
description: "Live Provider-Runtime (GET /providers): pro Mega-Account/Debrid-Link-Key der AKTIVE Cooldown (until/remainingMs/Grund/Kategorie), in-flight-Tiefe, Mega-Rotationscursor, Empty-Response-Streaks. Die 'warum kuehlt es JETZT ab'-Ansicht — beantwortet Cooldown-Fragen direkt statt aus Log-Arithmetik.",
|
||||||
|
inputSchema: { ...CODE_FIELD }
|
||||||
|
},
|
||||||
|
async (args) => requestTool(args, "/providers", {})
|
||||||
|
);
|
||||||
|
|
||||||
server.registerTool(
|
server.registerTool(
|
||||||
"rd_host",
|
"rd_host",
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user