From 20bf7e271f59bf922728785081e64a54ad4fbe28 Mon Sep 17 00:00:00 2001 From: Hong Jiarong Date: Wed, 8 Jul 2026 02:20:49 +0800 Subject: [PATCH] =?UTF-8?q?fix(hub):=20=E5=90=AF=E5=8A=A8=E9=87=8D?= =?UTF-8?q?=E7=BD=AE=E7=8A=B6=E6=80=81=20+=20lazy-init=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E7=A9=BA=E7=99=BD=E6=B6=88=E6=81=AF=20+=20busy=20?= =?UTF-8?q?=E6=94=B9=E8=A1=A8=E6=83=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 三个修复: 1. 启动时清锁+标死run为FAILED(防重启后卡死) 2. 空白消息修复:lazy-init 改为第一个 text-delta 立即创建消息 (不走 throttle);.then 加 fallback:没收到 text-delta 时用 result.text 发完整回复 3. busy 提示从文本'项目正在处理中'改为⏳表情回复 tsc rc=0。 --- hub/src/feishu/trigger.ts | 25 ++++++++++++++++++------- hub/src/server.ts | 10 ++++++++++ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/hub/src/feishu/trigger.ts b/hub/src/feishu/trigger.ts index 28ff748..0793b00 100644 --- a/hub/src/feishu/trigger.ts +++ b/hub/src/feishu/trigger.ts @@ -156,7 +156,7 @@ export function makeTriggerHandler(deps: TriggerDeps) { // ADR-0002: if locked by another run, reply busy. const existing = await currentLockRunId(deps.prisma, projectId); if (existing !== null) { - await sendText(rt, chatId, "项目正在处理中,请稍候。"); + await reactToMessage(rt, msg.message_id, "Hourglass"); return; } @@ -243,7 +243,7 @@ export function makeTriggerHandler(deps: TriggerDeps) { data: { status: "FAILED", error: "lock race", finishedAt: new Date() }, }); await writeAudit(deps.prisma, { runId: run.id, projectId, action: "run.lock_race", metadata: {} }); - await sendText(rt, chatId, "项目正在处理中,请稍候。"); + await reactToMessage(rt, msg.message_id, "Hourglass"); return; } @@ -257,17 +257,23 @@ export function makeTriggerHandler(deps: TriggerDeps) { // Emoji reaction on the user's message (acknowledge receipt). await reactToMessage(rt, msg.message_id, "THUMBSUP"); - // Stream agent response as text-as-card messages — patchable, markdown, - // no truncation. Lower throttle (400ms) for smoother streaming. - let currentMsgId = await sendTextMessage(rt, chatId, ""); + // Stream agent response as text-as-card messages. Lazy-init: don't send + // anything until the first text-delta arrives (avoids empty placeholder). + let currentMsgId: string | null = null; let accText = ""; let lastPatch = 0; const PATCH_INTERVAL = 400; const MAX_MSG_LEN = 4000; const flushText = async () => { + if (currentMsgId === null) { + // First chunk — create the message immediately (no throttle on init). + currentMsgId = await sendTextMessage(rt, chatId, accText); + lastPatch = Date.now(); + return; + } const now = Date.now(); - if (now - lastPatch < PATCH_INTERVAL || currentMsgId === null) return; + if (now - lastPatch < PATCH_INTERVAL) return; lastPatch = now; if (accText.length > MAX_MSG_LEN) { await patchTextMessage(rt, currentMsgId, accText.slice(0, MAX_MSG_LEN)); @@ -294,9 +300,14 @@ export function makeTriggerHandler(deps: TriggerDeps) { }, }) .then(async (result) => { - // Final flush: make sure the last message has complete text. + // Final flush: patch the last message with complete text, or send + // the full response if no streaming message was created yet. if (currentMsgId !== null && accText.length > 0) { await patchTextMessage(rt, currentMsgId, accText); + } else if (currentMsgId === null && result.text.length > 0) { + // No text-delta was received (e.g. model returned all at once). + // Send the full text now. + await sendTextMessage(rt, chatId, result.text); } await deps.prisma.agentRun.update({ where: { id: run.id }, diff --git a/hub/src/server.ts b/hub/src/server.ts index 798f675..d78c60c 100644 --- a/hub/src/server.ts +++ b/hub/src/server.ts @@ -42,6 +42,16 @@ async function main(): Promise { const app = Fastify({ logger: true }); + // Startup reset: clear stale locks + mark dead runs as FAILED. + await prisma.projectAgentLock.deleteMany({}); + await prisma.agentRun.updateMany({ + where: { status: "ACTIVE" }, + data: { status: "FAILED", error: "process restart", finishedAt: new Date() }, + }); + app.log.info("startup: cleared stale locks + dead runs"); + + + app.get("/api/healthz", async () => ({ ok: true, ts: Date.now() })); // Model registry (role-as-data, ADR-0017). Claude Code SDK uses model