{"version":3,"file":"web-panel.cjs","names":["http"],"sources":["../src/web-panel.ts"],"sourcesContent":["import { spawn } from 'node:child_process';\nimport http from 'node:http';\n\ninterface TemplateNode {\n  id: string;\n  tag: string;\n  kind: 'element' | 'component' | 'text';\n  snippet: string;\n  keyRefs: string[];\n  attrs: Array<{\n    name: string;\n    value: string;\n  }>;\n  text: string | null;\n  children: TemplateNode[];\n}\n\nexport interface RuntimeSnapshot {\n  connected: boolean;\n  status: string;\n  route: string;\n  rows: Array<{\n    source: string;\n    key: string;\n    value: unknown;\n    kind: string;\n    confidence: string;\n    expressionSummary: string;\n    wxmlUsages: string[];\n  }>;\n  updatedAt: string;\n  templateTree?: TemplateNode | null;\n  debug?: {\n    pageId?: number | string;\n    rawRoute?: string;\n    keymapPages?: string[];\n    error?: string;\n  };\n  rawData?: Record<string, unknown>;\n}\n\nexport interface WebPanel {\n  url: string;\n  update(snapshot: RuntimeSnapshot): void;\n  close(): void;\n}\n\ninterface WebPanelOptions {\n  port?: number;\n  onReconnect?: () => void | Promise<void>;\n}\n\nconst DEFAULT_SNAPSHOT: RuntimeSnapshot = {\n  connected: false,\n  status: '正在启动...',\n  route: '',\n  rows: [],\n  updatedAt: new Date().toISOString(),\n};\n\nfunction html(): string {\n  return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>uniappx 运行时映射</title>\n<style>\n:root { --ink:#17211d; --muted:#68736d; --paper:#f8f1df; --card:#fffaf0; --line:#dfd3b9; --green:#1f7a54; --red:#b33d32; --blue:#205c82; --amber:#a66b00; --flash:#f6d365; }\n* { box-sizing:border-box; }\nbody { margin:0; min-height:100vh; color:var(--ink); background:radial-gradient(circle at 10% 5%, #cbeebc 0, transparent 28rem), radial-gradient(circle at 90% 20%, #bdd7ef 0, transparent 26rem), linear-gradient(135deg, #f8f1df, #ecdec0); font:15px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }\nheader { padding:36px clamp(18px,5vw,72px) 18px; display:flex; justify-content:space-between; gap:24px; align-items:flex-end; }\nh1 { margin:0; font:800 clamp(34px,7vw,78px)/.92 Georgia, 'Times New Roman', serif; letter-spacing:-.055em; }\n.meta { color:var(--muted); text-align:right; }\nmain { padding:0 clamp(18px,5vw,72px) 64px; display:grid; gap:18px; }\n.card { background:rgba(255,250,240,.92); border:1px solid var(--line); border-radius:28px; box-shadow:0 22px 70px rgba(61,44,15,.12); overflow:hidden; }\n.toolbar { display:flex; justify-content:space-between; gap:18px; padding:20px 24px; border-bottom:1px solid var(--line); align-items:center; }\n.actions { display:flex; align-items:center; gap:12px; flex-wrap:wrap; justify-content:flex-end; }\n.toggles { display:flex; flex-wrap:wrap; gap:10px; padding:0 24px 18px; }\n.toggle-group { display:flex; flex-wrap:wrap; gap:8px; align-items:center; }\n.toggle-label { color:var(--muted); font-size:12px; text-transform:uppercase; letter-spacing:.08em; font-weight:800; }\nbutton { appearance:none; border:1px solid #b9a98b; background:#1f7a54; color:#fffaf0; border-radius:999px; padding:10px 16px; font:800 13px/1 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; cursor:pointer; box-shadow:0 8px 20px rgba(31,122,84,.18); }\nbutton.secondary { background:#fffaf0; color:#17211d; }\nbutton.secondary.active { background:#205c82; color:#fffaf0; border-color:#205c82; box-shadow:0 8px 20px rgba(32,92,130,.18); }\nbutton:disabled { cursor:not-allowed; opacity:.45; filter:none; box-shadow:none; }\nbutton:hover { filter:brightness(1.06); }\nbutton:active { transform:translateY(1px); }\ninput { border:1px solid #b9a98b; background:#fffaf0; border-radius:999px; padding:10px 14px; min-width:220px; font:800 13px/1 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color:var(--ink); }\n.status { display:inline-flex; align-items:center; gap:10px; font-weight:800; }\n.dot { width:12px; height:12px; border-radius:999px; background:var(--red); box-shadow:0 0 0 6px rgba(179,61,50,.12); }\n.connected .dot { background:var(--green); box-shadow:0 0 0 6px rgba(31,122,84,.12); }\n.route { color:var(--blue); font-weight:800; }\ntable { width:100%; border-collapse:collapse; }\nth, td { text-align:left; padding:15px 18px; border-bottom:1px solid #eadfc7; vertical-align:top; }\nth { color:var(--muted); font-size:12px; letter-spacing:.08em; text-transform:uppercase; }\ntr.flash { animation: flash 1.05s ease-out; }\n@keyframes flash { 0% { background:rgba(246,211,101,.75); } 100% { background:transparent; } }\n.key { display:inline-grid; min-width:34px; min-height:34px; place-items:center; color:var(--paper); background:var(--ink); border-radius:12px; font-weight:900; }\n.value { font-size:18px; font-weight:800; word-break:break-word; }\n.diff { color:var(--amber); font-size:12px; margin-top:4px; }\n.unknown-group-row td { background:#f6ead2; }\n.event-group-row td { background:#eaf2df; }\n.unknown-group-toggle, .event-group-toggle { display:flex; width:100%; justify-content:space-between; gap:12px; align-items:center; color:var(--ink); font-weight:900; text-decoration:none; }\n.unknown-group-toggle:hover, .event-group-toggle:hover { text-decoration:none; }\n.unknown-group-meta, .event-group-meta { color:var(--muted); font-size:12px; font-weight:800; }\n.muted { color:var(--muted); }\n.empty { padding:36px 24px; color:var(--muted); }\n.diagnostics { padding:0 24px 24px; }\n.diagnostics-card { border-top:1px solid var(--line); padding-top:18px; }\n.diagnostics-grid { display:grid; gap:12px; margin-top:12px; }\n.diagnostic-item { border:1px solid #e4d5b8; border-radius:18px; background:#fff7ea; padding:14px 16px; }\n.diagnostic-head { display:flex; flex-wrap:wrap; justify-content:space-between; gap:8px; align-items:center; }\n.diagnostic-title { font-weight:900; }\n.diagnostic-meta { color:var(--muted); font-size:12px; }\n.diagnostic-expression { margin-top:8px; font-size:13px; color:var(--ink); word-break:break-word; }\n.diagnostic-usages { margin-top:10px; display:flex; flex-wrap:wrap; gap:8px; }\n.snippet { display:inline-block; border:1px solid #dbc7a3; border-radius:999px; padding:5px 9px; background:#f6ead2; font-size:12px; max-width:100%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }\n.template-explorer { padding:0 24px 24px; }\n.template-shell { border-top:1px solid var(--line); padding-top:18px; display:grid; grid-template-columns:minmax(280px, 420px) minmax(0, 1fr); gap:16px; }\n.template-tree, .template-detail { border:1px solid #e4d5b8; border-radius:22px; background:#fff7ea; min-height:220px; }\n.template-pane-title { padding:14px 16px; border-bottom:1px solid #eadfc7; font-weight:900; }\n.template-pane-body { padding:14px 16px; }\n.template-tools { display:flex; flex-wrap:wrap; gap:10px; margin-bottom:12px; align-items:center; }\n.template-search { width:100%; margin-bottom:12px; }\n.template-search.with-tools { margin-bottom:0; flex:1 1 220px; min-width:180px; }\n.template-list { display:grid; gap:6px; }\n.template-node { border:0; background:transparent; width:100%; text-align:left; padding:6px 10px; border-radius:12px; cursor:pointer; color:var(--ink); }\n.template-node:hover { background:#f6ead2; box-shadow:none; }\n.template-node.active { background:#205c82; color:#fffaf0; }\n.template-node.match:not(.active) { background:#fdf2cc; }\n.template-node-row { display:flex; align-items:center; gap:8px; }\n.template-indent { display:inline-block; width:calc(var(--depth) * 12px); flex:0 0 calc(var(--depth) * 12px); }\n.template-toggle { display:inline-grid; place-items:center; width:18px; height:18px; border-radius:999px; border:1px solid #d4c09a; background:#fffaf0; font-size:11px; color:var(--muted); }\n.template-toggle.placeholder { visibility:hidden; }\n.template-tag { font-weight:900; }\n.template-kind { font-size:11px; color:var(--muted); }\n.template-count { margin-left:auto; font-size:11px; color:inherit; opacity:.8; }\n.template-highlight { box-shadow:inset 0 0 0 1px rgba(246,211,101,.9); }\n.template-summary { color:var(--muted); font-size:12px; margin-top:6px; }\n.template-detail-grid { display:grid; gap:12px; }\n.template-detail-block { display:grid; gap:6px; }\n.template-detail-label { color:var(--muted); font-size:12px; text-transform:uppercase; letter-spacing:.08em; }\n.template-breadcrumb { display:flex; flex-wrap:wrap; gap:8px; align-items:center; }\n.template-crumb { display:inline-flex; align-items:center; gap:8px; }\n.template-sep { color:var(--muted); }\n.template-bindings { display:grid; gap:10px; }\n.binding-card { border:1px solid #eadfc7; border-radius:16px; background:#fffaf0; padding:12px 14px; }\n.binding-head { display:flex; flex-wrap:wrap; gap:8px; justify-content:space-between; align-items:center; }\n.binding-value { margin-top:6px; font-size:14px; word-break:break-word; }\n.binding-snippets { display:flex; flex-wrap:wrap; gap:8px; margin-top:8px; }\n.binding-table-wrap { overflow:auto; border:1px solid #eadfc7; border-radius:16px; background:#fffaf0; }\n.binding-table { width:100%; border-collapse:collapse; }\n.binding-table th, .binding-table td { padding:10px 12px; border-bottom:1px solid #eadfc7; text-align:left; vertical-align:top; }\n.binding-table th { font-size:11px; color:var(--muted); text-transform:uppercase; letter-spacing:.08em; }\n.binding-table tr:last-child td { border-bottom:0; }\n.mini-link { appearance:none; background:transparent; border:0; padding:0; margin:0; color:inherit; cursor:pointer; border-radius:0; box-shadow:none; font:inherit; }\n.mini-link:hover { text-decoration:underline; filter:none; }\n.mini-link.key-link { display:inline-grid; }\n.debug { padding:0 24px 24px; }\ndetails { border-top:1px solid var(--line); padding:16px 0 0; }\nsummary { cursor:pointer; color:var(--blue); font-weight:900; }\npre { overflow:auto; max-height:320px; background:#1e2924; color:#f8f0dc; border-radius:16px; padding:16px; }\n.pages { display:flex; gap:8px; flex-wrap:wrap; margin-top:10px; }\n.page-pill { border:1px solid #d9caaa; border-radius:999px; padding:5px 9px; color:var(--muted); background:#fffaf0; }\n.page-pill.active { color:#fffaf0; background:var(--blue); border-color:var(--blue); }\n@media (max-width: 760px) { header, .toolbar { display:block; } .actions { justify-content:flex-start; margin-top:12px; } .toggles { padding-top:0; } .template-shell { grid-template-columns:1fr; } .meta { text-align:left; margin-top:12px; } input { width:100%; } table, thead, tbody, tr, th, td { display:block; } thead { display:none; } tr { padding:12px 0; border-bottom:1px solid var(--line); } td { border:0; padding:6px 18px; } td::before { display:block; color:var(--muted); font-size:11px; text-transform:uppercase; letter-spacing:.08em; } td:nth-child(1)::before { content:'变量名'; } td:nth-child(2)::before { content:'key'; } td:nth-child(3)::before { content:'值'; } td:nth-child(4)::before { content:'类型'; } .unknown-group-row td::before { content:'' !important; display:none; } }\n</style>\n</head>\n<body>\n<header>\n  <div><h1>uniapp-miniprogram-devtool</h1><div class=\"muted\">查看编译后小程序 key 对应的运行时变量值。</div></div>\n  <div class=\"meta\"><div id=\"updated\">-</div><div>每 300ms 自动刷新</div></div>\n</header>\n<main>\n  <section id=\"card\" class=\"card\">\n    <div class=\"toolbar\">\n      <div id=\"status\" class=\"status\"><span class=\"dot\"></span><span>正在启动...</span></div>\n      <div class=\"actions\"><input id=\"filter\" placeholder=\"筛选变量名 / key / 值\"><button id=\"refresh\" type=\"button\">立即刷新</button><button id=\"reconnect\" class=\"secondary\" type=\"button\">重新连接</button><div>页面：<span id=\"route\" class=\"route\">-</span></div></div>\n    </div>\n    <div class=\"toggles\">\n      <div class=\"toggle-group\">\n        <span class=\"toggle-label\">视图</span>\n        <button id=\"view-business\" class=\"secondary active\" type=\"button\">业务变量</button>\n        <button id=\"view-all\" class=\"secondary\" type=\"button\">全部变量</button>\n      </div>\n      <div class=\"toggle-group\">\n        <span class=\"toggle-label\">变化</span>\n        <button id=\"toggle-changed\" class=\"secondary\" type=\"button\">仅看变化项</button>\n      </div>\n    </div>\n    <div id=\"content\" class=\"empty\">等待连接微信开发者工具自动化...</div>\n    <div id=\"template-explorer\" class=\"template-explorer\"></div>\n    <div id=\"diagnostics\" class=\"diagnostics\"></div>\n    <div id=\"debug\" class=\"debug\">\n      <details id=\"debug-details\">\n        <summary>调试信息</summary>\n        <div id=\"debug-meta\"></div>\n        <div id=\"debug-pages\" class=\"pages\"></div>\n        <pre id=\"debug-pre\"></pre>\n      </details>\n    </div>\n  </section>\n</main>\n<script>\nwindow.__lastValues = window.__lastValues || {};\nwindow.__changedKeys = window.__changedKeys || {};\nwindow.__lastSnapshot = null;\nwindow.__pendingDebugData = null;\nwindow.__lastDebugFingerprint = '';\nwindow.__lastDebugBodyText = '';\nwindow.__refreshing = false;\nwindow.__currentView = 'business';\nwindow.__showChangedOnly = false;\nwindow.__unknownRowsCollapsed = true;\nwindow.__eventRowsCollapsed = true;\nwindow.__selectedTemplateNodeId = '';\nwindow.__collapsedTemplateNodes = {};\nwindow.__templateSearch = '';\nwindow.__templateBoundOnly = false;\nwindow.__templateChangedOnly = false;\nwindow.__pinTemplateSelection = false;\nwindow.__scrollTemplateSelection = false;\nvar cardEl = document.getElementById('card');\nvar statusTextEl = document.getElementById('status').lastElementChild;\nvar routeEl = document.getElementById('route');\nvar updatedEl = document.getElementById('updated');\nvar filterEl = document.getElementById('filter');\nvar contentEl = document.getElementById('content');\nvar templateExplorerEl = document.getElementById('template-explorer');\nvar diagnosticsEl = document.getElementById('diagnostics');\nvar debugDetailsEl = document.getElementById('debug-details');\nvar debugMetaEl = document.getElementById('debug-meta');\nvar debugPagesEl = document.getElementById('debug-pages');\nvar debugPreEl = document.getElementById('debug-pre');\nvar viewBusinessEl = document.getElementById('view-business');\nvar viewAllEl = document.getElementById('view-all');\nvar toggleChangedEl = document.getElementById('toggle-changed');\nvar NON_BUSINESS_KINDS = {\n  'css-var': true,\n  'virtual-host-class': true,\n  'virtual-host-style': true,\n  'virtual-host-hidden': true,\n  'element-id': true,\n  'static-asset': true\n};\nfunction formatValue(value) {\n  if (typeof value === 'undefined') return 'undefined';\n  if (value === null) return 'null';\n  if (typeof value === 'string') return value;\n  try { return JSON.stringify(value); } catch (error) { return String(value); }\n}\nfunction escapeHtml(value) {\n  return String(value).replace(/[&<>\"']/g, function (ch) { return ({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'})[ch]; });\n}\nfunction matches(row, filter) {\n  if (!filter) return true;\n  var text = [row.source, row.key, row.kind, row.confidence, formatValue(row.value)].join(' ').toLowerCase();\n  return text.indexOf(filter.toLowerCase()) >= 0;\n}\nfunction isBusinessRow(row) {\n  return !NON_BUSINESS_KINDS[row.kind];\n}\nfunction isDiagnosticRow(row) {\n  return row.kind === 'unknown' || row.confidence === 'low' || row.confidence === 'medium';\n}\nfunction isUnknownUnknownRow(row) {\n  return String(row.source || '').toLowerCase() === 'unknown' && String(row.kind || '').toLowerCase() === 'unknown';\n}\nfunction isEventRow(row) {\n  return row.kind === 'event-handler';\n}\nfunction renderToolbarState() {\n  viewBusinessEl.classList.toggle('active', window.__currentView === 'business');\n  viewAllEl.classList.toggle('active', window.__currentView === 'all');\n  toggleChangedEl.classList.toggle('active', !!window.__showChangedOnly);\n  toggleChangedEl.textContent = window.__showChangedOnly ? '显示全部项' : '仅看变化项';\n}\nfunction templateNodeHasBinding(node) {\n  if ((node.keyRefs || []).length) return true;\n  return (node.children || []).some(templateNodeHasBinding);\n}\nfunction templateNodeHasChanged(node, route, rowsByKey) {\n  if (templateNodeHasChangedBinding(node, route, rowsByKey)) return true;\n  return (node.children || []).some(function (child) {\n    return templateNodeHasChanged(child, route, rowsByKey);\n  });\n}\nfunction flattenTemplateNodes(node, output, depth, route, rowsByKey) {\n  if (!node) return;\n  if (window.__templateBoundOnly && !templateNodeHasBinding(node)) return;\n  if (window.__templateChangedOnly && !templateNodeHasChanged(node, route, rowsByKey)) return;\n  output.push({ node: node, depth: depth });\n  if (window.__collapsedTemplateNodes[node.id]) return;\n  (node.children || []).forEach(function (child) {\n    flattenTemplateNodes(child, output, depth + 1, route, rowsByKey);\n  });\n}\nfunction findTemplateNodeById(node, id) {\n  if (!node) return null;\n  if (node.id === id) return node;\n  for (var index = 0; index < (node.children || []).length; index += 1) {\n    var found = findTemplateNodeById(node.children[index], id);\n    if (found) return found;\n  }\n  return null;\n}\nfunction findTemplatePathById(node, id, ancestors) {\n  if (!node) return null;\n  if (node.id === id) return ancestors.concat(node);\n  for (var index = 0; index < (node.children || []).length; index += 1) {\n    var found = findTemplatePathById(node.children[index], id, ancestors.concat(node));\n    if (found) return found;\n  }\n  return null;\n}\nfunction hasChangedBinding(route, row) {\n  var changed = window.__changedKeys[(route || '') + ':' + row.key];\n  return !!(changed && Date.now() - changed.at < 1200);\n}\nfunction templateNodeHasChangedBinding(node, route, rowsByKey) {\n  return (node.keyRefs || []).some(function (key) {\n    return rowsByKey[key] && hasChangedBinding(route, rowsByKey[key]);\n  });\n}\nfunction expandChangedTemplateNodes(root, route, rowsByKey) {\n  var firstChangedNodeId = '';\n  function walk(node, ancestors) {\n    var changedHere = templateNodeHasChangedBinding(node, route, rowsByKey);\n    var changedInChildren = false;\n    (node.children || []).forEach(function (child) {\n      if (walk(child, ancestors.concat(node))) changedInChildren = true;\n    });\n    if (changedHere || changedInChildren) {\n      ancestors.forEach(function (ancestor) {\n        delete window.__collapsedTemplateNodes[ancestor.id];\n      });\n      delete window.__collapsedTemplateNodes[node.id];\n      if (!firstChangedNodeId && changedHere) firstChangedNodeId = node.id;\n      return true;\n    }\n    return false;\n  }\n  (root.children || []).forEach(function (child) {\n    walk(child, []);\n  });\n  return firstChangedNodeId;\n}\nfunction templateNodeLabel(node) {\n  return node.kind === 'text' ? (node.text || '#text') : '<' + node.tag + '>';\n}\nfunction templateNodeSearchText(node, rowsByKey) {\n  var bindingTexts = (node.keyRefs || []).map(function (key) {\n    var row = rowsByKey[key];\n    return row ? [row.source, row.key, row.expressionSummary, formatValue(row.value)].join(' ') : key;\n  }).join(' ');\n  return [templateNodeLabel(node), node.snippet || '', bindingTexts].join(' ').toLowerCase();\n}\nfunction findFirstTemplateMatch(root, query, rowsByKey) {\n  if (!query) return null;\n  function walk(node, ancestors) {\n    if (templateNodeSearchText(node, rowsByKey).indexOf(query) >= 0) {\n      return { node: node, ancestors: ancestors };\n    }\n    for (var index = 0; index < (node.children || []).length; index += 1) {\n      var child = node.children[index];\n      var found = walk(child, ancestors.concat(node));\n      if (found) return found;\n    }\n    return null;\n  }\n  for (var i = 0; i < (root.children || []).length; i += 1) {\n    var found = walk(root.children[i], []);\n    if (found) return found;\n  }\n  return null;\n}\nfunction findFirstTemplateNodeByKey(root, key) {\n  function walk(node, ancestors) {\n    if ((node.keyRefs || []).indexOf(key) >= 0) return { node: node, ancestors: ancestors };\n    for (var index = 0; index < (node.children || []).length; index += 1) {\n      var child = node.children[index];\n      var found = walk(child, ancestors.concat(node));\n      if (found) return found;\n    }\n    return null;\n  }\n  for (var i = 0; i < (root.children || []).length; i += 1) {\n    var found = walk(root.children[i], []);\n    if (found) return found;\n  }\n  return null;\n}\nfunction selectTemplateNodeByKey(snapshot, key) {\n  if (!snapshot || !snapshot.templateTree) return;\n  var found = findFirstTemplateNodeByKey(snapshot.templateTree, key);\n  if (!found) return;\n  window.__selectedTemplateNodeId = found.node.id;\n  found.ancestors.forEach(function (ancestor) {\n    delete window.__collapsedTemplateNodes[ancestor.id];\n  });\n  window.__scrollTemplateSelection = true;\n  render(snapshot, true);\n}\nfunction countChangedTemplateNodes(nodes, route, rowsByKey) {\n  var total = 0;\n  nodes.forEach(function (node) {\n    if (templateNodeHasChangedBinding(node, route, rowsByKey)) total += 1;\n  });\n  return total;\n}\nfunction renderTemplateExplorer(data) {\n  var root = data.templateTree;\n  if (!root || !(root.children || []).length) {\n    templateExplorerEl.innerHTML = '<div class=\"template-shell\"><div class=\"template-tree\"><div class=\"template-pane-title\">模板树</div><div class=\"template-pane-body muted\">当前页面没有可解析的模板结构。</div></div><div class=\"template-detail\"><div class=\"template-pane-title\">节点详情</div><div class=\"template-pane-body muted\">选择模板节点后可查看关联变量与当前值。</div></div></div>';\n    return;\n  }\n  if (!window.__selectedTemplateNodeId) {\n    window.__selectedTemplateNodeId = root.children[0] ? root.children[0].id : root.id;\n  }\n  var rowsByKey = {};\n  (data.rows || []).forEach(function (row) {\n    rowsByKey[row.key] = row;\n  });\n  var searchQuery = String(window.__templateSearch || '').trim().toLowerCase();\n  var matched = findFirstTemplateMatch(root, searchQuery, rowsByKey);\n  if (matched && !window.__pinTemplateSelection) {\n    window.__selectedTemplateNodeId = matched.node.id;\n    matched.ancestors.forEach(function (ancestor) {\n      delete window.__collapsedTemplateNodes[ancestor.id];\n    });\n  }\n  var flattened = [];\n  root.children.forEach(function (child) {\n    flattenTemplateNodes(child, flattened, 0, data.route, rowsByKey);\n  });\n  var selectedNode = findTemplateNodeById(root, window.__selectedTemplateNodeId) || root.children[0] || root;\n  if (flattened.length && !flattened.some(function (item) { return item.node.id === selectedNode.id; })) {\n    selectedNode = flattened[0].node;\n  }\n  window.__selectedTemplateNodeId = selectedNode.id;\n  var visibleNodes = flattened.map(function (item) { return item.node; });\n  var selectedPath = findTemplatePathById(root, selectedNode.id, []) || [selectedNode];\n  var breadcrumbNodes = selectedPath.filter(function (node) { return node.id !== 'root'; });\n  var selectedNodeIndex = visibleNodes.findIndex(function (node) { return node.id === selectedNode.id; });\n  var prevNode = selectedNodeIndex > 0 ? visibleNodes[selectedNodeIndex - 1] : null;\n  var nextNode = selectedNodeIndex >= 0 && selectedNodeIndex < visibleNodes.length - 1 ? visibleNodes[selectedNodeIndex + 1] : null;\n  var hasAnyChangedNode = visibleNodes.some(function (node) { return templateNodeHasChangedBinding(node, data.route, rowsByKey); });\n  var changedNodeCount = countChangedTemplateNodes(visibleNodes, data.route, rowsByKey);\n  var detailRows = (selectedNode.keyRefs || []).map(function (key) {\n    return rowsByKey[key];\n  }).filter(Boolean);\n  templateExplorerEl.innerHTML = '<div class=\"template-shell\">' +\n    '<div class=\"template-tree\">' +\n      '<div class=\"template-pane-title\">模板树</div>' +\n      '<div class=\"template-pane-body\">' +\n        '<div class=\"template-tools\">' +\n          '<input class=\"template-search with-tools\" data-role=\"template-search\" placeholder=\"搜索标签 / 片段 / 变量\" value=\"' + escapeHtml(window.__templateSearch || '') + '\">' +\n          '<button class=\"secondary ' + (window.__templateBoundOnly ? 'active' : '') + '\" data-role=\"toggle-bound-only\" type=\"button\">' + (window.__templateBoundOnly ? '显示全部节点' : '仅显示有绑定节点') + '</button>' +\n          '<button class=\"secondary ' + (window.__templateChangedOnly ? 'active' : '') + '\" data-role=\"toggle-changed-only\" type=\"button\">' + (window.__templateChangedOnly ? '显示全部变化范围' : '仅显示变化节点') + '</button>' +\n          '<button class=\"secondary ' + (window.__pinTemplateSelection ? 'active' : '') + '\" data-role=\"toggle-pin-selection\" type=\"button\">' + (window.__pinTemplateSelection ? '取消固定选中' : '固定当前选中') + '</button>' +\n          '<button class=\"secondary\" data-role=\"expand-changed\" type=\"button\"' + (hasAnyChangedNode ? '' : ' disabled') + '>展开变化节点</button>' +\n        '</div>' +\n        '<div class=\"template-summary\">当前可见节点 ' + visibleNodes.length + ' 个，变化节点 ' + changedNodeCount + ' 个。</div>' +\n        '<div class=\"template-list\">' + flattened.map(function (item) {\n        var node = item.node;\n        var depth = item.depth;\n        var isSelected = node.id === selectedNode.id;\n        var hasChildren = !!((node.children || []).length);\n        var bindingCount = (node.keyRefs || []).length;\n        var hasChange = (node.keyRefs || []).some(function (key) { return rowsByKey[key] && hasChangedBinding(data.route, rowsByKey[key]); });\n        var isMatch = searchQuery && templateNodeSearchText(node, rowsByKey).indexOf(searchQuery) >= 0;\n        var title = templateNodeLabel(node);\n        return '<button class=\"template-node ' + (isSelected ? 'active ' : '') + (hasChange ? 'template-highlight ' : '') + (isMatch ? 'match' : '') + '\" type=\"button\" data-role=\"select-node\" data-node-id=\"' + escapeHtml(node.id) + '\">' +\n          '<span class=\"template-node-row\">' +\n            '<span class=\"template-indent\" style=\"--depth:' + depth + ';\"></span>' +\n            '<span class=\"template-toggle ' + (hasChildren ? '' : 'placeholder') + '\" data-role=\"' + (hasChildren ? 'toggle-node' : '') + '\" data-node-id=\"' + escapeHtml(node.id) + '\">' + (hasChildren ? (window.__collapsedTemplateNodes[node.id] ? '+' : '-') : '+') + '</span>' +\n            '<span class=\"template-tag\">' + escapeHtml(title) + '</span>' +\n            '<span class=\"template-kind\">' + escapeHtml(node.kind) + '</span>' +\n            '<span class=\"template-count\">' + bindingCount + ' 个绑定</span>' +\n          '</span>' +\n        '</button>';\n      }).join('') + '</div>' +\n        (!flattened.length ? '<div class=\"muted\">当前筛选下没有可展示的模板节点。</div>' : '') +\n        (searchQuery && !matched ? '<div class=\"muted\">没有找到匹配节点。</div>' : '') +\n        (searchQuery && matched && window.__pinTemplateSelection ? '<div class=\"muted\">已固定当前选中节点，搜索结果仅高亮不自动跳转。</div>' : '') +\n      '</div>' +\n    '</div>' +\n    '<div class=\"template-detail\">' +\n      '<div class=\"template-pane-title\">节点详情</div>' +\n      '<div class=\"template-pane-body\"><div class=\"template-detail-grid\">' +\n        '<div class=\"template-detail-block\"><div><button class=\"secondary\" type=\"button\" data-role=\"select-node\" data-node-id=\"' + escapeHtml(prevNode ? prevNode.id : '') + '\"' + (prevNode ? '' : ' disabled') + '>上一个节点</button> <button class=\"secondary\" type=\"button\" data-role=\"select-node\" data-node-id=\"' + escapeHtml(nextNode ? nextNode.id : '') + '\"' + (nextNode ? '' : ' disabled') + '>下一个节点</button></div></div>' +\n        '<div class=\"template-detail-block\"><div class=\"template-detail-label\">节点</div><div>' + escapeHtml(templateNodeLabel(selectedNode)) + '</div></div>' +\n        '<div class=\"template-detail-block\"><div class=\"template-detail-label\">路径</div><div class=\"template-breadcrumb\">' + breadcrumbNodes.map(function (node, index) {\n          return '<span class=\"template-crumb\"><button class=\"mini-link\" type=\"button\" data-role=\"select-node\" data-node-id=\"' + escapeHtml(node.id) + '\">' + escapeHtml(templateNodeLabel(node)) + '</button>' + (index < breadcrumbNodes.length - 1 ? '<span class=\"template-sep\">/</span>' : '') + '</span>';\n        }).join('') + '</div></div>' +\n        '<div class=\"template-detail-block\"><div class=\"template-detail-label\">模板片段</div><code>' + escapeHtml(selectedNode.snippet || '-') + '</code></div>' +\n        '<div class=\"template-detail-block\"><div class=\"template-detail-label\">属性</div><div>' + ((selectedNode.attrs || []).length ? selectedNode.attrs.map(function (attr) { return '<code>' + escapeHtml(attr.name + (attr.value ? '=\"' + attr.value + '\"' : '')) + '</code>'; }).join(' ') : '<span class=\"muted\">无</span>') + '</div></div>' +\n        '<div class=\"template-detail-block\"><div class=\"template-detail-label\">关联变量</div><div class=\"template-bindings\">' + (detailRows.length ? detailRows.map(function (row) {\n          var snippets = (row.wxmlUsages || []).slice(0, 2).map(function (snippet) { return '<span class=\"snippet\">' + escapeHtml(snippet) + '</span>'; }).join('');\n          return '<div class=\"binding-card\">' +\n            '<div class=\"binding-head\"><strong><button class=\"mini-link\" type=\"button\" data-role=\"locate-key\" data-key=\"' + escapeHtml(row.key) + '\">' + escapeHtml(row.source) + '</button></strong><span class=\"muted\"><button class=\"mini-link\" type=\"button\" data-role=\"locate-key\" data-key=\"' + escapeHtml(row.key) + '\">' + escapeHtml(row.key) + '</button> / ' + escapeHtml(row.confidence) + '</span></div>' +\n            '<div class=\"binding-value\">' + escapeHtml(formatValue(row.value)) + '</div>' +\n            '<div class=\"muted\">' + escapeHtml(row.expressionSummary || '-') + '</div>' +\n            (snippets ? '<div class=\"binding-snippets\">' + snippets + '</div>' : '') +\n          '</div>';\n        }).join('') : '<div class=\"muted\">当前节点没有关联到可识别的运行时变量。</div>') + '</div></div>' +\n        '<div class=\"template-detail-block\"><div class=\"template-detail-label\">该节点变量表</div>' + (detailRows.length ? '<div class=\"binding-table-wrap\"><table class=\"binding-table\"><thead><tr><th>变量名</th><th>Key</th><th>值</th><th>类型</th></tr></thead><tbody>' + detailRows.map(function (row) {\n          var changed = hasChangedBinding(data.route, row);\n          var diff = changed ? window.__changedKeys[(data.route || '') + ':' + row.key] : null;\n          return '<tr>' +\n            '<td><strong><button class=\"mini-link\" type=\"button\" data-role=\"locate-key\" data-key=\"' + escapeHtml(row.key) + '\">' + escapeHtml(row.source) + '</button></strong><div class=\"muted\">' + escapeHtml(row.confidence) + '</div></td>' +\n            '<td><button class=\"mini-link key-link\" type=\"button\" data-role=\"locate-key\" data-key=\"' + escapeHtml(row.key) + '\"><span class=\"key\">' + escapeHtml(row.key) + '</span></button></td>' +\n            '<td>' + escapeHtml(formatValue(row.value)) + (diff ? '<div class=\"diff\">' + escapeHtml(diff.from) + ' -> ' + escapeHtml(diff.to) + '</div>' : '') + '</td>' +\n            '<td>' + escapeHtml(row.kind) + '</td>' +\n          '</tr>';\n        }).join('') + '</tbody></table></div>' : '<div class=\"muted\">当前节点没有可展示的变量表。</div>') + '</div>' +\n      '</div></div>' +\n    '</div>' +\n  '</div>';\n  if (window.__scrollTemplateSelection) {\n    window.__scrollTemplateSelection = false;\n    var activeNodeEl = templateExplorerEl.querySelector('.template-node.active');\n    activeNodeEl && activeNodeEl.scrollIntoView && activeNodeEl.scrollIntoView({ block: 'nearest' });\n  }\n}\nfunction renderDebug(data, force) {\n  var debug = data.debug || {};\n  var pages = debug.keymapPages || [];\n  var active = data.route || '';\n  var fingerprint = JSON.stringify({\n    status: data.status || '',\n    route: active,\n    pageId: debug.pageId == null ? null : debug.pageId,\n    rawRoute: debug.rawRoute || '',\n    keymapPages: pages\n  });\n  window.__pendingDebugData = data;\n  var debugChanged = fingerprint !== window.__lastDebugFingerprint;\n  if (debugChanged || force) {\n    window.__lastDebugFingerprint = fingerprint;\n    debugMetaEl.innerHTML =\n      '<div class=\"muted\">pageId: ' + escapeHtml(debug.pageId == null ? '-' : debug.pageId) + '</div>' +\n      '<div class=\"muted\">rawRoute: ' + escapeHtml(debug.rawRoute || '-') + '</div>' +\n      '<div class=\"muted\">status: ' + escapeHtml(data.status || '-') + '</div>';\n    debugPagesEl.innerHTML = pages.map(function (page) {\n      return '<span class=\"page-pill ' + (page === active ? 'active' : '') + '\">' + escapeHtml(page) + '</span>';\n    }).join('');\n  } else if (!debugDetailsEl.open) {\n    return;\n  }\n  if (debugDetailsEl.open) {\n    var debugBodyText = JSON.stringify({ debug: debug, rawData: data.rawData || {} }, null, 2);\n    if (debugBodyText !== window.__lastDebugBodyText) {\n      debugPreEl.textContent = debugBodyText;\n      window.__lastDebugBodyText = debugBodyText;\n    }\n  } else {\n    if (window.__lastDebugBodyText !== '展开调试信息后可查看原始页面数据。') {\n      debugPreEl.textContent = '展开调试信息后可查看原始页面数据。';\n      window.__lastDebugBodyText = '展开调试信息后可查看原始页面数据。';\n    }\n  }\n}\nfunction renderDiagnostics(data, rows) {\n  var diagnosticRows = rows.filter(isDiagnosticRow);\n  if (!diagnosticRows.length) {\n    diagnosticsEl.innerHTML = '<div class=\"diagnostics-card\"><div class=\"diagnostic-head\"><div class=\"diagnostic-title\">低置信度 / 未识别诊断</div><div class=\"diagnostic-meta\">当前页面没有需要额外诊断的项</div></div></div>';\n    return;\n  }\n  diagnosticsEl.innerHTML = '<div class=\"diagnostics-card\">' +\n    '<div class=\"diagnostic-head\">' +\n    '<div class=\"diagnostic-title\">低置信度 / 未识别诊断</div>' +\n    '<div class=\"diagnostic-meta\">当前页面共 ' + diagnosticRows.length + ' 项，建议优先检查 kind=unknown 或 confidence=low 的项</div>' +\n    '</div>' +\n    '<div class=\"diagnostics-grid\">' + diagnosticRows.map(function (row) {\n      var usages = (row.wxmlUsages || []).slice(0, 3).map(function (snippet) {\n        return '<span class=\"snippet\">' + escapeHtml(snippet) + '</span>';\n      }).join('');\n      return '<div class=\"diagnostic-item\">' +\n        '<div class=\"diagnostic-head\">' +\n          '<div class=\"diagnostic-title\">' + escapeHtml(row.key) + ' -> ' + escapeHtml(row.source || 'unknown') + '</div>' +\n          '<div class=\"diagnostic-meta\">kind: ' + escapeHtml(row.kind) + ' / confidence: ' + escapeHtml(row.confidence) + '</div>' +\n        '</div>' +\n        '<div class=\"diagnostic-expression\">表达式：' + escapeHtml(row.expressionSummary || '-') + '</div>' +\n        '<div class=\"diagnostic-expression\">当前值：' + escapeHtml(formatValue(row.value)) + '</div>' +\n        (usages ? '<div class=\"diagnostic-usages\">' + usages + '</div>' : '') +\n      '</div>';\n    }).join('') + '</div>' +\n  '</div>';\n}\nfunction renderRuntimeRow(data, row, now) {\n  var id = (data.route || '') + ':' + row.key;\n  var changed = window.__changedKeys[id] && now - window.__changedKeys[id].at < 1200;\n  var diff = changed ? '<div class=\"diff\">' + escapeHtml(window.__changedKeys[id].from) + ' -> ' + escapeHtml(window.__changedKeys[id].to) + '</div>' : '';\n  return '<tr class=\"' + (changed ? 'flash' : '') + '\"><td><strong><button class=\"mini-link\" type=\"button\" data-role=\"locate-key\" data-key=\"' + escapeHtml(row.key) + '\">' + escapeHtml(row.source) + '</button></strong><div class=\"muted\">' + escapeHtml(row.confidence) + '</div></td><td><button class=\"mini-link key-link\" type=\"button\" data-role=\"locate-key\" data-key=\"' + escapeHtml(row.key) + '\"><span class=\"key\">' + escapeHtml(row.key) + '</span></button></td><td class=\"value\">' + escapeHtml(formatValue(row.value)) + diff + '</td><td>' + escapeHtml(row.kind) + '</td></tr>';\n}\nfunction renderUnknownGroupRow(data, rows, now) {\n  var changedCount = rows.filter(function (row) {\n    var changed = window.__changedKeys[(data.route || '') + ':' + row.key];\n    return !!(changed && now - changed.at < 1200);\n  }).length;\n  var sampleKeys = rows.slice(0, 8).map(function (row) { return row.key; }).join(', ');\n  var meta = rows.length + ' 项' + (changedCount ? ' / ' + changedCount + ' 项有变化' : '') + (sampleKeys ? ' / ' + sampleKeys : '');\n  return '<tr class=\"unknown-group-row\">' +\n    '<td colspan=\"4\"><button class=\"mini-link unknown-group-toggle\" type=\"button\" data-role=\"toggle-unknown-group\">' +\n      '<span>' + (window.__unknownRowsCollapsed ? '+' : '-') + ' unknown 变量集合</span>' +\n      '<span class=\"unknown-group-meta\">' + escapeHtml(meta) + '</span>' +\n    '</button></td>' +\n  '</tr>';\n}\nfunction renderEventGroupRow(data, rows, now) {\n  var changedCount = rows.filter(function (row) {\n    var changed = window.__changedKeys[(data.route || '') + ':' + row.key];\n    return !!(changed && now - changed.at < 1200);\n  }).length;\n  var sampleKeys = rows.slice(0, 8).map(function (row) { return row.key; }).join(', ');\n  var meta = rows.length + ' 项' + (changedCount ? ' / ' + changedCount + ' 项有变化' : '') + (sampleKeys ? ' / ' + sampleKeys : '');\n  return '<tr class=\"event-group-row\">' +\n    '<td colspan=\"4\"><button class=\"mini-link event-group-toggle\" type=\"button\" data-role=\"toggle-event-group\">' +\n      '<span>' + (window.__eventRowsCollapsed ? '+' : '-') + ' 事件集合</span>' +\n      '<span class=\"event-group-meta\">' + escapeHtml(meta) + '</span>' +\n    '</button></td>' +\n  '</tr>';\n}\nfunction render(data, force) {\n  var filter = filterEl.value || '';\n  var allRows = data.rows || [];\n  cardEl.className = 'card' + (data.connected ? ' connected' : '');\n  statusTextEl.textContent = data.status || (data.connected ? '已连接' : '未连接');\n  routeEl.textContent = data.route || '-';\n  updatedEl.textContent = data.updatedAt ? new Date(data.updatedAt).toLocaleTimeString() : '-';\n  renderToolbarState();\n  renderDebug(data, force);\n  renderTemplateExplorer(data);\n  var now = Date.now();\n  allRows.forEach(function (row) {\n    var id = (data.route || '') + ':' + row.key;\n    var current = formatValue(row.value);\n    if (Object.prototype.hasOwnProperty.call(window.__lastValues, id) && window.__lastValues[id] !== current) {\n      window.__changedKeys[id] = { at: now, from: window.__lastValues[id], to: current };\n    }\n    window.__lastValues[id] = current;\n  });\n  var rows = allRows\n    .filter(function (row) { return window.__currentView === 'all' ? true : isBusinessRow(row); })\n    .filter(function (row) {\n      if (!window.__showChangedOnly) return true;\n      var changed = window.__changedKeys[(data.route || '') + ':' + row.key];\n      return !!(changed && now - changed.at < 1200);\n    })\n    .filter(function (row) { return matches(row, filter); });\n  var fingerprint = JSON.stringify({\n    connected: data.connected,\n    status: data.status,\n    route: data.route,\n    rows: rows,\n    filter: filter,\n    view: window.__currentView,\n    changedOnly: window.__showChangedOnly,\n    unknownRowsCollapsed: window.__unknownRowsCollapsed,\n    eventRowsCollapsed: window.__eventRowsCollapsed\n  });\n  renderDiagnostics(data, allRows);\n  if (!force && fingerprint === window.__lastUniappxFingerprint) return;\n  window.__lastUniappxFingerprint = fingerprint;\n  if (!rows.length) {\n    contentEl.className = 'empty';\n    contentEl.textContent = data.connected ? '当前筛选条件下没有可展示的运行时变量。' : '等待连接微信开发者工具自动化...';\n    return;\n  }\n  contentEl.className = '';\n  var eventRows = rows.filter(isEventRow);\n  var unknownRows = rows.filter(isUnknownUnknownRow);\n  var knownRows = rows.filter(function (row) { return !isEventRow(row) && !isUnknownUnknownRow(row); });\n  var bodyHtml = knownRows.map(function (row) { return renderRuntimeRow(data, row, now); }).join('');\n  if (eventRows.length) {\n    bodyHtml += renderEventGroupRow(data, eventRows, now);\n    if (!window.__eventRowsCollapsed) {\n      bodyHtml += eventRows.map(function (row) { return renderRuntimeRow(data, row, now); }).join('');\n    }\n  }\n  if (unknownRows.length) {\n    bodyHtml += renderUnknownGroupRow(data, unknownRows, now);\n    if (!window.__unknownRowsCollapsed) {\n      bodyHtml += unknownRows.map(function (row) { return renderRuntimeRow(data, row, now); }).join('');\n    }\n  }\n  contentEl.innerHTML = '<table><thead><tr><th>变量名</th><th>Key</th><th>值</th><th>类型</th></tr></thead><tbody>' + bodyHtml + '</tbody></table>';\n}\nasync function refresh(force) {\n  if (window.__refreshing) return;\n  window.__refreshing = true;\n  try {\n    var res = await fetch('/api/snapshot?ts=' + Date.now(), { cache: 'no-store' });\n    window.__lastSnapshot = await res.json();\n    render(window.__lastSnapshot, !!force);\n  } catch (error) {\n    statusTextEl.textContent = '面板连接已断开';\n  } finally {\n    window.__refreshing = false;\n  }\n}\nasync function reconnect() {\n  statusTextEl.textContent = '正在重新连接...';\n  try {\n    await fetch('/api/reconnect', { method: 'POST', cache: 'no-store' });\n  } catch (error) {}\n  window.__lastUniappxFingerprint = '';\n  window.__lastDebugFingerprint = '';\n  refresh(true);\n}\ndebugDetailsEl.addEventListener('toggle', function () {\n  if (!debugDetailsEl.open || !window.__pendingDebugData) return;\n  var data = window.__pendingDebugData;\n  var debugBodyText = JSON.stringify({ debug: data.debug || {}, rawData: data.rawData || {} }, null, 2);\n  debugPreEl.textContent = debugBodyText;\n  window.__lastDebugBodyText = debugBodyText;\n});\ndocument.getElementById('refresh').addEventListener('click', function () {\n  window.__lastUniappxFingerprint = '';\n  window.__lastDebugFingerprint = '';\n  refresh(true);\n});\ndocument.getElementById('reconnect').addEventListener('click', reconnect);\ncontentEl.addEventListener('click', function (event) {\n  var target = event.target;\n  while (target && target !== contentEl) {\n    var role = target.getAttribute && target.getAttribute('data-role');\n    var key = target.getAttribute && target.getAttribute('data-key');\n    if (role === 'toggle-unknown-group') {\n      window.__unknownRowsCollapsed = !window.__unknownRowsCollapsed;\n      window.__lastUniappxFingerprint = '';\n      if (window.__lastSnapshot) render(window.__lastSnapshot, true);\n      return;\n    }\n    if (role === 'toggle-event-group') {\n      window.__eventRowsCollapsed = !window.__eventRowsCollapsed;\n      window.__lastUniappxFingerprint = '';\n      if (window.__lastSnapshot) render(window.__lastSnapshot, true);\n      return;\n    }\n    if (role === 'locate-key' && key) {\n      selectTemplateNodeByKey(window.__lastSnapshot, key);\n      return;\n    }\n    target = target.parentNode;\n  }\n});\ntemplateExplorerEl.addEventListener('click', function (event) {\n  var target = event.target;\n  while (target && target !== templateExplorerEl) {\n    var role = target.getAttribute && target.getAttribute('data-role');\n    var nodeId = target.getAttribute && target.getAttribute('data-node-id');\n    var key = target.getAttribute && target.getAttribute('data-key');\n    if (role === 'toggle-node' && nodeId) {\n      window.__collapsedTemplateNodes[nodeId] = !window.__collapsedTemplateNodes[nodeId];\n      if (window.__lastSnapshot) render(window.__lastSnapshot, true);\n      return;\n    }\n    if (role === 'expand-changed') {\n      if (window.__lastSnapshot && window.__lastSnapshot.templateTree) {\n        var rowsByKey = {};\n        (window.__lastSnapshot.rows || []).forEach(function (row) {\n          rowsByKey[row.key] = row;\n        });\n        var firstChangedNodeId = expandChangedTemplateNodes(window.__lastSnapshot.templateTree, window.__lastSnapshot.route, rowsByKey);\n        if (firstChangedNodeId) window.__selectedTemplateNodeId = firstChangedNodeId;\n        window.__scrollTemplateSelection = true;\n        render(window.__lastSnapshot, true);\n      }\n      return;\n    }\n    if (role === 'toggle-bound-only') {\n      window.__templateBoundOnly = !window.__templateBoundOnly;\n      window.__scrollTemplateSelection = true;\n      if (window.__lastSnapshot) render(window.__lastSnapshot, true);\n      return;\n    }\n    if (role === 'toggle-changed-only') {\n      window.__templateChangedOnly = !window.__templateChangedOnly;\n      window.__scrollTemplateSelection = true;\n      if (window.__lastSnapshot) render(window.__lastSnapshot, true);\n      return;\n    }\n    if (role === 'toggle-pin-selection') {\n      window.__pinTemplateSelection = !window.__pinTemplateSelection;\n      if (window.__lastSnapshot) render(window.__lastSnapshot, true);\n      return;\n    }\n    if (role === 'locate-key' && key) {\n      selectTemplateNodeByKey(window.__lastSnapshot, key);\n      return;\n    }\n    if (role === 'select-node' && nodeId) {\n      window.__selectedTemplateNodeId = nodeId;\n      if (window.__lastSnapshot) render(window.__lastSnapshot, true);\n      return;\n    }\n    target = target.parentNode;\n  }\n});\ntemplateExplorerEl.addEventListener('input', function (event) {\n  var target = event.target;\n  if (target && target.getAttribute && target.getAttribute('data-role') === 'template-search') {\n    window.__templateSearch = target.value || '';\n    if (window.__lastSnapshot) render(window.__lastSnapshot, true);\n  }\n});\nviewBusinessEl.addEventListener('click', function () {\n  window.__currentView = 'business';\n  window.__lastUniappxFingerprint = '';\n  if (window.__lastSnapshot) render(window.__lastSnapshot, true);\n});\nviewAllEl.addEventListener('click', function () {\n  window.__currentView = 'all';\n  window.__lastUniappxFingerprint = '';\n  if (window.__lastSnapshot) render(window.__lastSnapshot, true);\n});\ntoggleChangedEl.addEventListener('click', function () {\n  window.__showChangedOnly = !window.__showChangedOnly;\n  window.__lastUniappxFingerprint = '';\n  if (window.__lastSnapshot) render(window.__lastSnapshot, true);\n});\nfilterEl.addEventListener('input', function () { if (window.__lastSnapshot) render(window.__lastSnapshot, true); });\nrenderToolbarState();\nrefresh(true);\nsetInterval(function () { refresh(false); }, 300);\n</script>\n</body>\n</html>`;\n}\n\nfunction openBrowser(url: string): void {\n  if (process.env.UNIAPP_MINIPROGRAM_DEVTOOL_NO_OPEN === '1' || process.env.UNIAPPX_KEYMAP_NO_OPEN === '1') return;\n  if (process.platform === 'darwin') spawn('open', [url], { detached: true, stdio: 'ignore' }).unref();\n}\n\nfunction defaultWebPanelPort(): number {\n  return Number(process.env.UNIAPP_MINIPROGRAM_DEVTOOL_PORT || process.env.UNIAPPX_KEYMAP_PORT || 17890);\n}\n\nasync function listen(server: http.Server, preferredPort: number): Promise<number> {\n  if (preferredPort <= 0) {\n    await new Promise<void>((resolve, reject) => {\n      const onError = (error: NodeJS.ErrnoException) => {\n        server.off('listening', onListening);\n        reject(error);\n      };\n      const onListening = () => {\n        server.off('error', onError);\n        resolve();\n      };\n      server.once('error', onError);\n      server.once('listening', onListening);\n      server.listen(0, '127.0.0.1');\n    });\n    const address = server.address();\n    if (!address || typeof address === 'string') throw new Error('无法解析本地 Web 面板端口。');\n    return address.port;\n  }\n\n  for (let offset = 0; offset < 20; offset += 1) {\n    const port = preferredPort + offset;\n    try {\n      await new Promise<void>((resolve, reject) => {\n        const onError = (error: NodeJS.ErrnoException) => {\n          server.off('listening', onListening);\n          reject(error);\n        };\n        const onListening = () => {\n          server.off('error', onError);\n          resolve();\n        };\n        server.once('error', onError);\n        server.once('listening', onListening);\n        server.listen(port, '127.0.0.1');\n      });\n      return port;\n    } catch (error) {\n      const code = (error as NodeJS.ErrnoException).code;\n      if (code !== 'EADDRINUSE' && code !== 'EACCES' && code !== 'EPERM') throw error;\n    }\n  }\n  throw new Error(`未找到可用的本地 Web 面板端口，尝试范围：${preferredPort} - ${preferredPort + 19}`);\n}\n\nexport async function startWebPanel(options: number | WebPanelOptions = defaultWebPanelPort()): Promise<WebPanel> {\n  const normalized = typeof options === 'number' ? { port: options } : options;\n  const preferredPort = normalized.port ?? defaultWebPanelPort();\n  let snapshot = { ...DEFAULT_SNAPSHOT };\n  const server = http.createServer((req, res) => {\n    if (req.url?.startsWith('/api/snapshot')) {\n      res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });\n      res.end(JSON.stringify(snapshot));\n      return;\n    }\n    if (req.url?.startsWith('/api/reconnect')) {\n      if (req.method !== 'POST') {\n        res.writeHead(405, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });\n        res.end(JSON.stringify({ ok: false, error: '请求方法不支持' }));\n        return;\n      }\n      snapshot = { ...snapshot, connected: false, status: '已发起重新连接...', updatedAt: new Date().toISOString() };\n      Promise.resolve(normalized.onReconnect?.())\n        .then(() => {\n          res.writeHead(202, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });\n          res.end(JSON.stringify({ ok: true }));\n        })\n        .catch((error) => {\n          const message = error instanceof Error ? error.message : String(error);\n          snapshot = {\n            ...snapshot,\n            connected: false,\n            status: `重新连接失败：${message}`,\n            updatedAt: new Date().toISOString(),\n          };\n          res.writeHead(500, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' });\n          res.end(JSON.stringify({ ok: false, error: message }));\n        });\n      return;\n    }\n    res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' });\n    res.end(html());\n  });\n\n  const actualPort = await listen(server, preferredPort);\n  const url = `http://127.0.0.1:${actualPort}`;\n  openBrowser(url);\n\n  return {\n    url,\n    update(next) {\n      snapshot = next;\n    },\n    close() {\n      server.close();\n    },\n  };\n}\n"],"mappings":"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"}