Skip to content

Commit 88bb490

Browse files
committed
feat(panel/adapter): plugin adapter auto-discovery + proxy from env
- src/utils/pluginBase.ts: add PanelSettingsAdapter, PanelSettingField, PanelFieldType types - src/utils/pluginManager.ts: add getPluginPanelAdapters() for dynamic discovery - src/utils/panel/settingsRegistry.ts: registerPluginPanelAdapters() for auto-registration - src/utils/panel/controller.ts: call registerPluginPanelAdapters() on startup; remove hardcoded providers - src/utils/panel/httpServer.ts: /api/settings merges plugin adapters - plugins/checkin.ts, music.ts, tts.ts, zpr.ts, parsehub.ts, codex_image.ts: add panelAdapter export - src/utils/runtimeManager.ts: getProxyFromEnv() for teleproto client from HTTP_PROXY/HTTPS_PROXY/ALL_PROXY/SOCKS_PROXY - src/utils/panel/botService.ts: getProxyAgent() for telegraf using http-proxy-agent/socks-proxy-agent - tsconfig.json: fix for TS 5.x compatibility - src/utils/errorHelpers.ts: new utility - src/utils/htmlEscape.ts: new utility Panel settings page now auto-discovers plugin configs — zero hardcoding.
1 parent b99e2e1 commit 88bb490

26 files changed

Lines changed: 6399 additions & 174 deletions

‎src/utils/errorHelpers.ts‎

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* Type-safe error handling utilities.
3+
*
4+
* These helpers replace `catch (error: any)` patterns with
5+
* `catch (error: unknown)` while still providing convenient access
6+
* to error message and code properties.
7+
*/
8+
9+
/**
10+
* Extract the message from an unknown caught error.
11+
* Safely handles Error objects, strings, and other types.
12+
*/
13+
export function getErrorMessage(error: unknown): string {
14+
if (error instanceof Error) return error.message;
15+
if (typeof error === "string") return error;
16+
if (
17+
error !== null &&
18+
error !== undefined &&
19+
typeof error === "object" &&
20+
"message" in error &&
21+
typeof (error as { message: unknown }).message === "string"
22+
) {
23+
return (error as { message: string }).message;
24+
}
25+
return String(error);
26+
}
27+
28+
/**
29+
* Extract the code from an unknown caught error (e.g., Node.js errno codes).
30+
*/
31+
export function getErrorCode(error: unknown): string | undefined {
32+
if (
33+
error !== null &&
34+
error !== undefined &&
35+
typeof error === "object" &&
36+
"code" in error &&
37+
typeof (error as { code: unknown }).code === "string"
38+
) {
39+
return (error as { code: string }).code;
40+
}
41+
return undefined;
42+
}
43+
44+
/**
45+
* Narrow an unknown caught value to an Error instance.
46+
* Returns the Error if it is one, otherwise wraps it in a new Error.
47+
*/
48+
export function toError(error: unknown): Error {
49+
if (error instanceof Error) return error;
50+
return new Error(getErrorMessage(error));
51+
}
52+
53+
/**
54+
* Extract stdout/stderr from a child_process.exec error (or any object
55+
* that carries those fields). Returns { stdout, stderr } with empty
56+
* strings for missing fields.
57+
*/
58+
export function getExecErrorOutput(error: unknown): {
59+
stdout: string;
60+
stderr: string;
61+
message: string;
62+
} {
63+
if (error !== null && error !== undefined && typeof error === "object") {
64+
const obj = error as Record<string, unknown>;
65+
return {
66+
stdout: typeof obj.stdout === "string" ? obj.stdout : String(obj.stdout ?? "").trim(),
67+
stderr: typeof obj.stderr === "string" ? obj.stderr : String(obj.stderr ?? "").trim(),
68+
message: getErrorMessage(error),
69+
};
70+
}
71+
return { stdout: "", stderr: "", message: getErrorMessage(error) };
72+
}

‎src/utils/panel/botService.ts‎

Lines changed: 71 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,62 @@ function buildOpenKeyboard(baseUrl: string) {
2323
]);
2424
}
2525

