forked from EduCraft/curriculum-project-hub
fix: reply to Feishu trigger messages
This commit is contained in:
@@ -20,7 +20,7 @@
|
|||||||
* 6. fail(errorText) — flush + transition to error card
|
* 6. fail(errorText) — flush + transition to error card
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { FeishuRuntime } from "../client.js";
|
import type { FeishuRuntime, SendMessageOptions } from "../client.js";
|
||||||
import { sendCard, patchCard, sendText } from "../client.js";
|
import { sendCard, patchCard, sendText } from "../client.js";
|
||||||
import { DEFAULT_MAX_MESSAGE_LENGTH, splitAtBoundary } from "../textStream.js";
|
import { DEFAULT_MAX_MESSAGE_LENGTH, splitAtBoundary } from "../textStream.js";
|
||||||
import {
|
import {
|
||||||
@@ -41,6 +41,7 @@ export interface StreamingCardOptions {
|
|||||||
readonly runId: string;
|
readonly runId: string;
|
||||||
readonly rt: FeishuRuntime;
|
readonly rt: FeishuRuntime;
|
||||||
readonly chatId: string;
|
readonly chatId: string;
|
||||||
|
readonly sendOptions?: SendMessageOptions | undefined;
|
||||||
readonly patchIntervalMs: number | undefined;
|
readonly patchIntervalMs: number | undefined;
|
||||||
readonly maxMessageLength: number | undefined;
|
readonly maxMessageLength: number | undefined;
|
||||||
}
|
}
|
||||||
@@ -61,6 +62,7 @@ export class StreamingAgentCard {
|
|||||||
private readonly runId: string;
|
private readonly runId: string;
|
||||||
private readonly rt: FeishuRuntime;
|
private readonly rt: FeishuRuntime;
|
||||||
private readonly chatId: string;
|
private readonly chatId: string;
|
||||||
|
private readonly sendOptions: SendMessageOptions | undefined;
|
||||||
private readonly patchIntervalMs: number;
|
private readonly patchIntervalMs: number;
|
||||||
private readonly maxMessageLength: number;
|
private readonly maxMessageLength: number;
|
||||||
|
|
||||||
@@ -68,6 +70,7 @@ export class StreamingAgentCard {
|
|||||||
this.runId = options.runId;
|
this.runId = options.runId;
|
||||||
this.rt = options.rt;
|
this.rt = options.rt;
|
||||||
this.chatId = options.chatId;
|
this.chatId = options.chatId;
|
||||||
|
this.sendOptions = options.sendOptions;
|
||||||
this.patchIntervalMs = options.patchIntervalMs ?? DEFAULT_PATCH_INTERVAL_MS;
|
this.patchIntervalMs = options.patchIntervalMs ?? DEFAULT_PATCH_INTERVAL_MS;
|
||||||
this.maxMessageLength = options.maxMessageLength ?? DEFAULT_MAX_MESSAGE_LENGTH;
|
this.maxMessageLength = options.maxMessageLength ?? DEFAULT_MAX_MESSAGE_LENGTH;
|
||||||
startToolUseTraceRun(this.runId);
|
startToolUseTraceRun(this.runId);
|
||||||
@@ -119,7 +122,7 @@ export class StreamingAgentCard {
|
|||||||
updated = await this.flushCard("complete", fallbackText);
|
updated = await this.flushCard("complete", fallbackText);
|
||||||
}
|
}
|
||||||
if (!updated && this.interrupted) {
|
if (!updated && this.interrupted) {
|
||||||
await sendText(this.rt, this.chatId, "\u5DF2\u4E2D\u65AD\u5F53\u524D\u8FD0\u884C\u3002");
|
await sendText(this.rt, this.chatId, "\u5DF2\u4E2D\u65AD\u5F53\u524D\u8FD0\u884C\u3002", this.sendOptions);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
clearToolUseTraceRun(this.runId);
|
clearToolUseTraceRun(this.runId);
|
||||||
@@ -178,7 +181,7 @@ export class StreamingAgentCard {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (this.currentMessageId === null) {
|
if (this.currentMessageId === null) {
|
||||||
this.currentMessageId = await sendCard(this.rt, this.chatId, card);
|
this.currentMessageId = await sendCard(this.rt, this.chatId, card, this.sendOptions);
|
||||||
let updated = this.currentMessageId !== null;
|
let updated = this.currentMessageId !== null;
|
||||||
// Send overflow chunks as new messages (rare for agent output)
|
// Send overflow chunks as new messages (rare for agent output)
|
||||||
for (const chunk of chunks.slice(1)) {
|
for (const chunk of chunks.slice(1)) {
|
||||||
@@ -192,7 +195,7 @@ export class StreamingAgentCard {
|
|||||||
interrupted: this.interrupted,
|
interrupted: this.interrupted,
|
||||||
runId: undefined,
|
runId: undefined,
|
||||||
});
|
});
|
||||||
const overflowMessageId = await sendCard(this.rt, this.chatId, overflowCard);
|
const overflowMessageId = await sendCard(this.rt, this.chatId, overflowCard, this.sendOptions);
|
||||||
updated = updated && overflowMessageId !== null;
|
updated = updated && overflowMessageId !== null;
|
||||||
this.currentMessageId = overflowMessageId;
|
this.currentMessageId = overflowMessageId;
|
||||||
}
|
}
|
||||||
@@ -211,7 +214,7 @@ export class StreamingAgentCard {
|
|||||||
interrupted: this.interrupted,
|
interrupted: this.interrupted,
|
||||||
runId: undefined,
|
runId: undefined,
|
||||||
});
|
});
|
||||||
const overflowMessageId = await sendCard(this.rt, this.chatId, overflowCard);
|
const overflowMessageId = await sendCard(this.rt, this.chatId, overflowCard, this.sendOptions);
|
||||||
updated = updated && overflowMessageId !== null;
|
updated = updated && overflowMessageId !== null;
|
||||||
this.currentMessageId = overflowMessageId;
|
this.currentMessageId = overflowMessageId;
|
||||||
}
|
}
|
||||||
|
|||||||
+106
-56
@@ -28,6 +28,10 @@ export interface OutboundPayload {
|
|||||||
readonly content: string;
|
readonly content: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SendMessageOptions {
|
||||||
|
readonly replyToMessageId?: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export async function withRetry<T>(
|
export async function withRetry<T>(
|
||||||
fn: () => Promise<T>,
|
fn: () => Promise<T>,
|
||||||
options: {
|
options: {
|
||||||
@@ -193,29 +197,24 @@ export function parsePostMessage(content: string): { text: string; imageKeys: st
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Send a text message using the best Feishu message type for the content. */
|
/** Send a text message using the best Feishu message type for the content. */
|
||||||
export async function sendTextMessage(rt: FeishuRuntime, chatId: string, text: string): Promise<string | null> {
|
export async function sendTextMessage(
|
||||||
|
rt: FeishuRuntime,
|
||||||
|
chatId: string,
|
||||||
|
text: string,
|
||||||
|
options?: SendMessageOptions,
|
||||||
|
): Promise<string | null> {
|
||||||
const payload = buildOutboundPayload(text);
|
const payload = buildOutboundPayload(text);
|
||||||
const client = rt.client as unknown as {
|
|
||||||
im: { v1: { message: { create: (p: unknown) => Promise<MessageCreateResponse> } } };
|
|
||||||
};
|
|
||||||
try {
|
try {
|
||||||
const res = await client.im.v1.message.create({
|
return await sendMessagePayload(rt, chatId, payload, options);
|
||||||
params: { receive_id_type: "chat_id" },
|
|
||||||
data: { receive_id: chatId, msg_type: payload.msgType, content: payload.content },
|
|
||||||
});
|
|
||||||
return messageIdFromResponse(res);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (payload.msgType === "post" && isPostContentFormatError(e)) {
|
if (payload.msgType === "post" && isPostContentFormatError(e)) {
|
||||||
try {
|
try {
|
||||||
const res = await client.im.v1.message.create({
|
return await sendMessagePayload(
|
||||||
params: { receive_id_type: "chat_id" },
|
rt,
|
||||||
data: {
|
chatId,
|
||||||
receive_id: chatId,
|
{ msgType: "text", content: JSON.stringify({ text: _stripMarkdownToPlainText(text) }) },
|
||||||
msg_type: "text",
|
options,
|
||||||
content: JSON.stringify({ text: _stripMarkdownToPlainText(text) }),
|
);
|
||||||
},
|
|
||||||
});
|
|
||||||
return messageIdFromResponse(res);
|
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -225,10 +224,15 @@ export async function sendTextMessage(rt: FeishuRuntime, chatId: string, text: s
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Send long text as multiple Feishu messages. Returns the last successful message_id. */
|
/** Send long text as multiple Feishu messages. Returns the last successful message_id. */
|
||||||
export async function sendLongText(rt: FeishuRuntime, chatId: string, text: string): Promise<string | null> {
|
export async function sendLongText(
|
||||||
|
rt: FeishuRuntime,
|
||||||
|
chatId: string,
|
||||||
|
text: string,
|
||||||
|
options?: SendMessageOptions,
|
||||||
|
): Promise<string | null> {
|
||||||
let lastMessageId: string | null = null;
|
let lastMessageId: string | null = null;
|
||||||
for (const chunk of splitAtBoundary(text, DEFAULT_MAX_MESSAGE_LENGTH)) {
|
for (const chunk of splitAtBoundary(text, DEFAULT_MAX_MESSAGE_LENGTH)) {
|
||||||
const messageId = await sendTextMessage(rt, chatId, chunk);
|
const messageId = await sendTextMessage(rt, chatId, chunk, options);
|
||||||
if (messageId !== null) {
|
if (messageId !== null) {
|
||||||
lastMessageId = messageId;
|
lastMessageId = messageId;
|
||||||
}
|
}
|
||||||
@@ -237,23 +241,31 @@ export async function sendLongText(rt: FeishuRuntime, chatId: string, text: stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Send a plain text message (fire-and-forget). */
|
/** Send a plain text message (fire-and-forget). */
|
||||||
export async function sendText(rt: FeishuRuntime, chatId: string, text: string): Promise<void> {
|
export async function sendText(
|
||||||
await sendTextMessage(rt, chatId, text);
|
rt: FeishuRuntime,
|
||||||
|
chatId: string,
|
||||||
|
text: string,
|
||||||
|
options?: SendMessageOptions,
|
||||||
|
): Promise<void> {
|
||||||
|
await sendTextMessage(rt, chatId, text, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Send an interactive card message (always patchable). Used for streaming. */
|
/** Send an interactive card message (always patchable). Used for streaming. */
|
||||||
export async function sendInteractiveCardMessage(rt: FeishuRuntime, chatId: string, text: string): Promise<string | null> {
|
export async function sendInteractiveCardMessage(
|
||||||
|
rt: FeishuRuntime,
|
||||||
|
chatId: string,
|
||||||
|
text: string,
|
||||||
|
options?: SendMessageOptions,
|
||||||
|
): Promise<string | null> {
|
||||||
const payload = buildOutboundPayload(text);
|
const payload = buildOutboundPayload(text);
|
||||||
const card = buildInteractiveCardFromOutboundPayload(payload);
|
const card = buildInteractiveCardFromOutboundPayload(payload);
|
||||||
const client = rt.client as unknown as {
|
|
||||||
im: { v1: { message: { create: (p: unknown) => Promise<MessageCreateResponse> } } };
|
|
||||||
};
|
|
||||||
try {
|
try {
|
||||||
const res = await client.im.v1.message.create({
|
return await sendMessagePayload(
|
||||||
params: { receive_id_type: "chat_id" },
|
rt,
|
||||||
data: { receive_id: chatId, msg_type: "interactive", content: JSON.stringify(card) },
|
chatId,
|
||||||
});
|
{ msgType: "interactive", content: JSON.stringify(card) },
|
||||||
return messageIdFromResponse(res);
|
options,
|
||||||
|
);
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -278,16 +290,19 @@ export async function patchTextMessage(rt: FeishuRuntime, messageId: string, tex
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Send a raw interactive card (arbitrary JSON). Returns message_id on success. */
|
/** Send a raw interactive card (arbitrary JSON). Returns message_id on success. */
|
||||||
export async function sendCard(rt: FeishuRuntime, chatId: string, card: Record<string, unknown>): Promise<string | null> {
|
export async function sendCard(
|
||||||
const client = rt.client as unknown as {
|
rt: FeishuRuntime,
|
||||||
im: { v1: { message: { create: (p: unknown) => Promise<MessageCreateResponse> } } };
|
chatId: string,
|
||||||
};
|
card: Record<string, unknown>,
|
||||||
|
options?: SendMessageOptions,
|
||||||
|
): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
const res = await client.im.v1.message.create({
|
return await sendMessagePayload(
|
||||||
params: { receive_id_type: "chat_id" },
|
rt,
|
||||||
data: { receive_id: chatId, msg_type: "interactive", content: JSON.stringify(card) },
|
chatId,
|
||||||
});
|
{ msgType: "interactive", content: JSON.stringify(card) },
|
||||||
return messageIdFromResponse(res);
|
options,
|
||||||
|
);
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -318,6 +333,7 @@ export async function sendApprovalCard(
|
|||||||
title: string,
|
title: string,
|
||||||
body: string,
|
body: string,
|
||||||
buttons: ReadonlyArray<{ label: string; value: string; style?: "primary" | "default" | "danger" }>,
|
buttons: ReadonlyArray<{ label: string; value: string; style?: "primary" | "default" | "danger" }>,
|
||||||
|
options?: SendMessageOptions,
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
const card = {
|
const card = {
|
||||||
config: { wide_screen_mode: true },
|
config: { wide_screen_mode: true },
|
||||||
@@ -335,15 +351,13 @@ export async function sendApprovalCard(
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
const client = rt.client as unknown as {
|
|
||||||
im: { v1: { message: { create: (p: unknown) => Promise<MessageCreateResponse> } } };
|
|
||||||
};
|
|
||||||
try {
|
try {
|
||||||
const res = await client.im.v1.message.create({
|
return await sendMessagePayload(
|
||||||
params: { receive_id_type: "chat_id" },
|
rt,
|
||||||
data: { receive_id: chatId, msg_type: "interactive", content: JSON.stringify(card) },
|
chatId,
|
||||||
});
|
{ msgType: "interactive", content: JSON.stringify(card) },
|
||||||
return messageIdFromResponse(res);
|
options,
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
rt.logger.warn({ chatId, err: e instanceof Error ? e.message : String(e) }, "sendApprovalCard failed");
|
rt.logger.warn({ chatId, err: e instanceof Error ? e.message : String(e) }, "sendApprovalCard failed");
|
||||||
return null;
|
return null;
|
||||||
@@ -491,11 +505,16 @@ export async function downloadMessageFile(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Upload and send a file to a chat. */
|
/** Upload and send a file to a chat. */
|
||||||
export async function sendFile(rt: FeishuRuntime, chatId: string, filePath: string, fileName: string): Promise<string | null> {
|
export async function sendFile(
|
||||||
|
rt: FeishuRuntime,
|
||||||
|
chatId: string,
|
||||||
|
filePath: string,
|
||||||
|
fileName: string,
|
||||||
|
options?: SendMessageOptions,
|
||||||
|
): Promise<string | null> {
|
||||||
const client = rt.client as unknown as {
|
const client = rt.client as unknown as {
|
||||||
im: { v1: {
|
im: { v1: {
|
||||||
file: { create: (p: unknown) => Promise<FileCreateResponse> }
|
file: { create: (p: unknown) => Promise<FileCreateResponse> }
|
||||||
message: { create: (p: unknown) => Promise<MessageCreateResponse> }
|
|
||||||
} };
|
} };
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
@@ -510,13 +529,9 @@ export async function sendFile(rt: FeishuRuntime, chatId: string, filePath: stri
|
|||||||
rt.logger.warn({ chatId, filePath }, "sendFile failed: upload response missing file_key");
|
rt.logger.warn({ chatId, filePath }, "sendFile failed: upload response missing file_key");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const msgRes = await withRetry(async () =>
|
const messageId = await withRetry(async () =>
|
||||||
client.im.v1.message.create({
|
sendMessagePayload(rt, chatId, { msgType: "file", content: JSON.stringify({ file_key: fileKey }) }, options),
|
||||||
params: { receive_id_type: "chat_id" },
|
|
||||||
data: { receive_id: chatId, msg_type: "file", content: JSON.stringify({ file_key: fileKey }) },
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
const messageId = messageIdFromResponse(msgRes);
|
|
||||||
if (messageId === null) {
|
if (messageId === null) {
|
||||||
rt.logger.warn({ chatId, filePath }, "sendFile failed: message response missing message_id");
|
rt.logger.warn({ chatId, filePath }, "sendFile failed: message response missing message_id");
|
||||||
}
|
}
|
||||||
@@ -527,6 +542,41 @@ export async function sendFile(rt: FeishuRuntime, chatId: string, filePath: stri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function sendMessagePayload(
|
||||||
|
rt: FeishuRuntime,
|
||||||
|
chatId: string,
|
||||||
|
payload: OutboundPayload,
|
||||||
|
options?: SendMessageOptions,
|
||||||
|
): Promise<string | null> {
|
||||||
|
const client = rt.client as unknown as {
|
||||||
|
im: { v1: { message: {
|
||||||
|
create: (p: unknown) => Promise<MessageCreateResponse>;
|
||||||
|
reply: (p: unknown) => Promise<MessageCreateResponse>;
|
||||||
|
} } };
|
||||||
|
};
|
||||||
|
const replyToMessageId = normalizeReplyMessageId(options?.replyToMessageId);
|
||||||
|
if (replyToMessageId !== null) {
|
||||||
|
const res = await client.im.v1.message.reply({
|
||||||
|
path: { message_id: replyToMessageId },
|
||||||
|
data: {
|
||||||
|
msg_type: payload.msgType,
|
||||||
|
content: payload.content,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return messageIdFromResponse(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await client.im.v1.message.create({
|
||||||
|
params: { receive_id_type: "chat_id" },
|
||||||
|
data: { receive_id: chatId, msg_type: payload.msgType, content: payload.content },
|
||||||
|
});
|
||||||
|
return messageIdFromResponse(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeReplyMessageId(messageId: string | undefined): string | null {
|
||||||
|
return messageId === undefined || messageId === "" ? null : messageId;
|
||||||
|
}
|
||||||
|
|
||||||
type MessageCreateResponse = {
|
type MessageCreateResponse = {
|
||||||
readonly message_id?: string | undefined;
|
readonly message_id?: string | undefined;
|
||||||
readonly data?: { readonly message_id?: string | undefined } | undefined;
|
readonly data?: { readonly message_id?: string | undefined } | undefined;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createSdkMcpServer, tool, type McpSdkServerConfigWithInstance } from "@anthropic-ai/claude-agent-sdk";
|
import { createSdkMcpServer, tool, type McpSdkServerConfigWithInstance } from "@anthropic-ai/claude-agent-sdk";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { sendApprovalCard, sendFile, type FeishuRuntime } from "./client.js";
|
import { sendApprovalCard, sendFile, type FeishuRuntime, type SendMessageOptions } from "./client.js";
|
||||||
import { resolveDeliverableFile } from "./fileDelivery.js";
|
import { resolveDeliverableFile } from "./fileDelivery.js";
|
||||||
import { readFeishuContext } from "./read.js";
|
import { readFeishuContext } from "./read.js";
|
||||||
import type { ApprovalManager } from "./approval.js";
|
import type { ApprovalManager } from "./approval.js";
|
||||||
@@ -11,6 +11,7 @@ export interface FileDeliveryToolOptions {
|
|||||||
readonly projectId: string;
|
readonly projectId: string;
|
||||||
readonly runId: string;
|
readonly runId: string;
|
||||||
readonly workspaceDir: string;
|
readonly workspaceDir: string;
|
||||||
|
readonly sendOptions?: SendMessageOptions | undefined;
|
||||||
readonly approvalManager: ApprovalManager;
|
readonly approvalManager: ApprovalManager;
|
||||||
readonly onDelivered?: (path: string) => void;
|
readonly onDelivered?: (path: string) => void;
|
||||||
}
|
}
|
||||||
@@ -43,7 +44,7 @@ export function createFileDeliveryMcpServer(options: FileDeliveryToolOptions): M
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageId = await sendFile(options.rt, options.chatId, file.path, args.name ?? file.name);
|
const messageId = await sendFile(options.rt, options.chatId, file.path, args.name ?? file.name, options.sendOptions);
|
||||||
if (messageId === null) {
|
if (messageId === null) {
|
||||||
return {
|
return {
|
||||||
isError: true,
|
isError: true,
|
||||||
@@ -106,6 +107,7 @@ export function createFileDeliveryMcpServer(options: FileDeliveryToolOptions): M
|
|||||||
value: option.value,
|
value: option.value,
|
||||||
...(option.style === undefined ? {} : { style: option.style }),
|
...(option.style === undefined ? {} : { style: option.style }),
|
||||||
})),
|
})),
|
||||||
|
options.sendOptions,
|
||||||
);
|
);
|
||||||
if (messageId === null) {
|
if (messageId === null) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { PrismaClient } from "@prisma/client";
|
|||||||
import type { FastifyBaseLogger } from "fastify";
|
import type { FastifyBaseLogger } from "fastify";
|
||||||
import type { ModelRegistry, RoleEntry } from "../agent/models.js";
|
import type { ModelRegistry, RoleEntry } from "../agent/models.js";
|
||||||
import type { RuntimeSettings } from "../settings/runtime.js";
|
import type { RuntimeSettings } from "../settings/runtime.js";
|
||||||
import { sendText, type FeishuRuntime } from "./client.js";
|
import { sendText, type FeishuRuntime, type SendMessageOptions } from "./client.js";
|
||||||
import type { TriggerQueue } from "./triggerQueue.js";
|
import type { TriggerQueue } from "./triggerQueue.js";
|
||||||
|
|
||||||
export interface SlashInvocation {
|
export interface SlashInvocation {
|
||||||
@@ -15,6 +15,7 @@ export interface SlashCommandRunContext {
|
|||||||
readonly projectId: string;
|
readonly projectId: string;
|
||||||
readonly chatId: string;
|
readonly chatId: string;
|
||||||
readonly rt: FeishuRuntime;
|
readonly rt: FeishuRuntime;
|
||||||
|
readonly sendOptions?: SendMessageOptions | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SlashCommandDefinition {
|
export interface SlashCommandDefinition {
|
||||||
@@ -68,9 +69,9 @@ export function createSlashCommandRegistry(deps: SlashCommandRegistryDeps): Read
|
|||||||
"不创建 agent run,也不改变当前会话。",
|
"不创建 agent run,也不改变当前会话。",
|
||||||
"支持 /help new 和 /new help 两种写法。",
|
"支持 /help new 和 /new help 两种写法。",
|
||||||
],
|
],
|
||||||
run: async ({ invocation, projectId, chatId, rt }) => {
|
run: async ({ invocation, projectId, chatId, rt, sendOptions }) => {
|
||||||
const registry = await deps.settings.modelRegistry({ projectId });
|
const registry = await deps.settings.modelRegistry({ projectId });
|
||||||
await sendText(rt, chatId, formatHelpCommandInvocation(invocation, registry, commands));
|
await sendText(rt, chatId, formatHelpCommandInvocation(invocation, registry, commands), sendOptions);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -82,12 +83,12 @@ export function createSlashCommandRegistry(deps: SlashCommandRegistryDeps): Read
|
|||||||
"归档当前未归档的 agent session。",
|
"归档当前未归档的 agent session。",
|
||||||
"不会清空当前项目的等待队列。",
|
"不会清空当前项目的等待队列。",
|
||||||
],
|
],
|
||||||
run: async ({ projectId, chatId, rt }) => {
|
run: async ({ projectId, chatId, rt, sendOptions }) => {
|
||||||
await deps.prisma.agentSession.updateMany({
|
await deps.prisma.agentSession.updateMany({
|
||||||
where: { projectId, archivedAt: null },
|
where: { projectId, archivedAt: null },
|
||||||
data: { archivedAt: new Date() },
|
data: { archivedAt: new Date() },
|
||||||
});
|
});
|
||||||
await sendText(rt, chatId, "已开新会话,下次 @bot 将从头开始。");
|
await sendText(rt, chatId, "已开新会话,下次 @bot 将从头开始。", sendOptions);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -99,21 +100,21 @@ export function createSlashCommandRegistry(deps: SlashCommandRegistryDeps): Read
|
|||||||
"只恢复当前项目最近归档的 agent session。",
|
"只恢复当前项目最近归档的 agent session。",
|
||||||
"没有可恢复会话时只回复提示,不会创建 agent run。",
|
"没有可恢复会话时只回复提示,不会创建 agent run。",
|
||||||
],
|
],
|
||||||
run: async ({ projectId, chatId, rt }) => {
|
run: async ({ projectId, chatId, rt, sendOptions }) => {
|
||||||
const latest = await deps.prisma.agentSession.findFirst({
|
const latest = await deps.prisma.agentSession.findFirst({
|
||||||
where: { projectId, archivedAt: { not: null } },
|
where: { projectId, archivedAt: { not: null } },
|
||||||
orderBy: { archivedAt: "desc" },
|
orderBy: { archivedAt: "desc" },
|
||||||
select: { id: true },
|
select: { id: true },
|
||||||
});
|
});
|
||||||
if (latest === null) {
|
if (latest === null) {
|
||||||
await sendText(rt, chatId, "没有可恢复的会话。");
|
await sendText(rt, chatId, "没有可恢复的会话。", sendOptions);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await deps.prisma.agentSession.update({
|
await deps.prisma.agentSession.update({
|
||||||
where: { id: latest.id },
|
where: { id: latest.id },
|
||||||
data: { archivedAt: null },
|
data: { archivedAt: null },
|
||||||
});
|
});
|
||||||
await sendText(rt, chatId, "已恢复上一个会话。");
|
await sendText(rt, chatId, "已恢复上一个会话。", sendOptions);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -125,7 +126,7 @@ export function createSlashCommandRegistry(deps: SlashCommandRegistryDeps): Read
|
|||||||
"归档当前未归档的 agent session。",
|
"归档当前未归档的 agent session。",
|
||||||
"清空当前项目已经排队、尚未开始的触发请求。",
|
"清空当前项目已经排队、尚未开始的触发请求。",
|
||||||
],
|
],
|
||||||
run: async ({ projectId, chatId, rt }) => {
|
run: async ({ projectId, chatId, rt, sendOptions }) => {
|
||||||
await deps.prisma.agentSession.updateMany({
|
await deps.prisma.agentSession.updateMany({
|
||||||
where: { projectId, archivedAt: null },
|
where: { projectId, archivedAt: null },
|
||||||
data: { archivedAt: new Date() },
|
data: { archivedAt: new Date() },
|
||||||
@@ -134,7 +135,7 @@ export function createSlashCommandRegistry(deps: SlashCommandRegistryDeps): Read
|
|||||||
if (cleared > 0) {
|
if (cleared > 0) {
|
||||||
deps.logger.info({ projectId, cleared }, "feishu trigger: cleared queued triggers on reset");
|
deps.logger.info({ projectId, cleared }, "feishu trigger: cleared queued triggers on reset");
|
||||||
}
|
}
|
||||||
await sendText(rt, chatId, "已重置,下次 @bot 将从头开始。");
|
await sendText(rt, chatId, "已重置,下次 @bot 将从头开始。", sendOptions);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
type CardActionEvent,
|
type CardActionEvent,
|
||||||
type FeishuRuntime,
|
type FeishuRuntime,
|
||||||
type MessageReceiveEvent,
|
type MessageReceiveEvent,
|
||||||
|
type SendMessageOptions,
|
||||||
} from "./client.js";
|
} from "./client.js";
|
||||||
import { runAgent as defaultRunAgent, type ProjectContext, type RunRequest, type RunResult } from "../agent/runner.js";
|
import { runAgent as defaultRunAgent, type ProjectContext, type RunRequest, type RunResult } from "../agent/runner.js";
|
||||||
import type { RuntimeSettings } from "../settings/runtime.js";
|
import type { RuntimeSettings } from "../settings/runtime.js";
|
||||||
@@ -124,6 +125,7 @@ export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
|
|||||||
options: { readonly queueIfLocked?: boolean } = {},
|
options: { readonly queueIfLocked?: boolean } = {},
|
||||||
): Promise<StartAgentRunOutcome> {
|
): Promise<StartAgentRunOutcome> {
|
||||||
const { msg, rt, chatId, projectId, senderOpenId, actor } = context;
|
const { msg, rt, chatId, projectId, senderOpenId, actor } = context;
|
||||||
|
const sendOptions = sendOptionsForTriggerMessage(msg);
|
||||||
const queueIfLocked = options.queueIfLocked ?? true;
|
const queueIfLocked = options.queueIfLocked ?? true;
|
||||||
|
|
||||||
// ADR-0002: if locked by another run, enqueue the trigger for this project.
|
// ADR-0002: if locked by another run, enqueue the trigger for this project.
|
||||||
@@ -169,7 +171,7 @@ export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
|
|||||||
sender: await senderAuditMetadata(rt, senderOpenId),
|
sender: await senderAuditMetadata(rt, senderOpenId),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await sendText(rt, chatId, `无权限使用角色 ${roleId}。`);
|
await sendText(rt, chatId, `无权限使用角色 ${roleId}。`, sendOptions);
|
||||||
return "skipped";
|
return "skipped";
|
||||||
}
|
}
|
||||||
const role = models.role(roleId);
|
const role = models.role(roleId);
|
||||||
@@ -279,7 +281,14 @@ export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
|
|||||||
// Streaming agent card: single interactive card through the full run
|
// Streaming agent card: single interactive card through the full run
|
||||||
// lifecycle (thinking → tool calls → streaming text → complete).
|
// lifecycle (thinking → tool calls → streaming text → complete).
|
||||||
// Shows tool-use trace panel + reasoning panel + answer text.
|
// Shows tool-use trace panel + reasoning panel + answer text.
|
||||||
const card = new StreamingAgentCard({ runId: run.id, rt, chatId, patchIntervalMs: undefined, maxMessageLength: undefined });
|
const card = new StreamingAgentCard({
|
||||||
|
runId: run.id,
|
||||||
|
rt,
|
||||||
|
chatId,
|
||||||
|
sendOptions,
|
||||||
|
patchIntervalMs: undefined,
|
||||||
|
maxMessageLength: undefined,
|
||||||
|
});
|
||||||
const deliveredFiles: string[] = [];
|
const deliveredFiles: string[] = [];
|
||||||
const fileDeliveryMcpServer = createFileDeliveryMcpServer({
|
const fileDeliveryMcpServer = createFileDeliveryMcpServer({
|
||||||
rt,
|
rt,
|
||||||
@@ -287,6 +296,7 @@ export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
|
|||||||
projectId,
|
projectId,
|
||||||
runId: run.id,
|
runId: run.id,
|
||||||
workspaceDir: project.workspaceDir,
|
workspaceDir: project.workspaceDir,
|
||||||
|
sendOptions,
|
||||||
approvalManager,
|
approvalManager,
|
||||||
onDelivered: (path) => {
|
onDelivered: (path) => {
|
||||||
deliveredFiles.push(path);
|
deliveredFiles.push(path);
|
||||||
@@ -422,11 +432,16 @@ export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
|
|||||||
});
|
});
|
||||||
if (position === 0) {
|
if (position === 0) {
|
||||||
await reactToMessage(context.rt, context.msg.message_id, "OnIt");
|
await reactToMessage(context.rt, context.msg.message_id, "OnIt");
|
||||||
await sendText(context.rt, context.chatId, "队列已满,请稍后再试");
|
await sendText(context.rt, context.chatId, "队列已满,请稍后再试", sendOptionsForTriggerMessage(context.msg));
|
||||||
return "rejected";
|
return "rejected";
|
||||||
}
|
}
|
||||||
|
|
||||||
await sendText(context.rt, context.chatId, `已加入队列(第${position}位),当前处理完成后将自动开始`);
|
await sendText(
|
||||||
|
context.rt,
|
||||||
|
context.chatId,
|
||||||
|
`已加入队列(第${position}位),当前处理完成后将自动开始`,
|
||||||
|
sendOptionsForTriggerMessage(context.msg),
|
||||||
|
);
|
||||||
return "queued";
|
return "queued";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -614,7 +629,7 @@ export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
|
|||||||
if (senderOpenId === "") {
|
if (senderOpenId === "") {
|
||||||
deps.logger.warn({ projectId, chatId }, "feishu trigger: sender has no open_id, denying");
|
deps.logger.warn({ projectId, chatId }, "feishu trigger: sender has no open_id, denying");
|
||||||
await writeAudit(deps.prisma, { projectId, action: "trigger.denied", metadata: { reason: "missing open_id" } });
|
await writeAudit(deps.prisma, { projectId, action: "trigger.denied", metadata: { reason: "missing open_id" } });
|
||||||
await sendText(rt, chatId, "无法识别发送者,拒绝触发。");
|
await sendText(rt, chatId, "无法识别发送者,拒绝触发。", sendOptionsForTriggerMessage(msg));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const actor = { feishuOpenId: senderOpenId, chatId };
|
const actor = { feishuOpenId: senderOpenId, chatId };
|
||||||
@@ -634,7 +649,7 @@ export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
|
|||||||
sender: await senderAuditMetadata(rt, senderOpenId),
|
sender: await senderAuditMetadata(rt, senderOpenId),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await sendText(rt, chatId, "无权限触发。");
|
await sendText(rt, chatId, "无权限触发。", sendOptionsForTriggerMessage(msg));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -689,14 +704,14 @@ export function makeTriggerHandler(deps: TriggerDeps): TriggerHandler {
|
|||||||
if (helpSubcommandTarget !== null) {
|
if (helpSubcommandTarget !== null) {
|
||||||
// Help is generated on demand because role/tool configuration is runtime data.
|
// Help is generated on demand because role/tool configuration is runtime data.
|
||||||
const models = await deps.settings.modelRegistry({ projectId });
|
const models = await deps.settings.modelRegistry({ projectId });
|
||||||
await sendText(rt, chatId, formatSlashHelpTarget(helpSubcommandTarget, models, slashCommands));
|
await sendText(rt, chatId, formatSlashHelpTarget(helpSubcommandTarget, models, slashCommands), sendOptionsForTriggerMessage(msg));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (invocation !== null) {
|
if (invocation !== null) {
|
||||||
const slashCommand = slashCommands.get(invocation.name);
|
const slashCommand = slashCommands.get(invocation.name);
|
||||||
if (slashCommand !== undefined) {
|
if (slashCommand !== undefined) {
|
||||||
await slashCommand.run({ invocation, projectId, chatId, rt });
|
await slashCommand.run({ invocation, projectId, chatId, rt, sendOptions: sendOptionsForTriggerMessage(msg) });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -772,6 +787,10 @@ function nonEmpty(value: string | undefined): string | undefined {
|
|||||||
return value === undefined || value === "" ? undefined : value;
|
return value === undefined || value === "" ? undefined : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sendOptionsForTriggerMessage(msg: MessageReceiveEvent["message"]): SendMessageOptions {
|
||||||
|
return { replyToMessageId: msg.message_id };
|
||||||
|
}
|
||||||
|
|
||||||
function approvalActionFromValue(value: unknown): string | null {
|
function approvalActionFromValue(value: unknown): string | null {
|
||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -62,6 +62,12 @@ export interface MockFeishuRuntime extends FeishuRuntime {
|
|||||||
readonly sentTexts: string[];
|
readonly sentTexts: string[];
|
||||||
readonly sentCards: unknown[];
|
readonly sentCards: unknown[];
|
||||||
readonly sentPatches: unknown[];
|
readonly sentPatches: unknown[];
|
||||||
|
readonly sentReplies: Array<{
|
||||||
|
readonly messageId: string;
|
||||||
|
readonly msgType: string;
|
||||||
|
readonly content: string;
|
||||||
|
readonly replyInThread: unknown;
|
||||||
|
}>;
|
||||||
readonly reactions: Array<{ readonly messageId: string; readonly emoji: string }>;
|
readonly reactions: Array<{ readonly messageId: string; readonly emoji: string }>;
|
||||||
readonly readableMessages: Map<string, MockFeishuMessage>;
|
readonly readableMessages: Map<string, MockFeishuMessage>;
|
||||||
readonly threadMessages: Map<string, readonly MockFeishuMessage[]>;
|
readonly threadMessages: Map<string, readonly MockFeishuMessage[]>;
|
||||||
@@ -84,6 +90,7 @@ export function mockFeishuRuntime(): MockFeishuRuntime {
|
|||||||
const sentTexts: string[] = [];
|
const sentTexts: string[] = [];
|
||||||
const sentCards: unknown[] = [];
|
const sentCards: unknown[] = [];
|
||||||
const sentPatches: unknown[] = [];
|
const sentPatches: unknown[] = [];
|
||||||
|
const sentReplies: MockFeishuRuntime["sentReplies"] = [];
|
||||||
const reactions: Array<{ messageId: string; emoji: string }> = [];
|
const reactions: Array<{ messageId: string; emoji: string }> = [];
|
||||||
const readableMessages = new Map<string, MockFeishuMessage>();
|
const readableMessages = new Map<string, MockFeishuMessage>();
|
||||||
const threadMessages = new Map<string, readonly MockFeishuMessage[]>();
|
const threadMessages = new Map<string, readonly MockFeishuMessage[]>();
|
||||||
@@ -108,19 +115,21 @@ export function mockFeishuRuntime(): MockFeishuRuntime {
|
|||||||
message: {
|
message: {
|
||||||
create: async (p: unknown) => {
|
create: async (p: unknown) => {
|
||||||
const payload = p as { data?: { msg_type?: string; content?: string } };
|
const payload = p as { data?: { msg_type?: string; content?: string } };
|
||||||
if (payload.data?.msg_type === "interactive") {
|
recordSentMessage(payload.data, sentTexts, sentCards);
|
||||||
const card = payload.data.content ? JSON.parse(payload.data.content) : null;
|
return { data: { message_id: "mock-msg-id" } };
|
||||||
sentCards.push(card);
|
},
|
||||||
const text = textFromCard(card);
|
reply: async (p: unknown) => {
|
||||||
if (text !== null) sentTexts.push(text);
|
const payload = p as {
|
||||||
} else {
|
path?: { message_id?: string };
|
||||||
try {
|
data?: { msg_type?: string; content?: string; reply_in_thread?: unknown };
|
||||||
const c = JSON.parse(payload.data?.content ?? "{}") as { text?: string };
|
};
|
||||||
sentTexts.push(c.text ?? payload.data?.content ?? "");
|
sentReplies.push({
|
||||||
} catch {
|
messageId: payload.path?.message_id ?? "",
|
||||||
sentTexts.push(payload.data?.content ?? "");
|
msgType: payload.data?.msg_type ?? "",
|
||||||
}
|
content: payload.data?.content ?? "",
|
||||||
}
|
replyInThread: payload.data?.reply_in_thread,
|
||||||
|
});
|
||||||
|
recordSentMessage(payload.data, sentTexts, sentCards);
|
||||||
return { data: { message_id: "mock-msg-id" } };
|
return { data: { message_id: "mock-msg-id" } };
|
||||||
},
|
},
|
||||||
patch: async (p: unknown) => {
|
patch: async (p: unknown) => {
|
||||||
@@ -153,6 +162,7 @@ export function mockFeishuRuntime(): MockFeishuRuntime {
|
|||||||
sentTexts,
|
sentTexts,
|
||||||
sentCards,
|
sentCards,
|
||||||
sentPatches,
|
sentPatches,
|
||||||
|
sentReplies,
|
||||||
reactions,
|
reactions,
|
||||||
readableMessages,
|
readableMessages,
|
||||||
threadMessages,
|
threadMessages,
|
||||||
@@ -160,6 +170,27 @@ export function mockFeishuRuntime(): MockFeishuRuntime {
|
|||||||
return rt;
|
return rt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function recordSentMessage(
|
||||||
|
data: { msg_type?: string; content?: string } | undefined,
|
||||||
|
sentTexts: string[],
|
||||||
|
sentCards: unknown[],
|
||||||
|
): void {
|
||||||
|
if (data?.msg_type === "interactive") {
|
||||||
|
const card = data.content ? JSON.parse(data.content) : null;
|
||||||
|
sentCards.push(card);
|
||||||
|
const text = textFromCard(card);
|
||||||
|
if (text !== null) sentTexts.push(text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const c = JSON.parse(data?.content ?? "{}") as { text?: string };
|
||||||
|
sentTexts.push(c.text ?? data?.content ?? "");
|
||||||
|
} catch {
|
||||||
|
sentTexts.push(data?.content ?? "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function textFromCard(card: unknown): string | null {
|
function textFromCard(card: unknown): string | null {
|
||||||
if (typeof card !== "object" || card === null) return null;
|
if (typeof card !== "object" || card === null) return null;
|
||||||
const elements = (card as { elements?: unknown }).elements;
|
const elements = (card as { elements?: unknown }).elements;
|
||||||
|
|||||||
@@ -104,8 +104,9 @@ describe("trigger full lifecycle (integration)", () => {
|
|||||||
it("creates a run, acquires + releases the lock, sends status card", async () => {
|
it("creates a run, acquires + releases the lock, sends status card", async () => {
|
||||||
await seedProject("proj-1", "chat-1");
|
await seedProject("proj-1", "chat-1");
|
||||||
const trigger = makeTriggerHandler({ prisma, settings, logger: silentLogger, runAgent, messageBatcherOptions: { maxMessages: 1 } });
|
const trigger = makeTriggerHandler({ prisma, settings, logger: silentLogger, runAgent, messageBatcherOptions: { maxMessages: 1 } });
|
||||||
|
const event = makeEvent("chat-1", "@_user_1 写教案");
|
||||||
|
|
||||||
await trigger(makeEvent("chat-1", "@_user_1 写教案"), rt);
|
await trigger(event, rt);
|
||||||
|
|
||||||
// Wait for the async run to complete (fire-and-forget in trigger).
|
// Wait for the async run to complete (fire-and-forget in trigger).
|
||||||
await vi.waitFor(async () => {
|
await vi.waitFor(async () => {
|
||||||
@@ -122,6 +123,11 @@ describe("trigger full lifecycle (integration)", () => {
|
|||||||
|
|
||||||
// A status card was sent.
|
// A status card was sent.
|
||||||
expect(rt.sentCards.length).toBeGreaterThanOrEqual(1);
|
expect(rt.sentCards.length).toBeGreaterThanOrEqual(1);
|
||||||
|
expect(rt.sentReplies[0]).toMatchObject({
|
||||||
|
messageId: event.message.message_id,
|
||||||
|
msgType: "interactive",
|
||||||
|
replyInThread: undefined,
|
||||||
|
});
|
||||||
expect(rt.sentTexts).toContain("mock response");
|
expect(rt.sentTexts).toContain("mock response");
|
||||||
expect(runAgentCalls).toHaveLength(1);
|
expect(runAgentCalls).toHaveLength(1);
|
||||||
expect(runAgentCalls[0]?.providerEnv).toMatchObject({
|
expect(runAgentCalls[0]?.providerEnv).toMatchObject({
|
||||||
|
|||||||
@@ -34,6 +34,23 @@ describe("Feishu client helpers", () => {
|
|||||||
await expect(sendTextMessage(rt, "chat-1", "hello")).resolves.toBe("message-1");
|
await expect(sendTextMessage(rt, "chat-1", "hello")).resolves.toBe("message-1");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("replies to a message without enabling Feishu topic mode", async () => {
|
||||||
|
const messageCreate = vi.fn();
|
||||||
|
const messageReply = vi.fn(async () => ({ data: { message_id: "reply-1" } }));
|
||||||
|
const rt = mockRuntime({ fileCreate: vi.fn(), messageCreate, messageReply });
|
||||||
|
|
||||||
|
await expect(sendTextMessage(rt, "chat-1", "hello", { replyToMessageId: "m-trigger" })).resolves.toBe("reply-1");
|
||||||
|
|
||||||
|
expect(messageCreate).not.toHaveBeenCalled();
|
||||||
|
expect(messageReply).toHaveBeenCalledWith({
|
||||||
|
path: { message_id: "m-trigger" },
|
||||||
|
data: {
|
||||||
|
msg_type: "text",
|
||||||
|
content: JSON.stringify({ text: "hello" }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("sends long text in chunks and returns the last successful message id", async () => {
|
it("sends long text in chunks and returns the last successful message id", async () => {
|
||||||
let messageNumber = 0;
|
let messageNumber = 0;
|
||||||
const messageCreate = vi.fn(async () => ({ data: { message_id: `message-${++messageNumber}` } }));
|
const messageCreate = vi.fn(async () => ({ data: { message_id: `message-${++messageNumber}` } }));
|
||||||
@@ -70,6 +87,7 @@ describe("Feishu client helpers", () => {
|
|||||||
function mockRuntime(options: {
|
function mockRuntime(options: {
|
||||||
readonly fileCreate: (payload: unknown) => Promise<unknown>;
|
readonly fileCreate: (payload: unknown) => Promise<unknown>;
|
||||||
readonly messageCreate: (payload: unknown) => Promise<unknown>;
|
readonly messageCreate: (payload: unknown) => Promise<unknown>;
|
||||||
|
readonly messageReply?: (payload: unknown) => Promise<unknown>;
|
||||||
readonly contactBasicBatch?: (payload: unknown) => Promise<unknown>;
|
readonly contactBasicBatch?: (payload: unknown) => Promise<unknown>;
|
||||||
readonly request?: (payload: unknown) => Promise<unknown>;
|
readonly request?: (payload: unknown) => Promise<unknown>;
|
||||||
}): FeishuRuntime {
|
}): FeishuRuntime {
|
||||||
@@ -86,7 +104,10 @@ function mockRuntime(options: {
|
|||||||
im: {
|
im: {
|
||||||
v1: {
|
v1: {
|
||||||
file: { create: options.fileCreate },
|
file: { create: options.fileCreate },
|
||||||
message: { create: options.messageCreate },
|
message: {
|
||||||
|
create: options.messageCreate,
|
||||||
|
reply: options.messageReply ?? vi.fn(async () => ({ data: { message_id: "reply-1" } })),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as unknown as FeishuRuntime["client"],
|
} as unknown as FeishuRuntime["client"],
|
||||||
|
|||||||
Reference in New Issue
Block a user