New features: TG Telegram robot terminal [One-click configuration] and [Subscription conversion]

This commit is contained in:
心隨緣動 2025-10-08 00:01:25 +08:00 committed by GitHub
parent 6bc349b4cf
commit 41152d745e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -1,3 +1,4 @@
{{ template "page/head_start" .}}
<style>
.ant-table:not(.ant-table-expanded-row .ant-table) {
@ -728,7 +729,7 @@
</a-form-item>
<p><b>{{ i18n "pages.inbounds.oneClick.descriptionTitle" }}</b></p>
<p>{{ i18n "pages.inbounds.oneClick.descriptionContent" }}</p>
<p style="white-space: pre-wrap;">{{ i18n "pages.inbounds.oneClick.descriptionContent" }}</p>
</div>
<div v-else style="padding: 24px;">
@ -1187,6 +1188,39 @@
}
},
// 【新增函数】: 专门用于调用后端 API 放行端口的方法
async openPort(port) {
if (!port || port <= 0) {
this.$message.error('无效端口号,无法放行。');
return false;
}
const loadingMessage = this.$message.loading(`正在向后端发送放行端口 ${port} 的指令...`, 0);
let success = true;
try {
// 〔中文注释〕: 调用后端API它现在会立刻返回一个表示“指令已发送”的JSON。
const msg = await HttpUtil.postForm('/panel/api/server/openPort', { port: port });
if (msg.success) {
// 〔中文注释〕: 将提示信息修改为更准确的“指令已发送”,因为任务正在后台运行。
this.$message.success(`端口 ${port} 放行指令已发送,后台执行中!`);
} else {
success = false;
// 〔中文注释〕: 如果API调用本身就失败了例如后端服务挂了则显示错误。
this.$message.error(`发送端口 ${port} 放行指令失败: ${msg.msg || '未知错误,请检查后端日志。'}`);
}
} catch (err) {
success = false;
console.error(`请求放行端口 ${port} 失败:`, err);
// 〔中文注释〕: 网络错误或返回了非JSON内容例如502网关错误会在这里捕获。
this.$message.error(`请求放行端口 ${port} 失败,请检查网络或后端服务状态。 ${err.message}`);
} finally {
loadingMessage(); // 无论成功或失败,都关闭加载提示
}
return success;
},
// 〔中文注释〕: 新增一个专门用于生成“一键配置”二维码的方法
generateOneClickQrCode(link) {
// 〔中文注释〕: this.$nextTick 是一个 Vue 特有的功能,它会等待 DOM 更新完成后再执行内部的代码。
@ -1301,6 +1335,13 @@
// 随机生成 10000-55535 之间的端口
const port = Math.floor(Math.random() * (55535 - 10000 + 1)) + 10000;
// 【新增代码】: 自动调用 openPort 接口放行端口
const portOpenSuccess = await this.openPort(port);
if (!portOpenSuccess) {
// 【不中断流程,给出警告提示】
this.$message.warning(`**注意:** 自动放行端口 ${port} 失败,请务必手动到您的 **VPS 防火墙**(例如 ufw、security group放行该端口否则入站将无法连接。`);
}
// 【中文注释】: 从预设列表中随机选择一个SNI
const randomDest = this.realityDestinations[Math.floor(Math.random() * this.realityDestinations.length)];
@ -1427,6 +1468,13 @@
// 随机生成2053, 2083, 2087, 2096, 8443这5个端口中的其中一个
const allowedPorts = [2053, 2083, 2087, 2096, 8443];
const port = allowedPorts[Math.floor(Math.random() * allowedPorts.length)];
// 【新增代码】: 自动调用 openPort 接口放行端口
const portOpenSuccess = await this.openPort(port);
if (!portOpenSuccess) {
// 【不中断流程,给出警告提示】
this.$message.warning(`**注意:** 自动放行端口 ${port} 失败,请务必手动到您的 **VPS 防火墙**(例如 ufw、security group放行该端口否则入站将无法连接。`);
}
// 随机生成一个8位大小写字母的路径
const path = "/" + Array.from({ length: 8 }, () =>
@ -1577,8 +1625,20 @@
}
},
// 【中文注释】: 处理“订阅转换”按钮的点击事件 (已更新逻辑)
// 〔中文注释〕: 处理“订阅转换”按钮的点击事件 (已更新为完整逻辑)
handleSubscriptionConversion() {
// 【第一步】: 检查当前协议是否为 HTTPS
if (window.location.protocol !== "https:") {
this.$warning({
title: '必须使用 HTTPS 访问',
content: '【订阅转换】功能需要一个安全的 HTTPS 环境才能运行。请先为您的面板域名正确配置 SSL 证书,然后再使用此功能。',
class: themeSwitcher.currentTheme,
});
return; // 如果是 http则终止后续所有操作
}
// 【第二步】:弹出确认框,询问用户是否继续
this.$confirm({
title: '{{ i18n "pages.inbounds.subConversion.modalTitle"}}',
content: '{{ i18n "pages.inbounds.subConversion.modalContent"}}',
@ -1587,29 +1647,150 @@
class: themeSwitcher.currentTheme,
onOk: () => {
const subConverterUrl = `https://${window.location.hostname}:15268`;
const loadingMessage = this.$message.loading('正在检测订阅转换服务...', 0);
const loadingMessage = this.$message.loading('正在检测订阅转换服务 (运行端口15268)...', 0);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 3000);
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5秒超时检测
// 【第三步】: 尝试通过 fetch API 访问订阅转换服务端口
fetch(subConverterUrl, {
method: 'GET',
mode: 'no-cors',
mode: 'no-cors', // 〔中文注释〕: 使用 no-cors 模式进行简单的网络连通性检查
signal: controller.signal
})
.then(response => {
// 【情况 A】: 访问成功,服务正在运行
clearTimeout(timeoutId);
loadingMessage();
this.$message.success('服务检测成功,正在跳转...', 2);
window.open(subConverterUrl, '_blank');
loadingMessage(); // 关闭加载提示
this.$message.success('服务检测成功,正在为您跳转...', 2);
window.open(subConverterUrl, '_blank'); // 在新标签页中打开
})
.catch(err => {
// 【情况 B】: 访问失败 (例如超时或连接被拒绝)
clearTimeout(timeoutId);
loadingMessage();
this.$info({
loadingMessage(); // 关闭加载提示
// 〔中文注释〕: 弹出新的确认框,提供“一键安装”选项
this.$confirm({
title: '{{ i18n "pages.inbounds.subConversion.notFoundTitle"}}',
content: '{{ i18n "pages.inbounds.subConversion.notFoundContent"}}',
content: `无法连接到订阅转换服务。这可能意味着它尚未安装或未在运行。您想现在远程执行安装指令吗?
【重要提示】: 执行前,请确保您服务器的防火墙已放行 8000 和 15268 这两个端口。`,
okText: '立即安装',
cancelText: '稍后再说',
class: themeSwitcher.currentTheme,
onOk: async () => {
// 〔中文注释〕:从这里开始是全新的轮询检测逻辑
// 确保 loginUrl 被定义,用于成功提示
const subConverterUrl = `https://${window.location.hostname}:15268`;
const loginUrl = subConverterUrl;
const portsToOpen = [8000, 15268];
let openPortFailed = false; // 用于标记是否有端口放行失败
// 【新增逻辑】:先尝试放行 8000 和 15268 端口 (不中断流程)
const openPortLoading = this.$message.loading(`正在尝试自动放行端口 ${portsToOpen.join(' 和 ')}...`, 0);
// 使用 Promise.allSettled 确保所有端口放行尝试完成后才继续
const openPortResults = await Promise.allSettled(
portsToOpen.map(port => this.openPort(port))
);
openPortLoading(); // 关闭放行加载提示
openPortResults.forEach(result => {
// 检查每个放行操作是否失败 (openPort 返回 false)
if (result.status === 'fulfilled' && result.value === false) {
openPortFailed = true;
}
// Promise.allSettled 的 rejected 状态表明 openPort 内部抛出异常 (如网络错误),也视为放行失败
if (result.status === 'rejected') {
openPortFailed = true;
}
});
if (openPortFailed) {
// 使用非模态、自动关闭的 this.$message.warning
this.$message.warning({
content: `端口自动放行失败。**安装流程将继续**,但请您务必进入 VPS 手动放行 8000 和 15268 端口。`,
duration: 10, // 持续 10 秒后自动关闭
key: 'portOpenWarning', // 可选指定一个key防止重复弹出
});
}
// 1. 先向后端发送安装指令
const installLoading = this.$message.loading('正在向后端发送安装指令...', 0);
try {
const msg = await HttpUtil.post('/panel/api/server/install/subconverter');
installLoading(); // 指令发送成功后,关闭这个短暂的加载提示
if (!msg.success) {
this.$error({
title: '指令发送失败',
content: msg.msg || '发生未知错误,请检查后端日志。',
class: themeSwitcher.currentTheme,
});
return;
}
} catch (error) {
installLoading();
this.$error({
title: '前端请求失败',
content: '无法连接到后端API接口请检查您的网络连接或后端服务是否正常运行。',
class: themeSwitcher.currentTheme,
});
return;
}
// 2. 指令发送成功后,创建并显示一个用于等待服务启动的模态框
const h = this.$createElement;
const modal = this.$info({
title: '⏳ 正在安装并启动服务...',
content: '安装指令已发送后台正在执行安装和启动。请耐心等待预计40秒内自动给出安装成功提示。',
class: themeSwitcher.currentTheme,
okText: '后台运行',
okButtonProps: {
props: {
loading: true // 让按钮一直处于加载状态
}
},
});
// 3. 核心妥协:使用固定延时代替不可靠的前端轮询
const SERVICE_STARTUP_WAIT_TIME = 40000; // 等待 40 秒钟
setTimeout(() => {
// 延时结束后,强制更新模态框为成功状态
modal.update({
title: '✅ 安装成功!',
// 使用 VNode 来创建指定的提示内容
content: h('div', { style: 'white-space: pre-wrap; text-align: left;' }, [
h('p', { style: 'font-weight: bold; font-size: 16px; margin-bottom: 10px;'}, '🎉 恭喜!【订阅转换】模块已成功安装!'),
h('p', '您现在可以使用以下地址访问 Web 界面:'),
h('p', [
h('strong', '🔗 登录地址: '),
h('a', {
attrs: {
href: loginUrl,
target: '_blank'
},
style: 'font-family: monospace; font-weight: bold;'
}, loginUrl)
]),
// 突出显示用户名和密码
h('p', [ '默认用户名: ', h('strong', 'admin') ]),
h('p', [ '默认 密码: ', h('strong', '123456') ]),
h('p', { style: 'font-size: 12px; color: #888; margin-top: 10px;'}, '可登录订阅转换后台修改您的密码!'),
h('p', { style: 'margin-top: 10px; font-style: italic; color: #ff4d4f;' }, '---------------------------------------'), // 使用红色分隔线
h('p', { style: 'font-style: italic; color: #ff4d4f;' }, '【重要提示】:若您无法访问上述地址,请进入 VPS 中手动放行 8000 和 15268 端口。') // 红色字体
]),
okText: '完成',
okButtonProps: {
props: {
loading: false
}
},
});
}, SERVICE_STARTUP_WAIT_TIME);
},
});
});
},