实现独立加密设备身份、网关 agent、持久生命周期任务和 Surge/FlClash 专属订阅凭证。 完善 Shadowrocket 规则转换与 Clash AI DNS,增加 Surge FFSB 适配、规则快照、运维工具及验收文档。
250 lines
53 KiB
HTML
250 lines
53 KiB
HTML
<div id="ps-wg-design" aria-label="Proxy Station 设备管理交互原型">
|
||
<style>
|
||
#ps-wg-design{--ps-bg:light-dark(#f7f7f4,#171c22);--ps-panel:light-dark(#fff,#202730);--ps-ink:light-dark(#2b3f5c,#e0e7ef);--ps-muted:light-dark(#68727f,#a5b0bd);--ps-line:light-dark(#e2e2dd,#35404c);--ps-teal:light-dark(#007a72,#6fd5c7);--ps-soft:light-dark(#eaf5f1,#253d3b);--ps-warn:light-dark(#956000,#f2c776);--ps-warn-bg:light-dark(#fff7e8,#3a3020);--ps-red:light-dark(#b13732,#ffb0a7);--ps-red-bg:light-dark(#fff1ee,#402925);color-scheme:light dark;background:var(--ps-bg);color:var(--ps-ink);font:14px/1.6 -apple-system,BlinkMacSystemFont,"PingFang SC",sans-serif;border-radius:12px;overflow-wrap:anywhere;min-height:650px}
|
||
#ps-wg-design *{box-sizing:border-box}#ps-wg-design h1,#ps-wg-design h2,#ps-wg-design h3,#ps-wg-design p{margin:0}#ps-wg-design h1{font-size:27px;line-height:1.3;letter-spacing:-.6px;font-weight:650}#ps-wg-design h2{font-size:19px;font-weight:650}#ps-wg-design h3{font-size:15px;font-weight:600}#ps-wg-design button,#ps-wg-design input,#ps-wg-design select{font:inherit;color:inherit}#ps-wg-design button{cursor:var(--cursor-interaction,auto)}#ps-wg-design button:disabled{opacity:.5;cursor:default}#ps-wg-design button{background:none;border:0}#ps-wg-design svg,#ps-wg-design i[data-lucide]{width:17px;height:17px;flex:none}#ps-wg-design [hidden]{display:none!important}
|
||
#ps-wg-design .ps-top{display:flex;align-items:center;flex-wrap:wrap;gap:22px;padding:20px 110px 17px 26px;background:var(--ps-panel);border-bottom:1px solid var(--ps-line)}#ps-wg-design .ps-brand{display:flex;align-items:center;gap:9px;font-size:17px;font-weight:650;letter-spacing:-.3px}#ps-wg-design .ps-mark{background:var(--ps-teal);color:var(--ps-panel);display:grid;place-items:center;width:28px;height:28px;border-radius:8px;font-size:17px}#ps-wg-design .ps-nav{display:flex;gap:3px;flex-wrap:wrap}#ps-wg-design .ps-nav button{padding:6px 11px;border-radius:7px;font-size:13px}#ps-wg-design .ps-nav .ps-active{background:var(--ps-soft);color:var(--ps-teal);font-weight:600}#ps-wg-design .ps-demo{font-size:11px;color:var(--ps-muted);border:1px solid var(--ps-line);padding:3px 7px;border-radius:5px}
|
||
#ps-wg-design .ps-body{padding:26px;max-width:1200px;margin:auto}#ps-wg-design .ps-heading{display:flex;justify-content:space-between;align-items:center;gap:18px;margin-bottom:22px}#ps-wg-design .ps-sub{color:var(--ps-muted);margin-top:7px;font-size:13px}#ps-wg-design .ps-button{display:inline-flex;align-items:center;justify-content:center;gap:7px;padding:9px 13px;border:1px solid var(--ps-line);border-radius:7px;background:var(--ps-panel);font-weight:500;min-height:39px;white-space:normal;text-align:center}#ps-wg-design .ps-primary{background:var(--ps-teal);border-color:var(--ps-teal);color:var(--ps-panel)}#ps-wg-design .ps-danger{color:var(--ps-red);border-color:var(--ps-line)}#ps-wg-design .ps-link{color:var(--ps-teal);padding:6px 0;display:inline-flex;gap:6px;align-items:center;text-align:left}#ps-wg-design .ps-small{font-size:12px;color:var(--ps-muted)}#ps-wg-design .ps-mono{font:12px/1.6 ui-monospace,SFMono-Regular,monospace;font-variant-numeric:tabular-nums}#ps-wg-design .ps-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap}#ps-wg-design .ps-spread{justify-content:space-between}#ps-wg-design .ps-stack{display:grid;gap:18px}#ps-wg-design .ps-panel{background:var(--ps-panel);border:1px solid var(--ps-line);border-radius:10px}#ps-wg-design .ps-pad{padding:20px}#ps-wg-design .ps-line{border:0;border-top:1px solid var(--ps-line);margin:20px 0}#ps-wg-design .ps-notice{display:flex;align-items:flex-start;gap:9px;background:var(--ps-soft);padding:12px 14px;border-radius:8px;font-size:13px}#ps-wg-design .ps-notice svg{margin-top:2px;color:var(--ps-teal)}#ps-wg-design .ps-warning{background:var(--ps-warn-bg);color:var(--ps-warn)}#ps-wg-design .ps-error{background:var(--ps-red-bg);color:var(--ps-red)}#ps-wg-design .ps-badge{display:inline-flex;align-items:center;gap:6px;white-space:nowrap;border-radius:5px;padding:3px 7px;font-size:11px;background:var(--ps-bg);color:var(--ps-muted)}#ps-wg-design .ps-badge:before{content:"";width:5px;height:5px;border-radius:50%;background:currentColor}#ps-wg-design .ps-good{background:var(--ps-soft);color:var(--ps-teal)}#ps-wg-design .ps-amber{background:var(--ps-warn-bg);color:var(--ps-warn)}#ps-wg-design .ps-bad{background:var(--ps-red-bg);color:var(--ps-red)}
|
||
#ps-wg-design .ps-network{display:flex;justify-content:space-between;align-items:center;gap:14px;padding:15px 18px;margin-bottom:20px}#ps-wg-design .ps-network-title{font-size:14px;font-weight:600}#ps-wg-design .ps-network-icon{display:grid;place-items:center;background:var(--ps-bg);border:1px solid var(--ps-line);border-radius:8px;width:36px;height:36px}#ps-wg-design .ps-toolbar{padding:15px 18px;display:flex;align-items:center;justify-content:space-between;gap:14px;flex-wrap:wrap;border-bottom:1px solid var(--ps-line)}#ps-wg-design .ps-filter{display:flex;gap:6px;flex-wrap:wrap}#ps-wg-design .ps-filter button{padding:5px 9px;color:var(--ps-muted);border-radius:5px;font-size:12px}#ps-wg-design .ps-filter button[aria-pressed=true]{background:var(--ps-soft);color:var(--ps-teal)}#ps-wg-design input,#ps-wg-design select{padding:9px 11px;background:var(--ps-panel);border:1px solid var(--ps-line);border-radius:7px;min-width:0;max-width:100%}#ps-wg-design .ps-search{max-width:220px;font-size:12px}#ps-wg-design table{width:100%;border-collapse:collapse;text-align:left}#ps-wg-design th{font-size:11px;font-weight:500;color:var(--ps-muted);background:var(--ps-bg);padding:10px 16px}#ps-wg-design td{padding:17px 16px;border-top:1px solid var(--ps-line);vertical-align:middle}#ps-wg-design .ps-device-name{display:flex;align-items:center;gap:11px}#ps-wg-design .ps-device-icon{display:grid;place-items:center;width:34px;height:34px;background:var(--ps-bg);border-radius:8px;color:var(--ps-muted);flex:none}#ps-wg-design .ps-name-button{text-align:left;font-weight:600;padding:0;display:block}#ps-wg-design .ps-name-button:hover,#ps-wg-design .ps-link:hover{text-decoration:underline}#ps-wg-design .ps-cell-sub{display:block;color:var(--ps-muted);font-size:11px;margin-top:2px}#ps-wg-design .ps-copy{display:inline-flex;padding:5px;color:var(--ps-muted);vertical-align:middle}#ps-wg-design .ps-copy svg{width:13px;height:13px}#ps-wg-design .ps-table-foot{padding:13px 18px;color:var(--ps-muted);font-size:11px;border-top:1px solid var(--ps-line)}#ps-wg-design .ps-empty{text-align:center;padding:44px 20px;color:var(--ps-muted)}#ps-wg-design .ps-footer{display:flex;gap:8px;align-items:center;justify-content:space-between;margin-top:16px;font-size:11px;color:var(--ps-muted);flex-wrap:wrap}
|
||
#ps-wg-design .ps-back{margin-bottom:17px;font-size:12px}#ps-wg-design .ps-columns{display:grid;grid-template-columns:minmax(0,1.6fr) minmax(0,1fr);gap:20px;align-items:start}#ps-wg-design dl{display:grid;grid-template-columns:120px minmax(0,1fr);gap:14px;margin:20px 0 0}#ps-wg-design dt{color:var(--ps-muted);font-size:12px}#ps-wg-design dd{margin:0;font-size:13px}#ps-wg-design .ps-tabs{display:flex;border-bottom:1px solid var(--ps-line);gap:22px;margin-bottom:20px}#ps-wg-design .ps-tabs button{padding:11px 0;color:var(--ps-muted);border-bottom:2px solid transparent}#ps-wg-design .ps-tabs button[aria-selected=true]{color:var(--ps-teal);border-color:var(--ps-teal)}#ps-wg-design .ps-usage{display:grid;grid-template-columns:1fr 1fr;gap:15px;margin:18px 0}#ps-wg-design .ps-usage strong{display:block;font-size:23px;font-weight:550;font-variant-numeric:tabular-nums;margin-top:3px}#ps-wg-design .ps-event{display:grid;grid-template-columns:70px 1fr;gap:14px;padding:14px 0;border-top:1px solid var(--ps-line);font-size:13px}#ps-wg-design .ps-event:first-of-type{margin-top:14px}#ps-wg-design .ps-actions{display:flex;gap:10px;flex-wrap:wrap;margin-top:20px}#ps-wg-design .ps-confirm{background:var(--ps-red-bg);padding:15px;border-radius:8px;margin-top:16px;font-size:13px}#ps-wg-design .ps-confirm .ps-actions{margin-top:12px}#ps-wg-design .ps-edit-form{display:flex;gap:8px;margin-top:12px;flex-wrap:wrap}
|
||
#ps-wg-design .ps-wizard{max-width:780px;margin:0 auto}#ps-wg-design .ps-steps{display:flex;justify-content:space-between;gap:10px;margin:24px 0 28px}#ps-wg-design .ps-step{display:flex;align-items:center;gap:7px;font-size:12px;color:var(--ps-muted)}#ps-wg-design .ps-step span{display:grid;place-items:center;border:1px solid var(--ps-line);border-radius:50%;width:24px;height:24px;font-size:11px;flex:none}#ps-wg-design .ps-step.ps-current{color:var(--ps-teal)}#ps-wg-design .ps-step.ps-current span{background:var(--ps-teal);border-color:var(--ps-teal);color:var(--ps-panel)}#ps-wg-design .ps-form{display:grid;gap:20px}#ps-wg-design .ps-field{display:grid;gap:7px}#ps-wg-design .ps-label{font-size:13px;font-weight:550}#ps-wg-design .ps-field input,#ps-wg-design .ps-field select{width:100%}#ps-wg-design .ps-options{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}#ps-wg-design .ps-option{padding:13px 11px!important;border:1px solid var(--ps-line)!important;border-radius:8px;text-align:left;background:var(--ps-panel)!important;display:flex;flex-direction:column;gap:5px;align-items:start;min-height:92px}#ps-wg-design .ps-option[aria-pressed=true]{border-color:var(--ps-teal)!important;background:var(--ps-soft)!important}#ps-wg-design .ps-option strong{font-size:13px;font-weight:550}#ps-wg-design .ps-option span{font-size:11px;color:var(--ps-muted)}#ps-wg-design .ps-option svg{margin-bottom:2px}#ps-wg-design details{border-top:1px solid var(--ps-line);padding-top:15px;font-size:12px}#ps-wg-design summary{color:var(--ps-muted);cursor:var(--cursor-interaction,auto)}#ps-wg-design .ps-wizard-foot{display:flex;align-items:center;justify-content:space-between;gap:15px;margin-top:25px;padding-top:18px;border-top:1px solid var(--ps-line)}#ps-wg-design .ps-delivery{display:grid;grid-template-columns:180px minmax(0,1fr);gap:23px;align-items:start;margin:22px 0}#ps-wg-design .ps-qr{background:var(--ps-panel);border-radius:10px;padding:12px;width:174px;max-width:100%;border:1px solid var(--ps-line)}#ps-wg-design .ps-qr img{width:100%;display:block;image-rendering:pixelated}#ps-wg-design .ps-qr-label{text-align:center;color:var(--ps-muted);font-size:11px;margin-top:8px}#ps-wg-design .ps-instructions{padding-left:18px;margin:14px 0;font-size:13px;display:grid;gap:9px}#ps-wg-design .ps-code{padding:14px 17px;letter-spacing:3px;background:var(--ps-bg);border-radius:7px;font:22px/1.5 ui-monospace,monospace;margin:15px 0;width:fit-content}#ps-wg-design .ps-success-icon{width:48px;height:48px;background:var(--ps-soft);color:var(--ps-teal);display:grid;place-items:center;border-radius:50%;margin-bottom:18px}#ps-wg-design .ps-success-icon svg{width:24px;height:24px}#ps-wg-design .ps-success{padding:30px 24px}#ps-wg-design .ps-checking{display:flex;align-items:center;gap:12px;padding:20px;background:var(--ps-bg);border-radius:8px;margin:20px 0}#ps-wg-design .ps-checking svg{color:var(--ps-teal)}#ps-wg-design .ps-subscription{padding:20px;margin-top:16px}#ps-wg-design .ps-url{display:block;background:var(--ps-bg);padding:10px 12px;margin-top:10px;border-radius:6px;overflow-wrap:anywhere}#ps-wg-design .ps-toast{margin:0 26px 20px;padding:11px 14px;background:var(--ps-soft);border:1px solid var(--ps-line);border-radius:8px;font-size:13px;color:var(--ps-teal)}
|
||
@media(max-width:800px){#ps-wg-design .ps-top{gap:12px;padding-left:20px}#ps-wg-design .ps-nav{order:3;width:100%}#ps-wg-design .ps-body{padding:22px 20px}#ps-wg-design th,#ps-wg-design td{padding-left:12px;padding-right:12px}#ps-wg-design .ps-traffic-col{display:none}#ps-wg-design .ps-columns{grid-template-columns:1fr 1fr}}
|
||
@media(max-width:600px){#ps-wg-design{min-height:600px}#ps-wg-design .ps-top{padding:17px 16px 12px}#ps-wg-design .ps-brand{padding-right:80px}#ps-wg-design .ps-demo{display:none}#ps-wg-design .ps-nav{gap:0;margin-right:0}#ps-wg-design .ps-nav button:last-child{display:none}#ps-wg-design .ps-nav button{padding:7px 10px;font-size:12px;min-height:44px}#ps-wg-design .ps-body{padding:22px 15px}#ps-wg-design h1{font-size:24px}#ps-wg-design .ps-heading{align-items:flex-start;flex-wrap:wrap;gap:14px}#ps-wg-design .ps-heading>.ps-button{min-height:42px}#ps-wg-design .ps-network{align-items:flex-start;padding:14px;flex-direction:column;gap:8px}#ps-wg-design .ps-toolbar{padding:12px;gap:10px}#ps-wg-design .ps-search{max-width:none;width:100%;font-size:16px}#ps-wg-design table,#ps-wg-design tbody{display:block}#ps-wg-design thead{display:none}#ps-wg-design tr{display:grid;grid-template-columns:minmax(0,1fr) auto;padding:14px;border-top:1px solid var(--ps-line);gap:8px 10px}#ps-wg-design td{display:block;border:0;padding:0}#ps-wg-design td:first-child{grid-column:1;grid-row:1}#ps-wg-design td:nth-child(2){grid-column:1;grid-row:2;padding-left:45px}#ps-wg-design td:nth-child(3){grid-column:2;grid-row:1}#ps-wg-design td:nth-child(4){display:none}#ps-wg-design td:last-child{grid-column:2;grid-row:2;align-self:end}#ps-wg-design .ps-columns{grid-template-columns:1fr}#ps-wg-design .ps-pad{padding:17px}#ps-wg-design .ps-options{grid-template-columns:1fr}#ps-wg-design .ps-option{min-height:70px;display:grid;grid-template-columns:22px 1fr;gap:3px 9px}#ps-wg-design .ps-option svg{grid-row:1/3}#ps-wg-design .ps-option span{grid-column:2}#ps-wg-design .ps-steps{gap:6px}#ps-wg-design .ps-step{flex-direction:column;font-size:11px;gap:5px;text-align:center;flex:1}#ps-wg-design .ps-delivery{grid-template-columns:1fr;gap:16px}#ps-wg-design .ps-qr-wrap{justify-self:center}#ps-wg-design .ps-button{min-height:44px}#ps-wg-design input,#ps-wg-design select{font-size:16px}#ps-wg-design .ps-wizard-foot{align-items:flex-start;flex-wrap:wrap}#ps-wg-design .ps-toast{margin-left:15px;margin-right:15px}#ps-wg-design dl{grid-template-columns:90px 1fr;gap:13px}#ps-wg-design .ps-filter button{min-height:36px}#ps-wg-design .ps-success{padding:24px 18px}}
|
||
#ps-wg-design .ps-sub-selectors{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.5fr);gap:20px;align-items:start}
|
||
#ps-wg-design .ps-config-preview{white-space:pre-wrap;overflow-wrap:anywhere;background:var(--ps-bg);color:var(--ps-ink);padding:15px;border-radius:7px;font:12px/1.8 ui-monospace,SFMono-Regular,monospace;margin:14px 0 0}
|
||
#ps-wg-design .ps-url{word-break:break-all}
|
||
@media(max-width:600px){#ps-wg-design .ps-sub-selectors{grid-template-columns:1fr;gap:16px}}
|
||
</style>
|
||
<header class="ps-top">
|
||
<div class="ps-brand"><span class="ps-mark">P</span>Proxy Station</div>
|
||
<nav class="ps-nav" aria-label="主导航">
|
||
<button type="button" disabled>节点</button><button type="button" disabled>策略组</button><button type="button" disabled>规则</button><button type="button" data-action="devices" class="ps-active">设备</button><button type="button" data-action="subscriptions">订阅</button><button type="button" disabled>设置</button>
|
||
</nav>
|
||
<span class="ps-demo">交互原型 · 演示数据</span>
|
||
</header>
|
||
<main class="ps-body" id="ps-wg-content"></main>
|
||
<div class="ps-toast" id="ps-wg-toast" role="status" aria-live="polite" hidden></div>
|
||
<script>
|
||
(() => {
|
||
const root = document.getElementById('ps-wg-design');
|
||
const main = root.querySelector('#ps-wg-content');
|
||
const toastBox = root.querySelector('#ps-wg-toast');
|
||
const initialDevices = [
|
||
{id:1,name:'Mac Studio',os:'macOS',ip:'10.203.0.3',status:'online',last:'12 秒前',up:'26.4 Mbps',down:'120 Kbps',incoming:true,events:['开始设备纳管,保留原有身份','首次连接已验证','最近握手:12 秒前']},
|
||
{id:2,name:'iPad Pro',os:'iPadOS',ip:'10.203.0.2',status:'online',last:'18 秒前',up:'108 Kbps',down:'26.2 Mbps',incoming:true,events:['开始设备纳管,保留原有身份','已领取 WireGuard 配置','最近握手:18 秒前']},
|
||
{id:3,name:'MacBook Air',os:'macOS',ip:'10.203.0.4',status:'online',last:'1 分钟前',up:'0 bps',down:'0 bps',incoming:true,events:['设备身份已创建','首次连接已验证']},
|
||
{id:4,name:'iPhone',os:'iOS',ip:'10.203.0.5',status:'offline',last:'4 小时前',up:'—',down:'—',incoming:true,events:['设备身份已创建','首次连接已验证','最后活动:4 小时前']},
|
||
{id:5,name:'工作笔记本',os:'Windows',ip:'10.203.0.6',status:'pending',last:'尚未连接',up:'—',down:'—',incoming:true,events:['设备身份已创建','网关配置已生效,等待首次握手']},
|
||
{id:6,name:'开发主机',os:'Linux',ip:'10.203.0.7',status:'paused',last:'昨天',up:'—',down:'—',incoming:true,events:['设备身份已创建','已暂停:网关已移除 peer 并持久化']}
|
||
];
|
||
let devices = initialDevices.map(d=>({...d,events:[...d.events]}));
|
||
let addressWatermark=7;
|
||
const activeJobs=new Set();
|
||
let subscriptionDevice=0,subscriptionFormat='surge',lastPublishedState='';
|
||
const publishedStates=new Set();
|
||
const demoQr='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKUAAAClAQMAAAAH5a+ZAAAABlBMVEUAAAD///+l2Z/dAAAAAnRSTlP//8i138cAAAAJcEhZcwAACxIAAAsSAdLdfvwAAAFpSURBVEiJ3ZYxjoUwDESNKChzBG4CF0MCiYvBTXKElBSI2bGBv3zplztb/BQRvBRJHM/Yhk/DvoQuZtYsVoPTCP5YK6Qr92xWzGhQhrYBdikt1jZHQsbB7XmQQU6ByaxLM/6Frpi4VEY99fjy2+I136IuoJE7qz+kT28ZJaAxlkQxVKRvahFQ3jM3S5rarcJuVjHSSuqgJ9iqwuv2WhoCPJgxW2d1/s0dDaX2znSNyG6WIr4qilJnLoyAi4M3jkxV0dh+LWP2+Lav+IroqfQ0562P9ftkIuqDZ8BJn2oRUN/eOiqdtMYdXxV1myYY+KLJX7OYkrpxEuxeGUj5paSgCt1C3T3ttG4hdZG7LrKFnxlLn5DeueO6oLlcDqOiUYcO9kbu1V5tdyU9eyOe4VI/4sYyencrPMP0iq+URmWouP1mz/5MREMcPeYchq2kHl8vtN6tuBQBIb16IzYRTmlvz07sz+mn8R30B4VjHnaCiv9uAAAAAElFTkSuQmCC';
|
||
let page='subscriptions',selected=1,detailTab='overview',filter='all',query='',confirmAction='',editing=false,wizard=null,toastTimer;
|
||
const design = {scenario:'正常采集'};
|
||
const labels={online:'近期在线',offline:'离线',pending:'待接入',paused:'已暂停',unknown:'状态未知',provisioning:'配置中',error:'配置异常',revoking:'正在暂停',restoring:'正在恢复'};
|
||
const badgeClass={online:'ps-good',pending:'ps-amber',error:'ps-bad',unknown:'ps-amber',provisioning:'ps-amber',revoking:'ps-amber',restoring:'ps-amber'};
|
||
const icon=(name)=>`<i data-lucide="${name}" aria-hidden="true"></i>`;
|
||
const esc=(value)=>String(value).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||
const button=(label,action,cls='',attrs='')=>`<button type="button" class="ps-button ${cls}" data-action="${action}" ${attrs}>${label}</button>`;
|
||
const isStale=()=>design.scenario==='网关失联';
|
||
const status=d=>isStale()&&['online','offline'].includes(d.status)?'unknown':d.status;
|
||
const badge=d=>`<span class="ps-badge ${badgeClass[status(d)]||''}">${labels[status(d)]}</span>`;
|
||
const osIcon=os=>['iPadOS','iOS'].includes(os)?(os==='iPadOS'?'tablet':'smartphone'):(os==='Linux'?'server':'monitor');
|
||
const copy=(value)=>{try{const p=navigator.clipboard?.writeText(value);if(p)p.then(()=>toast('已复制:'+value)).catch(()=>toast('请手动复制:'+value));else toast('请手动复制:'+value);}catch{toast('请手动复制:'+value);}};
|
||
function toast(message){clearTimeout(toastTimer);toastBox.textContent=message;toastBox.hidden=false;toastTimer=setTimeout(()=>{toastBox.hidden=true;},5500);}
|
||
function save(){
|
||
try{
|
||
const snapshot={modelContent:{page,selectedDevice:devices.find(d=>d.id===selected)?.name||null,scenario:design.scenario,subscriptionDevice:devices.find(d=>d.id===subscriptionDevice)?.name||null,subscriptionFormat},privateContent:{version:2,page,selected,subscriptionDevice,subscriptionFormat,devices:devices.map(d=>({...d,events:d.events.slice(-5)})),filter,query,addressWatermark,wizard,scenario:design.scenario}};
|
||
if(new TextEncoder().encode(JSON.stringify(snapshot)).length>14500)snapshot.privateContent.devices.forEach(d=>{d.events=d.events.slice(-1);});
|
||
lastPublishedState=JSON.stringify(snapshot.privateContent);
|
||
publishedStates.add(lastPublishedState);if(publishedStates.size>32)publishedStates.delete(publishedStates.values().next().value);
|
||
window.openai?.setWidgetState?.(snapshot)?.catch(()=>{});
|
||
}catch{}
|
||
}
|
||
function restore(snapshot){const s=snapshot?.privateContent;if(!s||s.version!==2)return;subscriptionDevice=Number(s.subscriptionDevice)||0;subscriptionFormat=['surge','shadowrocket','shadowrocket-conf','clash'].includes(s.subscriptionFormat)?s.subscriptionFormat:'surge';if(['正常采集','网关失联','配置下发失败'].includes(s.scenario))design.scenario=s.scenario;if(Array.isArray(s.devices)&&s.devices.length<=20&&s.devices.every(d=>typeof d.name==='string'&&typeof d.ip==='string'&&Array.isArray(d.events)))devices=s.devices;wizard=s.wizard&&[1,2,3].includes(s.wizard.step)?s.wizard:null;page=['devices','detail','subscriptions','gateway','wizard','export'].includes(s.page)?s.page:'devices';if(page==='wizard'&&!wizard)page='devices';selected=Number(s.selected)||1;filter=s.filter||'all';query=typeof s.query==='string'?s.query:'';addressWatermark=Math.max(7,Number(s.addressWatermark)||7,...devices.map(d=>Number(d.ip.split('.').pop())));}
|
||
restore(window.openai?.widgetState);
|
||
window.addEventListener('openai:set_globals',e=>{if(e.detail?.globals?.widgetState){if(publishedStates.has(JSON.stringify(e.detail.globals.widgetState.privateContent)))return;restore(e.detail.globals.widgetState);render();resumeQueued();}});
|
||
function gatewayNotice(){return isStale()?`<div class="ps-notice ps-warning">${icon('wifi-off')}<div><strong>网关已 3 分钟未更新</strong><br>设备连接状态暂时未知。暂停等操作需等网关恢复后确认。</div></div>`:'';}
|
||
function network(){return `<section class="ps-panel ps-network"><div class="ps-row"><span class="ps-network-icon">${icon('network')}</span><div><div class="ps-network-title">个人设备网络 <span class="ps-small">/ njcq</span></div><div class="ps-small ps-mono">10.203.0.0/24 · 设备间互访</div></div></div><div class="ps-row"><span class="ps-badge ${isStale()?'ps-amber':'ps-good'}">${isStale()?'采集失联 · 3 分钟前':'网关正常 · 8 秒前更新'}</span><button type="button" class="ps-link ps-small" data-action="gateway">查看网关 ${icon('arrow-up-right')}</button></div></section>`;}
|
||
function listRows(){const filtered=devices.filter(d=>(filter==='all'||status(d)===filter)&&(!query||(d.name+' '+d.ip).toLowerCase().includes(query.toLowerCase())));return filtered.length?filtered.map(d=>`<tr><td><div class="ps-device-name"><span class="ps-device-icon">${icon(osIcon(d.os))}</span><div><button type="button" class="ps-name-button" data-action="detail" data-id="${d.id}">${esc(d.name)}</button><span class="ps-cell-sub">${esc(d.os)}</span></div></div></td><td><span class="ps-mono">${d.ip}</span><button type="button" class="ps-copy" aria-label="复制 ${esc(d.name)} 的 IP" data-action="copy" data-value="${d.ip}">${icon('copy')}</button></td><td>${badge(d)}</td><td><span class="ps-small">${status(d)==='unknown'?'采集已过期':d.last}</span></td><td class="ps-traffic-col"><span class="ps-mono">${status(d)==='online'?d.up:'—'}</span><span class="ps-cell-sub ps-mono">${status(d)==='online'?d.down:'—'}</span></td><td><button type="button" class="ps-link ps-small" data-action="${d.status==='pending'||d.status==='error'?'continue':'detail'}" data-id="${d.id}">${d.status==='pending'||d.status==='error'?'继续接入':'详情'} ${icon('chevron-right')}</button></td></tr>`).join(''):`<tr><td colspan="6"><div class="ps-empty">没有匹配的设备${query?',试试其他名称或 IP。':'。'}</div></td></tr>`;}
|
||
function devicesPage(){const count=devices.filter(d=>status(d)==='online').length;return `<div class="ps-heading"><div><h1>设备</h1><p class="ps-sub">管理私网中的设备,连接到它们的固定 IP。</p></div>${button(icon('plus')+' 接入新设备','new','ps-primary')}</div>${network()}<div class="ps-stack">${gatewayNotice()}<section class="ps-panel"><div class="ps-toolbar"><div class="ps-filter" aria-label="设备状态筛选">${[['all','全部 '+devices.length],['online','近期在线 '+count],['pending','待接入'],['paused','已暂停']].map(([v,l])=>`<button type="button" data-action="filter" data-value="${v}" aria-pressed="${filter===v}">${l}</button>`).join('')}</div><input class="ps-search" id="ps-search" aria-label="搜索设备名称或 IP" placeholder="搜索名称或 IP" value="${esc(query)}"></div><table aria-label="WireGuard 接入设备"><thead><tr><th>设备</th><th>私网 IP</th><th>状态</th><th>最后活动</th><th class="ps-traffic-col">上行 / 下行</th><th>操作</th></tr></thead><tbody id="ps-device-rows">${listRows()}</tbody></table><div class="ps-table-foot">流量方向以设备为准:上行 = 设备 → 网关;下行 = 网关 → 设备。近期握手不代表远程桌面已就绪。</div></section></div><div class="ps-footer"><span>每 15 秒更新 · 演示数据,未连接生产网关</span><button type="button" class="ps-link ps-small" data-action="subscriptions">原默认订阅仍在「订阅」中 ${icon('arrow-right')}</button></div>`;}
|
||
function compatibility(d){return `<div class="ps-notice">${icon('monitor')}<div><strong>通过固定 IP 访问设备</strong><br>在远程桌面中填写目标设备的私网 IP。网络握手与屏幕共享服务是否可用分开确认。</div></div>`;}
|
||
function detailsPage(){const d=devices.find(x=>x.id===selected)||devices[0];if(!d)return devicesPage();return `<button type="button" class="ps-link ps-back" data-action="devices">${icon('arrow-left')} 返回设备</button><div class="ps-heading"><div><div class="ps-row"><h1>${esc(d.name)}</h1>${badge(d)}</div><p class="ps-sub">${esc(d.os)} · 个人设备网络</p></div>${button(icon('copy')+' 复制 IP','copy','',`data-value="${d.ip}"`)}</div>${gatewayNotice()}<div class="ps-tabs" role="tablist" aria-label="设备详情"><button type="button" role="tab" id="ps-tab-overview" aria-controls="ps-detail-panel" aria-selected="${detailTab==='overview'}" data-action="tab" data-value="overview">连接概况</button><button type="button" role="tab" id="ps-tab-events" aria-controls="ps-detail-panel" aria-selected="${detailTab==='events'}" data-action="tab" data-value="events">操作记录 <span class="ps-small">${d.events.length}</span></button></div><div id="ps-detail-panel" role="tabpanel" aria-labelledby="ps-tab-${detailTab}">${detailTab==='events'?`<section class="ps-panel ps-pad"><h2>操作记录</h2>${[...d.events].reverse().map((ev,i)=>`<div class="ps-event"><span class="ps-small">${i===0?'最近':'此前'}</span><div>${esc(ev)}<div class="ps-small">演示记录</div></div></div>`).join('')}</section>`:`<div class="ps-columns"><div class="ps-stack"><section class="ps-panel ps-pad"><div class="ps-row ps-spread"><h2>连接信息</h2><button type="button" class="ps-link ps-small" data-action="edit">编辑名称 ${icon('pencil')}</button></div>${editing?`<form id="ps-edit-form" class="ps-edit-form"><input name="name" aria-label="设备名称" value="${esc(d.name)}" required maxlength="40"><button type="button" data-action="submit-form" data-form="ps-edit-form" class="ps-button ps-primary">保存</button><button type="button" class="ps-link" data-action="edit-cancel">取消</button></form>`:''}<dl><dt>固定私网 IP</dt><dd class="ps-mono">${d.ip}</dd><dt>最近活动</dt><dd>${status(d)==='unknown'?'采集已过期,暂时未知':d.last}</dd><dt>接入网关</dt><dd><button type="button" class="ps-link" style="padding:0" data-action="gateway">njcq ${icon('arrow-up-right')}</button></dd><dt>连接验证</dt><dd>${d.status==='pending'?'等待首次握手':d.status==='paused'?'已撤销网关接入':d.status==='error'?'网关配置未生效':'已验证首次握手'}<div class="ps-small">远程桌面可用性需在客户端单独确认</div></dd><dt>设备身份</dt><dd>此设备独立持有 <span class="ps-small">· 私钥不在列表展示</span></dd></dl><hr class="ps-line"><h3>当前流量</h3><div class="ps-usage"><div><span class="ps-small">设备上行</span><strong>${status(d)==='online'?d.up:'—'}</strong></div><div><span class="ps-small">设备下行</span><strong>${status(d)==='online'?d.down:'—'}</strong></div></div><p class="ps-small">${status(d)==='unknown'?'采集失联,已停止显示缓存速率。':'网关采样值 · 不代表 VNC 的画面码率'}</p></section>${compatibility(d)}</div><div class="ps-stack">${deviceConfigPanel(d)}<section class="ps-panel ps-pad"><h3>管理接入</h3><p class="ps-sub">暂停后,这台设备将无法通过私网通信。地址与记录保留。</p>${confirmAction?`<div class="ps-confirm" role="alert"><strong>${confirmAction==='pause'?'暂停 '+esc(d.name)+'?':confirmAction==='rotate'?'重置设备身份?':'移除设备?'}</strong><p style="margin-top:6px">${confirmAction==='pause'?'正在进行的私网连接会中断。需等网关确认撤销后,才会显示已暂停。':confirmAction==='rotate'?'旧配置会被撤销,此设备必须重新导入新配置。私网 IP 保持不变。':'先撤销网关接入,再归档设备。旧地址暂不分配给其他设备。'}</p><div class="ps-actions">${button('取消','cancel-confirm')}${button('确认'+(confirmAction==='pause'?'暂停':confirmAction==='rotate'?'重置':'移除'),'confirm','ps-danger')}</div></div>`:`<div class="ps-actions">${button(d.status==='paused'?icon('play')+' 恢复接入':icon('pause')+' 暂停接入',d.status==='paused'?'resume':'pause',d.status==='paused'?'':'ps-danger',['revoking','restoring','provisioning'].includes(d.status)?'disabled':'')}</div><div class="ps-row" style="margin-top:14px"><button type="button" class="ps-link ps-small" data-action="rotate">重置身份</button><span class="ps-small">·</span><button type="button" class="ps-link ps-small" data-action="remove">移除设备</button></div>`}</section></div></div>`}</div>`;}
|
||
function gatewayPage(){return `<button type="button" class="ps-link ps-back" data-action="devices">${icon('arrow-left')} 返回设备</button><div class="ps-heading"><div><h1>njcq 网关</h1><p class="ps-sub">个人设备网络的固定接入点</p></div><span class="ps-badge ${isStale()?'ps-amber':'ps-good'}">${isStale()?'采集失联':'运行正常'}</span></div><div class="ps-columns"><section class="ps-panel ps-pad"><h2>网关信息</h2><dl><dt>接入端点</dt><dd class="ps-mono">wg.example.net:51820 <span class="ps-small">(演示)</span></dd><dt>私网地址</dt><dd class="ps-mono">10.203.0.1/24</dd><dt>网络 MTU</dt><dd>1280</dd><dt>最近采集</dt><dd>${isStale()?'3 分钟前,已超时':'8 秒前'}</dd><dt>配置同步</dt><dd>${isStale()?'无法确认最新版本':'已确认运行与持久配置一致'}</dd><dt>纳管设备</dt><dd>${devices.length} 台</dd><dt>互访范围</dt><dd>10.203.0.0/24 · 设备间互访</dd></dl></section><div class="ps-stack">${gatewayNotice()||`<div class="ps-notice">${icon('check')}<div><strong>网关采集正常</strong><br>连接证据每 15 秒刷新。只有收到网关应用回执,操作才算生效。</div></div>`}<section class="ps-panel ps-pad"><h3>连接状态说明</h3><p class="ps-sub">近期在线:180 秒内有握手或流量增长。超过 45 秒没有采集,连接状态显示未知。</p><hr class="ps-line"><p class="ps-small">WireGuard 握手不能证明屏幕共享服务已开启,也不能给出端到端画面延迟和媒体丢包率。</p></section></div></div>`;}
|
||
function subscriptionsPage(){
|
||
const surge=subscriptionFormat==='surge';
|
||
const d=devices.find(x=>x.id===subscriptionDevice);
|
||
const attached=surge&&subscriptionDevice!==0;
|
||
const blocked=attached&&(!d||['paused','revoking','restoring','provisioning','error'].includes(d.status));
|
||
const formatNames={surge:'Surge',shadowrocket:'Shadowrocket 节点','shadowrocket-conf':'Shadowrocket 规则',clash:'Clash'};
|
||
const url=subscriptionUrl();
|
||
return `<div class="ps-heading"><div><h1>订阅</h1><p class="ps-sub">选择订阅格式,按需附加一台设备的 WireGuard 身份。</p></div><span class="ps-badge ps-good">原默认链接保留</span></div>
|
||
<section class="ps-panel ps-subscription">
|
||
<div class="ps-row ps-spread"><div><h2>Proxy Station</h2><p class="ps-small">同一份节点、策略组与规则 · 按设备生成 Surge 链接</p></div><span class="ps-badge ps-good">已启用</span></div>
|
||
<hr class="ps-line">
|
||
<div class="ps-sub-selectors">
|
||
<label class="ps-field"><span class="ps-label">订阅格式</span><select id="ps-sub-format" aria-label="订阅格式">${Object.entries(formatNames).map(([value,label])=>`<option value="${value}" ${subscriptionFormat===value?'selected':''}>${label}</option>`).join('')}</select></label>
|
||
${surge?`<label class="ps-field"><span class="ps-label">附加设备 WireGuard <span class="ps-small">(可选)</span></span><select id="ps-sub-device" aria-label="附加设备 WireGuard"><option value="0" ${subscriptionDevice===0?'selected':''}>不附加设备 · 纯代理订阅</option>${!d&&subscriptionDevice!==0?'<option selected disabled>所选设备已移除,请重新选择</option>':''}${devices.map(device=>`<option value="${device.id}" ${subscriptionDevice===device.id?'selected':''} ${['paused','revoking','restoring','provisioning','error'].includes(device.status)?'disabled':''}>${esc(device.name)} · ${device.ip} · ${labels[status(device)]}</option>`).join('')}</select></label>`:''}
|
||
</div>
|
||
${surge?`<p class="ps-small" style="margin-top:10px">设备来自「设备」列表。选择只影响当前生成的链接,不修改其他设备已保存的链接。</p>`:''}
|
||
<div class="ps-notice ${blocked?'ps-warning':''}" style="margin-top:20px" aria-live="polite">${icon(blocked?'circle-alert':attached?'network':'layers')}<div>${blocked?`<strong>此设备暂时不可生成接入配置</strong><br>${d?'请先恢复接入或处理配置异常,再选择此设备。':'设备已移除,原设备链接应停止下发配置。'}`:attached?`<strong>已附加 ${esc(d.name)} · <span class="ps-mono">${d.ip}</span></strong><br>保留原代理内容,同时包含此设备的私钥、地址与入站配置。此链接仅用于这台设备。`:`<strong>${surge?'纯代理订阅 · 不包含 WireGuard':'按原格式导出'}</strong><br>${surge?'只下发节点、策略组与规则,不附加设备地址或密钥。':'该格式保持原有导出行为,不附加本次设备身份。'}`}</div></div>
|
||
<div style="margin-top:22px"><div class="ps-row ps-spread"><h3>${attached?'设备专属订阅链接':'订阅链接'}</h3><span class="ps-small">示例地址 · 不含真实凭证</span></div><code id="ps-generated-url" class="ps-url ps-mono">${esc(url)}</code></div>
|
||
<div class="ps-actions">${button(icon('copy')+' 复制链接','copy-subscription','ps-primary',blocked?'disabled':'')}${button(icon('external-link')+' '+(surge?'导入 Surge':'导入客户端'),'import-subscription','',blocked?'disabled':'')}${surge&&subscriptionDevice?button('清除设备选择','clear-subscription-device'):''}${attached&&d?button('检查设备连接','check-selected-device','',blocked?'disabled':''):''}</div>
|
||
${surge?`<p class="ps-small" style="margin-top:12px">${attached?'清除设备选择后恢复原默认链接。每次更新仍按链接中绑定的设备生成配置。':'这就是原默认 Surge 地址。选择设备后,地址会带上专属接入凭证。'}</p>`:''}
|
||
<details style="margin-top:24px"><summary>查看下发内容结构</summary><p class="ps-small" style="margin-top:12px">结构预览,密钥已隐藏,不能作为真实配置导入。</p><pre id="ps-config-preview" class="ps-config-preview">${esc(subscriptionPreview())}</pre></details>
|
||
</section>
|
||
<div class="ps-footer"><span>交互原型 · 选项与链接均为演示</span><button type="button" class="ps-link ps-small" data-action="devices">管理设备 ${icon('arrow-right')}</button></div>`;
|
||
}
|
||
function startWizard(device){
|
||
wizard={step:device?2:1,id:device?.id||null,name:device?.name||'',os:device?.os||'iPadOS',incoming:true,busy:false,checking:false,verified:false,error:device?.status==='error'?'网关未能确认配置已生效。可重试同一设备任务。':'',fieldError:''};
|
||
page='wizard';render();
|
||
}
|
||
function delivery(w){
|
||
const d=devices.find(x=>x.id===w.id);if(!d)return '';
|
||
return `<div class="ps-row ps-spread"><div><h2>${esc(d.name)} 已创建</h2><p class="ps-sub">固定地址 <span class="ps-mono">${d.ip}</span> · 独立设备身份</p></div><span class="ps-badge ps-amber">等待首次连接</span></div>
|
||
<div class="ps-notice" style="margin-top:20px">${icon('check')}<div><strong>网关配置已生效</strong><br>接下来在 Surge 订阅中选择这台设备,或导出独立的 WireGuard 配置。</div></div>
|
||
<div class="ps-actions">${button(icon('link')+' 前往 Surge 订阅','use-surge-device','ps-primary',`data-id="${d.id}"`)}${button(icon('download')+' 导出独立 WireGuard 配置','export','',`data-id="${d.id}"`)}</div>
|
||
<hr class="ps-line"><p class="ps-small">添加设备不改变默认订阅。只有在 Surge 订阅中选择设备,生成的链接才包含它的 WireGuard 配置。</p>
|
||
<div class="ps-actions">${button('已完成导入,检查连接','to-verify')}${button('稍后接入','devices')}</div>`;
|
||
}
|
||
function wizardPage(){
|
||
const w=wizard;if(!w)return devicesPage();const d=devices.find(x=>x.id===w.id);
|
||
return `<div class="ps-wizard"><button type="button" class="ps-link ps-back" data-action="devices">${icon('arrow-left')} 返回设备</button>
|
||
<div class="ps-heading"><div><h1>${w.id?'设备接入':'接入新设备'}</h1><p class="ps-sub">设备管理身份与固定 IP,订阅页选择是否附加它。</p></div><span class="ps-demo">全流程模拟</span></div>
|
||
<div class="ps-steps">${['设备信息','获取配置','验证连接'].map((label,i)=>`<div class="ps-step ${w.step===i+1?'ps-current':''}"><span>${w.step>i+1?'✓':i+1}</span>${label}</div>`).join('')}</div>
|
||
<section class="ps-panel ps-pad">${w.step===1?`<h2>这是哪台设备?</h2><form id="ps-device-form" class="ps-form" style="margin-top:22px">
|
||
<label class="ps-field"><span class="ps-label">设备名称</span><input id="ps-new-name" name="name" aria-label="设备名称" placeholder="例如:我的 iPad Pro" value="${esc(w.name)}" maxlength="40" required autocomplete="off"></label>
|
||
<label class="ps-field"><span class="ps-label">操作系统</span><select id="ps-new-os" name="os" aria-label="操作系统">${['iPadOS','iOS','macOS','Windows','Linux'].map(os=>`<option ${os===w.os?'selected':''}>${os}</option>`).join('')}</select></label>
|
||
<div class="ps-notice">${icon('network')}<div><strong>加入「个人设备网络」</strong><br>自动生成独立密钥并分配固定 IP。通过 njcq 互访 10.203.0.0/24 中的设备。</div></div>
|
||
<details><summary>网络配置</summary><dl><dt>网络 MTU</dt><dd>1280</dd><dt>密钥</dt><dd>自动生成独立身份</dd><dt>入站访问</dt><dd>Surge 配置启用入站访问</dd></dl></details>
|
||
${w.fieldError?`<p class="ps-error ps-notice" role="alert">${esc(w.fieldError)}</p>`:''}
|
||
<div class="ps-wizard-foot"><span class="ps-small">网关确认后可在订阅中选择此设备</span><button type="button" data-action="submit-form" data-form="ps-device-form" class="ps-button ps-primary" ${w.busy?'disabled':''}>${w.busy?'正在配置网关…':'创建设备并继续'} ${icon('arrow-right')}</button></div></form>`:
|
||
w.step===2?w.error?`<div class="ps-notice ps-error" role="alert">${icon('circle-alert')}<div><strong>设备已保留,网关配置未成功</strong><br>${esc(w.error)}</div></div><dl><dt>设备</dt><dd>${esc(d.name)}</dd><dt>已预留 IP</dt><dd class="ps-mono">${d.ip}</dd><dt>重试方式</dt><dd>沿用同一身份与任务,不重复创建</dd></dl><div class="ps-actions">${button(w.busy?'正在重试…':'重试网关任务','retry','ps-primary',w.busy?'disabled':'')}${button('稍后继续','devices')}</div>`:delivery(w):
|
||
w.verified?`<div class="ps-success"><div class="ps-success-icon">${icon('check')}</div><h2>${esc(d.name)} 已完成接入</h2><p class="ps-sub">网关已观察到此设备的首次有效握手。</p><dl><dt>固定私网 IP</dt><dd class="ps-mono">${d.ip}</dd><dt>接入网关</dt><dd>njcq</dd><dt>连接验证</dt><dd>首次握手已确认 <span class="ps-small">(模拟)</span></dd></dl><div class="ps-notice" style="margin-top:23px">${icon('monitor')}<div>在远程桌面中填写目标设备的私网 IP。握手验证与远程桌面服务可用性分开确认。</div></div><div class="ps-actions">${button('查看设备详情','finish','ps-primary')}${button('返回设备列表','devices')}</div></div>`:
|
||
`<h2>等待首次连接</h2><p class="ps-sub">在这台设备启用已导入的隧道,再检查网关是否收到握手。</p><div class="ps-checking">${icon(w.checking?'loader-circle':'radio')}<div><strong>${w.checking?'正在等待网关确认…':'配置已就绪,设备尚未验证'}</strong><div class="ps-small">${d.ip} · ${esc(d.name)}</div></div></div>${isStale()?gatewayNotice():''}<p class="ps-small">生成订阅链接不代表设备在线。此原型会模拟收到首次握手。</p><div class="ps-actions">${button(w.checking?'正在检查…':'我已启用,检查连接','verify','ps-primary',w.checking||isStale()?'disabled':'')}${button('返回获取配置','install-back')}${button('稍后再验证','devices')}</div>`}
|
||
</section><div class="ps-footer"><span>交互原型 · 创建、导出、导入和握手均为模拟</span></div></div>`;
|
||
}
|
||
function subscriptionUrl(){
|
||
const base=`https://ps.example.net/sub/demo/${subscriptionFormat}`;
|
||
return subscriptionFormat==='surge'&&subscriptionDevice?`${base}?wg=demo-binding-${subscriptionDevice}`:base;
|
||
}
|
||
function subscriptionPreview(){
|
||
if(subscriptionFormat!=='surge')return '按所选格式输出原有节点或规则。\n不附加设备 WireGuard 身份。';
|
||
const d=devices.find(x=>x.id===subscriptionDevice);
|
||
const attached=subscriptionDevice!==0;
|
||
if(attached&&(!d||['paused','revoking','restoring','provisioning','error'].includes(d.status)))return '此设备不可用。正式接口应拒绝此设备链接,\n不会改为下发其他设备身份或静默降级为纯代理配置。';
|
||
return `#!MANAGED-CONFIG ${subscriptionUrl()} interval=86400
|
||
|
||
[Proxy]
|
||
# 原有代理节点
|
||
${attached?'NJCQ LAN = wireguard, section-name=njcq-lan\n':''}
|
||
[Proxy Group]
|
||
# 原有策略组;公网组排除 WG 节点
|
||
|
||
[Rule]
|
||
${attached?'IP-CIDR,10.203.0.0/24,NJCQ LAN,no-resolve\n':''}# 原有规则与 FINAL${attached?`\n
|
||
[WireGuard njcq-lan]
|
||
private-key = <${d.name} 的私钥,已隐藏>
|
||
self-ip = ${d.ip}
|
||
mtu = 1280
|
||
accept-incoming = true
|
||
peer = (public-key = <网关公钥>, preshared-key = <已隐藏>, allowed-ips = 10.203.0.0/24, endpoint = wg.example.net:51820, keepalive = 25)` : ''}`;
|
||
}
|
||
function exportPage(){
|
||
const d=devices.find(x=>x.id===selected);if(!d)return devicesPage();
|
||
const blocked=['paused','revoking','restoring','provisioning','error'].includes(d.status);
|
||
return `<div class="ps-wizard"><button type="button" class="ps-link ps-back" data-action="detail" data-id="${d.id}">${icon('arrow-left')} 返回设备详情</button>
|
||
<div class="ps-heading"><div><h1>导出 WireGuard 配置</h1><p class="ps-sub">${esc(d.name)} · <span class="ps-mono">${d.ip}</span></p></div>${badge(d)}</div>
|
||
<section class="ps-panel ps-pad"><h2>独立配置</h2><p class="ps-sub">用于标准 WireGuard 客户端,与代理订阅独立。同一设备的身份保持一致。</p>
|
||
${blocked?`<div class="ps-notice ps-warning" style="margin-top:18px">${icon('circle-alert')}<div>此设备当前不可领取接入配置,请先恢复接入或处理网关任务。</div></div>`:''}
|
||
<div class="ps-delivery"><div class="ps-qr-wrap"><div class="ps-qr"><img src="${demoQr}" alt="演示二维码,不含真实 WireGuard 密钥"></div><p class="ps-qr-label">演示二维码 · 无设备密钥</p></div><div><h3>下载或扫描导入</h3><ol class="ps-instructions"><li>在目标设备的 WireGuard 客户端添加隧道。</li><li>导入 .conf 文件,或扫描配置二维码。</li><li>启用隧道后,回到设备页检查连接。</li></ol>${button(icon('download')+' 下载 .conf','download','ps-primary',blocked?'disabled':'')}<p class="ps-small" style="margin-top:10px">此处仅演示,不下载真实私钥。</p></div></div>
|
||
<div class="ps-notice">${icon('shield')}<div>配置只属于 ${esc(d.name)}。请不要在另一台同时在线的设备上复用。</div></div>
|
||
<div class="ps-actions">${button('使用 Surge 订阅接入','use-surge-device','',`data-id="${d.id}" ${blocked?'disabled':''}`)}${button('检查设备连接','check-device','',`data-id="${d.id}" ${blocked?'disabled':''}`)}</div>
|
||
</section></div>`;
|
||
}
|
||
function deviceConfigPanel(d){
|
||
const blocked=['paused','revoking','restoring','provisioning','error'].includes(d.status);
|
||
return `<section class="ps-panel ps-pad"><h3>获取设备配置</h3><p class="ps-sub">在 Surge 订阅中附加此设备身份,或导出标准 WireGuard 配置。</p><div class="ps-actions">${button(icon('link')+' 用于 Surge 订阅','use-surge-device','ps-primary',`data-id="${d.id}" ${blocked?'disabled':''}`)}${button(icon('download')+' 导出独立 WireGuard 配置','export','',`data-id="${d.id}"`)}</div><hr class="ps-line"><p class="ps-small">订阅页不选择设备时,仍是干净的节点、策略组与规则订阅。导出的独立配置使用同一个设备身份。</p></section>`;
|
||
}
|
||
function render(){root.querySelectorAll('.ps-nav [data-action]').forEach(n=>{const active=n.dataset.action==='subscriptions'?page==='subscriptions':page!=='subscriptions';n.classList.toggle('ps-active',active);if(active)n.setAttribute('aria-current','page');else n.removeAttribute('aria-current');});main.innerHTML=page==='devices'?devicesPage():page==='detail'?detailsPage():page==='gateway'?gatewayPage():page==='subscriptions'?subscriptionsPage():page==='export'?exportPage():wizardPage();root.querySelectorAll('button,summary').forEach(el=>el.classList.add('cursor-interaction'));if(globalThis.lucide)lucide.createIcons({attrs:{width:16,height:16}});}
|
||
function event(d,text){d.events.push(text);if(d.events.length>15)d.events.shift();}
|
||
function applyAction(kind,deviceId=selected){
|
||
const d=devices.find(d=>d.id===deviceId);if(!d||activeJobs.has(deviceId))return;
|
||
confirmAction='';
|
||
if(!d.queuedAction){d.queuedAction=kind;d.status=kind==='resume'?'restoring':'revoking';event(d,'已请求'+({pause:'暂停',remove:'移除',rotate:'重置身份',resume:'恢复接入'}[kind])+',等待网关确认');}
|
||
render();
|
||
if(isStale()){toast('操作已排队,网关恢复并确认前不会标记成功。');save();return;}
|
||
activeJobs.add(deviceId);
|
||
setTimeout(()=>{
|
||
activeJobs.delete(deviceId);
|
||
const target=devices.find(x=>x.id===deviceId);if(!target)return;
|
||
if(isStale()){save();return;}
|
||
delete target.queuedAction;
|
||
if(kind==='remove'){devices=devices.filter(x=>x.id!==deviceId);if(page==='detail'&&selected===deviceId)page='devices';toast('模拟:网关撤销已确认,设备已归档。');}
|
||
else if(kind==='rotate'){target.status='pending';target.last='等待重新导入';event(target,'旧身份已撤销,新身份已应用,等待更新订阅或重新导入');toast('模拟:身份已重置,请更新设备订阅或重新导入独立配置。');}
|
||
else{target.status=kind==='pause'?'paused':'offline';target.last=kind==='pause'?'已撤销网关接入':'等待重新连接';event(target,kind==='pause'?'网关确认:peer 已从运行与持久配置移除':'网关确认:peer 已恢复,等待设备连接');toast(kind==='pause'?'模拟:网关确认撤销,此设备已暂停。':'模拟:接入已恢复,等待设备重新连接。');}
|
||
render();save();
|
||
},650);
|
||
}
|
||
function resumeQueued(){if(!isStale())devices.filter(d=>d.queuedAction).forEach(d=>applyAction(d.queuedAction,d.id));}
|
||
root.addEventListener('click',e=>{
|
||
const el=e.target.closest('button[data-action]');if(!el||el.disabled)return;
|
||
const a=el.dataset.action,v=el.dataset.value;
|
||
if(a==='submit-form'){const form=root.querySelector('#'+el.dataset.form);if(form.reportValidity())handleForm(form);return;}
|
||
const d=devices.find(x=>x.id===selected);
|
||
if(a==='copy'){copy(v);return;}
|
||
if(a==='copy-subscription'){copy(subscriptionUrl());return;}
|
||
if(a==='import-subscription'){toast('演示:将以当前链接导入'+(subscriptionFormat==='surge'?' Surge':'客户端')+'。不会修改真实客户端或将设备标为在线。');return;}
|
||
if(['devices','subscriptions','gateway'].includes(a)){page=a;confirmAction='';editing=false;render();save();return;}
|
||
if(a==='new'){startWizard();return;}
|
||
if(a==='detail'){selected=Number(el.dataset.id);page='detail';detailTab='overview';confirmAction='';editing=false;}
|
||
if(a==='filter')filter=v;
|
||
if(a==='tab')detailTab=v;
|
||
if(a==='edit')editing=true;
|
||
if(a==='edit-cancel')editing=false;
|
||
if(a==='continue'){selected=Number(el.dataset.id);startWizard(devices.find(x=>x.id===selected));return;}
|
||
if(a==='export'){selected=Number(el.dataset.id)||selected;page='export';}
|
||
if(a==='use-surge-device'){subscriptionDevice=Number(el.dataset.id)||selected;subscriptionFormat='surge';page='subscriptions';}
|
||
if(a==='clear-subscription-device')subscriptionDevice=0;
|
||
if(a==='check-selected-device'||a==='check-device'){
|
||
const target=devices.find(x=>x.id===(a==='check-selected-device'?subscriptionDevice:Number(el.dataset.id)));
|
||
if(target){selected=target.id;startWizard(target);wizard.step=3;render();save();}return;
|
||
}
|
||
if(['pause','rotate','remove'].includes(a))confirmAction=a;
|
||
if(a==='cancel-confirm')confirmAction='';
|
||
if(a==='confirm'){applyAction(confirmAction);return;}
|
||
if(a==='resume'){applyAction('resume');return;}
|
||
if(a==='download'){toast('演示:正式产品将下载此设备的独立 .conf;原型不含真实私钥。');return;}
|
||
if(a==='to-verify')wizard.step=3;
|
||
if(a==='install-back')wizard.step=2;
|
||
if(a==='finish'){selected=wizard.id;page='detail';detailTab='overview';}
|
||
if(a==='retry'){
|
||
const id=wizard.id;if(wizard.busy)return;wizard.busy=true;render();
|
||
setTimeout(()=>{const target=devices.find(x=>x.id===id);if(!target)return;target.status='pending';event(target,'重试成功:同一身份已在网关生效');design.scenario='正常采集';if(wizard?.id===id){wizard.busy=false;wizard.error='';}render();save();},700);return;
|
||
}
|
||
if(a==='verify'){
|
||
const id=wizard.id;if(wizard.checking||isStale())return;wizard.checking=true;render();
|
||
setTimeout(()=>{const target=devices.find(x=>x.id===id);if(!target)return;target.status='online';target.last='刚刚';target.up='12 Kbps';target.down='8 Kbps';event(target,'首次有效握手已确认(原型模拟)');if(wizard?.id===id){wizard.checking=false;wizard.verified=true;}render();save();},950);return;
|
||
}
|
||
render();save();
|
||
});
|
||
root.addEventListener('input',e=>{if(e.target.id==='ps-search'){query=e.target.value;root.querySelector('#ps-device-rows').innerHTML=listRows();if(globalThis.lucide)lucide.createIcons({attrs:{width:16,height:16}});save();}if(e.target.id==='ps-new-name')wizard.name=e.target.value;});
|
||
root.addEventListener('change',e=>{
|
||
if(e.target.id==='ps-new-os')wizard.os=e.target.value;
|
||
else if(e.target.id==='ps-sub-format'){subscriptionFormat=e.target.value;subscriptionDevice=0;}
|
||
else if(e.target.id==='ps-sub-device')subscriptionDevice=Number(e.target.value);
|
||
else return;
|
||
render();save();
|
||
});
|
||
function handleForm(form){if(form.id==='ps-edit-form'){const name=new FormData(form).get('name').trim();if(!name)return;const d=devices.find(x=>x.id===selected);d.name=name;event(d,'更新设备名称');editing=false;render();save();return;}if(form.id!=='ps-device-form'||wizard.busy)return;const w=wizard;w.name=w.name.trim();w.fieldError='';if(!w.name){w.fieldError='请填写设备名称。';render();return;}if(devices.some(d=>d.name===w.name)){w.fieldError='已有同名设备,请用位置或用途区分。';render();return;}if(devices.length>=20){w.fieldError='演示最多创建 20 台设备。';render();return;}w.busy=true;render();setTimeout(()=>{const maxOctet=addressWatermark++;const id=maxOctet;const failure=design.scenario==='配置下发失败'||isStale();const d={id,name:w.name,os:w.os,ip:'10.203.0.'+(maxOctet+1),incoming:w.incoming,status:failure?'error':'pending',last:'尚未连接',up:'—',down:'—',events:['已创建独立身份并预留 IP',failure?'网关未确认配置,等待重试':'网关确认运行与持久配置已生效']};devices.push(d);w.id=id;w.busy=false;w.step=2;if(failure)w.error=isStale()?'网关采集失联,暂时无法确认应用结果。':'配置同步超时。设备身份和预留地址已保留,可安全重试。';render();save();},750);}
|
||
root.addEventListener('submit',e=>{e.preventDefault();handleForm(e.target);});
|
||
root.addEventListener('keydown',e=>{const form=e.target.closest('form');if(e.key==='Enter'&&form&&e.target.tagName!=='BUTTON'){e.preventDefault();if(form.reportValidity())handleForm(form);}});
|
||
render();resumeQueued();
|
||
if(globalThis.Tweak){const tweak=new Tweak({container:root,onChange:()=>{render();save();resumeQueued();}});tweak.addSelect(design,'scenario',{label:'网关与接入情境',options:['正常采集','网关失联','配置下发失败']});}
|
||
})();
|
||
</script>
|
||
</div>
|