fix(zcode): 真模型跑通后发现的三处缺陷(register / 工具活动日志 / SSE 关停)
真模型端到端(场景 A 通过:6893 事件、175 秒、530 字回信)把三处只有真跑才
暴露的问题照了出来:
1. **register 调不通**:驱动按 pi 的客户端 API 写了 `client.register()`,
而本插件的 GatewayClient 没有这个方法 —— 靠此前手工注册过才没暴露。
补上后才发现第二个坑:`/agent/register` 的认证与其它接口**不同**,
它只认 `Authorization: Bearer` 或 **body 里的 `secret`**,不认 `X-Agent-Secret`
头(其它接口认)。实测报错:
HTTP 400 需要 Authorization: Bearer <密钥> 或 body 里的 secret
所以没密钥时把 secret 放进 body。
2. **一轮 6893 条事件,日志里什么也看不见**:邮件驱动的会话没有界面,
「模型正在干什么」只能来自日志,否则一个五分钟的回合与一个卡死的回合
在外部完全一样。新增 `describeRunEvent`,只记工具调用与权限事件
(全记等于没有日志),并由 runTurn 通过 onEvent 逐个交出来。
3. **关停没真断 SSE**:驱动调的是 `client.stopSSE?.()`,而客户端没有这个方法
(`?.` 让它静默变成空操作)。改成持有 createSSEClient 的句柄并在关停时 stop。
验证:单元 325/325、授权桥 e2e 5/5、驱动 e2e(桩)7/7、快照握手 12 项。
This commit is contained in:
@ -107,6 +107,24 @@ export function describeError(e) {
|
||||
return e?.message || String(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* 把 ZCode 的事件流里「值得进日志」的那几条提出来。
|
||||
*
|
||||
* 一轮实测能吐 6893 条事件,全记等于没有日志。只记两类:
|
||||
* **工具调用**(模型在干什么、有没有触发授权)与**错误**。
|
||||
* 邮件驱动的会话没有界面,这两类是唯一能回答
|
||||
* 「它是不是卡住了 / 为什么一直没有授权询问」的信息。
|
||||
*/
|
||||
export function describeRunEvent(event) {
|
||||
const t = String(event?.type || '');
|
||||
if (t === 'tool.call.started') return `工具 ${event.toolName || '?'}`;
|
||||
if (t.startsWith('tool.permission')) {
|
||||
return `权限 ${event.decision || event.behavior || '?'}(${event.toolName || event.ruleId || '?'})`;
|
||||
}
|
||||
if (/error|failed|denied|aborted/i.test(t)) return `事件 ${t}`;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 造一个驱动实例。
|
||||
*
|
||||
@ -188,7 +206,7 @@ export function createDriver({ client, runTurnFn = runTurn, logFn = log, env = p
|
||||
logFn(`注意:--mode ${mode} 不会产生权限询问(本档如此设计)`);
|
||||
}
|
||||
|
||||
const prompt = buildMailPrompt({ agentName: CONFIG.agentName, data });
|
||||
const prompt = buildMailPrompt({ agentName: CFG.agentName, data });
|
||||
|
||||
const outcome = await runTurnFn(
|
||||
{
|
||||
@ -207,11 +225,18 @@ export function createDriver({ client, runTurnFn = runTurn, logFn = log, env = p
|
||||
AGENTMAIL_PERMISSION_MODE: tier,
|
||||
AGENTMAIL_MAIL_SUBJECT: data?.subject || '',
|
||||
AGENTMAIL_REPLY_TO: data?.mail_id || ''
|
||||
},
|
||||
onEvent: event => {
|
||||
const line = describeRunEvent(event);
|
||||
if (line) logFn(line);
|
||||
}
|
||||
},
|
||||
{ log: logFn, onChild: kill => {
|
||||
{
|
||||
log: logFn,
|
||||
onChild: kill => {
|
||||
currentKill = kill;
|
||||
} }
|
||||
}
|
||||
}
|
||||
);
|
||||
currentKill = null;
|
||||
|
||||
@ -375,12 +400,21 @@ async function main() {
|
||||
log,
|
||||
onEvent: (type, data) => driver.handleEvent(type, data)
|
||||
});
|
||||
// 留住句柄:关停时要真的断开,否则重连定时器还在跑(进程虽然马上就退,
|
||||
// 但那是侥幸而不是设计)。
|
||||
const sse = createSSEClient({
|
||||
authHeaders: () => client.authHeaders(),
|
||||
baseURL: client.baseURL,
|
||||
path: '/api/v1/events/stream',
|
||||
log,
|
||||
onEvent: (type, data) => driver.handleEvent(type, data)
|
||||
});
|
||||
|
||||
const shutdown = reason => {
|
||||
log(`收到 ${reason},关停中…(已处理 ${driver.stats.turns} 轮,回信 ${driver.stats.relays} 封)`);
|
||||
if (timer) clearInterval(timer);
|
||||
driver.abort();
|
||||
client.stopSSE?.();
|
||||
sse.stop();
|
||||
// 给杀进程留一点时间再退:自己先死会把 ZCode 变成孤儿。
|
||||
setTimeout(() => process.exit(0), 1200);
|
||||
};
|
||||
|
||||
@ -103,7 +103,8 @@ export function parseStreamLine(line) {
|
||||
* @param {{prompt:string, cwd:string, mode:string, maxTurns?:number,
|
||||
* resumeSessionId?:string, turnTimeoutMs?:number,
|
||||
* env?:Record<string,string>, cliPath?:string, nodePath?:string,
|
||||
* onChild?:(kill:(signal?:string)=>void)=>void}} opts
|
||||
* onChild?:(kill:(signal?:string)=>void)=>void,
|
||||
* onEvent?:(event:any)=>void}} opts
|
||||
* @param {{spawn?:Function, log?:Function}} [deps] spawn 可注入以便测试
|
||||
* @returns {Promise<{sessionId:string, response:string, events:any[], exitCode:number,
|
||||
* unparsable:number, timedOut:boolean, killed:boolean,
|
||||
@ -239,6 +240,16 @@ export function runTurn(opts, deps = {}) {
|
||||
result = parsed;
|
||||
} else {
|
||||
events.push(parsed.event);
|
||||
// 逐个事件交给调用方:邮件驱动的会话没有界面,
|
||||
// 「模型正在干什么」只能靠日志,否则一个五分钟的回合在外部看起来
|
||||
// 与一个卡死的回合完全一样。
|
||||
if (typeof opts.onEvent === 'function') {
|
||||
try {
|
||||
opts.onEvent(parsed.event);
|
||||
} catch {
|
||||
/* 调用方的日志出错不该影响这一轮 */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user