fix(hub): 启动重置状态 + lazy-init 修复空白消息 + busy 改表情

三个修复:
1. 启动时清锁+标死run为FAILED(防重启后卡死)
2. 空白消息修复:lazy-init 改为第一个 text-delta 立即创建消息
  (不走 throttle);.then 加 fallback:没收到 text-delta 时用
  result.text 发完整回复
3. busy 提示从文本'项目正在处理中'改为表情回复

tsc rc=0。
This commit is contained in:
2026-07-08 02:20:49 +08:00
parent eaa7fd4fd8
commit 20bf7e271f
2 changed files with 28 additions and 7 deletions
+18 -7
View File
@@ -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 },
+10
View File
@@ -42,6 +42,16 @@ async function main(): Promise<void> {
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