feat: improve Feishu markdown rendering and processing status feedback

- Add buildOutboundPayload: detect markdown format, isolate code blocks,
  force plain text for tables, fallback from post to text on API rejection
- Add addReaction/removeReaction for processing status lifecycle
- Replace THUMBSUP with Typing→(remove on success | CrossMark on failure)
- Add unit tests for markdown rendering (15) and reactions (6)
This commit is contained in:
2026-07-08 16:31:07 +08:00
parent a00e4f9871
commit 6227e1931b
5 changed files with 693 additions and 9 deletions
+11 -3
View File
@@ -11,7 +11,7 @@
import type { Prisma, PrismaClient } from "@prisma/client";
import { z } from "zod";
import type { FastifyBaseLogger } from "fastify";
import { sendText, sendTextMessage, patchTextMessage, reactToMessage, downloadMessageFile, type FeishuRuntime, type MessageReceiveEvent } from "./client.js";
import { sendText, sendTextMessage, patchTextMessage, reactToMessage, addReaction, removeReaction, downloadMessageFile, type FeishuRuntime, type MessageReceiveEvent } from "./client.js";
import { runAgent as defaultRunAgent, type ProjectContext, type RunRequest, type RunResult } from "../agent/runner.js";
import type { RuntimeSettings } from "../settings/runtime.js";
import { acquireLock, currentLockRunId, releaseLock } from "../lock.js";
@@ -309,8 +309,11 @@ export function makeTriggerHandler(deps: TriggerDeps) {
workspaceDir: project.workspaceDir,
};
// Emoji reaction on the user's message (acknowledge receipt).
await reactToMessage(rt, msg.message_id, "THUMBSUP");
const processingReactionId = await addReaction(rt, msg.message_id, "Typing");
const removeProcessingReaction = async (): Promise<boolean> => {
if (processingReactionId === null) return false;
return removeReaction(rt, msg.message_id, processingReactionId);
};
// Stream agent response as text-as-card messages. Lazy-init: don't send
// anything until the first text-delta arrives (avoids empty placeholder).
@@ -381,8 +384,13 @@ export function makeTriggerHandler(deps: TriggerDeps) {
action: "run.finished",
metadata: { status: result.status, deliveredFiles },
});
await removeProcessingReaction();
})
.catch(async (e) => {
const removedProcessingReaction = await removeProcessingReaction();
if (removedProcessingReaction) {
await addReaction(rt, msg.message_id, "CrossMark");
}
try {
await deps.prisma.agentRun.update({
where: { id: run.id },