26+
/**
27+
* Get HTTP/HTTPS agent with proxy from environment variables.
28+
* Supports HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, and SOCKS_PROXY.
29+
*/
30+
function getProxyAgent(): any | undefined {
31+
// Check standard env vars (case-insensitive)
32+
const httpProxy = process.env.HTTP_PROXY || process.env.http_proxy;
33+
const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy;
34+
const allProxy = process.env.ALL_PROXY || process.env.all_proxy;
35+
const socksProxy = process.env.SOCKS_PROXY || process.env.socks_proxy;
36+
37+
// For HTTPS requests (Telegram Bot API uses HTTPS), prefer HTTPS_PROXY
38+
const proxyUrl = httpsProxy || httpProxy || allProxy || socksProxy;
39+
if (!proxyUrl) return undefined;
40+
41+
try {
42+
const url = new URL(proxyUrl);
43+
const host = url.hostname;
44+
const port = Number(url.port);
45+
const username = url.username ? decodeURIComponent(url.username) : undefined;
46+
const password = url.password ? decodeURIComponent(url.password) : undefined;
47+
48+
if (!host || !port) {
49+
logger.warn("[panel-bot] 环境变量代理 URL 格式无效,缺少 host 或 port:", proxyUrl);
50+
return undefined;
51+
}
52+
53+
const protocol = url.protocol.toLowerCase();
54+
55+
// For telegraf, we need an HTTP/HTTPS agent
56+
// SOCKS proxies need special handling (e.g., socks-proxy-agent package)
57+
if (protocol === "socks5:" || protocol === "socks4:") {
58+
logger.warn(
59+
"[panel-bot] SOCKS 代理需要 socks-proxy-agent 包支持,当前仅支持 HTTP/HTTPS 代理。建议配置 HTTP_PROXY/HTTPS_PROXY 环境变量。"
60+
);
61+
return undefined;
62+
}
63+
64+
if (protocol === "http:" || protocol === "https:") {
65+
const auth = username && password ? `${username}:${password}@` : "";
66+
const proxyAddress = `${protocol}//${auth}${host}:${port}`;
67+
68+
// Use http-proxy-agent for HTTP/HTTPS proxy
69+
const { HttpProxyAgent } = require("http-proxy-agent");
70+
71+
return new HttpProxyAgent(proxyAddress);
72+
}
73+
74+
logger.warn("[panel-bot] 不支持的代理协议:", protocol);
75+
return undefined;
76+
} catch (e: unknown) {
77+
logger.warn("[panel-bot] 解析环境变量代理失败:", e);
78+
return undefined;
79+
}
80+
}
81+
2682
export function isBotRunning(): boolean {
2783
return !!bot;
2884
}
@@ -43,7 +99,14 @@ export async function startPanelBot(): Promise<void> {
4399
}
44100
await stopPanelBot();
45101

46-
const instance = new Telegraf(cfg.botToken);
102+
const proxyAgent = getProxyAgent();
103+
const instance = new Telegraf(cfg.botToken, {
104+
telegram: proxyAgent
105+
? {
106+
agent: proxyAgent,
107+
}
108+
: undefined,
109+
});
47110
const token = cfg.botToken;
48111

49112
instance.start(async (ctx) => {
@@ -54,7 +117,7 @@ export async function startPanelBot(): Promise<void> {
54117
const latest = await readPanelConfig();
55118
if (!gate.allowed) {
56119
await ctx.reply(
57-
"⛔ 你没有 TeleBox Panel 管理权限。\n请联系 owner 使用 `.panel admin add <userid>` 授权。",
120+
"⛔ 你没有 TeleBox Panel 管理权限。\n请联系 owner 使用 `.panel admin add <userid>` 授权。"
58121
);
59122
return;
60123
}
@@ -63,7 +126,7 @@ export async function startPanelBot(): Promise<void> {
63126
"⚠️ Panel 已启用,但未设置公网 HTTPS 地址。\n" +
64127
"请在 userbot 中执行:\n" +
65128
"`.panel url https://你的域名`\n" +
66-
"Telegram 小程序要求公网 HTTPS。",
129+
"Telegram 小程序要求公网 HTTPS。"
67130
);
68131
return;
69132
}
@@ -75,7 +138,7 @@ export async function startPanelBot(): Promise<void> {
75138
{
76139
parse_mode: "HTML",
77140
...buildOpenKeyboard(latest.publicBaseUrl),
78-
},
141+
}
79142
);
80143
} catch (e: unknown) {
81144
logger.error("[panel-bot] /start failed", e);
@@ -112,7 +175,7 @@ export async function startPanelBot(): Promise<void> {
112175
`panelAdmin: ${gate.allowed ? "yes" : "no"}`,
113176
`isOwner: ${gate.isOwner ? "yes" : "no"}`,
114177
`ownerId: ${ownerId ?? "unknown"}`,
115-
].join("\n"),
178+
].join("\n")
116179
);
117180
});
118181

