v25.9.17
v25.9.17
This commit is contained in:
parent
5451570d43
commit
6fd483f648
@ -6,9 +6,28 @@
|
|||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
// 【新增】: 确保表格标题不换行
|
||||||
|
.ant-table-thead > tr > th {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
.dark .ant-table:not(.ant-table-expanded-row .ant-table) {
|
.dark .ant-table:not(.ant-table-expanded-row .ant-table) {
|
||||||
outline-color: var(--dark-color-table-ring);
|
outline-color: var(--dark-color-table-ring);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-modal-body .ant-input-group.ant-input-group-compact {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-body .ant-input-group.ant-input-group-compact .ant-input-affix-wrapper,
|
||||||
|
.ant-modal-body .ant-input-group.ant-input-group-compact .ant-input-search-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-body .ant-input-group.ant-input-group-compact .ant-input {
|
||||||
|
white-space: normal;
|
||||||
|
height: auto;
|
||||||
|
min-height: 80px; /* 调整此值以控制显示行数 */
|
||||||
|
}
|
||||||
.ant-table .ant-table-content .ant-table-scroll .ant-table-body {
|
.ant-table .ant-table-content .ant-table-scroll .ant-table-body {
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
@ -273,12 +292,18 @@
|
|||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
</a-menu>
|
</a-menu>
|
||||||
</a-dropdown>
|
</a-dropdown>
|
||||||
|
<a-button type="primary" icon="thunderbolt" @click="openOneClickConfig">
|
||||||
|
<template v-if="!isMobile">{{ i18n "pages.inbounds.oneClickConfig" }}</template>
|
||||||
|
</a-button>
|
||||||
|
<a-button type="primary" icon="link" @click="handleSubscriptionConversion">
|
||||||
|
<template v-if="!isMobile">{{ i18n "pages.inbounds.is_subConversion" }}</template>
|
||||||
|
</a-button>
|
||||||
</a-space>
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<a-button-group>
|
<a-button-group>
|
||||||
<a-button icon="sync" @click="manualRefresh" :loading="refreshing"></a-button>
|
<a-button icon="sync" @click="manualRefresh" :loading="refreshing"></a-button>
|
||||||
<a-popover placement="bottomRight" trigger="click" :overlay-class-name="themeSwitcher.currentTheme">
|
<a-popover placement="bottomRight" trigger="click" :overlay-class-name="themeSwitcher.currentTheme">
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="ant-custom-popover-title">
|
<div class="ant-custom-popover-title">
|
||||||
<a-switch v-model="isRefreshEnabled" @change="toggleRefresh" size="small"></a-switch>
|
<a-switch v-model="isRefreshEnabled" @change="toggleRefresh" size="small"></a-switch>
|
||||||
@ -667,11 +692,76 @@
|
|||||||
</template>
|
</template>
|
||||||
</a-table>
|
</a-table>
|
||||||
</a-space>
|
</a-space>
|
||||||
</a-card>
|
</a-card>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
</transition>
|
</transition>
|
||||||
</a-spin>
|
</a-spin>
|
||||||
|
<a-modal
|
||||||
|
:title="`{{ i18n "pages.inbounds.oneClick.title" }}`"
|
||||||
|
:visible="oneClickModalVisible"
|
||||||
|
:ok-text="`{{ i18n "pages.inbounds.confirmCreate" }}`"
|
||||||
|
:cancel-text="`{{ i18n "cancel" }}`"
|
||||||
|
:class="themeSwitcher.currentTheme"
|
||||||
|
@ok="handleOneClickOk"
|
||||||
|
@cancel="handleOneClickCancel"
|
||||||
|
>
|
||||||
|
<a-tabs default-active-key="1">
|
||||||
|
<a-tab-pane key="1" :tab="`{{ i18n "pages.inbounds.oneClick.generateTab" }}`">
|
||||||
|
<div v-if="!inboundLink" style="padding: 24px;">
|
||||||
|
<p>{{ i18n "pages.inbounds.oneClick.prompt" }}</p>
|
||||||
|
|
||||||
|
<a-form-item :label="`{{ i18n "pages.inbounds.oneClick.presetType" }}`">
|
||||||
|
<a-button @click="handleOneClickOk('vless_reality')" block style="margin-bottom: 10px;">
|
||||||
|
{{ i18n "pages.inbounds.oneClick.preset.vless_reality" }}
|
||||||
|
</a-button>
|
||||||
|
<a-button @click="handleOneClickOk('vless_tls_encryption')" block style="margin-bottom: 10px;">
|
||||||
|
{{ i18n "pages.inbounds.oneClick.preset.vless_tls_encryption" }}
|
||||||
|
</a-button>
|
||||||
|
<a-button @click="handleOneClickOk('switch_vision_seed')" disabled block>
|
||||||
|
{{ i18n "pages.inbounds.oneClick.preset.switch_vision_seed_dev" }}
|
||||||
|
</a-button>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<p><b>{{ i18n "pages.inbounds.oneClick.descriptionTitle" }}</b></p>
|
||||||
|
<p>{{ i18n "pages.inbounds.oneClick.descriptionContent" }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<p><b>{{ i18n "pages.inbounds.oneClick.linkCreated" }}</b></p>
|
||||||
|
<a-input-search
|
||||||
|
:value="inboundLink"
|
||||||
|
read-only
|
||||||
|
:enter-button="`{{ i18n "copy" }}`"
|
||||||
|
@search="copyLink(inboundLink)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a-spin>
|
||||||
|
</a-tab-pane>
|
||||||
|
|
||||||
|
<a-tab-pane v-if="linkHistory.length > 0" key="2" :tab="`{{ i18n "pages.inbounds.oneClick.historyTab" }}`">
|
||||||
|
<div style="padding: 24px;">
|
||||||
|
<a-table
|
||||||
|
:dataSource="linkHistory"
|
||||||
|
:pagination="false"
|
||||||
|
:columns="linkHistoryColumns"
|
||||||
|
rowKey="time"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.dataIndex === 'link'">
|
||||||
|
<a-input-search
|
||||||
|
:value="record.link"
|
||||||
|
read-only
|
||||||
|
:enter-button="`{{ i18n "copy" }}`"
|
||||||
|
@search="copyLink(record.link)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</div>
|
||||||
|
</a-tab-pane>
|
||||||
|
</a-tabs>
|
||||||
|
</a-modal>
|
||||||
</a-layout-content>
|
</a-layout-content>
|
||||||
</a-layout>
|
</a-layout>
|
||||||
</a-layout>
|
</a-layout>
|
||||||
@ -733,7 +823,7 @@
|
|||||||
dataIndex: "deviceLimit",
|
dataIndex: "deviceLimit",
|
||||||
align: "center",
|
align: "center",
|
||||||
width: 40,
|
width: 40,
|
||||||
customRender: (text) => (text > 0 ? text : i18n("pages.inbounds.unlimited")),
|
customRender: (text) => (text > 0 ? text : {{ i18n "pages.inbounds.unlimited" }}),
|
||||||
}, {
|
}, {
|
||||||
title: '{{ i18n "pages.inbounds.traffic" }}',
|
title: '{{ i18n "pages.inbounds.traffic" }}',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
@ -776,14 +866,14 @@
|
|||||||
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
|
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
|
||||||
{ title: '{{ i18n "pages.inbounds.traffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'traffic' } },
|
{ title: '{{ i18n "pages.inbounds.traffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'traffic' } },
|
||||||
{ title: '{{ i18n "pages.inbounds.allTimeTraffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'allTime' } },
|
{ title: '{{ i18n "pages.inbounds.allTimeTraffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'allTime' } },
|
||||||
|
|
||||||
<!-- 中文注释: 在子表格中新增“限速”列 -->
|
// 中文注释: 在子表格中新增“限速”列
|
||||||
{
|
{
|
||||||
title: '{{ i18n "pages.inbounds.speedLimit" }}',
|
title: '{{ i18n "pages.inbounds.speedLimit" }}',
|
||||||
width: 80,
|
width: 80,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
dataIndex: 'speedLimit',
|
dataIndex: 'speedLimit',
|
||||||
customRender: (text) => (text > 0 ? `${text} KB/s` : i18n("pages.inbounds.unlimited")),
|
customRender: (text) => (text > 0 ? `${text} KB/s` : {{ i18n "pages.inbounds.unlimited" }}),
|
||||||
},
|
},
|
||||||
|
|
||||||
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 80, align: 'center', scopedSlots: { customRender: 'expiryTime' } },
|
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 80, align: 'center', scopedSlots: { customRender: 'expiryTime' } },
|
||||||
@ -834,7 +924,62 @@
|
|||||||
showAlert: false,
|
showAlert: false,
|
||||||
ipLimitEnable: false,
|
ipLimitEnable: false,
|
||||||
pageSize: 50,
|
pageSize: 50,
|
||||||
|
|
||||||
|
inboundLink: '', // 新增:用来存储生成的可复制链接
|
||||||
|
// 【中文注释】: 新增数据属性,用于控制“一键配置”模态框的显示状态
|
||||||
|
oneClickModalVisible: false,
|
||||||
|
|
||||||
|
// 【新增】: 用于存储最近生成的链接记录
|
||||||
|
linkHistory: [],
|
||||||
|
|
||||||
|
// 【新增】: 定义链接历史记录表格的列
|
||||||
|
linkHistoryColumns: [
|
||||||
|
{
|
||||||
|
title: `{{ i18n "pages.inbounds.oneClick.historyType" }}`,
|
||||||
|
dataIndex: 'type',
|
||||||
|
width: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: `{{ i18n "pages.inbounds.oneClick.historyLink" }}`,
|
||||||
|
dataIndex: 'link',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: `{{ i18n "pages.inbounds.oneClick.historyTime" }}`,
|
||||||
|
dataIndex: 'time',
|
||||||
|
width: 220,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// 【中文注释】: 新增数据属性,用于存储“一键配置”模态框中选择的预设值
|
||||||
|
oneClickPreset: 'vless_reality',
|
||||||
|
// 【中文注释】: 新增Reality SNI的备选列表,用于随机选择
|
||||||
|
realityDestinations: [
|
||||||
|
'tesla.com:443',
|
||||||
|
'sega.com:443',
|
||||||
|
'microsoft.com:443',
|
||||||
|
'apple.com:443',
|
||||||
|
'icloud.com:443',
|
||||||
|
'amazon.com:443',
|
||||||
|
'nintendo.com:443',
|
||||||
|
'lovelive-anime.jp:443',
|
||||||
|
'meta.com:443',
|
||||||
|
'stanford.edu:443',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.getDefaultSettings();
|
||||||
|
if (this.isRefreshEnabled) {
|
||||||
|
this.startDataRefreshLoop();
|
||||||
|
} else {
|
||||||
|
this.getDBInbounds();
|
||||||
|
}
|
||||||
|
// 【新增】: 从本地存储加载历史记录
|
||||||
|
const history = localStorage.getItem('oneClickLinkHistory');
|
||||||
|
if (history) {
|
||||||
|
this.linkHistory = JSON.parse(history);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
loading(spinning = true) {
|
loading(spinning = true) {
|
||||||
this.loadingStates.spinning = spinning;
|
this.loadingStates.spinning = spinning;
|
||||||
@ -849,7 +994,7 @@
|
|||||||
|
|
||||||
await this.getLastOnlineMap();
|
await this.getLastOnlineMap();
|
||||||
await this.getOnlineUsers();
|
await this.getOnlineUsers();
|
||||||
|
|
||||||
this.setInbounds(msg.obj);
|
this.setInbounds(msg.obj);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.refreshing = false;
|
this.refreshing = false;
|
||||||
@ -1016,6 +1161,348 @@
|
|||||||
this.searchedInbounds = this.dbInbounds.slice();
|
this.searchedInbounds = this.dbInbounds.slice();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
// 【修改后】: 按照固定长度 2,4,6,8,10,12,14,16 来生成 shortIds
|
||||||
|
generateShortIds() {
|
||||||
|
const chars = "0123456789abcdef"; // 只使用十六进制字符
|
||||||
|
const lengths = [2, 4, 6, 8, 10, 12, 14, 16];
|
||||||
|
const shortIds = [];
|
||||||
|
for (const len of lengths) {
|
||||||
|
let shortId = "";
|
||||||
|
for (let j = 0; j < len; j++) {
|
||||||
|
shortId += chars[Math.floor(Math.random() * chars.length)];
|
||||||
|
}
|
||||||
|
shortIds.push(shortId);
|
||||||
|
}
|
||||||
|
return shortIds;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 【新增】: 保存链接到历史记录,并同步到本地存储
|
||||||
|
saveLinkToHistory(type, link) {
|
||||||
|
const newRecord = { type: type, link: link, time: new Date().toLocaleString() };
|
||||||
|
this.linkHistory.unshift(newRecord); // 添加到数组开头
|
||||||
|
if (this.linkHistory.length > 10) {
|
||||||
|
this.linkHistory.pop(); // 保持最多10条记录
|
||||||
|
}
|
||||||
|
localStorage.setItem('oneClickLinkHistory', JSON.stringify(this.linkHistory));
|
||||||
|
},
|
||||||
|
|
||||||
|
// 【中文注释】: 以下是新增和修改后的方法,用于处理“一键配置”和“订阅转换”
|
||||||
|
// 【中文注释】: 打开“一键配置”模态框
|
||||||
|
openOneClickConfig() {
|
||||||
|
this.oneClickModalVisible = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 【中文注释】: 处理“一键配置”模态框的确认操作
|
||||||
|
async handleOneClickOk(presetType) {
|
||||||
|
this.loadingStates.spinning = true;
|
||||||
|
|
||||||
|
// 【中文注释】: 根据选择的预设值调用不同的创建函数
|
||||||
|
if (presetType === 'vless_reality') {
|
||||||
|
await this.createVlessRealityInbound();
|
||||||
|
} else if (presetType === 'vless_tls_encryption') {
|
||||||
|
await this.createVlessTlsEncryptionInbound();
|
||||||
|
} else if (presetType === 'switch') {
|
||||||
|
this.$message.info('Switch 模式配置尚未实现');
|
||||||
|
} else {
|
||||||
|
this.$message.error('未知的预设配置!');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 【中文注释】: 无论如何,最后都关闭加载状态
|
||||||
|
this.loadingStates.spinning = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 【中文注释】: 处理“一键配置”模态框的取消操作
|
||||||
|
handleOneClickCancel() {
|
||||||
|
this.oneClickModalVisible = false;
|
||||||
|
this.inboundLink = ''; // 关闭时清空链接,以便下次重新打开时显示选项
|
||||||
|
},
|
||||||
|
|
||||||
|
// 【中文注释】: 创建 VLESS + Reality 入站的具体实现
|
||||||
|
async createVlessRealityInbound() {
|
||||||
|
try {
|
||||||
|
// 【中文注释】: 使用 Promise.all 并行请求后端的两个接口,以获取密钥对和UUID
|
||||||
|
// 【重要】: 这里调用的都是项目中已经存在的后端接口
|
||||||
|
const [keyPairMsg, uuidMsg] = await Promise.all([
|
||||||
|
HttpUtil.get('/panel/api/server/getNewX25519Cert'),
|
||||||
|
HttpUtil.get('/panel/api/server/getNewUUID')
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!keyPairMsg.success || !keyPairMsg.obj || !uuidMsg.success || !uuidMsg.obj) {
|
||||||
|
this.$message.error('无法从服务器获取 Reality 密钥对或 UUID,请检查后端服务!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { privateKey, publicKey } = keyPairMsg.obj;
|
||||||
|
const { uuid } = uuidMsg.obj;
|
||||||
|
const remark = Array.from({length:8}, () =>
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||||
|
[Math.floor(Math.random() * 62)]
|
||||||
|
).join("");
|
||||||
|
|
||||||
|
// 随机生成 10000-55535 之间的端口
|
||||||
|
const port = Math.floor(Math.random() * (55535 - 10000 + 1)) + 10000;
|
||||||
|
|
||||||
|
// 【中文注释】: 从预设列表中随机选择一个SNI
|
||||||
|
const randomDest = this.realityDestinations[Math.floor(Math.random() * this.realityDestinations.length)];
|
||||||
|
|
||||||
|
// 从 randomDest (例如 'microsoft.com:443') 中提取出域名 ('microsoft.com')
|
||||||
|
const randomSni = randomDest.split(':')[0];
|
||||||
|
|
||||||
|
// 【随机生成】: 调用新的函数 generateShortIds()
|
||||||
|
const shortIds = this.generateShortIds();
|
||||||
|
const linkSid = shortIds[Math.floor(Math.random() * shortIds.length)];
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
up: 0,
|
||||||
|
down: 0,
|
||||||
|
total: 0,
|
||||||
|
remark: remark,
|
||||||
|
enable: true,
|
||||||
|
expiryTime: 0,
|
||||||
|
listen: '',
|
||||||
|
port: port,
|
||||||
|
protocol: 'vless',
|
||||||
|
settings: JSON.stringify({
|
||||||
|
clients: [{
|
||||||
|
id: uuid, // 【中文注释】: 使用后端生成的 UUID
|
||||||
|
flow: "xtls-rprx-vision",
|
||||||
|
email: remark,
|
||||||
|
level: 0
|
||||||
|
}],
|
||||||
|
decryption: "none",
|
||||||
|
fallbacks: []
|
||||||
|
}),
|
||||||
|
streamSettings: JSON.stringify({
|
||||||
|
network: "tcp",
|
||||||
|
security: "reality",
|
||||||
|
realitySettings: {
|
||||||
|
show: false,
|
||||||
|
target: randomDest,
|
||||||
|
xver: 0,
|
||||||
|
serverNames: [ randomSni, `www.${randomSni}` ],
|
||||||
|
privateKey: privateKey,
|
||||||
|
publicKey: publicKey,
|
||||||
|
maxClientVer: "",
|
||||||
|
minClientVer: "",
|
||||||
|
maxTimediff: 0,
|
||||||
|
mldsa65Seed: "", // 【中文注释】: 一般情况下【留空】
|
||||||
|
"shortIds": shortIds,
|
||||||
|
"spiderX": "/"
|
||||||
|
},
|
||||||
|
tcpSettings: {
|
||||||
|
acceptProxyProtocol: false,
|
||||||
|
header: {
|
||||||
|
type: "none"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 【中文注释】:构建完整的 sniffing 对象
|
||||||
|
sniffing: JSON.stringify({
|
||||||
|
enabled: true,
|
||||||
|
destOverride: ["http", "tls", "quic", "fakedns"],
|
||||||
|
metadataOnly: false,
|
||||||
|
routeOnly: false
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const msg = await HttpUtil.post('/panel/api/inbounds/add', data);
|
||||||
|
|
||||||
|
if (msg.success) {
|
||||||
|
const link = `vless://${uuid}@${window.location.hostname}:${port}?type=tcp&encryption=none&security=reality&pbk=${publicKey}&fp=chrome&sni=${randomSni}&sid=${linkSid}&spx=%2F&flow=xtls-rprx-vision#${remark}-${remark}`;
|
||||||
|
this.inboundLink = link;
|
||||||
|
|
||||||
|
// 【新增】: 调用保存方法,将链接保存到历史记录
|
||||||
|
this.saveLinkToHistory('vless_reality', link);
|
||||||
|
|
||||||
|
this.$message.success('一键配置入站创建成功!');
|
||||||
|
await this.getDBInbounds();
|
||||||
|
} else {
|
||||||
|
// 如果后端返回失败,显示后端的消息
|
||||||
|
this.$message.error('创建失败: ' + msg.msg);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("创建一键配置入站时出错:", err);
|
||||||
|
this.$message.error('创建过程中发生网络错误,请检查后端日志。');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 【方法】: 创建 VLESS + TLS + Encryption 的入站配置
|
||||||
|
async createVlessTlsEncryptionInbound() {
|
||||||
|
try {
|
||||||
|
const [encMsg, uuidMsg] = await Promise.all([
|
||||||
|
HttpUtil.get('/panel/api/server/getNewVlessEnc'),
|
||||||
|
HttpUtil.get('/panel/api/server/getNewUUID')
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 【修正】: 增加对 API 返回数据的严格检查
|
||||||
|
if (!encMsg.success || !encMsg.obj || !Array.isArray(encMsg.obj.auths) || encMsg.obj.auths.length === 0 || !uuidMsg.success || !uuidMsg.obj) {
|
||||||
|
this.$message.error('无法从服务器获取 Vless 加密配置或 UUID,请检查后端服务!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 【修正】: 遍历找到正确的 ML-KEM-768 密钥
|
||||||
|
const mlkemAuth = encMsg.obj.auths.find(auth => auth.label === "ML-KEM-768, Post-Quantum");
|
||||||
|
|
||||||
|
if (!mlkemAuth || !mlkemAuth.decryption || !mlkemAuth.encryption) {
|
||||||
|
this.$message.error('未能从服务器获取到 ML-KEM-768 加密密钥,请检查 Xray 版本!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 【修正】: 正确从嵌套数据中解构出 decryption 和 encryption
|
||||||
|
const { decryption, encryption } = mlkemAuth;
|
||||||
|
|
||||||
|
const { uuid } = uuidMsg.obj;
|
||||||
|
const remark = Array.from({length:8}, () =>
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||||
|
[Math.floor(Math.random() * 62)]
|
||||||
|
).join("");
|
||||||
|
|
||||||
|
// 随机生成 10000-55535 之间的端口
|
||||||
|
const port = Math.floor(Math.random() * (55535 - 10000 + 1)) + 10000;
|
||||||
|
|
||||||
|
// 【重要】: 随机选择 SNI
|
||||||
|
const randomDest = this.realityDestinations[Math.floor(Math.random() * this.realityDestinations.length)];
|
||||||
|
const serverName = randomDest.split(':')[0]; // 从域名列表中提取 SNI
|
||||||
|
|
||||||
|
// 【必须】: 动态设置证书路径
|
||||||
|
const certPath = `/root/cert/${window.location.hostname}/fullchain.pem`;
|
||||||
|
const keyPath = `/root/cert/${window.location.hostname}/privkey.pem`;
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
up: 0,
|
||||||
|
down: 0,
|
||||||
|
total: 0,
|
||||||
|
remark: remark,
|
||||||
|
enable: true,
|
||||||
|
expiryTime: 0,
|
||||||
|
listen: '',
|
||||||
|
port: port,
|
||||||
|
protocol: 'vless',
|
||||||
|
settings: JSON.stringify({
|
||||||
|
clients: [{
|
||||||
|
id: uuid, // 【中文注释】: 使用后端生成的 UUID
|
||||||
|
flow: "xtls-rprx-vision",
|
||||||
|
email: remark,
|
||||||
|
level: 0
|
||||||
|
}],
|
||||||
|
decryption: decryption, // 使用从 API 获取的 decryption
|
||||||
|
encryption: encryption, // 【重要】: 加上 encryption 参数
|
||||||
|
selectedAuth: "ML-KEM-768, Post-Quantum"
|
||||||
|
}),
|
||||||
|
streamSettings: JSON.stringify({
|
||||||
|
network: "tcp",
|
||||||
|
security: "tls",
|
||||||
|
tcpSettings: {
|
||||||
|
acceptProxyProtocol: false,
|
||||||
|
header: {
|
||||||
|
type: "none"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tlsSettings: {
|
||||||
|
alpn: ["http/1.1"],
|
||||||
|
certificates: [{
|
||||||
|
buildChain: false,
|
||||||
|
certificateFile: certPath,
|
||||||
|
keyFile: keyPath,
|
||||||
|
oneTimeLoading: false,
|
||||||
|
usage: "encipherment"
|
||||||
|
}],
|
||||||
|
cipherSuites: "",
|
||||||
|
disableSystemRoot: false,
|
||||||
|
echForceQuery: "none",
|
||||||
|
echServerKeys: "",
|
||||||
|
enableSessionResumption: false,
|
||||||
|
maxVersion: "1.3",
|
||||||
|
minVersion: "1.2",
|
||||||
|
rejectUnknownSni: false,
|
||||||
|
serverName: serverName, // 使用我们定义的 SNI
|
||||||
|
verifyPeerCertInNames: ["dns.google", "cloudflare-dns.com"]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
sniffing: JSON.stringify({
|
||||||
|
enabled: false,
|
||||||
|
destOverride: ["http", "tls", "quic", "fakedns"],
|
||||||
|
metadataOnly: false,
|
||||||
|
routeOnly: false
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const msg = await HttpUtil.post('/panel/api/inbounds/add', data);
|
||||||
|
|
||||||
|
if (msg.success) {
|
||||||
|
// 【重要】: 分享链接格式,添加 encryption 和 flow
|
||||||
|
const link = `vless://${uuid}@${window.location.hostname}:${port}?type=tcp&encryption=${encryption}&security=tls&fp=chrome&alpn=http%2F1.1&sni=${serverName}&flow=xtls-rprx-vision#${remark}-${remark}`;
|
||||||
|
this.inboundLink = link;
|
||||||
|
|
||||||
|
// 【新增】: 调用保存方法,将链接保存到历史记录
|
||||||
|
this.saveLinkToHistory('vless_tls_encryption', link);
|
||||||
|
|
||||||
|
this.$message.success('VLESS + TLS + Encryption 配置创建成功!');
|
||||||
|
await this.getDBInbounds();
|
||||||
|
} else {
|
||||||
|
this.$message.error('创建失败: ' + msg.msg);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("创建 VLESS + TLS + Encryption 入站时出错:", err);
|
||||||
|
this.$message.error('创建过程中发生网络错误,请检查后端日志。');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async copyLink(linkToCopy) {
|
||||||
|
const link = linkToCopy || this.inboundLink;
|
||||||
|
if (!link) {
|
||||||
|
this.$message.warning('没有可复制的链接。');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(link);
|
||||||
|
this.$message.success('链接已成功复制到剪贴板!');
|
||||||
|
} catch (err) {
|
||||||
|
this.$message.error('复制失败,请手动复制。');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 【中文注释】: 处理“订阅转换”按钮的点击事件 (已更新逻辑)
|
||||||
|
handleSubscriptionConversion() {
|
||||||
|
this.$confirm({
|
||||||
|
title: '{{ i18n "pages.inbounds.subConversion.modalTitle"}}',
|
||||||
|
content: '{{ i18n "pages.inbounds.subConversion.modalContent"}}',
|
||||||
|
okText: '{{ i18n "pages.inbounds.subConversion.modalOk"}}',
|
||||||
|
cancelText: '{{ i18n "cancel" }}',
|
||||||
|
class: themeSwitcher.currentTheme,
|
||||||
|
onOk: () => {
|
||||||
|
const subConverterUrl = `https://${window.location.hostname}:15268`;
|
||||||
|
const loadingMessage = this.$message.loading('正在检测订阅转换服务...', 0);
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), 3000);
|
||||||
|
|
||||||
|
fetch(subConverterUrl, {
|
||||||
|
method: 'GET',
|
||||||
|
mode: 'no-cors',
|
||||||
|
signal: controller.signal
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
loadingMessage();
|
||||||
|
this.$message.success('服务检测成功,正在跳转...', 2);
|
||||||
|
window.open(subConverterUrl, '_blank');
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
loadingMessage();
|
||||||
|
this.$info({
|
||||||
|
title: '{{ i18n "pages.inbounds.subConversion.notFoundTitle"}}',
|
||||||
|
content: '{{ i18n "pages.inbounds.subConversion.notFoundContent"}}',
|
||||||
|
class: themeSwitcher.currentTheme,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
generalActions(action) {
|
generalActions(action) {
|
||||||
switch (action.key) {
|
switch (action.key) {
|
||||||
case "import":
|
case "import":
|
||||||
@ -1148,9 +1635,9 @@
|
|||||||
remark: dbInbound.remark,
|
remark: dbInbound.remark,
|
||||||
enable: dbInbound.enable,
|
enable: dbInbound.enable,
|
||||||
expiryTime: dbInbound.expiryTime,
|
expiryTime: dbInbound.expiryTime,
|
||||||
|
|
||||||
<!-- 新增这一行 -->
|
// 新增这一行
|
||||||
deviceLimit: dbInbound.deviceLimit,
|
deviceLimit: dbInbound.deviceLimit,
|
||||||
|
|
||||||
listen: inbound.listen,
|
listen: inbound.listen,
|
||||||
port: inbound.port,
|
port: inbound.port,
|
||||||
@ -1174,8 +1661,8 @@
|
|||||||
remark: dbInbound.remark,
|
remark: dbInbound.remark,
|
||||||
enable: dbInbound.enable,
|
enable: dbInbound.enable,
|
||||||
expiryTime: dbInbound.expiryTime,
|
expiryTime: dbInbound.expiryTime,
|
||||||
<!-- 新增这一行 -->
|
// 新增这一行
|
||||||
deviceLimit: dbInbound.deviceLimit,
|
deviceLimit: dbInbound.deviceLimit,
|
||||||
|
|
||||||
listen: inbound.listen,
|
listen: inbound.listen,
|
||||||
port: inbound.port,
|
port: inbound.port,
|
||||||
@ -1335,8 +1822,8 @@
|
|||||||
checkFallback(dbInbound) {
|
checkFallback(dbInbound) {
|
||||||
newDbInbound = new DBInbound(dbInbound);
|
newDbInbound = new DBInbound(dbInbound);
|
||||||
if (dbInbound.listen.startsWith("@")){
|
if (dbInbound.listen.startsWith("@")){
|
||||||
rootInbound = this.inbounds.find((i) =>
|
rootInbound = this.inbounds.find((i) =>
|
||||||
i.isTcp &&
|
i.isTcp &&
|
||||||
['trojan','vless'].includes(i.protocol) &&
|
['trojan','vless'].includes(i.protocol) &&
|
||||||
i.settings.fallbacks.find(f => f.dest === dbInbound.listen)
|
i.settings.fallbacks.find(f => f.dest === dbInbound.listen)
|
||||||
);
|
);
|
||||||
@ -1390,17 +1877,17 @@
|
|||||||
await this.getDBInbounds();
|
await this.getDBInbounds();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getInboundClients(dbInbound) {
|
getInboundClients(dbInbound) {
|
||||||
<!-- 先把 dbInbound 转成 inbound 对象 -->
|
// 先把 dbInbound 转成 inbound 对象
|
||||||
const inbound = dbInbound.toInbound();
|
const inbound = dbInbound.toInbound();
|
||||||
const clients = inbound.clients || [];
|
const clients = inbound.clients || [];
|
||||||
|
|
||||||
<!-- 遍历 clients,保证 speedLimit 字段存在且是数字 -->
|
// 遍历 clients,保证 speedLimit 字段存在且是数字
|
||||||
return clients.map(c => ({
|
return clients.map(c => ({
|
||||||
...c, <!-- 保留原有字段 -->
|
...c, // 保留原有字段
|
||||||
speedLimit: Number(c.speedLimit ?? 0), <!-- 如果没设置就默认为 0 -->
|
speedLimit: Number(c.speedLimit ?? 0), // 如果没设置就默认为 0
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
resetClientTraffic(client, dbInboundId, confirmation = true) {
|
resetClientTraffic(client, dbInboundId, confirmation = true) {
|
||||||
if (confirmation){
|
if (confirmation){
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user