revert: hub.js/server.js/panel.html 回滚到 v1.6.37(用户指令:1.6.37 为最新版本)

- src/hub.js: 89545 → 89332 字节(去除 1.6.70 增强)
- src/server.js: 42219 → 41961 字节
- web/panel.html: 89203 → 84523 字节(撤掉 isSessionConnected holding/isHung 修改)
- 本地 /Users/karuo/.persistent-chat-local/ 三个核心文件已同步覆盖 + uchg 锁定
- Hub 服务已重启到 1.6.37(API 返回 panelVersion: 1.6.37)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
卡若AI
2026-06-27 22:18:45 +08:00
parent 31c0361ee0
commit 0c836af847
4 changed files with 52 additions and 156 deletions

View File

@@ -9,7 +9,7 @@
"env": {
"PCHAT_HTTP_PORT": "13458",
"PCHAT_NODE": "/usr/local/opt/node@22/bin/node",
"PCHAT_HUB_HOST": "192.168.110.101",
"PCHAT_HUB_HOST": "127.0.0.1",
"NO_PROXY": "192.168.110.101,127.0.0.1,localhost",
"no_proxy": "192.168.110.101,127.0.0.1,localhost"
},

View File

@@ -711,9 +711,6 @@ function shouldAlwaysKeepAlive(s) {
function buildKeepAliveContinueText(s, review) {
const mode = getSessionContinueMode(s);
const goal = String((s && s.projectGoal) || (review && review.projectGoal) || '').slice(0, 150);
if (review && review.isIdleNext) {
return `【挂起】当前会话已保持连接,下一步状态是 wait / 待命。等待下一条消息即可,不要结束对话。`;
}
if (review && review.nextStep && !review.isIdleNext) {
return `【保活续跑】继续执行▶下一步:${review.nextStep.slice(0, 400)}。保持连接勿停。`;
}
@@ -728,7 +725,7 @@ function buildKeepAliveContinueText(s, review) {
return '【保活续跑】保持连接,继续优化流程:总结→检索→补文档→开发,按学习清单推进。';
}
if (goal) {
return `挂起】目标已记录${goal}当前状态维持 wait / 待命,等待下一条消息继续,不要结束对话`;
return `保活续跑】继续推进${goal}按复盘与▶下一步执行,保持连接勿停`;
}
return String(
hubSettings.keepAliveText

View File

@@ -34,10 +34,9 @@ const WORKSPACE = (!WORKSPACE_RAW
? (process.cwd() || HOME)
: WORKSPACE_RAW;
const HTTP_PORT = parseInt(process.env.PCHAT_HTTP_PORT || arg('port', '13458'), 10);
const HUB_HOST = process.env.PCHAT_HUB_HOST || '127.0.0.1';
const HUB = `http://${HUB_HOST}:${HTTP_PORT}`;
const HUB = `http://127.0.0.1:${HTTP_PORT}`;
const HOST_IP = getHostIp();
const HUB_LABEL = `${HUB_HOST}:${HTTP_PORT}`;
const HUB_LABEL = `127.0.0.1:${HTTP_PORT}`;
/** 卡若AI 能力真源(每轮对话门禁) */
const KARUO_AI_ROOT = '/Users/karuo/Documents/个人/卡若AI';
@@ -656,7 +655,7 @@ const TOOLS = [
},
{
name: 'wait_for_user_input',
description: '挂起 Agent等待远端主链面板 http://192.168.110.101:13458 回复;本机 127.0.0.1 仅兜底。必须使用本线程锁定的 pchat-token。',
description: '挂起 Agent等待本地面板 http://127.0.0.1:13458 回复。必须使用本线程锁定的 pchat-token。',
inputSchema: {
type: 'object',
properties: {
@@ -1112,12 +1111,9 @@ rl.on('line', async (line) => {
/** Cursor 可能关闭 MCP stdin挂起 poll 期间禁止 exit否则 Hub 仍 waiting 但无 poller */
let activeWaitToken = null;
let idleKeepAliveTimer = null;
process.stdin.on('end', () => {
log('stdin end', { activeWait: activeWaitToken });
if (activeWaitToken) return;
if (!idleKeepAliveTimer) idleKeepAliveTimer = setInterval(() => {}, 60 * 1000);
log('stdin end ignored: keep alive while idle to preserve transport');
if (!activeWaitToken) setTimeout(() => process.exit(0), 200);
});
process.on('SIGTERM', () => process.exit(0));

View File

@@ -81,8 +81,6 @@
aside .sess.active { background:rgba(37,99,235,.08); border-color:rgba(37,99,235,.28); box-shadow:inset 3px 0 0 var(--kir-brand); }
aside .sess.waiting { background:linear-gradient(90deg,rgba(255,138,0,.1),transparent); }
aside .sess.waiting.active { background:linear-gradient(90deg,rgba(255,138,0,.16),rgba(37,99,235,.06)); }
aside .sess.abandoned { opacity:.62; }
aside .sess.abandoned .badge { color:var(--fg2); }
aside .sess .title { font-weight:600; font-size:13px; line-height:1.35; }
aside .sess .meta { font-size:11px; color:var(--fg2); margin-top:4px; display:flex; gap:6px; align-items:center; flex-wrap:wrap; }
.badge { padding:2px 8px; border-radius:var(--kir-radius-pill); font-size:10px; font-weight:600; letter-spacing:.02em; }
@@ -197,16 +195,13 @@
</head>
<body>
<div class="app-shell">
<div id="vsixBanner" class="alert-bar" style="display:flex;background:linear-gradient(90deg,#2563eb,#7c3aed);cursor:default">
<span>🔌 <strong>VSIX 4.6.0 官方</strong> · 主界面=<strong>Cursor 侧栏</strong>(开 MCP + 持续对话)· Hub <code>http://localhost:13458</code> 排队续跑</span>
</div>
<div id="alertBar" class="alert-bar" style="display:none" onclick="jumpToWaiting()">
<span class="pulse"></span>
<span id="alertText">有 1 个会话正在等待你回复</span>
<span style="margin-left:auto;font-size:12px;opacity:.85">点击跳转 →</span>
</div>
<header class="app-bar kir-glass-solid">
<h1>卡若AI · VSIX 4.6.0 Hub <span id="panelVer" style="font-size:11px;color:var(--fg2);font-weight:500;-webkit-text-fill-color:var(--fg2)"></span></h1>
<h1>卡若AI · 持久对话 <span id="panelVer" style="font-size:11px;color:var(--fg2);font-weight:500;-webkit-text-fill-color:var(--fg2)"></span></h1>
<div class="right">
<span id="conn" class="conn-pill"><span class="dot warn"></span> 连接中…</span>
<div class="more-menu">
@@ -231,8 +226,7 @@
<div class="empty">
<div class="big">还没开始?</div>
<button type="button" class="copy-btn" data-copy-prompt style="display:inline-block">📋 复制启动提示词</button> 粘贴到 Cursor ChatAgent 模式),或点左侧 <strong>+ 新建任务</strong><br><br>
Agent 会 <code>init_conversation</code> 创建会话,再 <code>wait_for_user_input</code> 挂起;之后从本面板回复即可。<br>
<small style="color:var(--fg2)">MCP 4 工具init · select · wait · merge · 与 Cursor 侧栏 4tools 一致</small>
Agent 会 <code>init_conversation</code> 创建会话,再 <code>wait_for_user_input</code> 挂起;之后从本面板回复即可。
</div>
</section>
</main>
@@ -248,31 +242,10 @@
</div>
<script>
const PANEL_VERSION = '1.6.70';
const MCP_4TOOLS = ['init_conversation', 'select_conversation', 'wait_for_user_input', 'merge_conversation'];
let hubMeta = { vsixCompat: '4.6.0', vsixVariant: '4tools', mcpToolCount: 4, panelVersion: PANEL_VERSION };
function isVsix4Tools() {
return hubMeta.vsixVariant === '4tools' || (Number(hubMeta.mcpToolCount) === 4 && hubMeta.vsixVariant !== 'standard');
}
function fetchWithTimeout(url, ms, init = {}) {
if (typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function') {
return fetch(url, { ...init, signal: AbortSignal.timeout(ms) });
}
const ac = new AbortController();
const timer = setTimeout(() => ac.abort(), ms);
return fetch(url, { ...init, signal: ac.signal }).finally(() => clearTimeout(timer));
}
const PANEL_VERSION = '1.6.37';
const SIDEBAR_ACTIVE_MS = 2 * 60 * 60 * 1000;
const PIN_STORAGE_KEY = 'pchat_pinned_token';
const PINNED_LIST_KEY = 'pchat_pinned_tokens';
/** v1.6.68/?token=ct_xxx 深链选中并聚焦回复框 */
let deepLinkToken = null;
try {
const _dl = new URLSearchParams(location.search);
deepLinkToken = _dl.get('token') || _dl.get('ct');
} catch {}
const MODE_LABELS = { standard: '复盘', turbo: '直通', optimize: '优化' };
const TIMED_REPLY_FIXED = [
{ label: '自动', ms: 0 },
@@ -332,15 +305,12 @@ function togglePinToken(token) {
updateTaskFooter();
}
const STARTUP_PROMPT = `请使用 persistent-chat MCPVSIX 4.6.0-4tools · 仅 4 工具
const STARTUP_PROMPT = `请使用 persistent-chat MCP卡若复盘 + 续跑三段式
1. select_conversation / init_conversationcursorTitle=当前标签名)。
2. **新计划必须 newPlan**:无关新任务 → init_conversation(newPlan:true, planLabel=计划名, cursorTitle=标签名)。
3. 每轮:执行 → **[卡若复盘]** 🎯📌💡📝▶ → wait_for_user_input 长挂起;**每轮只 wait 一次**
4. **[RENEWAL] 零工**:同 turn 立刻 renewalOnly 再 wait禁止 RENEWAL 之间夹 Read/Shell
5. 续跑在 **13458 面板** 或 **侧栏** 发下一条;勿在 Cursor 聊天打「继续」
6. 「结束持久对话」才停循环。
MCP 4 工具init_conversation · select_conversation · wait_for_user_input · merge_conversation
2. **新计划必须 newPlan**若与旧任务无关新任务 → init_conversation(newPlan:true, planLabel=计划名, cursorTitle=标签名),禁止挂旧线程
3. 每轮:执行 → **[卡若复盘]** 🎯📌💡📝▶ → wait\\\`\\\`\\\`text 包裹)
4. 挂起后续跑①追问→「追问完成」→②WebSearch+GitHub→「检索完成」→③执行▶
5. 「结束持久对话」才停循环
第一项任务:`;
@@ -382,10 +352,10 @@ function sessionLastActiveAt(s) {
return Number.isFinite(t) ? t : 0;
}
/** 真正保持连接:挂起 / 续跑 / 活跃 wait(废弃会话不算) */
/** 真正保持连接:挂起 / 续跑 / 活跃 wait */
function isSessionConnected(s) {
if (!s || s.isAbandoned || s.status === 'abandoned') return false;
return isAgentSuspended(s) || !!(s.autoRunning || s.isActiveWait || s.connectionHeld || s.isHung || s.status === 'holding');
if (!s) return false;
return isAgentSuspended(s) || !!(s.autoRunning || s.isActiveWait);
}
/** 2 小时内有实质对话(有消息且最近活跃) */
@@ -541,12 +511,10 @@ function sessionMode(s) {
}
function sessionAutoOn(s) {
if (isVsix4Tools()) return s.sessionAutoContinue === true;
return s.sessionAutoContinue !== false;
}
function sessionTimedOn(s) {
if (isVsix4Tools()) return s.sessionTimedReply === true;
if (typeof s.sessionTimedReply === 'boolean') return s.sessionTimedReply;
return s.timedReplyEnabled !== false;
}
@@ -702,47 +670,16 @@ function patchDetailDynamic() {
});
}
function setConnected(ok, meta) {
const connEl = document.getElementById('conn');
if (!connEl) return;
if (ok) {
const variant = (meta && meta.vsixVariant) || hubMeta.vsixVariant || '4tools';
const tools = (meta && meta.mcpToolCount) || hubMeta.mcpToolCount || 4;
connEl.innerHTML = `<span class="dot"></span> 已连接 · ${variant} · MCP ${tools}`;
connEl.className = 'conn-pill ok';
} else {
connEl.innerHTML = '<span class="dot warn"></span> 离线';
connEl.className = 'conn-pill';
}
}
async function pingHealth() {
try {
const r = await fetchWithTimeout('/api/health', 8000, { cache: 'no-store' });
if (!r.ok) throw new Error('health ' + r.status);
const d = await r.json();
hubMeta = { ...hubMeta, ...d };
setConnected(true, d);
const ver = document.getElementById('panelVer');
if (ver) ver.textContent = `v${d.panelVersion || PANEL_VERSION} · MCP ${d.mcpToolCount || 4}`;
return true;
} catch (e) {
console.warn('pingHealth failed', e);
setConnected(false);
return false;
}
}
async function fetchState(){
if (isTyping() && Date.now() < pollPauseUntil + 120000) return;
try {
const r = await fetchWithTimeout('/api/state?lite=1', 45000, { cache: 'no-store' });
if (!r.ok) throw new Error('state ' + r.status);
const r = await fetch('/api/state', { cache: 'no-store' });
state = await r.json();
if (!Array.isArray(state.sessions)) state.sessions = [];
if (state.vsixVariant) hubMeta.vsixVariant = state.vsixVariant;
if (state.mcpToolCount) hubMeta.mcpToolCount = state.mcpToolCount;
setConnected(true, hubMeta);
const connEl = document.getElementById('conn');
if (connEl && !connEl.classList.contains('ok')) {
connEl.innerHTML = '<span class="dot"></span> 已连接';
connEl.className = 'conn-pill ok';
}
const waiting = state.sessions.filter(s => s.status === 'waiting' || s.hasPending);
const active = state.activeWaitToken;
@@ -798,10 +735,12 @@ async function fetchState(){
patchTimedPresetUi(getTimedMs(selSess || {}));
}
updateTaskFooter();
applyDeepLinkToken();
} catch (e) {
console.error('fetchState failed', e);
setConnected(false);
const connEl = document.getElementById('conn');
if (connEl) {
connEl.innerHTML = '<span class="dot warn"></span> 离线';
connEl.className = 'conn-pill';
}
}
}
@@ -825,21 +764,6 @@ function jumpToWaiting(){
}
}
function applyDeepLinkToken() {
if (!deepLinkToken) return;
const tok = deepLinkToken;
deepLinkToken = null;
const hit = state.sessions.find((s) => s.token === tok);
if (!hit) return;
selectSession(tok);
userPinnedSelection = true;
const ta = document.getElementById('replyBox');
if (ta) {
ta.focus();
ta.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
}
}
function selectSession(token) {
selected = token;
userPinnedSelection = true;
@@ -872,14 +796,11 @@ function sessionLiveBadge(s) {
return { text: '挂起', cls: 'suspend-waiting', title: 'Agent 已 wait_for_user_input等待你回复' };
}
if (s.autoRunning || s.isActiveWait) return { text: '续跑', cls: 'suspend-auto', title: '自动续跑中' };
if (isConnectionOnly(s)) return { text: '待wait', cls: 'suspend-auto', title: '仅保活连接Cursor 尚未调用 wait_for_user_input' };
if (isConnectionOnly(s)) return { text: '保活', cls: 'suspend-auto', title: '计划保活中Cursor 尚未调用 wait' };
return null;
}
function sessionBadge(s) {
if (s.isAbandoned || s.status === 'abandoned') {
return { text: '废弃', cls: 'idle' };
}
const live = sessionLiveBadge(s);
if (live) {
const sideCls = live.cls === 'suspend-waiting' ? 'waiting' : (live.text === '续跑' ? 'auto' : 'waiting');
@@ -953,7 +874,6 @@ function patchSessionItem(div, s) {
if (selected === s.token) cls.push('active');
if (isTokenPinned(s.token)) cls.push('pinned');
if (s.hasPending || s.status === 'waiting' || s.connectionHeld || s.isHung) cls.push('waiting');
if (s.isAbandoned || s.status === 'abandoned') cls.push('abandoned');
div.className = cls.join(' ');
div.oncontextmenu = (e) => showSessContextMenu(e, s.token);
const badge = sessionBadge(s);
@@ -983,7 +903,7 @@ function patchSessionItem(div, s) {
}
const pendEl = div.querySelector('.sess-pending');
if (pendEl) {
pendEl.textContent = preview ? `挂起: ${preview}` : (s.steerQueueCount ? `排队 ${s.steerQueueCount}` : (isAgentSuspended(s) ? '挂起中,可面板回复…' : (isConnectionOnly(s) ? '未挂起,等 Agent wait…' : '')));
pendEl.textContent = preview ? `挂起: ${preview}` : (s.steerQueueCount ? `排队 ${s.steerQueueCount}` : (isAgentSuspended(s) ? '挂起中,可面板回复…' : (isConnectionOnly(s) ? '保活中,等 Agent wait 接入…' : '')));
pendEl.style.display = preview || s.hasPending || s.steerQueueCount || s.connectionHeld ? 'block' : 'none';
}
const badgeEl = div.querySelector('.badge');
@@ -1050,13 +970,13 @@ function renderSidebar(){
fold.id = 'sidebarStaleFold';
const head = document.createElement('div');
head.className = 'sidebar-fold-head';
const foldLabel = '▸ 已废弃 · 超过2小时无对话';
const foldLabel = '▸ 已归类 · 未连接或超过2小时';
head.innerHTML = `<span>${foldLabel}</span><span class="count">${stale.length}</span>`;
head.onclick = (e) => {
e.stopPropagation();
sidebarStaleExpanded = !sidebarStaleExpanded;
fold.classList.toggle('open', sidebarStaleExpanded);
head.querySelector('span').textContent = (sidebarStaleExpanded ? '▾' : '▸') + ' 已废弃 · 超过2小时无对话';
head.querySelector('span').textContent = (sidebarStaleExpanded ? '▾' : '▸') + ' 已归类 · 未连接或超过2小时';
lastSidebarKey = '';
};
const body = document.createElement('div');
@@ -1064,7 +984,7 @@ function renderSidebar(){
fold.appendChild(head);
fold.appendChild(body);
list.appendChild(fold);
if (sidebarStaleExpanded) head.querySelector('span').textContent = '▾ 已废弃 · 超过2小时无对话';
if (sidebarStaleExpanded) head.querySelector('span').textContent = '▾ 已归类 · 未连接或超过2小时';
for (const s of stale) {
keep.add(s.token);
ensureSessionRow(list, body, s);
@@ -1136,12 +1056,7 @@ function renderDetail(){
banner += `<div class="steer-queue"><div class="steer-head">📥 排队方向(${s.steerQueueCount})· wait 后送达 · 可单删 <button type="button" class="ghost" id="steerClearAll">清空</button></div>${steerItems}</div>`;
}
if (suspended) {
const sub = (s.panelSubTask || review.nextStep || '').trim();
if (sub && !review.isIdleNext) {
banner += `<div class="status-banner ok" style="border-color:rgba(89,211,156,.5)">📋 <strong>子任务已挂起</strong> · 请到本面板点「▶ 点击继续开发」分配执行<br/><span style="opacity:.9;font-size:13px">${esc(sub)}</span><br/><small style="opacity:.75">勿在 Cursor 对话框输入「继续」</small></div>`;
} else {
banner += `<div class="status-banner ok">⏸ <strong>已挂起</strong> · 请在本面板点「继续开发」或发消息续跑</div>`;
}
banner += `<div class="status-banner ok">⏸ <strong>已挂起</strong> · Hub 已收到 <code>wait_for_user_input</code>,可从本面板回复或点「继续开发」</div>`;
} else if (connOnly) {
banner += `<div class="status-banner proc">🔗 <strong>计划保活中(未挂起)</strong> · Cursor 里该任务若还在跑、未调用 wait面板不会显示挂起请让 Agent 复盘后 <code>wait_for_user_input</code>,或在本面板发消息排队</div>`;
}
@@ -1186,27 +1101,6 @@ function renderDetail(){
const cfgAutoCls = autoOn ? 'on' : '';
const cfgTimedCls = timedOn ? 'on' : '';
const timedMs = getTimedMs(s);
const autoTimedControls = isVsix4Tools()
? `<span class="chip on" title="4tools仅面板/侧栏手动发令Hub 不自动续跑">4tools · 手动发令</span>
<span class="chip" title="MCP 4 工具与 Cursor 侧栏一致">MCP 4 · init/select/wait/merge</span>`
: `<label class="chip ${cfgAutoCls}" id="sessAutoToggle" title="自动续跑">
<input type="checkbox" id="sessAutoCb" ${autoOn ? 'checked' : ''} hidden /> 自动续跑
</label>
<label class="chip ${cfgTimedCls}" id="timedReplyToggle" title="定时保活Hub 按间隔发继续)">
<input type="checkbox" id="timedReplyCb" ${timedOn ? 'checked' : ''} hidden /> 自动定时
</label>`;
const timedPresetControls = isVsix4Tools()
? ''
: `${TIMED_REPLY_FIXED.map((p) =>
`<button type="button" class="chip timed-preset${timedMs === p.ms ? ' on' : ''}" data-ms="${p.ms}">${p.label}</button>`
).join('')}
<label class="chip timed-custom${(timedMs > 0 && !TIMED_REPLY_FIXED.some((p) => p.ms === timedMs)) ? ' on' : ''}" title="自定义分钟">
<input id="timedCustomMin" type="number" min="1" max="999" placeholder="分" value="${(timedMs > 0 && !TIMED_REPLY_FIXED.some((p) => p.ms === timedMs)) ? Math.max(1, Math.round(timedMs / 60000)) : ''}" />
<span>自定义</span>
</label>`;
const composerLegend = isVsix4Tools()
? '4tools本面板输入 → 排队 → Agent wait 首 poll 送达 Cursor → 自动续跑 ▶(勿开 turbo 自动续跑)'
: '定时:自动=10秒保活 · 10秒/30秒/4分/9分 · 排队方向可单删';
det.innerHTML = `
<div class="header2">
<div class="title">${esc(sessionDisplayTitle(s))} ${titleBadge}</div>
@@ -1215,13 +1109,24 @@ function renderDetail(){
</div>
<div class="control-bar">
<span class="chip-label">控制</span>
${autoTimedControls}
<label class="chip ${cfgAutoCls}" id="sessAutoToggle" title="自动续跑">
<input type="checkbox" id="sessAutoCb" ${autoOn ? 'checked' : ''} hidden /> 自动续跑
</label>
<label class="chip ${cfgTimedCls}" id="timedReplyToggle" title="定时保活Hub 按间隔发继续)">
<input type="checkbox" id="timedReplyCb" ${timedOn ? 'checked' : ''} hidden /> 自动定时
</label>
<div class="mode-switch" id="sessModeSwitch">
<button type="button" class="mode-btn${standardOn ? ' active' : ''}" data-mode="standard">复盘</button>
<button type="button" class="mode-btn turbo${turboOn ? ' active' : ''}" data-mode="turbo">直通</button>
<button type="button" class="mode-btn optimize${optimizeOn ? ' active' : ''}" data-mode="optimize">优化</button>
</div>
${timedPresetControls}
${TIMED_REPLY_FIXED.map((p) =>
`<button type="button" class="chip timed-preset${timedMs === p.ms ? ' on' : ''}" data-ms="${p.ms}">${p.label}</button>`
).join('')}
<label class="chip timed-custom${(timedMs > 0 && !TIMED_REPLY_FIXED.some((p) => p.ms === timedMs)) ? ' on' : ''}" title="自定义分钟">
<input id="timedCustomMin" type="number" min="1" max="999" placeholder="分" value="${(timedMs > 0 && !TIMED_REPLY_FIXED.some((p) => p.ms === timedMs)) ? Math.max(1, Math.round(timedMs / 60000)) : ''}" />
<span>自定义</span>
</label>
</div>
</div>
<div class="msg" id="msgBox">
@@ -1243,7 +1148,7 @@ function renderDetail(){
<span class="hint">${composerHint}</span>
<button id="sendBtn">${minimalComposer ? '插队发送 →' : (canSend ? '发送回复 →' : '排队发送 →')}</button>
</div>
<div class="legend">${composerLegend}</div>
<div class="legend">定时:自动=10秒保活 · 10秒/30秒/4分/9分 · 排队方向可单删</div>
</div>
`;
{
@@ -1458,7 +1363,7 @@ async function manualContinue() {
return;
}
lastSent[selected] = { at: Date.now(), text: (j.text || '继续').slice(0, 80), auto: false };
showToast('▶ 子任务已从面板发出 · Cursor 将自动续跑stop hook', 'ok', 5500);
showToast('▶ 已发送继续 · Cursor 应马上开跑', 'ok', 4500);
lastSidebarKey = '';
lastDetailKey = '';
await fetchState();
@@ -1829,11 +1734,9 @@ if ('Notification' in window && Notification.permission === 'default') {
}
setInterval(() => { if (!isTyping()) fetchState(); }, 2000);
pingHealth().finally(() => {
fetchState().catch((e) => {
console.error('fetchState failed', e);
showToast('面板加载失败,请 Cmd+Shift+R 硬刷新', 'err', 6000);
});
fetchState().catch((e) => {
console.error('fetchState failed', e);
showToast('面板加载失败,请 Cmd+Shift+R 硬刷新', 'err', 6000);
});
</script>
</body>