@@ -127,7 +190,7 @@ export async function startPanelBot(): Promise<void> {
127190
},
128191
(e: unknown) => {
129192
logger.error("[panel-bot] launch failed", e);
130-
},
193+
}
131194
);
132195

133196
bot = instance;
@@ -172,5 +235,5 @@ function escapeHtml(s: string): string {
172235
.replace(/&/g, "&")
173236
.replace(/</g, "<")
174237
.replace(/>/g, ">")
175-
.replace(/"/g, '"');
176-
}
238+
.replace(/"/g, """);
239+
}

‎src/utils/panel/controller.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ import {
1515
registerAiPanelProviders,
1616
unregisterAiPanelProviders,
1717
} from "./aiPanelProviders";
18+
import {
19+
registerPluginPanelAdapters,
20+
unregisterPluginPanelAdapters,
21+
} from "./settingsRegistry";
1822
import { startTunnel, stopTunnel, getTunnelUrl, isTunnelRunning } from "./cloudflareTunnel";
1923

2024
let providersReady = false;
@@ -24,13 +28,15 @@ export function ensurePanelProviders(): void {
2428
if (providersReady) return;
2529
registerBuiltinPanelProviders();
2630
registerAiPanelProviders();
31+
registerPluginPanelAdapters();
2732
providersReady = true;
2833
}
2934

3035
export function teardownPanelProviders(): void {
3136
if (!providersReady) return;
3237
unregisterBuiltinPanelProviders();
3338
unregisterAiPanelProviders();
39+
unregisterPluginPanelAdapters();
3440
providersReady = false;
3541
}
3642

‎src/utils/panel/httpServer.ts‎

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import path from "path";
1010
import { URL } from "url";
1111
import { logger } from "@utils/logger";
1212
import { readDisplayVersion } from "@utils/teleboxInfoHelper";
13-
import { listCommands } from "@utils/pluginManager";
13+
import { listCommands, getPluginPanelAdapters } from "@utils/pluginManager";
1414
import {
1515
readPanelConfig,
1616
updatePanelConfig,
@@ -382,11 +382,33 @@ async function routeApi(
382382

383383
// ---- Settings hooks ----
384384
if (method === "GET" && p === "/api/settings") {
385+
// Merge built-in providers + plugin-provided adapters
385386
// Only show settings for plugins that are currently loaded/installed
386387
const loadedPlugins = await help.listLoadedPlugins();
387-
const loadedPluginNames = new Set(loadedPlugins.map(p => p.name));
388+
const loadedPluginNames = new Set(loadedPlugins.map((p) => p.name));
388389

389-
const list = listPanelSettingsProviders()
390+
// Get plugin adapters (auto-discovered from loaded plugins)
391+
const pluginAdapters = getPluginPanelAdapters();
392+
393+
// Build merged list: built-in providers + plugin adapters
394+
const builtinProviders = listPanelSettingsProviders();
395+
const allProviders = [...builtinProviders];
396+
397+
// Add plugin adapters as virtual providers
398+
for (const adapter of pluginAdapters) {
399+
allProviders.push({
400+
id: adapter.id,
401+
title: adapter.title,
402+
description: adapter.description,
403+
category: adapter.category || "插件配置",
404+
icon: adapter.icon || "⚙️",
405+
getSchema: adapter.getSchema,
406+
getValues: adapter.getValues,
407+
setValues: adapter.setValues,
408+
} as typeof builtinProviders[0]);
409+
}
410+
411+
const list = allProviders
390412
.filter((x) => {
391413
// System plugins always show
392414
if (x.category === "系统") return true;

0 commit comments

Comments
 (0)