forked from bai/curriculum-project-hub
fix(hub): 保留飞书回复上下文
This commit is contained in:
+105
-6
@@ -19,6 +19,7 @@ import { createPermissionAuthorizer, type AuthorizationDecision, type Permission
|
||||
import { writeAudit } from "../audit.js";
|
||||
import { PatchableTextStream } from "./textStream.js";
|
||||
import { createFileDeliveryMcpServer } from "./fileDeliveryTool.js";
|
||||
import { readFeishuContext } from "./read.js";
|
||||
|
||||
interface TriggerDeps {
|
||||
readonly prisma: PrismaClient;
|
||||
@@ -45,7 +46,7 @@ export function makeTriggerHandler(deps: TriggerDeps) {
|
||||
// would spawn a second AgentRun — the lock would refuse it, but a FAILED
|
||||
// run row + a busy reply would leak. Record the event_id up front; if it
|
||||
// already exists, this is a redelivery → stop.
|
||||
const eventId = event.header?.event_id;
|
||||
const eventId = feishuEventId(event);
|
||||
if (eventId !== undefined && eventId !== "") {
|
||||
const existing = await deps.prisma.feishuEventReceipt.findUnique({
|
||||
where: { eventId },
|
||||
@@ -58,7 +59,7 @@ export function makeTriggerHandler(deps: TriggerDeps) {
|
||||
await deps.prisma.feishuEventReceipt.create({
|
||||
data: {
|
||||
eventId,
|
||||
eventType: event.header?.event_type ?? "im.message.receive_v1",
|
||||
eventType: feishuEventType(event),
|
||||
messageId: msg.message_id,
|
||||
},
|
||||
});
|
||||
@@ -227,6 +228,14 @@ export function makeTriggerHandler(deps: TriggerDeps) {
|
||||
// is enforced inside runAgent when it builds the SDK tools record. `tools:
|
||||
// undefined` means the full registered set (unrestricted role).
|
||||
const systemPrompt = withFileDeliveryInstructions(role?.systemPrompt);
|
||||
const feishuTriggerContext = await buildFeishuTriggerContext({
|
||||
msg,
|
||||
chatId,
|
||||
projectId,
|
||||
workspaceDir: project.workspaceDir,
|
||||
rt,
|
||||
});
|
||||
const promptForAgent = withFeishuTriggerContext(agentPrompt, feishuTriggerContext);
|
||||
|
||||
// ADR-0017: provider+model-bound session. Reuse if one exists for this
|
||||
// (project, provider, model) triple; otherwise create.
|
||||
@@ -260,14 +269,24 @@ export function makeTriggerHandler(deps: TriggerDeps) {
|
||||
requestedByUserId: roleDecision.actorUserId ?? null,
|
||||
entrypoint: "FEISHU",
|
||||
status: "ACTIVE",
|
||||
prompt: agentPrompt,
|
||||
prompt: promptForAgent,
|
||||
model,
|
||||
provider: providerId,
|
||||
metadata: { roleId },
|
||||
metadata: agentRunMetadata(roleId, agentPrompt, feishuTriggerContext),
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
await writeAudit(deps.prisma, { runId: run.id, projectId, action: "run.created", metadata: { roleId, model, prompt: agentPrompt.slice(0, 200) } });
|
||||
await writeAudit(deps.prisma, {
|
||||
runId: run.id,
|
||||
projectId,
|
||||
action: "run.created",
|
||||
metadata: {
|
||||
roleId,
|
||||
model,
|
||||
prompt: agentPrompt.slice(0, 200),
|
||||
...(feishuTriggerContext === null ? {} : { feishuTriggerContext }),
|
||||
},
|
||||
});
|
||||
|
||||
// ADR-0002: acquire the project lock for this run.
|
||||
try {
|
||||
@@ -303,6 +322,8 @@ export function makeTriggerHandler(deps: TriggerDeps) {
|
||||
const fileDeliveryMcpServer = createFileDeliveryMcpServer({
|
||||
rt,
|
||||
chatId,
|
||||
projectId,
|
||||
runId: run.id,
|
||||
workspaceDir: project.workspaceDir,
|
||||
onDelivered: (path) => {
|
||||
deliveredFiles.push(path);
|
||||
@@ -310,7 +331,7 @@ export function makeTriggerHandler(deps: TriggerDeps) {
|
||||
});
|
||||
|
||||
runAgent({
|
||||
prompt: agentPrompt,
|
||||
prompt: promptForAgent,
|
||||
model,
|
||||
project: projectCtx,
|
||||
systemPrompt,
|
||||
@@ -417,6 +438,84 @@ function mergeSessionMetadata(metadata: unknown, patch: SessionMetadata): Prisma
|
||||
return base;
|
||||
}
|
||||
|
||||
function feishuEventId(event: MessageReceiveEvent): string | undefined {
|
||||
return nonEmpty(event.event_id) ?? nonEmpty(event.header?.event_id);
|
||||
}
|
||||
|
||||
function feishuEventType(event: MessageReceiveEvent): string {
|
||||
return nonEmpty(event.event_type) ?? nonEmpty(event.header?.event_type) ?? "im.message.receive_v1";
|
||||
}
|
||||
|
||||
function nonEmpty(value: string | undefined): string | undefined {
|
||||
return value === undefined || value === "" ? undefined : value;
|
||||
}
|
||||
|
||||
interface BuildFeishuTriggerContextArgs {
|
||||
readonly msg: MessageReceiveEvent["message"];
|
||||
readonly chatId: string;
|
||||
readonly projectId: string;
|
||||
readonly workspaceDir: string;
|
||||
readonly rt: FeishuRuntime;
|
||||
}
|
||||
|
||||
async function buildFeishuTriggerContext(args: BuildFeishuTriggerContextArgs): Promise<Prisma.InputJsonObject | null> {
|
||||
const replyToMessageId = nonEmpty(args.msg.parent_id);
|
||||
const rootId = nonEmpty(args.msg.root_id);
|
||||
const threadId = nonEmpty(args.msg.thread_id);
|
||||
if (replyToMessageId === undefined && rootId === undefined && threadId === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const context: Record<string, Prisma.InputJsonValue> = {
|
||||
trigger_message_id: args.msg.message_id,
|
||||
};
|
||||
if (replyToMessageId !== undefined) context["reply_to_message_id"] = replyToMessageId;
|
||||
if (rootId !== undefined) context["root_id"] = rootId;
|
||||
if (threadId !== undefined) context["thread_id"] = threadId;
|
||||
|
||||
if (replyToMessageId !== undefined) {
|
||||
const rawReplyContext = await readFeishuContext(
|
||||
{ chat_id: args.chatId, anchor: "reply", id: replyToMessageId },
|
||||
{
|
||||
runId: "pending",
|
||||
projectId: args.projectId,
|
||||
boundChatId: args.chatId,
|
||||
workspaceDir: args.workspaceDir,
|
||||
},
|
||||
args.rt,
|
||||
);
|
||||
context["replied_to_message"] = parseJsonForMetadata(rawReplyContext);
|
||||
}
|
||||
|
||||
return context as Prisma.InputJsonObject;
|
||||
}
|
||||
|
||||
function parseJsonForMetadata(text: string): Prisma.InputJsonValue {
|
||||
try {
|
||||
return JSON.parse(text) as Prisma.InputJsonValue;
|
||||
} catch {
|
||||
return { error: "invalid json", raw: text };
|
||||
}
|
||||
}
|
||||
|
||||
function withFeishuTriggerContext(prompt: string, context: Prisma.InputJsonObject | null): string {
|
||||
if (context === null) return prompt;
|
||||
return `Feishu trigger context:\n${JSON.stringify(context, null, 2)}\n\nUser request:\n${prompt}`;
|
||||
}
|
||||
|
||||
function agentRunMetadata(
|
||||
roleId: string,
|
||||
rawPrompt: string,
|
||||
feishuTriggerContext: Prisma.InputJsonObject | null,
|
||||
): Prisma.InputJsonObject {
|
||||
const metadata: Record<string, Prisma.InputJsonValue> = { roleId };
|
||||
if (feishuTriggerContext !== null) {
|
||||
metadata["rawPrompt"] = rawPrompt;
|
||||
metadata["feishuTriggerContext"] = feishuTriggerContext;
|
||||
}
|
||||
return metadata as Prisma.InputJsonObject;
|
||||
}
|
||||
|
||||
function auditDecision(decision: AuthorizationDecision): Record<string, unknown> {
|
||||
return {
|
||||
allowed: decision.allowed,
|
||||
|
||||
Reference in New Issue
Block a user