@@ -667,11 +692,76 @@
-
+
+
+
+
+
+
{{ i18n "pages.inbounds.oneClick.prompt" }}
+
+
+
+ {{ i18n "pages.inbounds.oneClick.preset.vless_reality" }}
+
+
+ {{ i18n "pages.inbounds.oneClick.preset.vless_tls_encryption" }}
+
+
+ {{ i18n "pages.inbounds.oneClick.preset.switch_vision_seed_dev" }}
+
+
+
+
{{ i18n "pages.inbounds.oneClick.descriptionTitle" }}
+
{{ i18n "pages.inbounds.oneClick.descriptionContent" }}
+
+
+
+
{{ i18n "pages.inbounds.oneClick.linkCreated" }}
+
+
+
+
+
+
+
+
+
+
@@ -733,7 +823,7 @@
dataIndex: "deviceLimit",
align: "center",
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" }}',
align: 'center',
@@ -776,14 +866,14 @@
{ 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.allTimeTraffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'allTime' } },
-
-
- {
+
+ // 中文注释: 在子表格中新增“限速”列
+ {
title: '{{ i18n "pages.inbounds.speedLimit" }}',
width: 80,
align: 'center',
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' } },
@@ -834,7 +924,62 @@
showAlert: false,
ipLimitEnable: false,
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: {
loading(spinning = true) {
this.loadingStates.spinning = spinning;
@@ -849,7 +994,7 @@
await this.getLastOnlineMap();
await this.getOnlineUsers();
-
+
this.setInbounds(msg.obj);
setTimeout(() => {
this.refreshing = false;
@@ -1016,6 +1161,348 @@
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) {
switch (action.key) {
case "import":
@@ -1148,9 +1635,9 @@
remark: dbInbound.remark,
enable: dbInbound.enable,
expiryTime: dbInbound.expiryTime,
-
-
- deviceLimit: dbInbound.deviceLimit,
+
+ // 新增这一行
+ deviceLimit: dbInbound.deviceLimit,
listen: inbound.listen,
port: inbound.port,
@@ -1174,8 +1661,8 @@
remark: dbInbound.remark,
enable: dbInbound.enable,
expiryTime: dbInbound.expiryTime,
-
- deviceLimit: dbInbound.deviceLimit,
+ // 新增这一行
+ deviceLimit: dbInbound.deviceLimit,
listen: inbound.listen,
port: inbound.port,
@@ -1335,8 +1822,8 @@
checkFallback(dbInbound) {
newDbInbound = new DBInbound(dbInbound);
if (dbInbound.listen.startsWith("@")){
- rootInbound = this.inbounds.find((i) =>
- i.isTcp &&
+ rootInbound = this.inbounds.find((i) =>
+ i.isTcp &&
['trojan','vless'].includes(i.protocol) &&
i.settings.fallbacks.find(f => f.dest === dbInbound.listen)
);
@@ -1390,17 +1877,17 @@
await this.getDBInbounds();
}
},
-
+
getInboundClients(dbInbound) {
-
+ // 先把 dbInbound 转成 inbound 对象
const inbound = dbInbound.toInbound();
const clients = inbound.clients || [];
-
+ // 遍历 clients,保证 speedLimit 字段存在且是数字
return clients.map(c => ({
- ...c,
- speedLimit: Number(c.speedLimit ?? 0),
- }));
+ ...c, // 保留原有字段
+ speedLimit: Number(c.speedLimit ?? 0), // 如果没设置就默认为 0
+ }));
},
resetClientTraffic(client, dbInboundId, confirmation = true) {
if (confirmation){