From 52dd9237ac27192297558b6837a06c491a0efe4b Mon Sep 17 00:00:00 2001 From: mike Date: Thu, 13 Aug 2026 11:00:22 +0800 Subject: [PATCH] Update App.jsx via upload script - 2026-08-13 11:00:02 --- App.jsx | 304 ++++++++++++++------------------------------------------ 1 file changed, 74 insertions(+), 230 deletions(-) diff --git a/App.jsx b/App.jsx index 3e1eed5..dfdf3dc 100644 --- a/App.jsx +++ b/App.jsx @@ -5,15 +5,13 @@ import { Building2, Globe, Phone, Save, RefreshCw, LayoutDashboard, Users, Package, Layers, Zap, Tag, Inbox, Paperclip, Menu, LogOut, GripVertical, BookOpen, FileText, FolderTree, Lock, Crown, Sparkles, Printer, Image as ImageIcon, - ShieldCheck, Settings2, Boxes, Download, Wifi, Radio, QrCode, Send + ShieldCheck, Settings2, Boxes, Download, Wifi, Radio, QrCode } from 'lucide-react'; import { storageGet, storageSet, uploadFile, deleteFile, authCheck, authLogin, authLogout, authChangePassword, getAiConfig, saveAiConfig, deleteAiConfig, testAiConfig, generateAiDraft, parseCatalogFiles, translateTerms, - getTelegramConfig, saveTelegramConfig, deleteTelegramConfig, testTelegramConfig, - checkJmsLine, } from './storage'; /* ----------------------------- constants ----------------------------- */ @@ -1600,26 +1598,11 @@ const Styles = () => ( .jms-card-top { display: flex; align-items: center; justify-content: space-between; gap: 10px; } .jms-card-title { display: flex; align-items: center; gap: 8px; min-width: 0; } .jms-card-name { font-weight: 700; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } - .jms-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; } - .jms-dot-normal { background: var(--led-green); box-shadow: 0 0 0 3px var(--led-green-bg); } - .jms-dot-abnormal { background: var(--led-red); box-shadow: 0 0 0 3px var(--led-red-bg); animation: jms-pulse-red 1.6s ease-in-out infinite; } - .jms-dot-checking { background: var(--led-amber); box-shadow: 0 0 0 3px var(--led-amber-bg); animation: jms-pulse-amber 1s ease-in-out infinite; } - @keyframes jms-pulse-red { 0%,100% { opacity: 1; } 50% { opacity: 0.35; } } - @keyframes jms-pulse-amber { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } } .jms-proto-chip { font-size: 10px; font-weight: 700; letter-spacing: 0.03em; padding: 3px 9px; border-radius: 100px; text-transform: uppercase; flex-shrink: 0; } .jms-proto-ss { background: var(--led-green-bg); color: var(--led-green); } - .jms-proto-vmess { background: var(--led-blue-bg); color: var(--blue); } - .jms-proto-vless-cdn { background: var(--copper-soft); color: var(--copper); } .jms-proto-vless-reality { background: var(--led-red-bg); color: var(--led-red); } - .jms-monitor-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; font-size: 11.5px; color: var(--ink-soft); } - .jms-status-abnormal { color: var(--led-red); font-weight: 600; } - .jms-check-btn { background: none; border: none; color: var(--copper); font-size: 11.5px; font-weight: 600; cursor: pointer; padding: 2px 4px; font-family: inherit; } - .jms-check-btn:hover { text-decoration: underline; } - .jms-check-btn:disabled { color: var(--ink-soft); cursor: default; text-decoration: none; } - .jms-check-note { font-size: 10.5px; color: var(--ink-soft); background: var(--bg); border: 1px dashed var(--line); border-radius: 6px; padding: 6px 8px; } - .jms-terminal { background: var(--ink); border-radius: 9px; padding: 12px 14px; font-family: 'IBM Plex Mono', monospace; font-size: 12px; line-height: 1.85; } .jms-terminal-row { display: flex; gap: 10px; align-items: baseline; } .jms-terminal-key { color: rgba(255,255,255,0.5); flex-shrink: 0; width: 56px; } @@ -1634,14 +1617,9 @@ const Styles = () => ( .jms-card-actions { display: flex; gap: 8px; flex-wrap: wrap; } .jms-card-actions .btn { flex: 1; justify-content: center; } - .jms-group-select { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; } - .jms-group-opt { border: 1.5px solid var(--line); border-radius: 9px; padding: 10px 8px; text-align: center; font-size: 12.5px; font-weight: 700; cursor: pointer; color: var(--ink-soft); } - .jms-group-selected.jms-group-selfbuilt { border-color: var(--copper); background: var(--copper-soft); color: var(--copper); } - .jms-group-selected.jms-group-jms { border-color: var(--blue); background: var(--led-blue-bg); color: var(--blue); } - .jms-proto-select { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; } + .jms-proto-select { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; } .jms-proto-opt { border: 1.5px solid var(--line); border-radius: 9px; padding: 9px 4px; text-align: center; font-size: 11px; font-weight: 700; cursor: pointer; letter-spacing: 0.02em; text-transform: uppercase; color: var(--ink-soft); } .jms-proto-selected.jms-proto-opt-ss { border-color: var(--led-green); background: var(--led-green-bg); color: var(--led-green); } - .jms-proto-selected.jms-proto-opt-vmess { border-color: var(--blue); background: var(--led-blue-bg); color: var(--blue); } .jms-proto-selected.jms-proto-opt-vless-reality { border-color: var(--led-red); background: var(--led-red-bg); color: var(--led-red); } .jms-qr-link { font-family: 'IBM Plex Mono', monospace; font-size: 11px; word-break: break-all; color: var(--ink-soft); background: var(--bg); padding: 10px; border-radius: 8px; max-height: 90px; overflow-y: auto; text-align: left; margin: 12px 0; } @@ -2099,10 +2077,6 @@ function MainApp({ username, onLogout }) { const [aiKeyInput, setAiKeyInput] = useState(''); const [aiSaving, setAiSaving] = useState(false); const [aiTesting, setAiTesting] = useState(false); - const [tgTokenInput, setTgTokenInput] = useState(''); - const [tgChatIdInput, setTgChatIdInput] = useState(''); - const [tgSaving, setTgSaving] = useState(false); - const [tgTesting, setTgTesting] = useState(false); const [aiGenerating, setAiGenerating] = useState(false); const [aiDraftModal, setAiDraftModal] = useState(null); // { channel, customerId, subject, body } @@ -2134,8 +2108,6 @@ function MainApp({ username, onLogout }) { const [catalogCategoryFilter, setCatalogCategoryFilter] = useState('all'); const [jmsLines, setJmsLines] = useState([]); const [backPanelTemplates, setBackPanelTemplates] = useState([]); - const [telegramConfigHasToken, setTelegramConfigHasToken] = useState(false); - const [telegramConfigChatId, setTelegramConfigChatId] = useState(''); useEffect(() => { getAiConfig().then((res) => setAiConfigHasKey(!!res.hasKey)).catch(() => {}); @@ -2164,12 +2136,6 @@ function MainApp({ username, onLogout }) { setLoaded(true); })(); }, []); - useEffect(() => { - getTelegramConfig().then(res => { - setTelegramConfigHasToken(!!res.hasToken); - setTelegramConfigChatId(res.chatId || ''); - }).catch(() => {}); - }, []); useEffect(() => { if (loaded) storageSet(STORAGE_KEYS.customers, JSON.stringify(customers)).catch(() => {}); }, [customers, loaded]); useEffect(() => { if (loaded) storageSet(STORAGE_KEYS.products, JSON.stringify(products)).catch(() => {}); }, [products, loaded]); useEffect(() => { if (loaded) storageSet(STORAGE_KEYS.settings, JSON.stringify(settings)).catch(() => {}); }, [settings, loaded]); @@ -2401,24 +2367,6 @@ function MainApp({ username, onLogout }) { setAiConfigHasKey(false); notify('已清除'); } - async function handleSaveTelegram() { - if (!tgTokenInput.trim() || !tgChatIdInput.trim()) { notify('Bot Token和Chat ID都要填'); return; } - setTgSaving(true); - try { - await saveTelegramSettings(tgTokenInput.trim(), tgChatIdInput.trim()); - setTgTokenInput(''); - } finally { - setTgSaving(false); - } - } - async function handleTestTelegram() { - setTgTesting(true); - try { await testTelegramSettings(); } finally { setTgTesting(false); } - } - async function handleClearTelegram() { - if (!window.confirm('确定清除已保存的Telegram配置?清除后线路异常将不再发送报警。')) return; - await removeTelegramSettings(); - } async function handleGenerateAi(customerId, channel) { if (!aiConfigHasKey) { notify('请先在设置中配置DeepSeek API Key'); return; } setAiGenerating(true); @@ -2575,49 +2523,7 @@ function MainApp({ username, onLogout }) { notify('已删除'); } - async function checkJmsLineNow(id) { - const line = jmsLines.find(l => l.id === id); - if (!line) return; - setJmsLines(prev => prev.map(l => (l.id === id ? { ...l, monitorStatus: 'checking' } : l))); - try { - const res = await checkJmsLine(line); - setJmsLines(prev => prev.map(l => (l.id === id ? { ...l, monitorStatus: res.status, lastChecked: res.checkedAt } : l))); - if (res.status === 'abnormal') notify(`⚠ ${line.name || line.addr} 检测异常`); - else notify(`${line.name || line.addr} 检测正常`); - } catch (e) { - setJmsLines(prev => prev.map(l => (l.id === id ? { ...l, monitorStatus: line.monitorStatus } : l))); - notify('检测失败:' + e.message); - } - } - async function saveTelegramSettings(botToken, chatId) { - try { - await saveTelegramConfig(botToken, chatId); - setTelegramConfigHasToken(true); - setTelegramConfigChatId(chatId); - notify('Telegram配置已保存'); - } catch (e) { - notify('保存失败:' + e.message); - } - } - async function removeTelegramSettings() { - try { - await deleteTelegramConfig(); - setTelegramConfigHasToken(false); - setTelegramConfigChatId(''); - notify('已清除Telegram配置'); - } catch (e) { - notify('清除失败:' + e.message); - } - } - async function testTelegramSettings() { - try { - await testTelegramConfig(); - notify('测试消息已发送,去Telegram看看'); - } catch (e) { - notify('测试失败:' + e.message); - } - } async function handleExportProductPdf() { if (!selectedProduct) return; @@ -3337,7 +3243,6 @@ function MainApp({ username, onLogout }) { lines={jmsLines} onSave={saveJmsLine} onDelete={deleteJmsLine} - onCheck={checkJmsLineNow} onNotify={notify} /> )} @@ -3821,35 +3726,6 @@ function MainApp({ username, onLogout }) { -
-
Telegram 报警(Just My Socks线路监控)
-
-
- 状态:{telegramConfigHasToken ? 已配置(Chat ID: {telegramConfigChatId}) : 未配置} -
-
-
- Bot Token - setTgTokenInput(e.target.value)} - placeholder={telegramConfigHasToken ? '已设置,重新填写可覆盖' : '找 @BotFather 申请'} - /> -
-
- Chat ID - setTgChatIdInput(e.target.value)} placeholder="接收报警的对话ID" /> -
-
-
- - - {telegramConfigHasToken && } -
-
- Just My Socks页面的线路每10分钟自动巡检一次,只在状态发生变化(正常↔异常)时才发消息,不会每次检测都刷屏。Bot Token加密保存在服务器本地。 -
-
-
)} @@ -4426,21 +4302,23 @@ function CustomerDetail({ const JMS_PROTO_META = { 'ss': { label: 'SS' }, - 'vmess': { label: 'VMess' }, 'vless-reality': { label: 'VLESS Reality' }, - 'vless-cdn': { label: 'VLESS CDN' }, }; -const JMS_PROTOCOLS = ['ss', 'vmess', 'vless-reality']; +const JMS_PROTOCOLS = ['ss', 'vless-reality']; +// Shadowsocks加密方式可选项——2022-blake3-*系列是新一代AEAD-2022标准,比老式aes/chacha更安全, +// justmysocks2.net等主流服务商目前都已支持。 +const JMS_SS_METHODS = [ + 'aes-256-gcm', + 'aes-128-gcm', + 'chacha20-ietf-poly1305', + '2022-blake3-aes-128-gcm', + '2022-blake3-aes-256-gcm', + '2022-blake3-chacha20-poly1305', +]; -function jmsTimeAgo(ts) { - if (!ts) return '尚未检测'; - const sec = Math.floor((Date.now() - ts) / 1000); - if (sec < 60) return `${sec}秒前`; - if (sec < 3600) return `${Math.floor(sec / 60)}分钟前`; - return `${Math.floor(sec / 3600)}小时前`; -} +function jmsMask(len = 8) { return '•'.repeat(Math.max(6, Math.min(len, 16))); } -// 按协议生成真实格式的订阅链接(ss:// / vmess:// / vless://),一键导入和二维码都用这个 +// 按协议生成真实格式的订阅链接(ss:// / vless://),一键导入和二维码都用这个 function jmsBuildLink(line) { const { protocol, addr, port, name } = line; const tag = encodeURIComponent(name || addr); @@ -4448,20 +4326,6 @@ function jmsBuildLink(line) { const userinfo = btoa(`${line.method}:${line.password}`).replace(/=+$/, ''); return `ss://${userinfo}@${addr}:${port}#${tag}`; } - if (protocol === 'vmess') { - const obj = { - v: '2', ps: name || addr, add: addr, port: String(port), id: line.uuid, aid: line.alterId || '0', - net: 'ws', type: 'none', host: line.host || addr, path: '/', tls: 'tls', - }; - return 'vmess://' + btoa(JSON.stringify(obj)); - } - if (protocol === 'vless-cdn') { - const q = new URLSearchParams({ - encryption: 'none', security: 'tls', type: 'ws', fp: 'chrome', - host: line.host || addr, path: line.ws_path || '/stickrouter', sni: line.sni || addr, - }); - return `vless://${line.uuid}@${addr}:${port}?${q.toString()}#${tag}`; - } if (protocol === 'vless-reality') { const q = new URLSearchParams({ encryption: 'none', security: 'reality', sni: line.sni, fp: line.fingerprint || 'chrome', @@ -4481,35 +4345,34 @@ async function jmsResolveIp(domain) { return answer ? answer.data : null; } -function JmsLineCard({ line, ip, onRefreshIp, onCheck, onCopy, onQr, onDuplicate, onEdit, onDelete }) { - const meta = JMS_PROTO_META[line.protocol]; - const status = line.monitorStatus || 'normal'; - const dotClass = status === 'abnormal' ? 'jms-dot-abnormal' : status === 'checking' ? 'jms-dot-checking' : 'jms-dot-normal'; - const statusLabel = status === 'abnormal' ? '异常,无法连接' : status === 'checking' ? '检测中…' : '正常'; - const isVlessCdn = line.protocol === 'vless-cdn'; - const checkNote = isVlessCdn - ? '检测方式:请求WS路径看是否返回520-524源站不可达错误,并做真实WebSocket握手(不是单纯测TCP)' - : '检测方式:TCP端口连通性测试'; - const ipDisplay = ip === 'resolving' ? '解析中…' : (ip || '点击右侧刷新解析'); +function jmsCopyText(text, label, onNotify) { + if (!text) { onNotify && onNotify('没有可复制的内容'); return; } + navigator.clipboard.writeText(text) + .then(() => onNotify && onNotify(`${label}已复制`)) + .catch(() => onNotify && onNotify('复制失败,请手动选择')); +} + +function JmsLineCard({ line, ip, onRefreshIp, onCopy, onQr, onDuplicate, onEdit, onDelete, onNotify }) { + const meta = JMS_PROTO_META[line.protocol] || { label: line.protocol }; + const ipDisplay = ip === 'resolving' ? '解析中…' : (ip || '解析中…'); + const hasPassword = line.protocol === 'ss' && !!line.password; return (
- {line.name || line.addr}
{meta.label}
-
- ● {statusLabel} · {jmsTimeAgo(line.lastChecked)} - -
-
{checkNote}
-
域名{line.addr}
+
+ 域名 + {line.addr} + +
解析IP {ipDisplay} @@ -4518,6 +4381,15 @@ function JmsLineCard({ line, ip, onRefreshIp, onCheck, onCopy, onQr, onDuplicate
端口{line.port}
+ {hasPassword && ( +
+ 密码 + {jmsMask(line.password.length)} + +
+ )}
@@ -4530,36 +4402,21 @@ function JmsLineCard({ line, ip, onRefreshIp, onCheck, onCopy, onQr, onDuplicate ); } -function JmsLineFormFields({ group, protocol, draft, setDraft }) { +function JmsLineFormFields({ protocol, draft, setDraft }) { const set = (patch) => setDraft(d => ({ ...d, ...patch })); - if (group === 'selfbuilt') { - return ( - <> -
UUID set({ uuid: e.target.value })} placeholder="用户UUID" />
-
-
WS路径 set({ ws_path: e.target.value })} placeholder="/stickrouter" />
-
SNI / Host set({ sni: e.target.value })} placeholder="留空则用服务器地址" />
-
-
* vless加密none · 勾选tls · 指纹chrome(固定,不需要手动填)
- - ); - } if (protocol === 'ss') { return (
-
加密方式 set({ method: e.target.value })} />
+
+ 加密方式 + +
密码 set({ password: e.target.value })} placeholder="连接密码" />
); } - if (protocol === 'vmess') { - return ( -
-
UUID set({ uuid: e.target.value })} placeholder="用户UUID" />
-
AlterId set({ alterId: e.target.value })} />
-
- ); - } if (protocol === 'vless-reality') { return ( <> @@ -4579,20 +4436,18 @@ function JmsLineFormFields({ group, protocol, draft, setDraft }) { return null; } -function JmsPage({ lines, onSave, onDelete, onCheck, onNotify }) { +function JmsPage({ lines, onSave, onDelete, onNotify }) { const [ipMap, setIpMap] = useState({}); const [modalOpen, setModalOpen] = useState(false); const [editingId, setEditingId] = useState(null); - const [group, setGroup] = useState('selfbuilt'); - const [protocol, setProtocol] = useState('vless-cdn'); + const [protocol, setProtocol] = useState('ss'); const [draft, setDraft] = useState({}); const [qrLine, setQrLine] = useState(null); const [qrDataUrl, setQrDataUrl] = useState(''); const total = lines.length; const ssCount = lines.filter(l => l.protocol === 'ss').length; - const vmessCount = lines.filter(l => l.protocol === 'vmess').length; - const vlessCount = lines.filter(l => l.protocol.startsWith('vless')).length; + const vlessCount = lines.filter(l => l.protocol === 'vless-reality').length; async function refreshIp(id, addr) { setIpMap(m => ({ ...m, [id]: 'resolving' })); @@ -4604,6 +4459,14 @@ function JmsPage({ lines, onSave, onDelete, onCheck, onNotify }) { } } + // 域名解析IP实时显示:线路列表加载/变化时,自动对还没解析过的线路跑一次DNS解析,不用用户手动点 + useEffect(() => { + lines.forEach(line => { + if (line.addr && ipMap[line.id] === undefined) refreshIp(line.id, line.addr); + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [lines]); + function copyLink(line) { const link = jmsBuildLink(line); navigator.clipboard.writeText(link).then(() => onNotify('订阅链接已复制')).catch(() => onNotify('复制失败,请手动选择')); @@ -4625,21 +4488,18 @@ function JmsPage({ lines, onSave, onDelete, onCheck, onNotify }) { function openAddModal() { setEditingId(null); - setGroup('selfbuilt'); - setProtocol('vless-cdn'); - setDraft({ name: '', addr: 'pb.impromx.com', port: '443', ws_path: '/stickrouter' }); + setProtocol('ss'); + setDraft({ name: '', addr: '', port: '' }); setModalOpen(true); } function openEditModal(line) { setEditingId(line.id); - setGroup(line.group); setProtocol(line.protocol); setDraft({ ...line }); setModalOpen(true); } function duplicateLine(line) { setEditingId(null); // 不带id,保存时会当成全新的一条线路,不会覆盖原线路 - setGroup(line.group); setProtocol(line.protocol); const { id, monitorStatus, lastChecked, ...rest } = line; setDraft({ ...rest, name: line.name ? `${line.name} 副本` : '' }); @@ -4648,20 +4508,14 @@ function JmsPage({ lines, onSave, onDelete, onCheck, onNotify }) { } function closeModal() { setModalOpen(false); } - function handleGroupChange(g) { - setGroup(g); - if (g === 'selfbuilt') { - setProtocol('vless-cdn'); - setDraft(d => ({ name: d.name, addr: d.addr || 'pb.impromx.com', port: d.port || '443', ws_path: d.ws_path || '/stickrouter' })); - } else { - setProtocol('ss'); - setDraft(d => ({ name: d.name, addr: d.addr, port: d.port })); - } + function handleProtocolChange(p) { + setProtocol(p); + setDraft(d => ({ name: d.name, addr: d.addr, port: d.port })); } function handleSave() { if (!draft.addr || !draft.port) { onNotify('请填写服务器地址和端口'); return; } - onSave({ ...draft, group, protocol }, editingId); + onSave({ ...draft, protocol }, editingId); closeModal(); } @@ -4670,7 +4524,7 @@ function JmsPage({ lines, onSave, onDelete, onCheck, onNotify }) {

Just My Socks 节点管理

-
管理自建VLESS-CDN线路和 justmysocks2.net 订阅线路 · DNS解析、订阅链接、二维码和连通性监控
+
管理 justmysocks2.net 订阅线路 · DNS解析、订阅链接、二维码
@@ -4678,7 +4532,6 @@ function JmsPage({ lines, onSave, onDelete, onCheck, onNotify }) {
{total}
线路总数
{ssCount}
SS
-
{vmessCount}
VMess
{vlessCount}
VLESS
@@ -4690,12 +4543,12 @@ function JmsPage({ lines, onSave, onDelete, onCheck, onNotify }) { line={line} ip={ipMap[line.id]} onRefreshIp={refreshIp} - onCheck={onCheck} onCopy={copyLink} onQr={openQr} onDuplicate={duplicateLine} onEdit={openEditModal} onDelete={onDelete} + onNotify={onNotify} /> ))}
@@ -4709,28 +4562,19 @@ function JmsPage({ lines, onSave, onDelete, onCheck, onNotify }) {
- 线路分组 -
-
handleGroupChange('selfbuilt')}>自建 VLESS-CDN
-
handleGroupChange('jms')}>Just My Socks
+ 协议 +
+ {JMS_PROTOCOLS.map(key => ( +
handleProtocolChange(key)}>{JMS_PROTO_META[key].label}
+ ))}
- {group === 'jms' && ( -
- 协议 -
- {JMS_PROTOCOLS.map(key => ( -
setProtocol(key)}>{JMS_PROTO_META[key].label}
- ))} -
-
- )}
名称备注 setDraft(d => ({ ...d, name: e.target.value }))} placeholder="如:SG-01 新加坡" />
服务器地址(域名) setDraft(d => ({ ...d, addr: e.target.value }))} placeholder="如:sg1.jms-relay.net" />
端口 setDraft(d => ({ ...d, port: e.target.value }))} placeholder="8388" />
- +