/** * Feishu context reader — the real implementation behind `feishuContextTool`. * * ADR-0003: Hub stores only anchors, reads Feishu context on demand. The tool * (`feishu_read_context`) is already chat-authorized by `tools.ts` (chat must * match the project binding). Here we actually call lark's `im.v1.message`: * * - `trigger_message` / `reply`: `message.get` by message_id. * - `status_card`: the run's status card message — same `message.get` by id. * - `thread`: lark's thread replies. The SDK exposes `message.list` with a * `parent_message_id` filter; we map "thread" to that. * * The lark SDK's `im.v1.message` methods are dynamic at runtime (weak types); * we cast through a known request/response shape and return a compact JSON for * the model. All calls use the chat-authorized `client` from the runtime — no * separate credential path, so the ADR-0003 invariant holds end-to-end. */ import type { FeishuRuntime } from "./client.js"; import type { FeishuContextArgs } from "../agent/tools.js"; import type { ToolContext } from "../agent/tools.js"; /** Pragmatic shape of the lark message object we consume. */ interface LarkMessage { message_id?: string; msg_type?: string; content?: string; body?: { content?: string }; create_time?: string; sender?: { id?: string; sender_type?: string }; chat_id?: string; parent_id?: string; parent_message_id?: string; root_id?: string; thread_id?: string; } interface ImV1Message { get: (p: { path: { message_id: string } }) => Promise<{ data?: { items?: LarkMessage[]; message?: LarkMessage } }>; list: (p: { params: { container_id_type: string; container_id: string; page_size?: number }; }) => Promise<{ data?: { items?: LarkMessage[] } }>; } function im(rt: FeishuRuntime): ImV1Message { return (rt.client as unknown as { im: { v1: { message: ImV1Message } } }).im.v1.message; } /** Read on-demand Feishu context for the current run. Entry point for the tool. */ export async function readFeishuContext( args: FeishuContextArgs, ctx: ToolContext, rt: unknown, ): Promise { const runtime = rt as FeishuRuntime; const api = im(runtime); try { if (args.chat_id !== ctx.boundChatId) { throw new Error("refused Feishu context read outside the current project's bound chat"); } switch (args.anchor) { case "trigger_message": case "status_card": case "reply": { // All three are single-message reads by id. const res = await api.get({ path: { message_id: args.id } }); const msg = res.data?.message ?? res.data?.items?.[0]; if (msg === undefined) return JSON.stringify({ error: "message not found", id: args.id }); assertBoundChat(msg, ctx.boundChatId); return JSON.stringify(compact(msg)); } case "thread": { // Thread = replies to a parent message. `container_id` is the parent's // message_id; container_id_type=message_id scopes the list to that thread. const res = await api.list({ params: { container_id_type: "message_id", container_id: args.id, page_size: 50, }, }); const items = res.data?.items ?? []; for (const item of items) assertBoundChat(item, ctx.boundChatId); return JSON.stringify(items.map(compact)); } default: return JSON.stringify({ error: `unknown anchor type: ${args.anchor as string}` }); } } catch (e) { runtime.logger.warn( { runId: ctx.runId, projectId: ctx.projectId, anchor: args.anchor, anchorId: args.id, err: e instanceof Error ? e.message : String(e), }, "Feishu context read refused or failed", ); return JSON.stringify({ error: e instanceof Error ? e.message : String(e) }); } } function assertBoundChat(message: LarkMessage, boundChatId: string): void { if (message.chat_id !== boundChatId) { throw new Error("refused Feishu context result outside the current project's bound chat"); } } /** Project a lark message down to the fields the model actually needs. */ function compact(m: LarkMessage): Record { const parentId = m.parent_id ?? m.parent_message_id; return { message_id: m.message_id, msg_type: m.msg_type, content: m.body?.content ?? m.content, create_time: m.create_time, chat_id: m.chat_id, sender_id: m.sender?.id, parent_id: parentId, parent_message_id: parentId, root_id: m.root_id, thread_id: m.thread_id, }; }