feat: add deployable alpha silo

This commit is contained in:
2026-07-11 00:25:45 +08:00
parent 44557da499
commit 9e954790dc
57 changed files with 2792 additions and 400 deletions
+40 -6
View File
@@ -25,6 +25,7 @@ export interface FeishuConfig {
export interface FeishuRuntime {
readonly client: lark.Client;
readonly logger: FastifyBaseLogger;
readonly isListenerReady?: () => boolean;
}
export interface OutboundPayload {
@@ -494,6 +495,7 @@ export async function downloadMessageFile(
workspaceDir: string,
workspaceRelativePath: string,
resourceType: "image" | "file",
maxBytes?: number,
): Promise<string> {
try {
return await withRetry(async () => {
@@ -509,6 +511,7 @@ export async function downloadMessageFile(
workspaceDir,
workspaceRelativePath,
source,
maxBytes,
);
} catch (error) {
source.destroy();
@@ -911,20 +914,41 @@ export function createLarkClient(config: FeishuConfig): lark.Client {
});
}
export function startFeishuListenerWithClient(
export async function startFeishuListenerWithClient(
config: FeishuConfig,
client: lark.Client,
logger: FastifyBaseLogger,
onMessage: (event: MessageReceiveEvent, rt: FeishuRuntime) => Promise<void>,
onCardAction?: (event: CardActionEvent, rt: FeishuRuntime) => Promise<void>,
): FeishuRuntime {
onTerminalError?: (error: Error) => void,
): Promise<FeishuRuntime> {
let state: "STARTING" | "READY" | "FAILED" = "STARTING";
let resolveReady!: () => void;
let rejectReady!: (error: Error) => void;
const ready = new Promise<void>((resolve, reject) => {
resolveReady = resolve;
rejectReady = reject;
});
const wsClient = new lark.WSClient({
appId: config.appId,
appSecret: config.appSecret,
domain: lark.Domain.Feishu,
loggerLevel: lark.LoggerLevel.info,
handshakeTimeoutMs: 10_000,
onReady: () => {
state = "READY";
resolveReady();
logger.info("Feishu listener ready");
},
onError: (error) => {
const wasStarting = state === "STARTING";
state = "FAILED";
logger.error({ err: error }, "Feishu listener terminal failure");
if (wasStarting) rejectReady(error);
else onTerminalError?.(error);
},
});
const rt: FeishuRuntime = { client, logger };
const rt: FeishuRuntime = { client, logger, isListenerReady: () => state === "READY" };
const handlers: Record<string, (data: unknown) => Promise<void>> = {
"im.message.receive_v1": async (data) => {
try { await onMessage(data as MessageReceiveEvent, rt); }
@@ -938,15 +962,25 @@ export function startFeishuListenerWithClient(
.catch((e) => { logger.error({ err: e }, "feishu card action handler threw"); });
};
}
void wsClient.start({ eventDispatcher: new lark.EventDispatcher({}).register(handlers) });
await wsClient.start({ eventDispatcher: new lark.EventDispatcher({}).register(handlers) });
let timeout: ReturnType<typeof setTimeout> | undefined;
const startupTimeout = new Promise<never>((_resolve, reject) => {
timeout = setTimeout(() => reject(new Error("Feishu listener did not become ready within 15 seconds")), 15_000);
timeout.unref();
});
try {
await Promise.race([ready, startupTimeout]);
} finally {
if (timeout !== undefined) clearTimeout(timeout);
}
return rt;
}
export function startFeishuListener(
export async function startFeishuListener(
config: FeishuConfig,
logger: FastifyBaseLogger,
onMessage: (event: MessageReceiveEvent, rt: FeishuRuntime) => Promise<void>,
onCardAction?: (event: CardActionEvent, rt: FeishuRuntime) => Promise<void>,
): FeishuRuntime {
): Promise<FeishuRuntime> {
return startFeishuListenerWithClient(config, createLarkClient(config), logger, onMessage, onCardAction);
}