/** * 输入截获 + agent_end hook * 截获来自其他 pi 的协议消息,处理需要回复/不需要回复的场景 * Worker 完成任务后自动回复并关闭 pane */ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent'; import { getMyPaneId, writeToPane, closeFloatingPane } from '../lib/zellij'; import { PiMessage, isProtocolMessage, parseMessage, buildMessage } from './msg-protocol'; import { readAgent } from '../lib/agents'; // 当前任务元数据(Worker 收到委托时设置,回复后清除) let currTask: (Omit & { receivedAt: number }) | null = null; // 挂起的延时关闭(用于 error 型 agent_end,等 retry 的 agent_start 来取消) // 单次最大退避 = baseDelay(2s) × 2^(maxRetries-1) = 8s,10s 留 2s 余量 let pendingClose: { timer: NodeJS.Timeout; task: typeof currTask } | null = null; /** 检查指定 PID 的进程是否存活 */ function isProcessAlive(pid: number | null | undefined): boolean { if (!pid) return false; try { process.kill(pid, 0); // signal 0 = 只检查存在性,不发送信号 return true; } catch { return false; } } /** 从 sessionManager 取最新 assistant 消息的文本 */ function getLatestAssistantText(ctx: any): string { try { const leaf = ctx.sessionManager.getLeafEntry() as any; return leaf?.message?.role === 'assistant' ? leaf.message.content .filter((c: any) => c.type === 'text') .map((c: any) => c.text) .join('') : ''; } catch (err) { console.log('[pi-in-zellij] getLeafEntry 取回复文本失败:', err); return ''; } } export function registerInterceptor(pi: ExtensionAPI) { // ---- input hook:截获来自其他 pi 的协议消息 ---- pi.on('input', async (event, ctx) => { if (event.source === 'extension') return { action: 'continue' }; if (!isProtocolMessage(event.text)) return { action: 'continue' }; const message = parseMessage(event.text); if (!message) return { action: 'continue' }; // ---- Summon 分支(独立处理,不走原有 agent/needReply 逻辑)---- if (message.commType === 'Summon') { const fromName = message.firstName; const assistant = message.assistant; ctx.ui.notify(`📨 Summoned by ${fromName} (${assistant})`, 'info'); currTask = { firstPaneId: message.firstPaneId, secondPaneId: message.secondPaneId, firstName: message.firstName, secondName: message.secondName, needReply: message.needReply, commId: message.commId, commType: message.commType, assistant: message.assistant, firstPid: message.firstPid, receivedAt: Date.now(), }; pi.sendUserMessage(`Delegated Task:\n${message.markdown}`); return { action: 'handled' }; } // 根据 commType 判断谁是实际发送方 const fromName = (message.commType === 'Report' || message.commType === 'Info') ? message.secondName // Report/Info 是 second 发的 : message.firstName; // Delegate/Chat 是 first 发的 // 处理 agent(如果有的话) let agentContent: string | null = null; if (message.agent && message.agent !== '') { agentContent = readAgent(message.agent); if (agentContent) { ctx.ui.notify(`📋 Agent loaded: ${message.agent}`, 'info'); } else { ctx.ui.notify(`⚠️ Failed to load agent "${message.agent}"`, 'warning'); } } // 组合内容发给 LLM const enhancedContent = agentContent ? `${agentContent}\n\n---\n**任务:**\n\n${message.markdown}` : message.markdown; // 不需要回复 → 转为普通输入送给 LLM if (!message.needReply) { return { action: 'transform', text: `Report from ${fromName}:\n${enhancedContent}`, }; } // 需要回复 → 记录发送方,把任务送给 LLM ctx.ui.notify(`📨 ${message.commType} from ${fromName}`, 'info'); currTask = { firstPaneId: message.firstPaneId, secondPaneId: message.secondPaneId, firstName: message.firstName, secondName: message.secondName, needReply: message.needReply, commId: message.commId, commType: message.commType, firstPid: message.firstPid, receivedAt: Date.now(), }; pi.sendUserMessage(`Delegated Task:\n${enhancedContent}`); return { action: 'handled' }; }); // ---- agent_start hook:retry 开始了 → 取消挂起的关闭 ---- pi.on('agent_start', () => { if (pendingClose) { clearTimeout(pendingClose.timer); currTask = pendingClose.task; // 还原 currTask,让 retry 成功后的最终 agent_end 能走正常分支 pendingClose = null; console.log('[pi-in-zellij] 检测到 retry agent_start,取消挂起的关闭'); } }); // ---- agent_end hook:区分中间态(可能要 retry)和真结束 ---- pi.on('agent_end', async (_event, _ctx) => { // Worker 侧:回复给 Main 并关闭 if (!currTask) return; // 找最后一条 assistant 消息,判断是否 error 型结束 const lastAssistant = [...(_event.messages as any[])].reverse() .find(m => m?.role === 'assistant') as any; const isError = lastAssistant?.stopReason === 'error'; // ---- 中间态(可能要 retry):不关,挂起延时关闭 ---- if (isError) { const task = { ...currTask } as typeof currTask; currTask = null; // 防重复触发 if (pendingClose) { clearTimeout(pendingClose.timer); // 防重叠 } pendingClose = { task, timer: setTimeout(async () => { pendingClose = null; // 10s 内没有 agent_start 到来 = retry 用尽或不可重试 // → 这是"真正失败":不回复(没有有效结果),直接关 pane if (!task) return; try { await closeFloatingPane(getMyPaneId(), task.secondName); } catch (e) { console.log('[pi-in-zellij] 延时关闭失败:', e); } }, 10_000), }; return; } // ---- 正常完成:立即回复 + 关闭(原逻辑,保持不变)---- // 立刻快照 + 清除,防止重复触发 const task = { ...currTask }; currTask = null; // 正常完成时清理可能残留的挂起关闭(防御性,正常情况下不会到这里) if (pendingClose) { clearTimeout(pendingClose.timer); pendingClose = null; } try { const replyText = getLatestAssistantText(_ctx); if (replyText) { // 检查发起方进程是否存活(而非仅检查 pane 是否存在) if (!isProcessAlive(task.firstPid)) { console.log('[pi-in-zellij] 发起方进程已退出,放弃回复'); // 不 return,继续走到 finally 关闭自己 } else { const replyMessage = buildMessage( task.firstPaneId, task.secondPaneId, task.firstName, task.secondName, false, task.commId, 'Report', replyText ); // 回复给 first(发起方) await writeToPane(task.firstPaneId, replyMessage); } } } catch (err) { console.log('[pi-in-zellij] 回复失败:', err); } finally { // 保存当前 pane 位置,然后关闭(无论是否成功回复) await closeFloatingPane(getMyPaneId(), task.secondName); } }); }