html_v2 = '''<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TR1 — 去中心化加密通讯网络</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap');
:root {
  --bg:#050508;--panel:#0c0c14;--panel2:#12121c;--border:#1e1e30;--border2:#2a2a40;
  --accent:#00f0c8;--accent2:#7b61ff;--accent3:#ff9f43;
  --text:#e2e2e8;--muted:#55556b;--danger:#ff3860;--success:#00d26a;--warn:#ffd166;
}
* { margin:0; padding:0; box-sizing:border-box; }
body {
  background:radial-gradient(ellipse at top,#0d0d18 0%,var(--bg) 60%);
  color:var(--text); font-family:'JetBrains Mono',monospace;
  min-height:100vh; display:flex; flex-direction:column; align-items:center;
  padding:16px; font-size:0.9rem;
}
h1 { font-size:2rem; letter-spacing:6px; color:var(--accent); margin-top:12px; text-shadow:0 0 12px rgba(0,240,200,0.3); }
.subtitle { color:var(--muted); font-size:0.78rem; margin-bottom:20px; letter-spacing:3px; }

.panel {
  background:var(--panel); border:1px solid var(--border); border-radius:14px;
  padding:22px; width:100%; max-width:720px; margin-bottom:14px;
  box-shadow:0 4px 24px rgba(0,0,0,0.4); transition:border-color 0.3s;
}
.panel:hover { border-color:var(--border2); }
.panel h2 { font-size:0.95rem; color:var(--accent2); margin-bottom:14px; display:flex; align-items:center; gap:8px; }
.panel h2 .icon { font-size:1.1rem; }

input, button, textarea, select {
  font-family:'JetBrains Mono',monospace; border-radius:8px; border:1px solid var(--border);
  background:var(--bg); color:var(--text); padding:10px 12px; font-size:0.88rem; outline:none; transition:0.2s;
}
input:focus, textarea:focus, select:focus { border-color:var(--accent); box-shadow:0 0 10px rgba(0,240,200,0.1); }
button {
  cursor:pointer; background:linear-gradient(135deg,var(--accent),var(--accent2));
  color:#000; font-weight:700; border:none; letter-spacing:0.5px; white-space:nowrap;
}
button:hover { opacity:0.9; transform:translateY(-1px); }
button:disabled { opacity:0.35; cursor:not-allowed; transform:none; }
button.danger { background:var(--danger); color:#fff; }
button.warn { background:var(--warn); color:#000; }
button.ghost { background:transparent; border:1px solid var(--border); color:var(--text); }
button.ghost:hover { border-color:var(--accent); color:var(--accent); }

.row { display:flex; gap:10px; margin-top:10px; align-items:center; }
.row input, .row select { flex:1; }
.row button { flex:0 0 auto; }

.status { font-size:0.78rem; margin-top:8px; min-height:18px; }
.status.ok { color:var(--success); }
.status.err { color:var(--danger); }
.status.info { color:var(--accent); }
.status.warn { color:var(--warn); }

.badge {
  display:inline-block; padding:2px 8px; border-radius:4px; font-size:0.72rem;
  background:rgba(0,240,200,0.08); color:var(--accent); border:1px solid var(--accent);
}
.badge.admin { background:rgba(255,159,67,0.08); color:var(--accent3); border-color:var(--accent3); }
.badge.sub { background:rgba(123,97,255,0.08); color:var(--accent2); border-color:var(--accent2); }
.badge.locked { background:rgba(255,56,96,0.08); color:var(--danger); border-color:var(--danger); }

.id-display {
  font-size:1.6rem; font-weight:700; color:var(--accent); text-align:center;
  padding:12px; border:2px dashed var(--accent); border-radius:10px; margin:8px 0; letter-spacing:6px;
  text-shadow:0 0 10px rgba(0,240,200,0.2);
}

.chat-box {
  height:280px; overflow-y:auto; border:1px solid var(--border); border-radius:8px;
  padding:12px; background:#07070c; margin-bottom:10px;
}
.msg { margin-bottom:8px; padding:8px 12px; border-radius:6px; font-size:0.82rem; line-height:1.5; max-width:88%; word-break:break-all; animation:fadeIn 0.3s ease; }
@keyframes fadeIn { from{opacity:0;transform:translateY(4px)} to{opacity:1;transform:translateY(0)} }
.msg.me { background:rgba(0,240,200,0.06); border-left:3px solid var(--accent); margin-left:auto; }
.msg.peer { background:rgba(123,97,255,0.06); border-left:3px solid var(--accent2); }
.msg.sys { background:rgba(255,209,102,0.05); border-left:3px solid var(--warn); color:var(--warn); text-align:center; margin-left:auto; margin-right:auto; font-size:0.75rem; }
.msg .time { font-size:0.68rem; color:var(--muted); margin-top:3px; }
.msg .tag { font-size:0.7rem; color:var(--accent); margin-bottom:2px; font-weight:700; }

.video-grid {
  display:grid; grid-template-columns:1fr 1fr; gap:10px; margin-bottom:12px;
}
.video-box {
  background:#000; border:1px solid var(--border); border-radius:8px; aspect-ratio:16/9;
  display:flex; align-items:center; justify-content:center; color:var(--muted); font-size:0.75rem;
  overflow:hidden; position:relative;
}
.video-box video { width:100%; height:100%; object-fit:cover; }
.video-box .label {
  position:absolute; bottom:6px; left:6px; background:rgba(0,0,0,0.7);
  padding:2px 6px; border-radius:4px; font-size:0.7rem; color:var(--accent);
}

.hidden { display:none !important; }

.tab-bar {
  display:flex; gap:4px; margin-bottom:14px; border-bottom:1px solid var(--border); padding-bottom:8px;
}
.tab-bar button {
  background:transparent; color:var(--muted); border:none; padding:8px 16px; font-size:0.8rem;
  border-bottom:2px solid transparent; border-radius:0;
}
.tab-bar button.active { color:var(--accent); border-bottom-color:var(--accent); }

.announcement-bar {
  background:linear-gradient(90deg,rgba(0,240,200,0.05),rgba(123,97,255,0.05));
  border:1px solid var(--border2); border-radius:8px; padding:10px 14px;
  margin-bottom:14px; font-size:0.82rem; display:flex; align-items:center; gap:10px;
}
.announcement-bar .label { color:var(--accent); font-weight:700; white-space:nowrap; }
.announcement-bar .content { flex:1; color:var(--text); }

.table-wrap { overflow-x:auto; }
table { width:100%; border-collapse:collapse; font-size:0.78rem; margin-top:8px; }
th, td { padding:8px 10px; text-align:left; border-bottom:1px solid var(--border); }
th { color:var(--accent); font-weight:600; background:rgba(0,240,200,0.03); }
td { color:var(--muted); }
tr:hover td { color:var(--text); background:rgba(255,255,255,0.01); }

.glow-text { text-shadow:0 0 8px rgba(0,240,200,0.25); }

#debugPanel {
  position:fixed; bottom:10px; right:10px; width:420px; max-height:340px;
  overflow-y:auto; background:rgba(5,5,8,0.96); border:1px solid var(--danger);
  border-radius:10px; padding:14px; font-size:0.72rem; color:var(--danger); z-index:200;
}
#debugPanel h3 { color:var(--danger); margin-bottom:8px; font-size:0.85rem; display:flex; justify-content:space-between; }
#debugLog { white-space:pre-wrap; word-break:break-all; max-height:240px; overflow-y:auto; }

::-webkit-scrollbar { width:6px; }
::-webkit-scrollbar-track { background:var(--bg); }
::-webkit-scrollbar-thumb { background:var(--border); border-radius:3px; }
::-webkit-scrollbar-thumb:hover { background:var(--accent); }

.footer-hint {
  position:fixed; bottom:10px; left:10px; font-size:0.65rem; color:var(--muted);
}

.modal-overlay {
  position:fixed; inset:0; background:rgba(0,0,0,0.7); z-index:150;
  display:flex; align-items:center; justify-content:center; backdrop-filter:blur(4px);
}
.modal-box {
  background:var(--panel); border:1px solid var(--border2); border-radius:14px;
  padding:24px; width:90%; max-width:480px; box-shadow:0 20px 60px rgba(0,0,0,0.5);
}
.modal-box h3 { color:var(--accent); margin-bottom:12px; font-size:1rem; }
.modal-box p { color:var(--muted); font-size:0.82rem; margin-bottom:16px; line-height:1.6; }

.encrypt-pulse {
  display:inline-flex; align-items:center; gap:6px; font-size:0.75rem; color:var(--accent);
}
.encrypt-pulse::before {
  content:""; width:7px; height:7px; background:var(--accent); border-radius:50%;
  box-shadow:0 0 8px var(--accent); animation:pulse 1.5s infinite;
}
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.2} }

.admin-section { border:1px dashed var(--accent3); border-radius:10px; padding:16px; margin-top:14px; background:rgba(255,159,67,0.02); }
.admin-section h3 { color:var(--accent3); font-size:0.88rem; margin-bottom:10px; }
</style>
</head>
<body>

<h1 class="glow-text">TR1</h1>
<div class="subtitle">无服务器 · P2P直连 · WHT/WHT2全链路加密 · 视频通话</div>

<!-- ==================== 注册面板 ==================== -->
<div id="registerPanel" class="panel">
  <h2><span class="icon">◈</span> 身份注册</h2>
  <p style="font-size:0.8rem;color:var(--muted);margin-bottom:12px;line-height:1.6;">
    请从 <b>000001 ~ 100000</b> 中选取通讯码。<br>
    <span style="color:var(--accent3)">000001</span> 为超级管理员 · 
    <span style="color:var(--accent3)">000002-000003</span> 为管理员 · 
    <span style="color:var(--accent2)">000004-000009</span> 为副管理员<br>
    特殊号码可直接选取,普通号码先到先得。
  </p>
  <div class="row">
    <input type="number" id="codeInput" min="1" max="100000" placeholder="输入通讯码 (如 000042)">
    <button onclick="register()">确认注册</button>
  </div>
  <div id="regStatus" class="status"></div>
</div>

<!-- ==================== 主面板 ==================== -->
<div id="mainPanel" class="panel hidden">
  <!-- 公告栏 -->
  <div class="announcement-bar" id="announcementBar">
    <span class="label">📢 公告</span>
    <span class="content" id="announcementText">暂无公告</span>
    <button class="ghost" id="editAnnounceBtn" style="padding:4px 10px;font-size:0.72rem;" onclick="editAnnouncement()">编辑</button>
  </div>

  <!-- 身份卡片 -->
  <div style="display:flex;align-items:center;gap:14px;margin-bottom:14px;">
    <div style="flex:1;">
      <h2 style="margin-bottom:4px;"><span class="icon">◈</span> 你的通讯码</h2>
      <div class="id-display" id="myIdDisplay">-------</div>
    </div>
    <div style="text-align:right;">
      <div id="roleBadge" class="badge">普通用户</div>
      <div class="encrypt-pulse" style="margin-top:8px;">WHT2 加密就绪</div>
    </div>
  </div>

  <!-- 标签页 -->
  <div class="tab-bar">
    <button class="active" onclick="switchTab('connect')" id="tab-connect">连接</button>
    <button onclick="switchTab('chat')" id="tab-chat">消息</button>
    <button onclick="switchTab('video')" id="tab-video">视频通话</button>
    <button onclick="switchTab('admin')" id="tab-admin" class="hidden">管理后台</button>
  </div>

  <!-- 连接页 -->
  <div id="page-connect">
    <p style="font-size:0.8rem;color:var(--muted);margin-bottom:10px;">输入对方通讯码建立 P2P 加密通道</p>
    <div class="row">
      <input type="number" id="targetInput" placeholder="对方通讯码">
      <button onclick="connectToPeer()">建立连接</button>
    </div>
    <div id="connStatus" class="status info">等待连接...</div>
    <div style="margin-top:14px;font-size:0.75rem;color:var(--muted);">
      <div>本地连接状态: <span id="localConnState" style="color:var(--muted)">空闲</span></div>
      <div style="margin-top:4px;">信号加密: <span style="color:var(--success)">WHT 核心加密</span> · 会话加密: <span style="color:var(--success)">WHT2 动态密钥</span></div>
    </div>
  </div>

  <!-- 消息页 -->
  <div id="page-chat" class="hidden">
    <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
      <span style="font-size:0.82rem;color:var(--accent2);">◈ 加密会话</span>
      <span class="badge" id="peerBadge">未连接</span>
    </div>
    <div class="chat-box" id="chatBox"></div>
    <div class="row">
      <input type="text" id="msgInput" placeholder="输入加密消息..." onkeydown="if(event.key==='Enter')sendMsg()">
      <button onclick="sendMsg()">发送</button>
    </div>
  </div>

  <!-- 视频通话页 -->
  <div id="page-video" class="hidden">
    <div class="video-grid">
      <div class="video-box" id="localVideoBox">
        <span>本地摄像头</span>
        <video id="localVideo" autoplay muted playsinline class="hidden"></video>
        <span class="label hidden" id="localLabel">我</span>
      </div>
      <div class="video-box" id="remoteVideoBox">
        <span>远程画面</span>
        <video id="remoteVideo" autoplay playsinline class="hidden"></video>
        <span class="label hidden" id="remoteLabel">对方</span>
      </div>
    </div>
    <div class="row" style="justify-content:center;">
      <button id="btnStartVideo" onclick="startVideoCall()">📹 发起视频通话</button>
      <button id="btnEndVideo" class="danger hidden" onclick="endVideoCall()">📴 结束通话</button>
    </div>
    <div id="videoStatus" class="status info" style="text-align:center;margin-top:8px;">点击按钮发起视频通话</div>
  </div>

  <!-- 管理后台页 -->
  <div id="page-admin" class="hidden">
    <!-- 积压记录 -->
    <div class="admin-section" id="backlogSection">
      <h3>📦 积压通讯记录(脱敏视图)</h3>
      <p style="font-size:0.75rem;color:var(--muted);margin-bottom:8px;">以下内容已加密,您只能查看元数据,无法读取具体通讯内容。</p>
      <div class="table-wrap">
        <table>
          <thead><tr><th>时间</th><th>发起方</th><th>接收方</th><th>消息数</th><th>状态</th></tr></thead>
          <tbody id="backlogTable"></tbody>
        </table>
      </div>
    </div>

    <!-- 锁号管理 -->
    <div class="admin-section" style="margin-top:14px;">
      <h3>🔒 号码锁定管理</h3>
      <div class="row">
        <input type="number" id="lockTargetInput" placeholder="输入要锁定/解锁的号码">
        <button class="danger" onclick="lockNumber()">锁定</button>
        <button class="warn" onclick="unlockNumber()">解锁</button>
      </div>
      <div id="lockStatus" class="status"></div>
      <div style="margin-top:10px;font-size:0.78rem;color:var(--muted);">
        当前锁定列表: <span id="lockedList">无</span>
      </div>
    </div>

    <!-- 系统日志 -->
    <div class="admin-section" style="margin-top:14px;">
      <h3>📋 系统日志</h3>
      <div class="table-wrap">
        <table>
          <thead><tr><th>时间</th><th>事件</th><th>详情</th></tr></thead>
          <tbody id="logTable"></tbody>
        </table>
      </div>
    </div>
  </div>
</div>

<!-- ==================== 后台调试面板 ==================== -->
<div id="debugPanel" class="hidden">
  <h3>⚠ TR1 后台监视器 <button class="ghost" style="padding:2px 8px;font-size:0.7rem;" onclick="document.getElementById('debugPanel').classList.add('hidden')">关闭</button></h3>
  <pre id="debugLog"></pre>
</div>

<!-- ==================== 编辑公告弹窗 ==================== -->
<div id="announceModal" class="modal-overlay hidden" onclick="if(event.target===this)closeAnnounceModal()">
  <div class="modal-box">
    <h3>编辑公告</h3>
    <p>此公告将对所有在线用户可见。</p>
    <textarea id="announceEditArea" rows="4" style="width:100%;resize:vertical;" placeholder="输入公告内容..."></textarea>
    <div class="row" style="justify-content:flex-end;margin-top:14px;">
      <button class="ghost" onclick="closeAnnounceModal()">取消</button>
      <button onclick="saveAnnouncement()">发布</button>
    </div>
  </div>
</div>

<script>
// ===================== 权限常量 =====================
const ROLES = {
  SUPER_ADMIN: ['000001'],
  ADMIN:       ['000001','000002','000003'],
  SUB_ADMIN:   ['000004','000005','000006','000007','000008','000009'],
  SPECIAL:     ['000001','000002','000003','000004','000005','000006','000007','000008','000009']
};
function isSuperAdmin(c){ return ROLES.SUPER_ADMIN.includes(c); }
function isAdmin(c){ return ROLES.ADMIN.includes(c); }
function isSubAdmin(c){ return ROLES.SUB_ADMIN.includes(c); }
function isSpecial(c){ return ROLES.SPECIAL.includes(c); }
function isNormal(c){ const n=parseInt(c); return n>=10 && n<=100000; }
function getRoleName(c){
  if(isSuperAdmin(c)) return '超级管理员';
  if(isAdmin(c)) return '管理员';
  if(isSubAdmin(c)) return '副管理员';
  return '普通用户';
}
function getRoleBadgeClass(c){
  if(isAdmin(c)) return 'admin';
  if(isSubAdmin(c)) return 'sub';
  return '';
}

// ===================== WHT / WHT2 加密引擎 =====================
const WHT = {
  key:"TR1_WHT_CORE_V2_2026_SECURE",
  rotate:(s,n)=>s.split('').map((c,i)=>String.fromCharCode(c.charCodeAt(0)^((n+i)%256))).join(''),
  scramble:(text)=>{
    let b64=btoa(unescape(encodeURIComponent(text)));
    let rev=b64.split('').reverse().join('');
    let kh=0; for(let i=0;i<WHT.key.length;i++) kh=((kh<<5)-kh)+WHT.key.charCodeAt(i);
    return WHT.rotate(rev, Math.abs(kh)%256);
  },
  unscramble:(cipher)=>{
    let kh=0; for(let i=0;i<WHT.key.length;i++) kh=((kh<<5)-kh)+WHT.key.charCodeAt(i);
    let rev=WHT.rotate(cipher, Math.abs(kh)%256);
    let b64=rev.split('').reverse().join('');
    return decodeURIComponent(escape(atob(b64)));
  }
};
const WHT2 = {
  genSessionKey:()=>{ const a=new Uint8Array(32); crypto.getRandomValues(a); return Array.from(a).map(b=>b.toString(16).padStart(2,'0')).join(''); },
  encrypt:(text,key)=>{ const x=WHT2._xor(text,key); const c=x.match(/.{1,4}/g).join('-'); return WHT.scramble(c); },
  decrypt:(cipher,key)=>{ const c=WHT.unscramble(cipher).replace(/-/g,''); return WHT2._xor(c,key); },
  _xor:(text,key)=>{ let o=''; for(let i=0;i<text.length;i++) o+=String.fromCharCode(text.charCodeAt(i)^key.charCodeAt(i%key.length)); return o; }
};

// ===================== 数据库层 =====================
const DB = {
  get:(k)=>{ try{return JSON.parse(localStorage.getItem('TR1_'+k));}catch(e){return null;} },
  set:(k,v)=>localStorage.setItem('TR1_'+k, JSON.stringify(v)),
  initRegistry:()=>{
    let r=DB.get('registry');
    if(!r){ r={}; DB.set('registry',r); }
    // 确保特殊号码有占位
    ROLES.SPECIAL.forEach(code=>{ if(!r[code]) r[code]={role:getRoleName(code),locked:false,ts:null,sig:null}; });
    DB.set('registry',r);
    return r;
  },
  getRegistry:()=>DB.get('registry')||DB.initRegistry(),
  claim:(code,role='普通用户')=>{
    const r=DB.getRegistry();
    r[code]={role,locked:false,ts:Date.now(),sig:WHT2.genSessionKey().slice(0,16)};
    DB.set('registry',r);
  },
  isTaken:(code)=>{ const r=DB.getRegistry(); return !!r[code] && !!r[code].ts; },
  isLocked:(code)=>{ const r=DB.getRegistry(); return !!(r[code]&&r[code].locked); },
  lock:(code,by)=>{ const r=DB.getRegistry(); if(r[code]){ r[code].locked=true; r[code].lockedBy=by; DB.set('registry',r); } },
  unlock:(code)=>{ const r=DB.getRegistry(); if(r[code]){ r[code].locked=false; delete r[code].lockedBy; DB.set('registry',r); } },
  getLockedList:()=>{
    const r=DB.getRegistry();
    return Object.entries(r).filter(([k,v])=>v.locked).map(([k,v])=>k);
  },
  getMyCode:()=>DB.get('myCode'),
  setMyCode:(code)=>DB.set('myCode',code),
  getAnnouncement:()=>DB.get('announcement')||'欢迎使用 TR1 加密通讯网络。请遵守网络规范,保护隐私安全。',
  setAnnouncement:(text)=>DB.set('announcement',text),
  // 积压记录: {id, from, to, timestamp, count, encryptedSnapshot}
  getBacklog:()=>DB.get('backlog')||[],
  addBacklog:(record)=>{ const b=DB.getBacklog(); b.push(record); DB.set('backlog',b); },
  clearBacklog:()=>DB.set('backlog',[]),
  getSystemLogs:()=>DB.get('syslogs')||[],
  addLog:(event,detail='')=>{ const L=DB.getSystemLogs(); L.unshift({ts:Date.now(),event,detail}); if(L.length>200)L.pop(); DB.set('syslogs',L); }
};

// ===================== 全局状态 =====================
let myCode=null;
let peerCode=null;
let sessionKey=null;
let pc=null, dc=null;
let videoPC=null;
let localStream=null;
let bc=null; // BroadcastChannel
const ICE=[{urls:'stun:stun.l.google.com:19302'},{urls:'stun:stun1.l.google.com:19302'}];

// ===================== 日志系统 =====================
function log(msg,type='info'){
  const ts=new Date().toLocaleTimeString();
  console.log(`[TR1 ${ts}] ${msg}`);
  const dl=document.getElementById('debugLog');
  if(dl) dl.textContent+=`[${ts}] [${type.toUpperCase()}] ${msg}\\n`;
  DB.addLog(type,msg);
  refreshLogTable();
}
function showStatus(id,text,cls){ const el=document.getElementById(id); el.textContent=text; el.className='status '+cls; }

// ===================== 初始化 =====================
window.onload=()=>{
  DB.initRegistry();
  myCode=DB.getMyCode();
  if(myCode){ enterMain(); }
  updateAnnouncementBar();
  initBroadcastChannel();
  log('TR1 v2.0 系统初始化完成','ok');
  log('WHT/WHT2 加密引擎已加载','ok');
  log('权限系统已加载','ok');
};

function initBroadcastChannel(){
  try{
    bc=new BroadcastChannel('TR1_NETWORK');
    bc.onmessage=(e)=>{
      const data=e.data;
      if(data.type==='admin-online' && isAdmin(myCode)){
        // 我是管理员,收到其他客户端的积压记录
        log(`收到来自 ${data.from} 的积压记录 (${data.backlog.length} 条)`,'info');
        data.backlog.forEach(r=>DB.addBacklog(r));
        refreshBacklogTable();
      }
      if(data.type==='admin-online-broadcast' && !isAdmin(myCode)){
        // 有管理员上线了,发送我的积压记录
        const myBacklog=DB.getBacklog();
        if(myBacklog.length>0){
          bc.postMessage({type:'backlog-submit',from:myCode,backlog:myBacklog});
          log(`管理员在线,已发送 ${myBacklog.length} 条积压记录`,'ok');
          DB.clearBacklog();
        }
      }
      if(data.type==='backlog-submit' && isAdmin(myCode)){
        data.backlog.forEach(r=>DB.addBacklog(r));
        refreshBacklogTable();
      }
      if(data.type==='announcement-update'){
        updateAnnouncementBar();
      }
      if(data.type==='registry-update'){
        // 同步注册表更新
      }
    };
    // 如果我是管理员,广播在线
    if(myCode && isAdmin(myCode)){
      setTimeout(()=>{
        bc.postMessage({type:'admin-online-broadcast',from:myCode,ts:Date.now()});
        log('管理员在线广播已发送','ok');
      },800);
    }
  }catch(e){ log('BroadcastChannel 不支持: '+e.message,'err'); }
}

// ===================== 注册流程 =====================
function register(){
  const val=document.getElementById('codeInput').value.trim();
  const codeNum=parseInt(val);
  if(isNaN(codeNum)||codeNum<1||codeNum>100000){
    showStatus('regStatus','请输入 000001 到 100000 之间的数字','err'); return;
  }
  const code=String(codeNum).padStart(6,'0');

  // 检查是否被锁定
  if(DB.isLocked(code)){
    showStatus('regStatus','该号码已被管理员锁定,暂时无法使用','err'); return;
  }

  // 特殊号码可以直接选(但已被占用的普通逻辑仍然适用,除非是管理员重登)
  if(DB.isTaken(code) && DB.getMyCode()!==code){
    // 如果是特殊号码,允许覆盖?不,用户说"可以直接选",但没说可以覆盖别人
    // 理解为:这些号码是预留的,先到先得,但注册界面可以直接输入这些号码
    showStatus('regStatus','该通讯码已被占用,请选择其他号码','err'); return;
  }

  const role=getRoleName(code);
  DB.claim(code,role);
  DB.setMyCode(code);
  myCode=code;
  log(`身份注册成功: ${myCode} [${role}]`,'ok');
  enterMain();
}

function enterMain(){
  document.getElementById('registerPanel').classList.add('hidden');
  document.getElementById('mainPanel').classList.remove('hidden');
  document.getElementById('myIdDisplay').textContent=myCode;
  const rb=document.getElementById('roleBadge');
  rb.textContent=getRoleName(myCode);
  rb.className='badge '+getRoleBadgeClass(myCode);

  // 管理员显示管理标签
  if(isAdmin(myCode)){
    document.getElementById('tab-admin').classList.remove('hidden');
    refreshBacklogTable();
    refreshLogTable();
    refreshLockedList();
  }
  // 副管理员也可以编辑公告
  if(isSubAdmin(myCode) || isAdmin(myCode)){
    document.getElementById('editAnnounceBtn').classList.remove('hidden');
  } else {
    document.getElementById('editAnnounceBtn').classList.add('hidden');
  }

  updateAnnouncementBar();
  showStatus('connStatus','等待连接... 输入对方通讯码建立P2P加密通道','info');

  // 广播管理员在线
  if(bc && isAdmin(myCode)){
    bc.postMessage({type:'admin-online-broadcast',from:myCode,ts:Date.now()});
  }
}

// ===================== 公告系统 =====================
function updateAnnouncementBar(){
  document.getElementById('announcementText').textContent=DB.getAnnouncement();
}
function editAnnouncement(){
  if(!isAdmin(myCode) && !isSubAdmin(myCode)){
    alert('权限不足:仅管理员/副管理员可编辑公告'); return;
  }
  document.getElementById('announceEditArea').value=DB.getAnnouncement();
  document.getElementById('announceModal').classList.remove('hidden');
}
function closeAnnounceModal(){ document.getElementById('announceModal').classList.add('hidden'); }
function saveAnnouncement(){
  const text=document.getElementById('announceEditArea').value.trim();
  DB.setAnnouncement(text||'暂无公告');
  updateAnnouncementBar();
  closeAnnounceModal();
  log('公告已更新','ok');
  if(bc) bc.postMessage({type:'announcement-update',from:myCode});
}

// ===================== 标签切换 =====================
function switchTab(name){
  ['connect','chat','video','admin'].forEach(t=>{
    document.getElementById('page-'+t).classList.add('hidden');
    document.getElementById('tab-'+t).classList.remove('active');
  });
  document.getElementById('page-'+name).classList.remove('hidden');
  document.getElementById('tab-'+name).classList.add('active');
}

// ===================== P2P 消息连接 =====================
async function connectToPeer(){
  const val=document.getElementById('targetInput').value.trim();
  const codeNum=parseInt(val);
  if(isNaN(codeNum)||codeNum<1||codeNum>100000){
    showStatus('connStatus','请输入有效的通讯码','err'); return;
  }
  peerCode=String(codeNum).padStart(6,'0');
  if(peerCode===myCode){ showStatus('connStatus','不能连接到自己','err'); return; }

  showStatus('connStatus','正在生成 WHT2 会话密钥并建立 ICE 通道...','info');
  sessionKey=WHT2.genSessionKey();
  log(`生成会话密钥: ${sessionKey.slice(0,8)}...`,'info');

  pc=new RTCPeerConnection({iceServers:ICE});
  pc.onicecandidate=(e)=>{ if(e.candidate) log(`ICE候选生成`,'info'); };
  pc.onconnectionstatechange=()=>{
    log(`连接状态: ${pc.connectionState}`, pc.connectionState==='connected'?'ok':'info');
    if(pc.connectionState==='connected'){
      showStatus('connStatus','P2P 加密通道已建立!','ok');
      document.getElementById('peerBadge').textContent='已连接: '+peerCode;
      document.getElementById('peerBadge').style.borderColor='var(--success)';
      document.getElementById('peerBadge').style.color='var(--success)';
      document.getElementById('localConnState').textContent='已连接 '+peerCode;
      document.getElementById('localConnState').style.color='var(--success)';
    }
    if(pc.connectionState==='disconnected'||pc.connectionState==='failed'){
      showStatus('connStatus','连接已断开','err');
      document.getElementById('peerBadge').textContent='未连接';
      document.getElementById('localConnState').textContent='断开';
      document.getElementById('localConnState').style.color='var(--danger)';
    }
  };

  dc=pc.createDataChannel('TR1_MSG',{ordered:true});
  setupDataChannel(dc);

  const offer=await pc.createOffer();
  await pc.setLocalDescription(offer);

  setTimeout(()=>{
    const encOffer=WHT.scramble(JSON.stringify(offer));
    navigator.clipboard.writeText(encOffer).then(()=>{
      showStatus('connStatus','Offer 已加密(WHT)并复制到剪贴板!请发送给对方,然后粘贴对方的 Answer。','ok');
    }).catch(()=>{});
    const answerEnc=prompt("请发送上方已复制的加密 Offer 给对方。\\n\\n粘贴对方发来的加密 Answer (或留空等待对方连接):");
    if(answerEnc&&answerEnc.trim()){
      try{
        const answer=JSON.parse(WHT.unscramble(answerEnc.trim()));
        pc.setRemoteDescription(new RTCSessionDescription(answer));
        log('远程描述已设置 (Answer)','ok');
      }catch(e){ showStatus('connStatus','Answer 解析失败: '+e.message,'err'); }
    }
  },500);
}

function setupDataChannel(channel){
  channel.onopen=()=>{
    log('数据通道已打开 — WHT2加密就绪','ok');
    switchTab('chat');
    addSystemMsg(`与 ${peerCode} 的加密通道已建立`);
  };
  channel.onmessage=(e)=>{
    try{
      const decrypted=WHT2.decrypt(e.data,sessionKey);
      const payload=JSON.parse(decrypted);
      addMsg(payload.text,false,payload.ts);
      // 记录到积压(元数据+加密内容)
      DB.addBacklog({
        id: WHT2.genSessionKey().slice(0,12),
        from: peerCode, to: myCode,
        timestamp: payload.ts,
        count: 1,
        encryptedSnapshot: e.data.slice(0,60)+'...'
      });
    }catch(err){ log('解密失败: '+err.message,'err'); }
  };
  channel.onclose=()=>{ log('数据通道已关闭','err'); addSystemMsg('对方已断开连接'); };
}

function sendMsg(){
  const input=document.getElementById('msgInput');
  const text=input.value.trim(); if(!text) return;
  if(!dc||dc.readyState!=='open'){ showStatus('connStatus','通道未就绪,无法发送','err'); return; }
  const payload={text,ts:Date.now(),from:myCode};
  const encrypted=WHT2.encrypt(JSON.stringify(payload),sessionKey);
  dc.send(encrypted);
  log(`发送加密消息: ${encrypted.slice(0,30)}...`,'info');
  addMsg(text,true,payload.ts);
  input.value='';
  // 记录到积压
  DB.addBacklog({
    id: WHT2.genSessionKey().slice(0,12),
    from: myCode, to: peerCode,
    timestamp: payload.ts,
    count: 1,
    encryptedSnapshot: encrypted.slice(0,60)+'...'
  });
}

function addMsg(text,isMe,ts){
  const box=document.getElementById('chatBox');
  const div=document.createElement('div');
  div.className='msg '+(isMe?'me':'peer');
  const time=new Date(ts).toLocaleTimeString();
  div.innerHTML=`<div class="tag">${isMe?'我':peerCode}</div>${escapeHtml(text)}<div class="time">${time}</div>`;
  box.appendChild(div); box.scrollTop=box.scrollHeight;
}
function addSystemMsg(text){
  const box=document.getElementById('chatBox');
  const div=document.createElement('div');
  div.className='msg sys'; div.textContent=text;
  box.appendChild(div); box.scrollTop=box.scrollHeight;
}
function escapeHtml(t){ const d=document.createElement('div'); d.textContent=t; return d.innerHTML; }

// ===================== 视频通话 =====================
async function startVideoCall(){
  if(!peerCode){ showStatus('videoStatus','请先建立消息连接,再发起视频通话','err'); return; }
  try{
    localStream=await navigator.mediaDevices.getUserMedia({video:true,audio:true});
    document.getElementById('localVideo').srcObject=localStream;
    document.getElementById('localVideo').classList.remove('hidden');
    document.getElementById('localLabel').classList.remove('hidden');
    document.getElementById('localVideoBox').querySelector('span').classList.add('hidden');

    videoPC=new RTCPeerConnection({iceServers:ICE});
    localStream.getTracks().forEach(t=>videoPC.addTrack(t,localStream));
    videoPC.ontrack=(e)=>{
      document.getElementById('remoteVideo').srcObject=e.streams[0];
      document.getElementById('remoteVideo').classList.remove('hidden');
      document.getElementById('remoteLabel').classList.remove('hidden');
      document.getElementById('remoteLabel').textContent=peerCode;
      document.getElementById('remoteVideoBox').querySelector('span').classList.add('hidden');
    };
    videoPC.onicecandidate=(e)=>{ if(e.candidate) log('视频ICE候选生成','info'); };

    const offer=await videoPC.createOffer();
    await videoPC.setLocalDescription(offer);

    setTimeout(()=>{
      const encOffer=WHT.scramble(JSON.stringify(offer));
      navigator.clipboard.writeText(encOffer).then(()=>{
        showStatus('videoStatus','视频Offer已加密复制!请发送给对方,然后粘贴对方的视频Answer。','ok');
      });
      const answerEnc=prompt("视频通话 Offer 已复制。\\n\\n粘贴对方发来的加密视频Answer:");
      if(answerEnc&&answerEnc.trim()){
        try{
          const answer=JSON.parse(WHT.unscramble(answerEnc.trim()));
          videoPC.setRemoteDescription(new RTCSessionDescription(answer));
          log('视频远程描述已设置','ok');
        }catch(e){ showStatus('videoStatus','视频Answer解析失败','err'); }
      }
    },500);

    document.getElementById('btnStartVideo').classList.add('hidden');
    document.getElementById('btnEndVideo').classList.remove('hidden');
  }catch(e){ showStatus('videoStatus','无法获取摄像头: '+e.message,'err'); }
}

function endVideoCall(){
  if(videoPC){ videoPC.close(); videoPC=null; }
  if(localStream){ localStream.getTracks().forEach(t=>t.stop()); localStream=null; }
  document.getElementById('localVideo').classList.add('hidden');
  document.getElementById('localLabel').classList.add('hidden');
  document.getElementById('remoteVideo').classList.add('hidden');
  document.getElementById('remoteLabel').classList.add('hidden');
  document.getElementById('localVideoBox').querySelector('span').classList.remove('hidden');
  document.getElementById('remoteVideoBox').querySelector('span').classList.remove('hidden');
  document.getElementById('btnStartVideo').classList.remove('hidden');
  document.getElementById('btnEndVideo').classList.add('hidden');
  showStatus('videoStatus','视频通话已结束','info');
}

// ===================== 管理后台功能 =====================
function refreshBacklogTable(){
  const tbody=document.getElementById('backlogTable');
  const data=DB.getBacklog();
  if(data.length===0){ tbody.innerHTML='<tr><td colspan="5" style="text-align:center;color:var(--muted)">暂无积压记录</td></tr>'; return; }
  tbody.innerHTML=data.map(r=>{
    const d=new Date(r.timestamp).toLocaleString();
    return `<tr>
      <td>${d}</td>
      <td>${r.from}</td>
      <td>${r.to}</td>
      <td>${r.count}</td>
      <td><span class="badge">已加密</span></td>
    </tr>`;
  }).join('');
}

function refreshLogTable(){
  const tbody=document.getElementById('logTable');
  const data=DB.getSystemLogs().slice(0,50);
  if(data.length===0){ tbody.innerHTML='<tr><td colspan="3" style="text-align:center;color:var(--muted)">暂无日志</td></tr>'; return; }
  tbody.innerHTML=data.map(r=>{
    const d=new Date(r.ts).toLocaleString();
    return `<tr><td>${d}</td><td>${r.event}</td><td>${r.detail}</td></tr>`;
  }).join('');
}

function refreshLockedList(){
  const list=DB.getLockedList();
  document.getElementById('lockedList').innerHTML=list.length===0?'无':list.map(c=>`<span class="badge locked">${c}</span>`).join(' ');
}

function lockNumber(){
  if(!isAdmin(myCode) && !isSubAdmin(myCode)){ alert('权限不足'); return; }
  const val=document.getElementById('lockTargetInput').value.trim();
  const code=String(parseInt(val)).padStart(6,'0');
  if(!code||code==='NaN'){ showStatus('lockStatus','请输入有效号码','err'); return; }

  // 副管理员不能锁管理员号
  if(isSubAdmin(myCode) && (isAdmin(code)||isSubAdmin(code))){
    showStatus('lockStatus','副管理员无法锁定管理员号码','err'); return;
  }
  // 不能锁自己
  if(code===myCode){ showStatus('lockStatus','不能锁定自己','err'); return; }

  DB.lock(code,myCode);
  showStatus('lockStatus',`号码 ${code} 已被锁定`,'ok');
  log(`号码 ${code} 被 ${myCode} 锁定`,'ok');
  refreshLockedList();
  if(bc) bc.postMessage({type:'registry-update',from:myCode});
}

function unlockNumber(){
  if(!isAdmin(myCode) && !isSubAdmin(myCode)){ alert('权限不足'); return; }
  const val=document.getElementById('lockTargetInput').value.trim();
  const code=String(parseInt(val)).padStart(6,'0');
  if(!code||code==='NaN'){ showStatus('lockStatus','请输入有效号码','err'); return; }

  const r=DB.getRegistry();
  if(r[code] && r[code].lockedBy && r[code].lockedBy!==myCode && !isSuperAdmin(myCode)){
    showStatus('lockStatus','只有锁定者或超级管理员可以解锁','err'); return;
  }

  DB.unlock(code);
  showStatus('lockStatus',`号码 ${code} 已解锁`,'ok');
  log(`号码 ${code} 被 ${myCode} 解锁`,'ok');
  refreshLockedList();
  if(bc) bc.postMessage({type:'registry-update',from:myCode});
}

// ===================== 特殊密码:后台入口 =====================
let keyBuffer='';
const SPECIAL_PASSWORD='TR1_ADMIN_2026';
document.addEventListener('keydown',(e)=>{
  keyBuffer+=e.key;
  if(keyBuffer.length>30) keyBuffer=keyBuffer.slice(-30);
  if(keyBuffer.includes(SPECIAL_PASSWORD)){
    document.getElementById('debugPanel').classList.toggle('hidden');
    keyBuffer='';
    log('后台监视器已切换','ok');
  }
  if(e.key==='Escape') keyBuffer='';
  if(e.ctrlKey&&e.altKey&&e.key==='d'){
    document.getElementById('debugPanel').classList.remove('hidden');
  }
});
</script>

<div class="footer-hint">
  TR1 v2.0 · 无服务器P2P网络 · Ctrl+Alt+D 或输入特殊密码开启后台 · 视频通话基于WebRTC
</div>

</body>
</html>'''

with open('/mnt/agents/output/TR1_Communication_System_v2.html', 'w', encoding='utf-8') as f:
    f.write(html_v2)

print("TR1 v2.0 已保存")

HTML```

1 条评论

  • 1